ai_sdk_mcp 3.0.0
ai_sdk_mcp: ^3.0.0 copied to clipboard
MCP (Model Context Protocol) client for AI SDK Dart. Connect to MCP servers via Streamable HTTP or stdio, discover tools, and use them with any LLM provider.
ai_sdk_mcp #
Model Context Protocol (MCP) client for AI SDK Dart. Connects to MCP servers over Streamable HTTP or stdio and exposes their tools as a typed ToolSet. The client has explicit legacy (2025-06-18) and modern (2026-07-28) protocol strategies.
Installation #
dependencies:
ai_sdk_dart: ^3.0.0
ai_sdk_mcp: ^3.0.0
Usage #
Streamable HTTP transport (remote server) #
StreamableHttpClientTransport speaks the MCP Streamable HTTP transport
(protocol 2025-06-18) against a single MCP endpoint such as
https://example.com/mcp. It:
- sends every JSON-RPC request as an HTTP
POST - accepts either
application/jsonortext/event-streamresponses - sends
notifications/initializedafter protocol negotiation succeeds - starts the optional
GETSSE listener after initialization to surface server-pushed notifications - reuses
Mcp-Session-IdandMCP-Protocol-Versionon laterPOST/GET/DELETErequests when the server negotiates a session - reconnects the optional
GETlistener withLast-Event-IDafter an unexpected disconnect, using exponential delays fromlistenerReconnectDelayup to 30 seconds while the listener is failing (a configured initial delay above 30 seconds is preserved) - sends a best-effort
notifications/cancellednotification when a request times out
import 'package:ai_sdk_dart/ai_sdk_dart.dart';
import 'package:ai_sdk_mcp/ai_sdk_mcp.dart';
import 'package:ai_sdk_openai/ai_sdk_openai.dart';
final transport = StreamableHttpClientTransport(
url: Uri.parse('https://mcp.example.com/mcp'),
headers: {
'Authorization': 'Bearer <short-lived-token>',
'X-Tenant-Id': 'acme',
},
requestTimeout: const Duration(seconds: 20),
);
final client = MCPClient(transport: transport);
await client.initialize();
// Discover tools and use them in a generateText call
final tools = await client.tools();
final result = await generateText(
model: openai('gpt-4.1-mini'),
prompt: 'Search for "Dart programming"',
tools: tools,
maxSteps: 3,
);
print(result.text);
await client.close();
For a modern stateless server, select the modern strategy. It probes
server/discover, adds per-request protocol metadata, sends the required
Streamable HTTP routing headers, and does not use MCP sessions or GET/DELETE
lifecycle requests:
final client = MCPClient(
transport: transport,
protocolMode: MCPProtocolMode.modern,
);
await client.initialize();
Progress is opt-in per request. The host supplies a unique string or integer token and listens to validated monotonic updates:
final updates = client.progress.listen((update) {
print('${update.progress}/${update.total ?? '?'} ${update.message ?? ''}');
});
await client.callTool('long_task', {}, progressToken: 'task-1');
await updates.cancel();
HTTP authorization is host-owned. Pass MCPAuthConfiguration with token and
refresh callbacks; the transport sends bearer credentials on every request
and retries a rejected 401 once after a refresh only when
retryAfterUnauthorized: true is set. Leave it false for calls that are not
safe to replay. It does not store tokens or open browser login flows. Use
validateAuthorizationIssuer for RFC 9207
issuer binding and validateRedirectUri before handing a callback URI to a
host browser integration.
headers are copied onto the transport's POST, optional GET, and DELETE
requests. This is the place to put required credential or routing headers.
Do not ship long-lived secrets inside browser or mobile client builds. Prefer
short-lived tokens minted by your backend or a trusted proxy.
Transport behavior notes #
initialize()negotiates MCP protocol version2025-06-18. Legacy HTTP+SSE servers that only speak older protocol versions are not supported by this transport.protocolMode: MCPProtocolMode.modernselects MCP2026-07-28; modern calls carry_metaprotocol metadata andtools/callcan return a typedMCPInputRequiredResultfor a multi-round-trip request. ItsrequestStateis an opaque server string: pass it back unchanged with the matchinginputResponsesin a subsequentcallToolinvocation. Modern result envelopes are validated before they reach the caller.- Modern progress notifications are exposed through
client.progress. They report progress only; MCP does not define arbitrary partial tool-output chunks, so this package does not synthesize such a stream. - The official
io.modelcontextprotocol/tasksextension is not implemented. Long-running work must use the host's own task/polling adapter until a versioned Tasks API is added.ttlMs/cacheScopecache hints are also not interpreted or used for client-side caching. - If the server returns
Mcp-Session-Idduring initialize, the transport includes it on later requests and sendsDELETEonclose()to end the session. A server may rejectDELETEwith405 Method Not Allowed; that is treated as an allowed shutdown path. - Server-pushed notifications are available on
transport.notifications, andnotifications/resources/updatedare wired intoMCPClientresource-subscription listeners automatically.
Stdio transport (local process) #
Desktop/CLI only.
StdioMCPTransportspawns an OS process and is not available on Flutter web — referencing it still compiles for web (via a stub), but constructing and using it on web throwsUnsupportedError.
final transport = StdioMCPTransport(
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '/tmp'],
);
final client = MCPClient(transport: transport);
await client.initialize();
final tools = await client.tools();
// Use tools with any generateText / streamText call...
await client.close();
Injecting your own http.Client #
import 'package:http/http.dart' as http;
final sharedClient = http.Client();
final transport = StreamableHttpClientTransport(
url: Uri.parse('https://mcp.example.com/mcp'),
client: sharedClient,
);
final client = MCPClient(transport: transport);
await client.initialize();
await client.close();
// The injected client is still yours to manage.
sharedClient.close();
Direct tool invocation #
final result = await client.callTool('readFile', {'path': '/tmp/hello.txt'});
print(result); // file contents as string
Error handling #
try {
await client.callTool('dangerousOp', {});
} on MCPException catch (e) {
print('MCP server error: ${e.message}');
}
License #
MIT