MCPInstall
mcp_client logo

mcp_client

by app-appplayer 17 stars app-appplayer/mcp_client

A Dart plugin for creating MCP clients to connect Flutter apps with LLM data and functionality.

READMEmain

MCP Client

A Dart plugin for implementing Model Context Protocol (MCP) clients. This plugin allows Flutter applications to connect with MCP servers and access data, functionality, and interaction patterns from Large Language Model (LLM) applications in a standardized way.

Features

  • Every MCP revision, 2024-11-05 through 2026-07-28, plus the 2026-07-28 tasks extension — see Protocol Versions below. connect picks the revision with the server (ProtocolEra.auto)
  • Unified transport configuration — sealed TransportConfig for stdio / SSE / Streamable HTTP
  • OAuth 2.1 — the specification's authorization flow (McpOAuth): discovery, registration, PKCE, resource, iss checks, step-up
  • Result-typed error handling — Result<Client, Error> from createAndConnect, plus McpError for spec error codes
  • Core MCP primitives:
    • Resources — server data, URI templates, subscriptions
    • Tools — server-side functions, progress tracking, structured output (2025-06-18+)
    • Prompts — reusable interaction templates with completion (2025-06-18+)
    • Completion — complete(ref, argument) for completion/complete argument suggestions
    • Roots — filesystem boundary configuration; the server requests them via roots/list
    • Sampling — register a host LLM completion handler so the server can drive sampling/createMessage
    • Elicitation (2025-06-18) — register a user-input handler so the server can collect structured input (form and URL modes)
    • Tasks (2025-11-25) — callToolAsTask, getTask, taskResult, cancelTask
  • Advanced:
    • Deferred Tool Loading — lightweight metadata + on-demand schema fetch (60–80% token reduction)
    • Progress notifications — outbound notifications/progress and inbound listener
    • Cancellation — notifyCancelled(requestId, reason) per spec notification
    • Resource subscriptions — notifications/resources/updated
    • Session management — automatic session validation and reconnection support
    • Ping — ping() probes the peer, and an inbound ping is answered automatically
  • Cross-platform: Android, iOS, web, Linux, Windows, macOS (on the web, Streamable HTTP; stdio and HTTP+SSE need dart:io)

Protocol Versions

Implements every revision of the Model Context Protocol; the one in use is agreed with each server.

VersionNotes
2024-11-05Original; HTTP+SSE transport
2025-03-26Streamable HTTP, authorization, JSON-RPC batches
2025-06-18Elicitation, structured tool output, resource links, MCP-Protocol-Version header; batches removed
2025-11-25Icons, sampling with tools, URL elicitation, tasks, OIDC discovery, Client ID Metadata Documents
2026-07-28Stateless: no handshake, server/discover, per-request _meta, subscriptions/listen, multi round-trip requests, caching hints; tasks extension

connect(transport) (ProtocolEra.auto) asks the server with server/discover and speaks 2026-07-28 when it answers, otherwise the initialize handshake negotiates the newest revision both support. ProtocolEra.modern / .legacy force one side; negotiatedProtocolVersion reports the result.

Runtime gates: McpProtocol.isStateless(v) / supportsBatching(v) / supportsElicitation(v) / supportsUrlElicitation(v) / supportsSamplingTools(v) / requiresProtocolHeader(v).

Coming from 2.x: MIGRATION-3.0.md.

JSON Schema

JsonSchemaValidator (exported by this package) evaluates the schemas MCP carries — tool inputSchema / outputSchema, elicitation requestedSchema.

  • Dialects: 2020-12 (used when a schema has no $schema), 2019-09, draft-07, draft-06, draft-04. A schema naming another dialect is refused with JsonSchemaException.
  • References resolve inside the schema, against documents passed in registry, and against the bundled meta-schemas. Nothing is fetched over the network; a reference that does not resolve rejects the schema.
  • Evaluation is bounded (JsonSchemaLimits: depth, subschema count, evaluations, time).
  • format is an annotation unless assertFormat: true, which checks email, uri, date and date-time.
  • Conformance: the official JSON-Schema-Test-Suite (required tests of all five dialects, plus the optional tests for the features above).

