
This tutorial is part of Build an MCP Server, within The MCP Guide. Want it built for you? See MCP development.
The Vercel AI SDK talks to MCP servers through createMCPClient from the @ai-sdk/mcp package. You create a client, call tools(), and pass the result to generateText or streamText like any other tool set. The client also reads resources and prompts.
This tutorial connects a local server, runs a multi-step tool call, adds an approval rule based on MCP tool annotations, and covers the closing and transport rules that cause most bugs. Code follows the AI SDK's MCP docs. On 2 November 2026 the client snippets below ran locally with @ai-sdk/mcp 2.0.89, ai 7.0.161 and Node.js 24 against the official @modelcontextprotocol/server-everything 2.1.0 test server. The model call was checked against the docs only.
Key Takeaways
- Install
aiand@ai-sdk/mcp; create a client withcreateMCPClient({ transport })and get AI SDK tools withawait mcpClient.tools(). - Use the HTTP transport in production; the docs say stdio "should only be used for connecting to local servers."
- The client supports both legacy
initializeservers and the stateless2026-07-28protocol, probing withserver/discoverfirst on stdio. - Tool annotations such as
readOnlyHintare exposed on each tool, but the SDK does not turn them into approvals; you write that policy. - Always close the client: in
onEndforstreamText, or infinallyforgenerateText.
Prerequisites
- Node.js (the run above used Node.js 24).
- A model provider. The docs' examples use plain model strings such as
"anthropic/claude-sonnet-5.5"; use whichever model setup your AI SDK project already has. - An MCP server. To follow along with zero setup, install the reference server:
npm install @modelcontextprotocol/server-everything.
Install
npm install ai @ai-sdk/mcp
Both are on npm: ai and @ai-sdk/mcp.
The docs also mention a shortcut for OpenAI's Responses API: the provider's built-in openai.tools.mcp tool lets OpenAI call a server directly. This tutorial uses @ai-sdk/mcp, which works with any model provider.
Minimal working example
This lists every tool on a local stdio server and then closes the client:
import { createMCPClient } from '@ai-sdk/mcp';
import { StdioMCPTransport } from '@ai-sdk/mcp/mcp-stdio';
const mcpClient = await createMCPClient({
transport: new StdioMCPTransport({
command: 'node',
args: ['node_modules/@modelcontextprotocol/server-everything/dist/index.js', 'stdio'],
}),
});
try {
const tools = await mcpClient.tools();
console.log(Object.keys(tools));
} finally {
await mcpClient.close();
}
Run it with node list.mjs. In the local run it printed 22 tool names, starting with echo, get-annotated-message and get-env. You can also call a tool directly, without a model:
const result = await tools['echo'].execute(
{ message: 'hello' },
{ messages: [], toolCallId: 'test-1' },
);
That returned a standard MCP result whose text content was Echo: hello.
Let the model use the tools
Pass the tools to generateText and allow more than one step. Without stopWhen, the tool calling docs say a call defaults to isStepCount(1), so the model can call a tool but never sees the result.
import { generateText, isStepCount } from 'ai';
import { createMCPClient, type MCPClient } from '@ai-sdk/mcp';
let mcpClient: MCPClient | undefined;
try {
mcpClient = await createMCPClient({
transport: { type: 'http', url: 'https://your-server.com/mcp' },
});
const tools = await mcpClient.tools();
const { text } = await generateText({
model: 'anthropic/claude-sonnet-5.5',
tools,
stopWhen: isStepCount(5),
prompt: 'Use the available tools to answer: what does the welcome note say?',
});
console.log(text);
} finally {
await mcpClient?.close();
}
Pick a transport
| Transport | How | When |
|---|---|---|
| HTTP (recommended) | transport: { type: 'http', url, headers } | Production and any remote server |
| SSE | transport: { type: 'sse', url } | Legacy servers only |
| stdio | new StdioMCPTransport({ command, args }) | Local development only |
The docs recommend HTTP "for production deployments" and say stdio "cannot be deployed to production environments." That matters on Vercel and other serverless hosts, where you cannot keep a child process alive between requests. Our SSE vs Streamable HTTP page explains why SSE is legacy.
The HTTP transport takes optional headers, an OAuth authProvider, and a redirect setting. Redirects default to 'error' "to prevent SSRF," so only set 'follow' for servers you trust.
Protocol versions
The docs say the client "supports both legacy initialization-based protocol versions and stateless MCP 2026-07-28." The stdio transport probes with server/discover and falls back to initialize for older servers. If an endpoint needs the legacy handshake, set protocolVersionDiscovery: false on createMCPClient. Our spec version explorer shows what changed between versions.
Add a tool
Tools come from the server. Add one there and the next tools() call returns it, because schema discovery lists everything the server offers.
If you want type safety, define schemas on the client instead:
import { z } from 'zod';
const tools = await mcpClient.tools({
schemas: {
'get-data': {
inputSchema: z.object({
query: z.string().describe('The data query'),
format: z.enum(['json', 'text']).optional(),
}),
},
},
});
The docs note that with schemas, "the client only pulls the explicitly defined tools." That doubles as an allow list. You can also add an outputSchema to validate a tool's structuredContent at runtime.
Approvals from tool annotations
MCP servers can mark tools with hints like readOnlyHint and destructiveHint. In the local run, the echo tool's metadata carried readOnlyHint: true and destructiveHint: false.
The docs are firm: annotations "are untrusted, server-provided hints," and the client "does not turn them into an approval policy automatically." Their conservative example auto-runs only tools marked read-only:
import { streamText } from 'ai';
import { type McpProviderMetadata } from '@ai-sdk/mcp';
const result = streamText({
model: 'anthropic/claude-sonnet-5.5',
tools,
toolApproval: ({ toolCall }) => {
const annotations = (
toolCall.toolMetadata as McpProviderMetadata | undefined
)?.annotations;
return annotations?.readOnlyHint === true
? 'not-applicable'
: {
type: 'user-approval',
reason: 'The MCP server does not mark this tool as read-only.',
};
},
prompt,
onEnd: async () => {
await mcpClient.close();
},
});
Combine this with a short allow list and scoped credentials. Our tool annotations guide explains why a hostile server could mislabel a tool.
Retries
Set maxRetries on createMCPClient to retry tools/call requests on transient HTTP failures. Retries are off by default. The docs warn that retrying "non-idempotent tools, such as tools that send emails or create records, can duplicate side effects," so enable it only where repeats are safe.
Test with the MCP Inspector
Check the server on its own before you debug the app:
npx @modelcontextprotocol/inspector --cli https://your-server.com/mcp --transport http --method tools/list
Our MCP Inspector lesson shows tool calls, exit codes and CI checks.
Deploy notes
- Close clients. For
streamText, close inonEnd. ForgenerateText, usetry/finally. Leaking clients leaks connections. - Short-lived vs long-lived. The docs suggest closing after each response for request handlers, and keeping one client open for the life of a CLI app.
- OAuth on servers you do not control. Implement
validateAuthorizationServerURLon yourauthProviderto allow only authorization servers you trust. - Hosting your own server. If the MCP server also lives on Vercel, see our Vercel deployment guide.
Pitfalls
- No
stopWhen. The model calls a tool and the run ends without an answer. - stdio in production. Works on your laptop, fails on serverless.
- Old
experimental_createMCPClientimports.createMCPClientis the documented name; the experimental alias still exported in 2.0.89, but follow the docs. - Expecting full client features. The docs call this "a lightweight client intended for use in tool conversion" without automatic session persistence, resumable streams or notifications.
Next step
If MCP is new to you, the free MCP course covers the protocol from first principles. If you need an MCP server built for your AI SDK app, with OAuth and safe tool design, see our MCP development service.
More in the MCP Guide
Free tools: server.json Validator · MCP Error Code Lookup
Latest research: State of MCP: October 2026
Frequently asked questions
Is the AI SDK MCP client stable?
Yes. createMCPClient is exported from @ai-sdk/mcp without the experimental_ prefix, and the docs use that name throughout. The package was at version 2.0.89 on 2 November 2026.
Can I use the official MCP TypeScript SDK transport instead?
Yes. The docs show passing StreamableHTTPClientTransport from @modelcontextprotocol/sdk/client/streamableHttp.js to createMCPClient, and StdioClientTransport for stdio.
Does the AI SDK MCP client support resources and prompts?
Yes. The client has listResources(), readResource({ uri }) and listResourceTemplates(). Resources are application-driven, so your code decides when to fetch them and pass them to the model.
Why does my tool call fail with an issuer mismatch during OAuth?
Pre-registered confidential clients must set issuer on the credentials returned by clientInformation(), and it must exactly match the discovered issuer. The SDK rejects a mismatch before sending credentials.











