Building a Model Context Protocol Server with Dart: Connecting to Claude Desktop
A step-by-step guide to implementing an MCP server that enables AI models to interact with your local system Today's AI assistants are becoming increasingly capable, but their ability to interact with our local environments remains limited. The Model Context Protocol (MCP) bridges this gap by providing a standardized way for AI models like Claude to communicate with external tools, access resources, and interact with local systems. In this guide, we'll implement an MCP server using Flutter and Dart, then connect it to Claude Desktop to unlock powerful new capabilities. What is Model Context Protocol (MCP)? Model Context Protocol is a JSON-RPC based protocol that standardizes communication between AI models and external environments. It defines a structured way for AI models to: Call tools (functions or APIs) Access resources (files or data sources) Use prompts (reusable conversation templates) By implementing an MCP server, you can give AI models like Claude the ability to interact with your local system, access specific files, or call custom functions that you define. Project Setup Let's start by setting up our Dart project with the required dependencies. # Create a new Dart project dart create mcp_server_example cd mcp_server_example Modify the pubspec.yaml file to include the needed dependencies: name: mcp_server_example description: A sample MCP server implementation version: 1.0.0 environment: sdk: '>=2.18.0 Developer Click "Edit Config" This will open the claude_desktop_config.json file. Modify it as follows: { "mcpServers": { "flutter-test": { "command": "/path/to/mcp_server/example/mcp_server_example", "args": ["--mcp-stdio-mode"] } } } Save the file and restart Claude Desktop Connecting and Using the Server After restarting Claude Desktop, click on the "MCP" dropdown menu in the bottom left Select "flutter-test" to connect to your MCP server When successfully connected, a hammer icon will appear at the bottom of the chat window Now Claude Desktop has access to the tools, resources, and prompts we've registered: The hello tool to generate greetings The calculator tool to perform arithmetic operations The currentDateTime tool to provide date and time information The dart://system-info resource to access system information The dart://env-vars resource to access environment variables You can test the connection by asking Claude to use one of these tools or access a resource. Server Logging and Health Monitoring Our MCP server includes logging and health monitoring features to help track its operation and troubleshoot issues. Logging Configuration // Set log level _logger.setLevel(LogLevel.debug); // Send log messages server.sendLog(McpLogLevel.info, 'Flutter MCP Server started successfully'); Health Monitoring The MCP server provides a health/check endpoint that returns information about its status: final health = server.getHealth(); _logger.debug('Server is running: ${health.isRunning}'); _logger.debug('Connected sessions: ${health.connectedSessions}'); _logger.debug('Registered tools: ${health.registeredTools}'); _logger.debug('Uptime: ${health.uptime.inSeconds} seconds'); Extending the MCP Server You can extend your MCP server in various ways to enhance its capabilities: Adding New Tools You can add new tools that interact with APIs or databases: // Example weather API tool server.addTool( name: 'weather', description: 'Get weather information for a specific city', inputSchema: { 'type': 'object', 'properties': { 'city': { 'type': 'string', 'description': 'City name' } }, 'required': ['city'] }, handler: (args) async { final city = args['city'] as String; // Weather API call code... return CallToolResult([TextContent(text: 'Current temperature in New York: 22°C, Clear')]); }, ); File System Access You can add resources that access the local file system: // File system resource example server.addResource( uri: 'file://{path}', name: 'File Resource', description: 'Access files on the system', mimeType: 'application/octet-stream', uriTemplate: { 'type': 'object', 'properties': { 'path': { 'type': 'string', 'description': 'Path to the file' } } }, handler: (uri, params) async { // File access code... } ); Database Connections You can create resources that connect to databases to provide information: // Database resource example server.addResource( uri: 'db://users/{id}', name: 'User Database', description: 'Access user information', mimeType: 'application/json', uriTemplate: { 'type': 'object', 'properties': { 'id': { 'type': 'string', 'description': 'User ID' } } }, handler: (uri, pa
A step-by-step guide to implementing an MCP server that enables AI models to interact with your local system
Today's AI assistants are becoming increasingly capable, but their ability to interact with our local environments remains limited. The Model Context Protocol (MCP) bridges this gap by providing a standardized way for AI models like Claude to communicate with external tools, access resources, and interact with local systems.
In this guide, we'll implement an MCP server using Flutter and Dart, then connect it to Claude Desktop to unlock powerful new capabilities.
What is Model Context Protocol (MCP)?
Model Context Protocol is a JSON-RPC based protocol that standardizes communication between AI models and external environments. It defines a structured way for AI models to:
- Call tools (functions or APIs)
- Access resources (files or data sources)
- Use prompts (reusable conversation templates)
By implementing an MCP server, you can give AI models like Claude the ability to interact with your local system, access specific files, or call custom functions that you define.
Project Setup
Let's start by setting up our Dart project with the required dependencies.
# Create a new Dart project
dart create mcp_server_example
cd mcp_server_example
Modify the pubspec.yaml
file to include the needed dependencies:
name: mcp_server_example
description: A sample MCP server implementation
version: 1.0.0
environment:
sdk: '>=2.18.0 <3.0.0'
dependencies:
uuid: ^3.0.7
mcp_server: ^1.0.0
Run the following command to get the dependencies:
dart pub get
Implementing the MCP Server
Our MCP server implementation will consist of:
- Server instance creation
- Transport mechanism configuration (STDIO or SSE)
- Registration of tools, resources, and prompts
- Server startup and connection handling
Here's the basic structure of our implementation:
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:mcp_server/mcp_server.dart';
final Logger _logger = Logger.getLogger('mcp_server_example');
void main(List<String> args) async {
_logger.setLevel(LogLevel.debug);
// STDIO Mode for direct connection with Claude Desktop
if (args.contains('--mcp-stdio-mode')) {
await startMcpServer(mode: 'stdio');
} else {
// SSE Mode for web connections
int port = 8999;
await startMcpServer(mode: 'sse', port: port);
}
}
The Server Startup Function
Future<void> startMcpServer({required String mode, int port = 8080}) async {
try {
// Create server with capabilities
final server = McpServer.createServer(
name: 'Flutter MCP Demo',
version: '1.0.0',
capabilities: ServerCapabilities(
tools: true,
toolsListChanged: true,
resources: true,
resourcesListChanged: true,
prompts: true,
promptsListChanged: true,
),
);
// Register tools, resources, and prompts
_registerTools(server);
_registerResources(server);
_registerPrompts(server);
// Set up transport based on mode
ServerTransport transport;
if (mode == 'stdio') {
_logger.debug('Starting server in STDIO mode');
transport = McpServer.createStdioTransport();
} else {
_logger.debug('Starting server in SSE mode on port $port');
transport = McpServer.createSseTransport(
endpoint: '/sse',
messagesEndpoint: '/message',
port: port,
fallbackPorts: [port + 1, port + 2, port + 3],
);
}
// Handle transport closure
transport.onClose.then((_) {
_logger.debug('Transport closed, shutting down.');
exit(0);
});
// Connect server to transport
server.connect(transport);
// Send initial log message
server.sendLog(McpLogLevel.info, 'Flutter MCP Server started successfully');
if (mode == 'sse') {
_logger.debug('SSE Server is running on:');
_logger.debug('- SSE endpoint: http://localhost:$port/sse');
_logger.debug('- Message endpoint: http://localhost:$port/message');
_logger.debug('Press Ctrl+C to stop the server');
} else {
_logger.debug('STDIO Server initialized and connected to transport');
}
} catch (e, stackTrace) {
_logger.debug('Error initializing MCP server: $e');
_logger.debug(stackTrace.toString());
exit(1);
}
}
Transport Mechanisms: STDIO vs. SSE
MCP servers support two primary transport mechanisms:
STDIO Transport
STDIO transport uses standard input/output streams for communication. This is ideal for direct integration with desktop applications like Claude Desktop.
transport = McpServer.createStdioTransport();
SSE (Server-Sent Events) Transport
SSE transport uses HTTP for communication, making it suitable for web-based applications. The server listens on a specified port and sends events to connected clients.
transport = McpServer.createSseTransport(
endpoint: '/sse',
messagesEndpoint: '/message',
port: port,
fallbackPorts: [port + 1, port + 2, port + 3],
);
Registering Tools, Resources, and Prompts
Now let's implement the core functionality of our MCP server by registering tools, resources, and prompts.
Tools Registration
Tools are functions that AI models can call. We'll implement a greeting tool, a calculator, and a date/time tool:
void _registerTools(Server server) {
// Hello world tool
server.addTool(
name: 'hello',
description: 'Says hello to someone',
inputSchema: {
'type': 'object',
'properties': {
'name': {
'type': 'string',
'description': 'Name to say hello to'
}
},
'required': []
},
handler: (args) async {
final name = args['name'] ?? 'world';
return CallToolResult([TextContent(text: 'Hello, $name!')]);
},
);
// Calculator tool
server.addTool(
name: 'calculator',
description: 'Perform basic arithmetic operations',
inputSchema: {
'type': 'object',
'properties': {
'operation': {
'type': 'string',
'enum': ['add', 'subtract', 'multiply', 'divide'],
'description': 'Mathematical operation to perform'
},
'a': {
'type': 'number',
'description': 'First operand'
},
'b': {
'type': 'number',
'description': 'Second operand'
}
},
'required': ['operation', 'a', 'b']
},
handler: (args) async {
final operation = args['operation'] as String;
final a = (args['a'] is int) ? (args['a'] as int).toDouble() : args['a'] as double;
final b = (args['b'] is int) ? (args['b'] as int).toDouble() : args['b'] as double;
double result;
switch (operation) {
case 'add':
result = a + b;
break;
case 'subtract':
result = a - b;
break;
case 'multiply':
result = a * b;
break;
case 'divide':
if (b == 0) {
throw McpError('Cannot divide by zero');
}
result = a / b;
break;
default:
throw McpError('Unknown operation: $operation');
}
return CallToolResult([TextContent(text: 'Result: $result')]);
},
);
// Date and time tool
server.addTool(
name: 'currentDateTime',
description: 'Get the current date and time',
inputSchema: {
'type': 'object',
'properties': {
'format': {
'type': 'string',
'description': 'Output format (full, date, time)',
'default': 'full'
}
},
'required': []
},
handler: (args) async {
try {
String format = args['format'] ?? 'full';
final now = DateTime.now();
String result;
switch (format) {
case 'date':
result = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
break;
case 'time':
result = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
break;
case 'full':
default:
result = now.toIso8601String();
break;
}
return CallToolResult([TextContent(text: result)]);
} catch (e) {
return CallToolResult(
[TextContent(text: "Error getting date/time: $e")],
isError: true
);
}
},
);
}
Resources Registration
Resources provide access to data sources that AI models can read. We'll implement system information and environment variables resources:
void _registerResources(Server server) {
// System info resource
server.addResource(
uri: 'flutter://system-info',
name: 'System Information',
description: 'Detailed information about the current system',
mimeType: 'application/json',
uriTemplate: {
'type': 'object',
'properties': {}
},
handler: (uri, params) async {
final systemInfo = {
'operatingSystem': Platform.operatingSystem,
'operatingSystemVersion': Platform.operatingSystemVersion,
'localHostname': Platform.localHostname,
'numberOfProcessors': Platform.numberOfProcessors,
'localeName': Platform.localeName,
'executable': Platform.executable,
'resolvedExecutable': Platform.resolvedExecutable,
'script': Platform.script.toString(),
};
final contents = systemInfo.entries.map((entry) =>
ResourceContent(
uri: 'flutter://system-info/${entry.key}',
text: '${entry.key}: ${entry.value}',
)
).toList();
return ReadResourceResult(
content: jsonEncode(systemInfo),
mimeType: 'application/json',
contents: contents,
);
}
);
// Environment variables resource
server.addResource(
uri: 'flutter://env-vars',
name: 'Environment Variables',
description: 'List of system environment variables',
mimeType: 'application/json',
uriTemplate: {
'type': 'object',
'properties': {}
},
handler: (uri, params) async {
final envVars = Platform.environment;
final contents = envVars.entries.map((entry) =>
ResourceContent(
uri: 'flutter://env-vars/${entry.key}',
text: '${entry.key}: ${entry.value}',
)
).toList();
return ReadResourceResult(
content: jsonEncode(envVars),
mimeType: 'application/json',
contents: contents,
);
}
);
}
Prompts Registration
Prompts are templates for conversations with AI models. We'll implement a greeting prompt and a code review prompt:
void _registerPrompts(Server server) {
// Simple greeting prompt
server.addPrompt(
name: 'greeting',
description: 'Generate a greeting for a user',
arguments: [
PromptArgument(
name: 'name',
description: 'Name of the person to greet',
required: true,
),
PromptArgument(
name: 'formal',
description: 'Whether to use formal greeting style',
required: false,
),
],
handler: (args) async {
final name = args['name'] as String;
final formal = args['formal'] as bool? ?? false;
final String systemPrompt = formal
? 'You are a formal assistant. Address the user with respect and formality.'
: 'You are a friendly assistant. Be warm and casual in your tone.';
final messages = [
Message(
role: MessageRole.system.toString().split('.').last,
content: TextContent(text: systemPrompt),
),
Message(
role: MessageRole.user.toString().split('.').last,
content: TextContent(text: 'Please greet $name'),
),
];
return GetPromptResult(
description: 'A ${formal ? 'formal' : 'casual'} greeting for $name',
messages: messages,
);
},
);
// Code review prompt
server.addPrompt(
name: 'codeReview',
description: 'Generate a code review for a code snippet',
arguments: [
PromptArgument(
name: 'code',
description: 'Code to review',
required: true,
),
PromptArgument(
name: 'language',
description: 'Programming language of the code',
required: true,
),
],
handler: (args) async {
final code = args['code'] as String;
final language = args['language'] as String;
final systemPrompt = '''
You are an expert code reviewer. Review the provided code with these guidelines:
1. Identify potential bugs or issues
2. Suggest optimizations for performance or readability
3. Highlight good practices used in the code
4. Provide constructive feedback for improvements
Be specific in your feedback and provide code examples when suggesting changes.
''';
final messages = [
Message(
role: MessageRole.system.toString().split('.').last,
content: TextContent(text: systemPrompt),
),
Message(
role: MessageRole.user.toString().split('.').last,
content: TextContent(text: 'Please review this $language code:\n\n```
$language\n$code\n
```'),
),
];
return GetPromptResult(
description: 'Code review for $language code',
messages: messages,
);
},
);
}
Connecting to Claude Desktop
Now let's connect our MCP server to Claude Desktop:
Building an Executable File
First, let's compile our Dart code into an executable file:
# Navigate to the project directory
cd /path/to/mcp_server/example
# Compile the Dart file to an executable
dart compile exe bin/mcp_server_example.dart -o mcp_server_example
# Add execute permission
chmod +x mcp_server_example
Configuring Claude Desktop
To connect Claude Desktop with our MCP server:
- Launch Claude Desktop
- Navigate to Settings > Developer
- Click "Edit Config"
- This will open the
claude_desktop_config.json
file. Modify it as follows:
{
"mcpServers": {
"flutter-test": {
"command": "/path/to/mcp_server/example/mcp_server_example",
"args": ["--mcp-stdio-mode"]
}
}
}
- Save the file and restart Claude Desktop
Connecting and Using the Server
- After restarting Claude Desktop, click on the "MCP" dropdown menu in the bottom left
- Select "flutter-test" to connect to your MCP server
- When successfully connected, a hammer icon will appear at the bottom of the chat window
Now Claude Desktop has access to the tools, resources, and prompts we've registered:
- The
hello
tool to generate greetings - The
calculator
tool to perform arithmetic operations - The
currentDateTime
tool to provide date and time information - The
dart://system-info
resource to access system information - The
dart://env-vars
resource to access environment variables
You can test the connection by asking Claude to use one of these tools or access a resource.
Server Logging and Health Monitoring
Our MCP server includes logging and health monitoring features to help track its operation and troubleshoot issues.
Logging Configuration
// Set log level
_logger.setLevel(LogLevel.debug);
// Send log messages
server.sendLog(McpLogLevel.info, 'Flutter MCP Server started successfully');
Health Monitoring
The MCP server provides a health/check
endpoint that returns information about its status:
final health = server.getHealth();
_logger.debug('Server is running: ${health.isRunning}');
_logger.debug('Connected sessions: ${health.connectedSessions}');
_logger.debug('Registered tools: ${health.registeredTools}');
_logger.debug('Uptime: ${health.uptime.inSeconds} seconds');
Extending the MCP Server
You can extend your MCP server in various ways to enhance its capabilities:
Adding New Tools
You can add new tools that interact with APIs or databases:
// Example weather API tool
server.addTool(
name: 'weather',
description: 'Get weather information for a specific city',
inputSchema: {
'type': 'object',
'properties': {
'city': {
'type': 'string',
'description': 'City name'
}
},
'required': ['city']
},
handler: (args) async {
final city = args['city'] as String;
// Weather API call code...
return CallToolResult([TextContent(text: 'Current temperature in New York: 22°C, Clear')]);
},
);
File System Access
You can add resources that access the local file system:
// File system resource example
server.addResource(
uri: 'file://{path}',
name: 'File Resource',
description: 'Access files on the system',
mimeType: 'application/octet-stream',
uriTemplate: {
'type': 'object',
'properties': {
'path': {
'type': 'string',
'description': 'Path to the file'
}
}
},
handler: (uri, params) async {
// File access code...
}
);
Database Connections
You can create resources that connect to databases to provide information:
// Database resource example
server.addResource(
uri: 'db://users/{id}',
name: 'User Database',
description: 'Access user information',
mimeType: 'application/json',
uriTemplate: {
'type': 'object',
'properties': {
'id': {
'type': 'string',
'description': 'User ID'
}
}
},
handler: (uri, params) async {
// Database access code...
}
);
Conclusion
In this guide, we've implemented an MCP server using Flutter and Dart, and connected it to Claude Desktop. This allows Claude to interact with our local system, access resources, and use custom tools that we've defined.
By implementing the Model Context Protocol, you can significantly enhance the capabilities of AI models like Claude, enabling them to perform tasks that would otherwise be impossible due to their isolation from local systems.
As you continue to develop your MCP server, consider adding more tools and resources to expand its functionality. With the right implementation, you can create powerful AI-powered applications that combine the intelligence of large language models with the capabilities of your local system.
Learn More
Support This Work
If you found this tutorial helpful, please consider supporting the development of more free content like this through Patreon. Your support helps me create more high-quality developer tutorials and tools.
This is part of our ongoing series on AI development with Flutter. Stay tuned for more articles on advanced MCP implementations!