Getting Started

Basic Usage

import 'package:mcp_client/mcp_client.dart';

void main() async {
  // Create client configuration
  final config = McpClient.simpleConfig(
    name: 'Example Client',
    version: '1.0.0',
    enableDebugLogging: true,
  );

  // Create transport configuration
  final transportConfig = TransportConfig.stdio(
    command: 'npx',
    arguments: ['-y', '@modelcontextprotocol/server-filesystem', '/path/to/allowed/directory'],
  );
  
  // Create and connect client
  final clientResult = await McpClient.createAndConnect(
    config: config,
    transportConfig: transportConfig,
  );
  
  final client = clientResult.fold(
    (c) => c,
    (error) => throw Exception('Failed to connect: $error'),
  );
  
  // List available tools on the server
  final tools = await client.listTools();
  print('Available tools: ${tools.map((t) => t.name).join(', ')}');
  
  // Call a tool
  final result = await client.callTool('calculator', {
    'operation': 'add',
    'a': 5,
    'b': 3,
  });
  print('Result: ${(result.content.first as TextContent).text}');
  
  // Disconnect when done
  client.disconnect();
}

Core Concepts

Client

The Client is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:

// Method 1: Using unified configuration
final config = McpClient.productionConfig(
  name: 'My App',
  version: '1.0.0',
  capabilities: ClientCapabilities(
    roots: true,
    rootsListChanged: true,
    sampling: true,
  ),
);

final clientResult = await McpClient.createAndConnect(
  config: config,
  transportConfig: transportConfig,
);

// Method 2: Manual client creation
final client = McpClient.createClient(config);

Connection State Monitoring

Monitor the connection state with event streams:

// Listen for connection events
client.onConnect.listen((serverInfo) {
  _logger.info('Connected to ${serverInfo.name} v${serverInfo.version}');
  _logger.info('Protocol version: ${client.negotiatedProtocolVersion}');
  // Initialize your application after connection
});

// Listen for disconnection events
client.onDisconnect.listen((reason) {
  _logger.info('Disconnected: $reason');
  
  // Handle different disconnect reasons
  switch (reason) {
    case DisconnectReason.transportError:
      // Attempt reconnection
      break;
    case DisconnectReason.serverDisconnected:
      // Show notification to user
      break;
    case DisconnectReason.clientDisconnected:
      // Normal shutdown
      break;
    case DisconnectReason.transportClosed:
    case DisconnectReason.pingTimeout:
    case DisconnectReason.sessionExpired:
    case DisconnectReason.unknown:
      // Connection ended; reconnect if the application needs it
      break;
  }
});

// Listen for error events
client.onError.listen((error) {
  _logger.error('Error: ${error.message}');
  // Log errors or show to user
});

// Clean up resources when done
client.dispose();

Resources

Resources provide access to data from MCP servers. They're similar to GET endpoints in a REST API:

// List available resources
final resources = await client.listResources();
_logger.debug('Available resources: ${resources.map((r) => r.name).join(', ')}');

// Read a resource
final resourceResult = await client.readResource('file:///path/to/file.txt');
final content = resourceResult.contents.first;
_logger.debug('Resource content: ${content.text}');

// Get a resource using a template
final templateResult = await client.getResourceWithTemplate('file:///{path}', {
  'path': 'example.txt'
});
_logger.debug('Template result: ${templateResult.contents.first.text}');

// Subscribe to resource updates
await client.subscribeResource('file:///path/to/file.txt');
client.onResourceContentUpdated((uri, content) {
  _logger.debug('Resource updated: $uri');
  _logger.debug('New content: ${content.text}');
});

// Unsubscribe when no longer needed
await client.unsubscribeResource('file:///path/to/file.txt');

Tools

Tools allow you to execute functionality exposed by MCP servers:

