Quick Reference

July 31, 2026 ยท View on GitHub

Use this page for common mcp_dart calls. The server guide, client guide, tools guide, and transport guide explain the full APIs and edge cases.

Install and import

dependencies:
  mcp_dart: ^2.4.0
import 'package:mcp_dart/mcp_dart.dart';

The 2.4.0 SDK requires Dart 3.4 or later. The stable 0.2.0 CLI requires Dart 3.12 or later.

Protocol profile

The 2.4.0 SDK defaults to McpProtocol.stable: try MCP 2026-07-28, then fall back to legacy initialization when needed. Body-only discovery probes are bounded to five seconds; HTTP retains its normal request timeout.

const legacyClientOptions = McpClientOptions(protocol: McpProtocol.legacy);
const strictServerOptions = McpServerOptions(
  protocol: McpProtocol.require2026,
);

See the MCP 2026-07-28 transition guide before depending on protocol-specific behavior.

Server

Create and connect

final server = McpServer(
  const Implementation(name: 'example-server', version: '1.0.0'),
  options: const McpServerOptions(
    capabilities: ServerCapabilities(
      tools: ServerCapabilitiesTools(listChanged: true),
      resources: ServerCapabilitiesResources(
        subscribe: true,
        listChanged: true,
      ),
      prompts: ServerCapabilitiesPrompts(listChanged: true),
    ),
  ),
);

await server.connect(StdioServerTransport());

Use StreamableMcpServer for a high-level HTTP server:

final httpServer = StreamableMcpServer(
  serverFactory: (_) => McpServer(
    const Implementation(name: 'remote-server', version: '1.0.0'),
  ),
  host: '127.0.0.1',
  port: 3000,
  path: '/mcp',
  allowedHosts: {'localhost', '127.0.0.1'},
  allowedOrigins: {'http://localhost:5173'},
);

await httpServer.start();

Keep DNS rebinding protection enabled and use exact host/origin allowlists for browser or remote deployments. See Streamable HTTP.

Register a tool

server.registerTool(
  'add',
  description: 'Add two numbers',
  inputSchema: JsonSchema.object(
    properties: {
      'a': JsonSchema.number(),
      'b': JsonSchema.number(),
    },
    required: ['a', 'b'],
  ),
  callback: (arguments, extra) async {
    final a = arguments['a'] as num;
    final b = arguments['b'] as num;
    return CallToolResult(
      content: [TextContent(text: '${a + b}')],
    );
  },
);

Return CallToolResult(isError: true, ...) for an expected tool failure. Throw McpError for a protocol-level failure. See Tools.

Register a resource

server.registerResource(
  'Status',
  'status://current',
  (description: 'Current service status', mimeType: 'application/json'),
  (uri, extra) async => ReadResourceResult(
    contents: [
      TextResourceContents(
        uri: uri.toString(),
        mimeType: 'application/json',
        text: '{"status":"ok"}',
      ),
    ],
  ),
);

Use registerResourceTemplate for parameterized URIs and declare resources.subscribe only when supporting legacy MCP 2025-11-25 resource subscriptions. MCP 2026-07-28 sends resource updates through subscriptions/listen.

Register a prompt

server.registerPrompt(
  'review',
  description: 'Review a code change',
  argsSchema: {
    'diff': PromptArgumentDefinition(
      description: 'Patch to review',
      required: true,
    ),
  },
  callback: (arguments, extra) async {
    final diff = arguments?['diff'] as String? ?? '';
    return GetPromptResult(
      messages: [
        PromptMessage(
          role: PromptMessageRole.user,
          content: TextContent(text: diff),
        ),
      ],
    );
  },
);

For tasks, MCP Apps metadata, completions, and advanced resource templates, see the server guide.

Client

Connect

final client = McpClient(
  const Implementation(name: 'example-client', version: '1.0.0'),
);

await client.connect(
  StdioClientTransport(
    const StdioServerParameters(
      command: 'dart',
      args: ['run', 'bin/server.dart'],
    ),
  ),
);

Remote or browser client:

await client.connect(
  StreamableHttpClientTransport(Uri.parse('https://mcp.example.com/mcp')),
);

Always close the client:

try {
  // Use the client.
} finally {
  await client.close();
}

Discover and use primitives

final tools = await client.listTools();
final toolResult = await client.callTool(
  const CallToolRequest(
    name: 'add',
    arguments: {'a': 2, 'b': 3},
  ),
);

final resources = await client.listResources();
final resource = await client.readResource(
  const ReadResourceRequest(uri: 'status://current'),
);

final prompts = await client.listPrompts();
final prompt = await client.getPrompt(
  const GetPromptRequest(
    name: 'review',
    arguments: {'diff': '...'},
  ),
);

Check advertised capabilities before optional operations:

final updates = client.listenSubscriptions(
  const SubscriptionsListenRequest(
    notifications: SubscriptionFilter(
      resourceSubscriptions: ['status://current'],
    ),
  ),
);
await updates.acknowledged;

For MCP 2025-11-25 stateful peers, check resources.subscribe before using the legacy subscribeResource/unsubscribeResource methods.

See the client guide for progress, sampling, roots, elicitation, completions, tasks, reconnect behavior, and subscriptions.

Content types

TextContent(text: 'hello');

ImageContent(
  data: base64Data,
  mimeType: 'image/png',
);

AudioContent(
  data: base64Audio,
  mimeType: 'audio/wav',
);

ResourceLink(
  uri: 'file:///report.txt',
  name: 'Report',
  mimeType: 'text/plain',
);

EmbeddedResource(
  resource: TextResourceContents(
    uri: 'memo://1',
    text: 'embedded text',
    mimeType: 'text/plain',
  ),
);

