MCP
Connect Model Context Protocol servers and bridge their tools into the loop.
The Model Context Protocol (MCP) is a standard way for servers to expose tools
to a model. MCPClient connects to an MCP server, lists its tools, and turns
each one into an ordinary tool you pass to generateText or streamText. The
bridged tools validate arguments against the server's schemas and run in the
same agent loop as your local tools.
Quick start
Point a client at a transport, connect, and hand tools() to a generation call.
This uses DeepWiki, a public MCP server that answers
questions about GitHub repositories and needs no key, so it runs as-is:
let mcp = MCPClient(transport: MCPHTTPTransport(
url: URL(string: "https://mcp.deepwiki.com/mcp")!
))
try await mcp.connect()
defer { Task { await mcp.close() } }
let result = try await generateText(
model: AnthropicModel("claude-sonnet-5"),
prompt: "Use the DeepWiki tools to explain how the modelcontextprotocol/modelcontextprotocol repository is organized.",
tools: try await mcp.tools(),
stopWhen: [stepCountIs(5)]
)tools() follows nextCursor pagination, so servers that page their tool list
are fully enumerated in one call. connect() runs the initialize handshake once
and is safe to call again; close() releases the transport (and, for stdio,
terminates the subprocess).
Transports
The client is transport-agnostic. Any MCPTransport works; the SDK ships three.
Streamable HTTP
MCPHTTPTransport is the default. It speaks JSON-RPC over POST with SSE or JSON
responses, carries mcp-session-id sessions, and takes custom headers for auth:
MCPHTTPTransport(
url: URL(string: "https://mcp.example.com/mcp")!,
headers: ["Authorization": "Bearer \(token)"]
)Stdio
MCPStdioTransport (macOS and Linux) launches a local server as a subprocess
and speaks newline-delimited JSON-RPC over its stdin and stdout. A
requestTimeout (default 60 seconds) bounds each call, so a hung or silent
server fails instead of blocking forever:
let mcp = MCPClient(transport: MCPStdioTransport(
command: "npx",
arguments: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
requestTimeout: 30
))
try await mcp.connect()
defer { Task { await mcp.close() } }Pass environment: to override variables for the child process and
workingDirectory: to set its launch directory.
Legacy HTTP+SSE
MCPSSETransport speaks the older HTTP+SSE protocol: a long-lived GET event
stream for server messages plus a POST endpoint the server advertises for
requests. Reach for it only with servers that predate Streamable HTTP. It takes
the same headers and requestTimeout options as the HTTP transport:
MCPSSETransport(url: URL(string: "https://legacy.example.com/sse")!)OAuth
Hosted MCP servers usually sit behind OAuth rather than a static bearer token.
Hand a transport an MCPOAuthSession and it attaches the access token, refreshes
it when the server answers 401, and tells you when a browser round-trip is
needed. Both MCPHTTPTransport and MCPSSETransport take an auth: argument:
let serverURL = URL(string: "https://mcp.notion.com/mcp")!
let auth = MCPOAuthSession(serverURL: serverURL, provider: myProvider)
let mcp = MCPClient(transport: MCPHTTPTransport(url: serverURL, auth: auth))
do {
let tools = try await mcp.tools()
} catch AIError.authorizationRequired(let url) {
// open `url`, then hand back the URL the browser was redirected to:
try await auth.complete(callbackURL: redirected)
}myProvider is your MCPOAuthClientProvider: it owns the redirect URI, the
client metadata used for dynamic client registration, and storage for tokens,
the registered client, the PKCE verifier, and the CSRF state. Persist tokens
and client information if you want sign-in to survive a relaunch.
The session performs the full MCP discovery chain on demand: the 401's
WWW-Authenticate header names the protected-resource metadata document, which
names the authorization server — commonly a different host than the MCP server
itself. Authorization uses PKCE (S256), sends the RFC 8707 resource
indicator on the authorization, token, and refresh requests, and verifies
state on the callback before exchanging the code. When the authorization
server advertises a registration_endpoint, the client registers itself
dynamically the first time; otherwise seed your provider with credentials you
obtained out of band.
MCPOAuthFlow exposes the same steps individually — discover,
registerClientIfNeeded, startAuthorization, handleCallback, refresh — if
you want to drive the flow yourself.
Detecting tool-definition drift (rug pull)
A compromised or malicious MCP server can change its tool definitions after you approved them, injecting instructions into a description or widening an input schema to capture more data. Fingerprint the tools you approved, then compare on every later fetch:
let baseline = fingerprintTools(try await mcp.tools())
// store baseline, then on a later fetch:
let drift = detectToolDrift(fingerprintTools(try await mcp.tools()), baseline: baseline)
if drift.hasDrift {
// drift.changed / drift.added / drift.removed
// block or force re-approval; do not silently pass mutated tools to the model
}Your app owns baseline storage and decides the response. A fingerprint covers each tool's description and resolved input schema and is independent of JSON key order, so a server reordering schema keys does not read as drift.
Examples
See 27-MCPTools.swift for runnable HTTP, stdio, legacy SSE, OAuth, and rug-pull examples.