// List available tools
final tools = await client.listTools();
_logger.debug('Available tools: ${tools.map((t) => t.name).join(', ')}');

// Call a tool
final result = await client.callTool('search-web', {
  'query': 'Model Context Protocol',
  'maxResults': 5,
});

// Per-call progress, timeout and cancellation
final cancel = Completer<Object?>();
final longResult = client.callTool('long-running-operation', {'parameter': 'value'},
    options: RequestOptions(
      timeout: const Duration(minutes: 5),
      onProgress: (p) => _logger.debug('progress ${p.progress}/${p.total ?? '?'} ${p.message ?? ''}'),
      cancel: cancel.future, // completing it sends notifications/cancelled
    ));
// cancel.complete('user requested');

// Or one handler for every progress notification
client.onProgress((progressToken, progress, message) {
  _logger.debug('Operation $progressToken: $progress - $message');
});

// Process the result
final content = result.content.first;
if (content is TextContent) {
  _logger.debug('Search results: ${content.text}');
}

Prompts

Prompts are reusable templates provided by servers that help with common interactions:

// List available prompts
final prompts = await client.listPrompts();
_logger.debug('Available prompts: ${prompts.map((p) => p.name).join(', ')}');

// Get a prompt result
final promptResult = await client.getPrompt('analyze-code', {
  'code': 'function add(a, b) { return a + b; }',
  'language': 'javascript',
});

// Process the prompt messages
for (final message in promptResult.messages) {
  final content = message.content;
  if (content is TextContent) {
    _logger.debug('${message.role}: ${content.text}');
  }
}

Roots

Roots scope the filesystem the server is allowed to operate on. Per spec, the server requests them from the client via roots/list — register them locally:

// Configure local roots — these are what the client returns when the server asks
client.addRoot(Root(
  uri: 'file:///path/to/allowed/directory',
  name: 'Project Files',
));

// Inspect the locally-configured roots
_logger.debug('Configured roots: ${client.roots.map((r) => r.name).join(', ')}');

// Remove a root
client.removeRoot('file:///path/to/allowed/directory');

// Override the default `roots/list` response handler if you need custom behavior
client.onListRoots(() async => client.roots);

Sampling

Sampling is server-initiated per the MCP spec. Declare it (ClientCapabilities(sampling: true), for example through McpClient.productionConfig(capabilities: ...)) and register a handler that fulfils completion requests using your host LLM; without the declaration the client refuses the request:

client.onSamplingRequest((request) async {
  // Forward to your LLM (e.g., via mcp_llm), then return the spec response shape.
  final reply = await myLlm.complete(request.messages);
  return CreateMessageResult(
    model: reply.model,
    role: Role.assistant,
    content: [TextContent(text: reply.text)],
  );
});

Elicitation (2025-06-18)

The server can request structured input from the user via elicitation/create. Declare elicitation (or elicitationForm / elicitationUrl) in ClientCapabilities and register a handler:

client.onElicitationRequestTyped((request) async {
  final form = request as ElicitRequestFormParams; // or ElicitRequestURLParams
  final answers = await ui.promptUser(form.message, schema: form.requestedSchema);
  return ElicitResult(action: ElicitResultAction.accept, content: answers);
});

Transport Layers

MCP Client supports stdio, HTTP+SSE (2024-11-05) and Streamable HTTP with one configuration type. HTTP responses are decompressed by the HTTP client itself; heartbeatInterval pings the server and ends a connection that stops answering.

Standard I/O

For command-line tools and direct integrations:

// Method 1: Using createAndConnect
final config = McpClient.simpleConfig(name: 'STDIO Client', version: '1.0.0');
final transportConfig = TransportConfig.stdio(
  command: 'npx',
  arguments: ['-y', '@modelcontextprotocol/server-filesystem', '/path/to/allowed/directory'],
  workingDirectory: '/path/to/working/directory',
  environment: {'ENV_VAR': 'value'},
);

