Skip to content

Instantly share code, notes, and snippets.

@rodydavis
Last active January 14, 2026 10:39
Show Gist options
  • Select an option

  • Save rodydavis/583360695f48c9704dc59133bd9f4eee to your computer and use it in GitHub Desktop.

Select an option

Save rodydavis/583360695f48c9704dc59133bd9f4eee to your computer and use it in GitHub Desktop.
Flutter App as a MCP server
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'mcp_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const McpApp());
}
/// The types of transport available for the MCP service.
enum McpTransportType {
/// Uses a local HTTP server with Server-Sent Events (SSE).
http,
/// Uses a Unix Domain Socket (UDS) for standard input/output bridging.
stdio,
}
/// Manages the connection state and lifecycle of the active MCP service.
///
/// This class handles:
/// - Switching between transport types (HTTP <-> UDS).
/// - Preserving application state (counter value) during switches.
/// - Toggling the server on and off.
class McpConnectionManager extends ChangeNotifier {
McpConnectionManager() {
_init();
}
McpService _service = HttpMcpService();
/// The currently active MCP service instance.
McpService get service => _service;
McpTransportType _transportType = McpTransportType.http;
/// The currently selected transport type.
McpTransportType get transportType => _transportType;
bool _isRunning = false;
/// Whether the MCP server is currently running.
bool get isRunning => _isRunning;
// Preserve counter value across service switches
int _lastCounterValue = 0;
void _init() {
// Initially not started
_service.set(_lastCounterValue);
}
/// Sets the transport type, stopping the current service and starting a new one.
Future<void> setTransport(McpTransportType type) async {
if (_transportType == type) return;
// Save state
_lastCounterValue = _service.counter;
// Stop current service if running
if (_isRunning) {
await _service.stopServer();
}
_transportType = type;
// Create new service
switch (type) {
case McpTransportType.http:
_service = HttpMcpService();
break;
case McpTransportType.stdio:
_service = StdioMcpService();
break;
}
// Restore state
_service.set(_lastCounterValue);
// Restart if was running
if (_isRunning) {
try {
await _service.startServer();
} catch (e) {
print("Failed to restart server after switch: $e");
_isRunning = false;
}
}
notifyListeners();
}
/// Toggles the server running state.
Future<void> toggleServer(bool value) async {
if (_isRunning == value) return;
_isRunning = value;
notifyListeners();
try {
if (_isRunning) {
await _service.startServer();
} else {
await _service.stopServer();
}
} catch (e) {
print("Error toggling server: $e");
_isRunning = false;
notifyListeners();
}
}
}
/// The root widget of the MCP Counter Application.
class McpApp extends StatelessWidget {
const McpApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => McpConnectionManager(),
child: const MaterialApp(
home: CounterPage(),
debugShowCheckedModeBanner: false,
),
);
}
}
/// The main page displaying the counter and MCP controls.
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
void _showConfig(BuildContext context, Map<String, dynamic> config) {
const encoder = JsonEncoder.withIndent(' ');
final jsonConfig = encoder.convert(config);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('MCP Configuration'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Add this to your MCP config:'),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(8),
color: Colors.grey[200],
child: Text(
jsonConfig,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
ElevatedButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: jsonConfig));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Config copied to clipboard!')),
);
Navigator.pop(context);
},
child: const Text('Copy'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
// Access manager to get the current service
final manager = context.watch<McpConnectionManager>();
// Provide the specific McpService to the subtree so Consumers work correctly
return ChangeNotifierProvider<McpService>.value(
value: manager.service,
child: Scaffold(
appBar: AppBar(
title: const Text('MCP Counter'),
actions: [
Builder(
builder: (context) {
return IconButton(
icon: const Icon(Icons.info_outline),
onPressed: () async {
// Use the current service from the manager
final config = await manager.service.getMcpServersConfig();
if (context.mounted) {
_showConfig(context, config);
}
},
tooltip: 'Show MCP Config',
);
},
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Transport Selection
Padding(
padding: const EdgeInsets.all(16.0),
child: SegmentedButton<McpTransportType>(
segments: const [
ButtonSegment(
value: McpTransportType.http,
label: Text('HTTP'),
icon: Icon(Icons.http),
),
ButtonSegment(
value: McpTransportType.stdio,
label: Text('STDIO (UDS)'),
icon: Icon(Icons.terminal),
),
],
selected: {manager.transportType},
onSelectionChanged: (Set<McpTransportType> newSelection) {
manager.setTransport(newSelection.first);
},
),
),
const SizedBox(height: 20),
// Server Toggle
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Server Status: '),
Switch(
value: manager.isRunning,
onChanged: (val) => manager.toggleServer(val),
),
Text(manager.isRunning ? 'Running' : 'Stopped'),
],
),
const Divider(),
const SizedBox(height: 20),
const Text('Current Value:', style: TextStyle(fontSize: 24)),
Consumer<McpService>(
builder: (context, mcp, child) {
return Text(
'${mcp.counter}',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
);
},
),
],
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Consumer<McpService>(
builder: (context, mcp, _) => FloatingActionButton(
onPressed: () => mcp.increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
),
const SizedBox(height: 10),
Consumer<McpService>(
builder: (context, mcp, _) => FloatingActionButton(
onPressed: () => mcp.decrement(),
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
),
],
),
),
);
}
}
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
/// Base class for the Model Context Protocol (MCP) service.
///
/// This abstract class defines the core functionality for an MCP service,
/// including state management (the counter) and message processing (JSON-RPC).
/// It extends [ChangeNotifier] to allow the UI to react to state changes.
abstract class McpService extends ChangeNotifier {
McpService();
int _counter = 0;
/// The current value of the counter.
int get counter => _counter;
// 1. Counter Logic
/// Increments the counter by 1.
void increment() {
_counter++;
notifyListeners();
}
/// Decrements the counter by 1.
void decrement() {
_counter--;
notifyListeners();
}
/// Sets the counter to a specific value.
void set(int val) {
_counter = val;
notifyListeners();
}
/// Resets the counter to 0.
void reset() {
_counter = 0;
notifyListeners();
}
/// Returns the full configuration for this MCP server, including the "mcpServers" key.
Future<Map<String, dynamic>> getMcpServersConfig() async {
return {
"mcpServers": {"CounterApp": await getConfig()},
};
}
/// Returns the transport-specific configuration for this server.
///
/// For HTTP servers, this includes the 'serverUrl'.
/// For Stdio/UDS servers, this includes the 'command' and 'args'.
Future<Map<String, dynamic>> getConfig();
/// Starts the MCP server.
Future<void> startServer();
/// Stops the MCP server and cleans up resources.
Future<void> stopServer();
/// Processes an incoming JSON-RPC request string and returns the JSON-RPC response string.
///
/// This method handles:
/// - JSON parsing
/// - Method dispatching (initialize, ping, tools/list, tools/call)
/// - Error handling
Future<String?> processRequest(String jsonString) async {
Map<String, dynamic>? request;
try {
request = jsonDecode(jsonString);
} catch (e) {
return jsonEncode({
"jsonrpc": "2.0",
"id": null,
"error": {"code": -32700, "message": "Parse error"},
});
}
if (request == null) return null;
final id = request['id'];
final method = request['method'];
final params = request['params'] ?? {};
try {
switch (method) {
case 'initialize':
return _success(id, {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}, "resources": {}, "prompts": {}},
"serverInfo": {"name": "flutter-counter-app", "version": "1.0.0"},
});
case 'notifications/initialized':
return null; // No response needed
case 'ping':
return _success(id, {});
case 'tools/list':
return _success(id, {
"tools": [
{
"name": "get_value",
"description": "Get the current value of the counter",
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "increment",
"description": "Increment the counter by 1",
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "decrement",
"description": "Decrement the counter by 1",
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "reset",
"description": "Reset the counter to 0",
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "set_value",
"description": "Set the counter to a specific value",
"inputSchema": {
"type": "object",
"properties": {
"value": {
"type": "integer",
"description": "The new value",
},
},
"required": ["value"],
},
},
],
});
case 'tools/call':
final name = params['name'];
final args = params['arguments'] ?? {};
dynamic toolResult;
switch (name) {
case 'get_value':
toolResult = _counter;
break;
case 'increment':
increment();
toolResult = "Counter incremented. New value: $_counter";
break;
case 'decrement':
decrement();
toolResult = "Counter decremented. New value: $_counter";
break;
case 'reset':
reset();
toolResult = "Counter reset to 0";
break;
case 'set_value':
if (args['value'] is int) {
set(args['value']);
toolResult = "Counter set to $_counter";
} else {
throw "Invalid arguments: value must be an integer";
}
break;
default:
throw "Tool not found: $name";
}
return _success(id, {
"content": [
{"type": "text", "text": toolResult.toString()},
],
});
default:
if (id != null) {
return _error(id, -32601, "Method not found");
}
return null;
}
} catch (e) {
if (id != null) {
return _error(id, -32603, "Internal error: $e");
}
return null;
}
}
String _success(dynamic id, dynamic result) {
return jsonEncode({"jsonrpc": "2.0", "id": id, "result": result});
}
String _error(dynamic id, int code, String message) {
return jsonEncode({
"jsonrpc": "2.0",
"id": id,
"error": {"code": code, "message": message},
});
}
}
/// An MCP service implementation that uses HTTP (SSE) for transport.
///
/// It binds to a local port (starting at 8080) and handles POST/OPTIONS/GET requests.
class HttpMcpService extends McpService {
HttpMcpService();
int? _port;
HttpServer? _server;
/// Returns the base URI of the running server.
/// Throws [StateError] if server is not started.
Future<String> get serverUri async {
if (_port == null) throw StateError("Server not started");
return "http://localhost:$_port";
}
@override
Future<Map<String, dynamic>> getConfig() async {
if (_port == null) {
return {"error": "Server not running"};
}
return {"serverUrl": await serverUri};
}
Future<void> _handleIncomingHttpRequest(HttpRequest request) async {
if (request.method == 'POST') {
try {
final content = await utf8.decoder.bind(request).join();
final response = await processRequest(content);
request.response.headers.contentType = ContentType.json;
// Add CORS headers
request.response.headers.add('Access-Control-Allow-Origin', '*');
request.response.headers.add(
'Access-Control-Allow-Methods',
'POST, OPTIONS',
);
request.response.headers.add(
'Access-Control-Allow-Headers',
'Content-Type',
);
if (response != null) {
request.response.statusCode = HttpStatus.ok;
request.response.write(response);
} else {
request.response.statusCode = HttpStatus.accepted;
}
} catch (e) {
request.response.statusCode = HttpStatus.internalServerError;
request.response.write(jsonEncode({"error": e.toString()}));
}
} else if (request.method == 'OPTIONS') {
// Handle pre-flight CORS requests
request.response.headers.add('Access-Control-Allow-Origin', '*');
request.response.headers.add(
'Access-Control-Allow-Methods',
'POST, OPTIONS',
);
request.response.headers.add(
'Access-Control-Allow-Headers',
'Content-Type',
);
request.response.statusCode = HttpStatus.ok;
} else if (request.method == 'GET' && request.uri.path == '/') {
request.response.headers.contentType = ContentType.text;
request.response.write('MCP Server Running');
} else {
request.response.statusCode = HttpStatus.methodNotAllowed;
}
await request.response.close();
}
@override
Future<void> startServer() async {
int port = 8080;
// Try to find an available port starting from 8080
while (_server == null) {
try {
_server = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
_port = port;
} catch (e) {
if (e is SocketException) {
port++;
if (port > 65535) {
throw Exception('Error: No available ports found.');
}
} else {
rethrow;
}
}
}
print('MCP Server listening on http://localhost:$_port');
_server!.listen(_handleIncomingHttpRequest);
}
@override
Future<void> stopServer() async {
await _server?.close();
_server = null;
}
}
/// An MCP service implementation that uses Stdio over a Unix Domain Socket (UDS).
///
/// It bridges standard input/output behavior for MCP clients by providing a
/// socket that clients can connect to via `nc -U` or native UDS support.
class StdioMcpService extends McpService {
StdioMcpService();
String? _socketPath;
ServerSocket? _server;
@override
Future<Map<String, dynamic>> getConfig() async {
return {
"command": "nc",
"args": ["-U", _socketPath ?? ""],
};
}
@override
Future<void> startServer() async {
try {
final dir = await getApplicationSupportDirectory();
_socketPath = p.join(dir.path, 'mcp.sock');
// Ensure directory exists
await dir.create(recursive: true);
// Clean up socket file using reliable type checking
final type = FileSystemEntity.typeSync(_socketPath!);
if (type != FileSystemEntityType.notFound) {
try {
File(_socketPath!).deleteSync();
} catch (e) {
print("Warning: Failed to delete existing socket file: $e");
}
}
final address = InternetAddress(
_socketPath!,
type: InternetAddressType.unix,
);
_server = await ServerSocket.bind(address, 0);
// Set permissions to 600 (owner read/write only)
try {
if (!Platform.isWindows) {
await Process.run('chmod', ['600', _socketPath!]);
}
} catch (e) {
print('Error setting socket permissions: $e');
}
print('MCP Server listening on unix socket: $_socketPath');
_server!.listen((Socket client) {
print('Client connected: ${client.remoteAddress}');
client
.cast<List<int>>()
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(
(String line) async {
if (line.trim().isEmpty) return;
try {
final response = await processRequest(line);
if (response != null) {
client.write('$response\n');
}
} catch (e) {
print('Error processing UDS request: $e');
// Optionally send JSON-RPC error back
}
},
onDone: () {
print('Client disconnected');
client.destroy();
},
onError: (e) {
print('Client connection error: $e');
client.destroy();
},
);
});
} catch (e) {
print('Failed to start StdioMcpService: $e');
rethrow;
}
}
@override
Future<void> stopServer() async {
await _server?.close();
_server = null;
if (_socketPath != null) {
final type = FileSystemEntity.typeSync(_socketPath!);
if (type != FileSystemEntityType.notFound) {
try {
File(_socketPath!).deleteSync();
} catch (_) {}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment