What is MCP?
Model Context Protocol (MCP) is an open standard that enables communication between AI models and external tools and services. Thousands of MCP servers exist for filesystems, databases, browsers, SaaS APIs, and more. Timbal agents can connect to any MCP server withMCPServer. The server’s tools are discovered at runtime and exposed to the LLM exactly like native tools — schemas, streaming, tracing, and error handling included.
This page covers consuming MCP servers from your agents. For the reverse direction — connecting your editor to the Timbal platform’s MCP server — see MCP Integration.
Basic Usage
Add anMCPServer to the agent’s tools list. Two transports are supported:
name= set, a tool declared as list_files on the filesystem server appears to the LLM as filesystem__list_files. The bare name is still used on the wire.
The project API
For routes exposed by the TypeScript SDK’stimbalMcp() plugin, use:
/mcp; deployed projects use the project
origin’s /api/mcp. Do not point MCP at the UI origin’s /mcp, capture a caller
bearer during module import, or put a project service secret in source.
Service mode cannot be combined with a manual URL or headers. Discovery and each
tool call open and close their own connection, so concurrent users never share a
connection authenticated as someone else. This adds a handshake per operation;
explicit-URL MCP servers retain their existing persistent connection behavior.
Discovery does not invoke application tools, and tool approvals still apply.
Configuration
Authentication
For servers that require auth, pass headers (http) or environment variables (stdio). Read secrets from the environment instead of hardcoding them:How Results Are Handled
MCP tool results are converted so the LLM always receives something useful:- Text content becomes a plain string (or a list of strings for multiple blocks)
- Images, audio, and binary resources become Timbal
Files, forwarded to the model as file content — vision models can see returned images directly - Structured content is returned as-is when the server sends no text representation
- Errors (
isError) raise inside the tool call, so the model receives a proper error tool result and can self-correct
Approval from Annotations
MCP tools carryToolAnnotations such as readOnlyHint and destructiveHint. approval= turns them into a client-side gate that runs through Timbal’s normal approval flow — a server cannot skip it:
"destructive"gates tools whose annotations mark them destructive (destructiveHintnotfalseandreadOnlyHintnottrue). Tools with no annotations at all are not gated — gating them would mean gating every tool of every server that never annotated. Note the spec default: a tool that sets any annotation withoutdestructiveHint: falsecounts as destructive, so a server that annotatesreadOnlyHint: falseon every writer gates every writer."all"gates every tool.- A one-argument callable receives the server’s
mcp.types.Toolonce, at resolve time, and returnsTrueto gate every call of that tool. - A two-argument callable receives the tool and the call’s arguments and is evaluated per call, so one tool that multiplexes subcommands can gate some and wave others through. It may be async and may return a
bool, a dict, or anApprovalPolicyDecision(the same contract asrequires_approval):
ApprovalEvent with kind="mcp_tool", the server’s input schema as input_schema, and ui={"server", "tool", "title", "annotations"}. Resume exactly like any other approval: resume={approval_id: True}. The gate runs client-side before anything is sent, so it is unaffected by parallel tool calls and survives reconnecting or rebuilding the MCPServer between the pause and the resume — the approval id is derived from the tool path and input, not the session.
Elicitation
Servers can ask the user a question mid tool call (elicitation) — for example, “Swap KB 1069 → 1586 (562 agents → 3)?” before a destructive operation. Timbal advertises the capability by default and bridges each request tosuspend(): the run ends with status input_required and an InteractionEvent of kind="mcp_elicitation".
Because the tool is re-invoked on resume, the server’s tool must be idempotent up to the
elicit() call and its message deterministic for identical input — the suspension id is a hash of the tool path, the server’s message and schema, and the LLM’s tool-call id. Two or more tool calls in flight on the same session cannot be told apart when one of them elicits (the protocol carries no correlation), so the question is answered cancel for it: a call the server then fails gets an error asking the model to call the tool on its own; a call the server answers normally (a server that treats a cancelled confirmation as “not swapped”) succeeds with a note appended to its text result saying the question was auto-cancelled — every call that was in flight carries the note, since which one asked is unknowable. Prefer approval= for confirmations on tools the model is likely to call in parallel. URL-mode elicitations surface payload["url"]; resume with True once the out-of-band flow is complete.
Set elicitation=False to hide the capability. Servers should then fail closed on tools that need confirmation rather than proceeding silently.
Server Notifications
tools/list_changedmarks the cached tool list stale. It is refetched at the next run, never mid-run — swapping tools under the model’s feet would also invalidate the prompt cache.- Log messages (
notifications/message) are forwarded to structlog as"MCP server log"withserver,level,logger, anddata. - Sampling (
sampling/createMessage) is served withsampling_model=when set. The call runs as anmcp_samplingLLM span under the in-flight tool call, so its usage shows up in the trace.
Connection Lifecycle
Connections are lazy: nothing is spawned or contacted until the agent first needs the server’s tools. The tool list is fetched once, sorted by name (so the LLM’s tools prefix stays stable for prompt caching), and cached for the life of the connection. Close the connection when you’re done:close() can be called from any task and in any order across servers. Failure handling on top of the SDK:
- Transport dies mid-call (server process exits, HTTP connection refused): the in-flight call fails immediately with
connection lost during tools/call ...instead of hanging, the connection is forgotten, and the next call reconnects. The cached tools stay valid. - Stale session (an HTTP server restarted or expired the session while you were idle, so it 404s your
Mcp-Session-IdwithSession terminated): the session is reset and the request retried once, transparently. Safe for any request — a 404 means the server never saw it. - Server unreachable:
ConnectionError("MCP server 'name' failed to connect: ConnectError: ...")with the real cause, not anyio’sunhandled errors in a TaskGroup. - Server accepts but never answers:
timeout=(default 600s, per tool viatool_timeouts=) turns it into aTimeoutError; the session stays up and the next call works. The handshake is bounded only byconnect_timeout=(defaultNone, but the TCP connect itself is capped at 30s). - Slow HTTP tools: the transport’s read timeout is unbounded, so a
tools/callthat takes minutes completes. (A barehttpx.AsyncClientapplied its 5-second default to the SSE read and the SDK swallowed the resultingReadTimeout, so any HTTP tool slower than 5s hung the call forever.) Connect stays bounded at 30s (orconnect_timeout) even whenconnect_timeout=None.
tools/call that loses its connection mid-flight is never retried automatically — the tool may already have run. tools/list is.
Dynamic Resolution
MCPServer is a ToolSet: tools are resolved at runtime, not import time. This means the available tools always reflect what the server currently offers, and you can mix MCP servers freely with functions, built-in tools, and other agents in the same tools list.
Codegen CLI
The codegen CLI can add MCP servers to an agent’s source file:add-mcp with the same --name replaces the server spec. Remove a server with remove-tool --name <server_name> — the assignment and import are cleaned up automatically.