Tool results can contain multiple content items. MCP 2026-07-28 also supports the documented structured-content helpers.

Prompts use the same content types. For example, a prompt callback can return an ImageContent in a PromptMessage when the selected prompt needs to supply an image to the model:

return GetPromptResult(
  messages: [
    PromptMessage(
      role: PromptMessageRole.user,
      content: ImageContent(
        data: base64Data,
        mimeType: 'image/png',
      ),
    ),
  ],
);

JSON Schema

final schema = JsonSchema.object(
  properties: {
    'query': JsonSchema.string(description: 'Search text'),
    'limit': JsonSchema.integer(minimum: 1, maximum: 100),
    'tags': JsonSchema.array(items: JsonSchema.string()),
    'mode': JsonSchema.string(enumValues: ['fast', 'thorough']),
  },
  required: ['query'],
);

The SDK validates JSON Schema Draft 2020-12 by default and accepts an explicitly declared Draft 7 schema for MCP 2025-11-25 compatibility. Same-document $ref and $dynamicRef references are supported; unresolved relative and network references and unsupported dialects are rejected. Custom vocabularies are preserved but not interpreted. Validate business rules in the callback as well as describing inputs in the schema.

Errors

// API or business-logic failure
return CallToolResult(
  isError: true,
  content: [TextContent(text: 'The requested record was not found.')],
);

// Input validation failure
return const CallToolResult(
  isError: true,
  content: [TextContent(text: 'Expected a non-empty query.')],
);

Use tool error results for input validation, API, and business-logic failures so the model can correct and retry. JSON-RPC errors are reserved for protocol-level problems such as malformed requests, unknown tools, and server failures. Common JSON-RPC codes are available through ErrorCode, including parseError, invalidRequest, methodNotFound, invalidParams, and internalError.

Notifications and logging

For MCP 2026-07-28, acknowledge the caller's subscriptions/listen filter before sending correlated notifications on that request stream:

server.server.setRequestHandler<JsonRpcSubscriptionsListenRequest>(
  Method.subscriptionsListen,
  (request, extra) async {
    final acknowledged = request.listenParams.notifications.acknowledgedBy(
      server.server.getCapabilities(),
    );
    await extra.sendSubscriptionAcknowledged(acknowledged);

    if (acknowledged.toolsListChanged == true) {
      await extra.sendSubscriptionNotification(
        const JsonRpcToolListChangedNotification(),
      );
    }
    if (acknowledged.promptsListChanged == true) {
      await extra.sendSubscriptionNotification(
        const JsonRpcPromptListChangedNotification(),
      );
    }
    return const EmptyResult();
  },
  (id, params, meta) => JsonRpcSubscriptionsListenRequest(
    id: id,
    listenParams: SubscriptionsListenRequest.fromJson(params!),
    meta: meta,
  ),
);

MCP 2026-07-28 deprecates protocol logging. The compatibility API belongs inside a request handler and emits only messages allowed by that request's log level:

await server.sendStatelessLoggingMessage(
  const LoggingMessageNotification(
    level: LoggingLevel.info,
    data: 'Working on the request',
  ),
  requestMeta: extra.meta,
  requestId: extra.requestId,
);

The request ID is mandatory for routing on Streamable HTTP, and the server must send only notification kinds present in the acknowledged filter. Legacy MCP 2025-11-25 peers instead use global capability-gated methods such as sendToolListChanged, sendPromptListChanged, sendResourceUpdated, and logging/setLevel.

Stdio servers must reserve stdout for MCP frames; send application logs to stderr. Configure internal SDK logs with setMcpLogHandler, silenceMcpLogs, or resetMcpLogHandler.

Basic utilities

Use ping to verify that a connected peer is still responsive. Clients can ping any connected server; server-to-client ping is available only in the legacy stateful profile:

await client.ping();

// MCP 2025-11-25 only:
await server.server.ping();

List operations use opaque cursors. Pass each nextCursor back unchanged and stop when it is absent. Guard against a faulty peer repeating a cursor:

String? cursor;
final seen = <String>{};

do {
  final page = await client.listTools(
    params: cursor == null ? null : ListToolsRequest(cursor: cursor),
  );
  for (final tool in page.tools) {
    print(tool.name);
  }

  cursor = page.nextCursor;
  if (cursor != null && !seen.add(cursor)) {
    throw StateError('tools/list repeated cursor "$cursor"');
  }
} while (cursor != null);

For cancellation, pass a BasicAbortController.signal in RequestOptions and abort it when the caller no longer needs the result. Long-running handlers should observe extra.signal.aborted and stop promptly:

final controller = BasicAbortController();
final pending = client.callTool(
  const CallToolRequest(name: 'long-running'),
  options: RequestOptions(signal: controller.signal),
);

controller.abort('No longer needed');
try {
  await pending;
} on AbortError {
  // Expected: the caller no longer needs this result.
}

See Tools: progress and cancellation for server-side progress and cleanup patterns.

Testing and verification

  • Use IO stream/custom transports for in-process unit tests.
  • Use mcp_dart inspect-server or inspect-client for a live target.
  • Use mcp_dart conformance for this repository's built-in regression cases; it is not a certification tool for arbitrary peers.
  • Run the linked interop fixtures for cross-SDK claims.

Platform reminders

TargetRecommended transport
Dart VM / desktop helperStdio or Streamable HTTP
Browser / Flutter WebStreamable HTTP client
Flutter mobileRemote Streamable HTTP; app-managed helper only for local IPC
Unit tests / in-processIO stream or custom transport

See Flutter recipes for lifecycle and secure-storage guidance.

Next steps