final clientResult = await McpClient.createAndConnect(
  config: config,
  transportConfig: transportConfig,
);

// Method 2: Manual transport creation
final transportResult = await McpClient.createStdioTransport(
  command: 'npx',
  arguments: ['-y', '@modelcontextprotocol/server-filesystem', '/path/to/allowed/directory'],
);
final transport = transportResult.fold((t) => t, (error) => throw error);
await client.connect(transport);

Server-Sent Events (SSE)

For HTTP-based communication with enhanced features:

// Basic SSE transport
final transportConfig = TransportConfig.sse(
  serverUrl: 'http://localhost:8080/sse',
  headers: {'User-Agent': 'MCP-Client/1.0'},
);

// SSE with Bearer token authentication
final transportConfig = TransportConfig.sse(
  serverUrl: 'https://secure-api.example.com/sse',
  bearerToken: 'your-bearer-token',
  headers: {'User-Agent': 'MCP-Client/1.0'},
);

// SSE with an OAuth access token obtained elsewhere (HTTP+SSE, 2024-11-05,
// has no authorization flow of its own)
final transportConfig = TransportConfig.sse(
  serverUrl: 'https://api.example.com/sse',
  oauthToken: token,
);

// SSE with a heartbeat: a ping every 30 s; three unanswered in a row end
// the connection
final transportConfig = TransportConfig.sse(
  serverUrl: 'http://localhost:8080/sse',
  heartbeatInterval: const Duration(seconds: 30),
  maxMissedHeartbeats: 3,
);

final clientResult = await McpClient.createAndConnect(
  config: config,
  transportConfig: transportConfig,
);

Streamable HTTP Transport

The current HTTP transport (2025-03-26 and later, required for 2026-07-28):

// Basic HTTP transport
final transportConfig = TransportConfig.streamableHttp(
  baseUrl: 'https://api.example.com',
  headers: {'User-Agent': 'MCP-Client/1.0'},
);

// HTTP with all features
final transportConfig = TransportConfig.streamableHttp(
  baseUrl: 'https://api.example.com',
  headers: {'User-Agent': 'MCP-Client/1.0'},
  timeout: const Duration(seconds: 60),
  maxConcurrentRequests: 20,
  // MCP authorization: a 401 starts discovery, registration and the
  // authorization code flow; see MIGRATION-3.0.md.
  oauth: McpOAuth(McpOAuthOptions(
    redirectUri: 'http://localhost:8765/callback',
    authorize: (url) => openBrowserAndWaitForRedirect(url),
  )),
  heartbeatInterval: const Duration(seconds: 60),
);

final clientResult = await McpClient.createAndConnect(
  config: config,
  transportConfig: transportConfig,
);

Logging

The package uses the standard Dart logging package:

import 'package:logging/logging.dart';

// Set up logging
Logger.root.level = Level.INFO;
Logger.root.onRecord.listen((record) {
  print('${record.level.name}: ${record.time}: ${record.message}');
});

// Create logger for your component
final Logger _logger = Logger('mcp_client.example');

// Log messages at different levels
_logger.fine('Debugging information');
_logger.info('Important information');
_logger.warning('Warning message');
_logger.severe('Error message');

// Enable debug logging in client config
final config = McpClient.simpleConfig(
  name: 'My Client',
  version: '1.0.0',
  enableDebugLogging: true, // This enables detailed transport logging
);

MCP Primitives

The MCP protocol defines three core primitives that clients can interact with:

PrimitiveControlDescriptionExample Use
PromptsUser-controlledInteractive templates invoked by user choiceSlash commands, menu options
ResourcesApplication-controlledContextual data managed by the client applicationFile contents, API responses
ToolsModel-controlledFunctions exposed to the LLM to take actionsAPI calls, data updates

Advanced Usage

Deferred Tool Loading

Reduce token usage by 60-80% when sending tool definitions to LLMs:

import 'package:mcp_client/mcp_client.dart';

// Create a tool registry for caching
final registry = ToolRegistry();

