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 MCP 2025-11-25 schema.

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') inside configure() to build absolute URIs dynamically, since configure is 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> — extends MCP, serialises directly as its map.
  • MapModel<K, V> — a plain map-like model without the MCP base.

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')],
    );
  }
  // ...
},

See Also