MCP Server Guide
This guide explains how to expose a Model Context Protocol (MCP) endpoint from your Finch application. MCP is a JSON-RPC–based protocol (version 2025-11-25) designed for AI agents and language models to discover and invoke server-side capabilities — tools, resources, and prompts — in a standardised way.
Finch integrates with the mcp_models package to provide:
McpServerController— an abstract Finch controller that handles all MCP JSON-RPC routing automatically.McpBuilder— a declarative builder for registering tools, resources, prompts, and custom method handlers.mcp_models— a zero-dependency Dart library with full coverage of the MCP2025-11-25schema.
How It Works
When a client (an AI agent, IDE plugin, etc.) sends an MCP request, it sends a standard JSON-RPC payload via HTTP to your Finch endpoint. McpServerController decodes the payload, routes it to the correct built-in handler or to a handler you registered with McpBuilder, and streams the response back as Server-Sent Events (SSE).
Client ──POST /mcp/books──► McpServerController.index()
│
▼
_dispatch(method, id, payload)
│
┌──────────────┼──────────────────┐
▼ ▼ ▼
tools/call resources/read prompts/get
│ │ │
▼ ▼ ▼
McpBuilder McpBuilder McpBuilder
.toolHandler() .resourceHandler() .promptHandler()
Registering Capabilities with McpBuilder
Tools
A tool is a callable function that an AI agent can invoke. Register one with mcp.tool():
mcp.tool(
name: 'add',
description: 'Adds two integers and returns the sum.',
inputSchema: ToolSchema(
type: 'object',
properties: {
'a': Schema(type: 'integer', description: 'First operand', title: 'A'),
'b': Schema(type: 'integer', description: 'Second operand', title: 'B'),
},
required: ['a', 'b'],
),
outputSchema: ToolSchema(
type: 'object',
properties: {
'result': Schema(type: 'integer', description: 'The sum', title: 'Result'),
},
required: ['result'],
),
handler: (CallToolRequest req) async {
final args = req.params.arguments ?? {};
final sum = (args['a'] as int) + (args['b'] as int);
return CallToolResult(
content: [TextContent(text: '$sum', mimeType: 'text/plain')],
structuredContent: {'result': sum},
);
},
);
| Parameter | Type | Description |
|---|---|---|
name |
String |
Unique tool identifier |
description |
String |
Human-readable description shown to AI agents |
inputSchema |
ToolSchema |
JSON Schema describing the expected arguments |
outputSchema |
ToolSchema? |
JSON Schema describing the structured output (optional) |
handler |
Future<CallToolResult> Function(CallToolRequest) |
Async handler invoked on tools/call |
Resources
A resource exposes a URI-addressable data source (a file, a database record, an API response, etc.):
mcp.resource(
name: 'config',
uri: 'file:///config.json',
description: 'Application configuration file.',
handler: (ReadResourceRequest req) async {
final content = await File('config.json').readAsString();
return ReadResourceResult(
contents: [
TextResourceContents(
uri: req.params.uri,
text: content,
mimeType: 'application/json',
),
],
);
},
);
| Parameter | Type | Description |
|---|---|---|
name |
String |
Unique resource name |
uri |
String |
The URI that identifies this resource |
description |
String? |
Optional description |
handler |
Future<ReadResourceResult> Function(ReadResourceRequest) |
Handler invoked on resources/read |
You can also use
rq.url('path')insideconfigure()to build absolute URIs dynamically, sinceconfigureis called with the live request context.
Resource Templates
For parameterised URIs (e.g. file:///books/{id}), use mcp.resourceTemplate():
mcp.resourceTemplate(
name: 'book',
uriTemplate: 'db:///books/{id}',
description: 'Fetches a single book by ID.',
);
Prompts
A prompt is a reusable message template that an AI agent can retrieve:
mcp.prompt(
name: 'greet',
description: 'Returns a greeting message.',
handler: (GetPromptRequest req) async {
final name = req.params.arguments?['name'] ?? 'World';
return GetPromptResult(
messages: [
PromptMessage(
role: Role.assistant,
content: TextContent(text: 'Hello, $name!'),
),
],
);
},
);
| Parameter | Type | Description |
|---|---|---|
name |
String |
Unique prompt identifier |
description |
String? |
Optional description |
handler |
Future<GetPromptResult> Function(GetPromptRequest) |
Handler invoked on prompts/get |
Custom Method Handlers
Override or extend any JSON-RPC method — including built-in ones — with mcp.method():
mcp.method(
'notifications/initialized',
(Map<String, Object?> payload) async {
// Custom logic on client initialisation notification.
return JSONRPCNotification(method: 'notifications/initialized');
},
);
Custom handlers registered via mcp.method() take priority over the built-in dispatcher.
Registering the Route
Add the MCP controller to your Finch route tree with Methods.ALL so both GET and POST requests are accepted:
import 'package:finch/finch_route.dart';
import 'controllers/my_mcp_controller.dart';
List<FinchRoute> getRoutes() {
return [
FinchRoute(
key: 'mcp.my_server',
path: 'mcp/my-server',
methods: Methods.ALL,
index: MyMcpController().index,
),
];
}
With Authentication
Protect your MCP endpoint by providing an AuthController:
import 'controllers/mcp_auth_controller.dart';
FinchRoute(
key: 'mcp.my_server',
path: 'mcp/my-server',
methods: Methods.ALL,
index: MyMcpController().index,
auth: McpAuthController(),
),
A typical AuthController for MCP checks a Bearer API key:
import 'package:finch/finch_route.dart';
class McpAuthController extends AuthController<String> {
final List<String> _allowedKeys = ['your-secret-api-key'];
@override
Future<bool> auth() async => (await checkLogin()).success;
@override
Future<bool> authApi() async {
final auth = rq.authorization;
if (auth.type == AuthType.bearer) {
return _allowedKeys.contains(auth.token);
}
return false;
}
@override
Future<String> loginForm() async => rq.renderJson(
data: {'error': 'Unauthorized'},
status: 401,
);
}
mcp_models Package
mcp_models provides plain Dart classes for every type in the MCP 2025-11-25 schema. There is no code generation — every class ships with:
- A
toMap()method for serialisation. - A named factory
TypeName.toMCP(Map)for deserialisation.
Key Types
| Category | Types |
|---|---|
| JSON-RPC | JSONRPCRequest, JSONRPCResultResponse, JSONRPCErrorResponse, JSONRPCNotification |
| Lifecycle | InitializeRequest, InitializeResult, InitializeResultResponse |
| Tools | Tool, ToolSchema, Schema, CallToolRequest, CallToolResult, CallToolResultResponse, ListToolsResult, ListToolsResultResponse |
| Resources | Resource, ResourceTemplate, ReadResourceRequest, ReadResourceResult, TextResourceContents, BlobResourceContents, ListResourcesResult, ListResourceTemplatesResult |
| Prompts | Prompt, PromptMessage, GetPromptRequest, GetPromptResult, ListPromptsResult |
| Content | TextContent, ImageContent, AudioContent, EmbeddedResource |
| Capabilities | ServerCapabilities, ClientCapabilities, Implementation |
| Errors | Error (JSON-RPC error object) |
| Misc | Result, EmptyResult, Role, LoggingLevel |
Serialisation Example
import 'package:mcp_models/mcp_models.dart';
// Build an initialize request.
final request = InitializeRequest(
id: '1',
params: InitializeRequestParams(
protocolVersion: '2025-11-25',
capabilities: ClientCapabilities({}),
clientInfo: Implementation(name: 'my_client', version: '1.0.0'),
),
);
// Serialise to a Map (ready for JSON encoding).
final json = request.toMap();
// Deserialise back.
final restored = InitializeRequest.toMCP(json);
ToolSchema and Schema
ToolSchema and Schema both describe JSON Schema objects used for tool input/output validation:
ToolSchema(
type: 'object',
properties: {
'name': Schema(
type: 'string',
description: 'The name of the item.',
defaultValue: '',
title: 'Name',
),
'count': Schema(
type: 'integer',
description: 'How many items.',
defaultValue: 1,
title: 'Count',
),
},
required: ['name'],
)
MapMC and MapModel Base Classes
For types whose serialised form is the underlying map itself (rather than a wrapper object), mcp_models provides two base classes:
MapMC<K, V>— extendsMCP, serialises directly as its map.MapModel<K, V>— a plain map-like model without theMCPbase.
ServerCapabilities and ClientCapabilities are built on MapMC.
Error Handling
Throw any Exception inside a tool handler — McpServerController wraps unhandled exceptions in a JSONRPCErrorResponse with code -32600 and streams a 400 SSE response to the client. For structured error propagation, return a CallToolResult with isError: true:
handler: (req) async {
final id = req.params.arguments?['id'];
if (id == null) {
return CallToolResult(
isError: true,
content: [TextContent(text: 'Missing required argument: id')],
);
}
// ...
},