// Fetch lightweight metadata instead of full schemas
final metadata = await client.listToolsMetadata(registry);

// Send only name + description to LLM (token-efficient)
for (final tool in metadata) {
  print('${tool.name}: ${tool.description}');
}

// Later, get full schema when needed for validation/execution
final fullSchema = registry.getSchema('calculator');
print('Full schema: $fullSchema');

// Invalidate cache when tools change
client.onToolsListChanged(() {
  registry.invalidateAll();
});

Event Handling

Register for server-side notifications:

// Handle tools list changes
client.onToolsListChanged(() {
  _logger.debug('Tools list has changed');
  client.listTools().then((tools) {
    _logger.debug('New tools: ${tools.map((t) => t.name).join(', ')}');
  });
});

// Handle resources list changes
client.onResourcesListChanged(() {
  _logger.debug('Resources list has changed');
  client.listResources().then((resources) {
    _logger.debug('New resources: ${resources.map((r) => r.name).join(', ')}');
  });
});

// Handle prompts list changes
client.onPromptsListChanged(() {
  _logger.debug('Prompts list has changed');
  client.listPrompts().then((prompts) {
    _logger.debug('New prompts: ${prompts.map((p) => p.name).join(', ')}');
  });
});

// Handle server logging
client.onLogging((level, message, logger, data) {
  _logger.debug('Server log [$level]${logger != null ? " [$logger]" : ""}: $message');
  if (data != null) {
    _logger.debug('Additional data: $data');
  }
});

Error Handling

The library uses Result types for robust error handling:

// Using createAndConnect with Result handling
final clientResult = await McpClient.createAndConnect(
  config: config,
  transportConfig: transportConfig,
);

final client = clientResult.fold(
  (client) {
    print('Successfully connected');
    return client;
  },
  (error) {
    print('Connection failed: $error');
    throw error;
  },
);

// Transport creation with Result handling
final transportResult = await McpClient.createStdioTransport(
  command: 'npx',
  arguments: ['-y', '@modelcontextprotocol/server-filesystem'],
);

await transportResult.fold(
  (transport) async {
    await client.connect(transport);
    print('Connected successfully');
  },
  (error) {
    print('Transport creation failed: $error');
  },
);

// Traditional try-catch for MCP protocol errors
try {
  await client.callTool('unknown-tool', {});
} on McpError catch (e) {
  print('MCP error (${e.code}): ${e.message}');
} catch (e) {
  print('Unexpected error: $e');
}

Wrapping a Client

DelegatingClient forwards every member to the client it wraps; extend it and override what you change:

class CountingClient extends DelegatingClient {
  CountingClient(super.inner);
  int calls = 0;

  @override
  Future<CallToolResult> callTool(String name, Map<String, dynamic> toolArguments,
      {Map<String, dynamic>? meta, RequestOptions? options}) {
    calls++;
    return super.callTool(name, toolArguments, meta: meta, options: options);
  }
}

Additional Examples

Check out the example directory for a complete sample application.

Resources

Issues and Feedback

Please file any issues, bugs, or feature requests in our issue tracker.

License

This project is licensed under the MIT License - see the LICENSE file for details.

More Developer Tools MCP servers

Similar servers from the same category.

Sourcebot

sourcebot-dev

Enables code search across multiple repository hosts including GitHub, GitLab, Gitea, Gerrit, and Bitbucket with advanced filtering options for exploring large codebases through natural language queries.

searchgit

CodeAlive

codealive-ai

Provides a bridge to CodeAlive's platform for semantic code search, repository exploration, and context-aware chat completions that leverage deep understanding of entire codebases including documentation and dependencies.

search

Provides a secure, sandboxed environment for executing Python, Node.js, and shell commands in ephemeral containers with file management capabilities for code testing, visualization, and data analysis.

filesystemdatabase

NovaCV

lightbaby

Integrates with NovaCV API to generate, analyze, and convert resumes in various formats without dealing with complex document formatting

PDFSupport+5