âš¡ TL;DR
Integrate a Model Context Protocol (MCP) server into your Flutter/Dart workflow. Step-by-step setup with Claude Code and real-world use cases.
Model Context Protocol (MCP) lets AI tools like Claude Code plug into your local development environment. In this guide, I’ll walk you through adding an MCP server to your Flutter/Dart tooling so Claude Code (or any MCP client) can read your project structure, run tests, and even generate code that matches your team’s patterns.
I’ve been using MCP with Flutter for 3 months now. It’s transformed how I write boilerplate, refactor widgets, and debug platform-specific issues. Here’s how to set it up.
What is MCP?
MCP is an open protocol that standardizes how AI applications connect to data sources and tools. Think of it as a USB-C port for AI: instead of every AI tool building custom integrations, they all plug into MCP servers that expose your data safely.
For Flutter/Dart, an MCP server can:
- Read your
pubspec.yamlto understand dependencies - Analyze your
lib/directory structure - Run
flutter analyzeand report issues - Generate code that follows your existing patterns
- Execute
dart testand report failures
Prerequisites
- Flutter SDK installed
- Claude Code CLI installed (
npm install -g @anthropic-ai/claude-code) - Basic Dart/Flutter knowledge
Step 1: Create a Dart MCP Server
Create a new Dart package for your MCP server:
dart create -t package mcp_flutter_server
cd mcp_flutter_serverAdd the MCP SDK dependency:
# pubspec.yaml
dependencies:
mcp_dart: ^0.3.0
path: ^1.8.0Step 2: Implement the MCP Server
Create bin/mcp_server.dart:
import 'package:mcp_dart/mcp_dart.dart';
import 'dart:io';
import 'dart:convert';
import 'package:path/path.dart' as path;
void main() async {
final server = McpServer(
name: 'flutter-mcp-server',
version: '1.0.0',
);
// Tool: List Flutter project structure
server.addTool(
name: 'getProjectStructure',
description: 'Get the structure of a Flutter project',
inputSchema: ToolInputSchema(
type: 'object',
properties: {
'projectPath': {
'type': 'string',
'description': 'Path to the Flutter project',
},
},
required: ['projectPath'],
),
callback: (args, extra) async {
final projectPath = args['projectPath'] as String;
final structure = <String, dynamic>{};
// Read pubspec.yaml
final pubspecFile = File(path.join(projectPath, 'pubspec.yaml'));
if (await pubspecFile.exists()) {
structure['pubspec'] = await pubspecFile.readAsString();
}
// List lib directory
final libDir = Directory(path.join(projectPath, 'lib'));
if (await libDir.exists()) {
final files = <String>[];
await for (final entity in libDir.list(recursive: true)) {
if (entity is File) {
files.add(path.relative(entity.path, from: projectPath));
}
}
structure['libFiles'] = files;
}
return CallToolResult(
content: [TextContent(text: jsonEncode(structure))],
);
},
);
// Tool: Run Flutter analyze
server.addTool(
name: 'runFlutterAnalyze',
description: 'Run flutter analyze on a project',
inputSchema: ToolInputSchema(
type: 'object',
properties: {
'projectPath': {
'type': 'string',
'description': 'Path to the Flutter project',
},
},
required: ['projectPath'],
),
callback: (args, extra) async {
final projectPath = args['projectPath'] as String;
final result = await Process.run(
'flutter',
['analyze'],
workingDirectory: projectPath,
runInShell: true,
);
return CallToolResult(
content: [
TextContent(text: 'Exit code: ${result.exitCode}'),
TextContent(text: result.stdout.toString()),
if (result.stderr.isNotEmpty) TextContent(text: result.stderr.toString()),
],
isError: result.exitCode != 0,
);
},
);
// Tool: Run Dart tests
server.addTool(
name: 'runDartTest',
description: 'Run dart test on a specific test file',
inputSchema: ToolInputSchema(
type: 'object',
properties: {
'projectPath': {'type': 'string'},
'testFile': {'type': 'string'},
},
required: ['projectPath', 'testFile'],
),
callback: (args, extra) async {
final projectPath = args['projectPath'] as String;
final testFile = args['testFile'] as String;
final result = await Process.run(
'dart',
['test', testFile],
workingDirectory: projectPath,
);
return CallToolResult(
content: [
TextContent(text: result.stdout.toString()),
if (result.stderr.isNotEmpty) TextContent(text: result.stderr.toString()),
],
isError: result.exitCode != 0,
);
},
);
// Start the server
await server.start();
}Step 3: Configure Claude Code to Use Your MCP Server
Create or edit ~/.claude-code/mcp_config.json:
{
"mcpServers": {
"flutter-dart": {
"command": "dart",
"args": ["/absolute/path/to/mcp_flutter_server/bin/mcp_server.dart"],
"description": "Flutter/Dart MCP server for Claude Code"
}
}
}Step 4: Use the MCP Server in Claude Code
Now when you start Claude Code in a Flutter project, it can use your MCP tools:
cd /path/to/your/flutter/project
claude-codeInside Claude Code, ask:
Use the MCP server to analyze my Flutter project structure and tell me:
1. What dependencies I have
2. What files exist in lib/
3. Any analyzer errorsClaude Code will call getProjectStructure and runFlutterAnalyze through your MCP server and report back.
Step 5: Advanced Usage - Auto-Generate Boilerplate
Add a tool to generate BLoC boilerplate:
server.addTool(
name: 'generateBloc',
description: 'Generate BLoC pattern files for a feature',
inputSchema: ToolInputSchema(
type: 'object',
properties: {
'featureName': {'type': 'string'},
'projectPath': {'type': 'string'},
},
required: ['featureName', 'projectPath'],
),
callback: (args, extra) async {
final featureName = args['featureName'] as String;
final projectPath = args['projectPath'] as String;
final featurePath = path.join(projectPath, 'lib', 'features', featureName);
// Create directories
await Directory(path.join(featurePath, 'bloc')).create(recursive: true);
await Directory(path.join(featurePath, 'ui')).create(recursive: true);
// Generate BLoC file
final blocFile = File(path.join(featurePath, 'bloc', '${featureName}_bloc.dart'));
await blocFile.writeAsString('''
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class ${featureName}Event {}
abstract class ${featureName}State {}
class ${featureName}Bloc extends Bloc<${featureName}Event, ${featureName}State> {
${featureName}Bloc() : super(${featureName}Initial()) {
on<${featureName}Event>((event, emit) {
// TODO: implement
});
}
}
''');
return CallToolResult(
content: [TextContent(text: 'Generated BLoC for $featureName')],
);
},
);Real-World Use Cases
1. Onboarding New Team Members
Ask Claude: "Explain the BLoC pattern used in the auth feature"The MCP server reads lib/features/auth/bloc/ and Claude explains the existing implementation.
2. Pre-Commit Hook Validation
Ask Claude: "Run flutter analyze and fix any issues"MCP runs analysis, Claude suggests fixes, you review before committing.
3. Documentation Generation
Ask Claude: "Generate API documentation for the UserRepository class"MCP reads the file, Claude generates markdown docs.
Security Considerations
- File System Access: MCP servers can read/write files. Only run servers from trusted sources.
- Command Execution: The
runFlutterAnalyzetool executes shell commands. Validate inputs to prevent injection. - Authentication: For web-based MCP servers, add authentication headers.
Troubleshooting
MCP Server Not Starting
Check logs:
dart /path/to/mcp_server.dart 2>&1Common issues:
- Dart not in PATH
- Missing
mcp_dartdependency (rundart pub get) - Port conflicts (change port in server config)
Claude Code Not Detecting MCP
Verify config:
cat ~/.claude-code/mcp_config.jsonEnsure the command and args match your Dart executable and server path.
Conclusion
Adding an MCP server to your Flutter/Dart tooling bridges AI assistance with your local dev environment. Instead of copying code snippets into chat windows, Claude Code can now understand your project structure, run analysis, and generate code that fits your existing architecture.
The ecosystem is young (MCP was announced in late 2024), but tooling is maturing fast. For Flutter developers, this is a game-changer for productivity.
