The Model Context Protocol (MCP) is the wire format the agent world has converged on for tool servers. Anthropic introduced it; Claude SDK, OpenAI, and most agent frameworks support some flavour. agentlib supports MCP through two transports: HTTP and WebSocket.
What MCP gives you
An MCP server is an external process (or service) that exposes a tool surface to your agent. Tools, resources,
prompts — all defined in a JSON-RPC schema. Connect once, list tools, call them. agentlib treats MCP tools the
same as native tools: they go into the /bin mount, they show up in Sh pipelines, hooks fire on them.
Connecting over HTTP
final mcp = await McpClient.connect(
HttpMcpTransport(Uri.parse('https://weather.mcp.example.com/rpc')),
);
final bin = BinMount()..registerMcp(mcp, prefix: 'weather');
Every tool from the server is now available as /bin/mcp.weather.<tool>. So the model can compose them in Sh:
Sh("weather forecast --city Tokyo | jq -r '.high_celsius' | tee /scratch/high.txt")
HTTP transport is request/response: each tool call is one POST. Latency is the network round trip + the server’s processing time. Fine for most tools.
Connecting over WebSocket
For tools that emit notifications (live subscriptions, long-running streams, watcher events), use WebSocket:
final mcp = await McpClient.connect(
WebSocketMcpTransport(Uri.parse('wss://watcher.mcp.example.com/ws')),
);
// Tools work as before.
final bin = BinMount()..registerMcp(mcp, prefix: 'watcher');
// But you also get a notifications stream.
mcp.notifications.listen((n) {
print('MCP notification: ${n.method} ${n.params}');
});
WebSocket transport keeps the connection open and multiplexes request/response over the same socket. That’s the right shape for:
- File watchers — the server emits an event when something changes.
- Subscriptions — the agent says “tell me when X happens”.
- Long-running tools — periodic progress events, not a single response.
- Real-time data — quotes, sensor feeds.
Listing tools
After connection, mcp.listTools() returns the server’s declared tool surface:
final tools = await mcp.listTools();
for (final t in tools) {
print('${t.name}: ${t.description}');
}
registerMcp calls this for you and translates each MCP tool spec into a native Tool object. The model never sees
the MCP layer; it sees CLIs in /bin.
Authentication
MCP doesn’t dictate an auth scheme. agentlib’s transports accept bearer tokens, custom headers, or query-string params:
final mcp = await McpClient.connect(
HttpMcpTransport(
Uri.parse('https://gh.mcp.example.com/rpc'),
headers: { 'Authorization': 'Bearer ${ghToken}' },
),
);
For tokens that refresh, register a callback on the transport and rotate as needed.
Reconnection
WebSocket transports include exponential-backoff reconnect by default. If the socket drops, the transport reconnects
and replays in-flight requests. You can override with a custom ReconnectPolicy:
final transport = WebSocketMcpTransport(
uri,
reconnectPolicy: ReconnectPolicy.fixed(delay: Duration(seconds: 5), maxAttempts: 10),
);
Patterns
Tool fanout. Connect to 3–4 MCP servers (weather, GitHub, Slack, the user’s email) and register them all under
different prefixes. The model can pipe between them: weather forecast | gh issue create.
Local MCP for OS access. Run a small native helper service that exposes OS-level capabilities (file system, sensors, share sheet) as MCP over a Unix socket. The Flutter app connects via the local transport.
Backend MCP for shared tools. A team-shared MCP server hosts company-specific tools (CRM, internal search, calendar). Every Flutter device connects; the surface is consistent everywhere.
Hooks still fire
Hooks fire on MCP tool calls the same as on native tools. So PreToolUse can deny external sends, audit hooks log
every call, PostToolUse rewrites results — independent of which transport the tool came from.
Read next
- CLI-first agent design — how MCP tools compose with Sh.
- Primitives — the full surface.