What is AI SDK MCP?
AI SDK MCP refers to the integration between Vercel's AI SDK and the Model Context Protocol (MCP) — an open standard that lets AI models discover and invoke external tools, access resources, and use reusable prompts through a unified interface.
In practice, @ai-sdk/mcp gives your application an MCP client that can connect to any compliant MCP server, pull its tool definitions, and expose them directly to generateText() or streamText(). Your LLM calls a tool. The AI SDK routes the call through MCP. The server executes and returns the result. The model continues.
This is the architecture that powers real agentic systems: databases, file systems, APIs, and internal services all reachable by the model through a single, standardized protocol.
How MCP Works with Vercel AI SDK
- The application creates an MCP client.
- The MCP client initializes a connection with the server.
- The client discovers available tools, resources, prompts, and capabilities.
- client.tools() converts MCP tools into AI SDK-compatible tool definitions.
- The application passes those tools to generateText() or streamText().
- The model selects a tool.
- The AI SDK executes the MCP-backed tool.
- The MCP server returns the result.
- The result is added to the model context.
- The model continues until the configured stop condition is met.
Why Use MCP with the Vercel AI SDK?
Before MCP, connecting tools to an LLM meant writing custom function-calling wrappers for every integration. Changing a tool's schema required touching both the integration layer and the prompt. Sharing tools across different AI frameworks meant re-implementing them for each SDK.
MCP solves this with a single client-server protocol:
| Without MCP | With MCP |
|---|---|
| Custom tool adapters per SDK | One server, any MCP-compatible client |
| Schema changes require app deploys | Server updates propagate automatically |
| Tools coupled to model provider | Provider-agnostic tool layer |
| No standard discovery mechanism | Tool discovery built into the protocol |
| Authentication reinvented per tool | Single auth layer at the transport level |
For Vercel AI SDK users specifically, MCP means you can connect to the growing ecosystem of MCP servers — databases, code execution environments, GitHub, Slack, internal APIs — without writing a single line of tool integration code. You discover, connect, and invoke.
Architecture Overview
┌──────────────────────────────────────────────────┐
│ Your Application │
│ │
│ generateText() / streamText() │
│ │ │
│ Vercel AI SDK (model routing) │
│ │ │
│ @ai-sdk/mcp (MCP Client) │
│ │ │
│ Transport Layer │
│ ┌──────┬──────┬──────┐ │
│ │ HTTP │ SSE │stdio │ │
│ └──────┴──────┴──────┘ │
└──────────────────┬───────────────────────────────┘
│ JSON-RPC 2.0
┌──────────▼──────────┐
│ MCP Server(s) │
│ tools / resources │
│ prompts │
└─────────────────────┘
The AI SDK sits between your application code and the model. The MCP client sits between the AI SDK and external tool servers. Tools flow up (discovery); calls flow down (invocation); results flow back up to the model.
Prerequisites
Before starting:
- A current AI SDK installation compatible with the installed @ai-sdk/mcp version
- Node.js version supported by your selected AI SDK release
- At least one MCP server reachable through Streamable HTTP, SSE, or stdio
- A configured AI SDK model provider or AI Gateway model
- Basic understanding of LLM tool calling
Installation
Install the core AI SDK and the MCP client package:
npm install ai @ai-sdk/mcp
If you're using a specific model provider, install its adapter:
# Anthropic Claude
npm install @ai-sdk/anthropic
# OpenAI
npm install @ai-sdk/openai
# Google Gemini
npm install @ai-sdk/google
Verify your installation resolves compatible versions. The @ai-sdk/mcp package has a peer dependency on the ai core package. Mismatched versions are the most common source of import errors.
npm list ai @ai-sdk/mcp
Transport Options: HTTP, SSE, and stdio
MCP defines three transport mechanisms. Choosing the right one matters for performance, security, and deployment architecture.
HTTP Transport (Streamable HTTP) — Recommended
Streamable HTTP is the modern MCP transport for remote servers. It uses HTTP POST requests for client-to-server messages and can use Server-Sent Events for streamed server responses and notifications when needed. This is the right choice for any remotely deployed MCP server.
import { createMCPClient } from 'ai';
import { StreamableHTTPClientTransport } from '@ai-sdk/mcp';
const client = await createMCPClient({
transport: new StreamableHTTPClientTransport(
new URL('https://your-mcp-server.com/mcp'),
),
});
The client automatically handles the MCP initialization handshake, capability negotiation, and session token exchange on first connect.
When to use HTTP transport:
- Remote MCP servers deployed on any cloud provider
- Serverless functions (Next.js API routes, Vercel Functions, AWS Lambda)
- Production deployments behind a load balancer
- Any scenario where you need stateless, scalable connections
SSE Transport (Server-Sent Events)
SSE transport uses the older MCP 2024-11-05 specification. The client establishes a GET connection for the SSE stream and sends requests via POST to a separate endpoint. Many early MCP servers still use this transport.
import { createMCPClient } from 'ai';
import { SSEClientTransport } from '@ai-sdk/mcp';
const client = await createMCPClient({
transport: new SSEClientTransport(
new URL('https://your-mcp-server.com/sse'),
),
});
When to use SSE transport:
- Connecting to existing MCP servers that haven't migrated to Streamable HTTP
- Environments that require persistent streaming connections
- Servers using older MCP SDK versions
Important: SSE transport maintains a persistent GET connection. In serverless environments, this connection will be dropped when your function times out. Use HTTP transport for serverless.
stdio Transport
stdio spawns a local process and communicates over stdin/stdout. This is how Claude Desktop and Cursor connect to locally installed MCP servers.
import { createMCPClient } from 'ai';
import { StdioClientTransport } from '@ai-sdk/mcp';
const client = await createMCPClient({
transport: new StdioClientTransport({
command: 'node',
args: ['./path/to/mcp-server.js'],
env: {
DATABASE_URL: process.env.DATABASE_URL!,
},
}),
});
When to use stdio transport:
- Local development and testing MCP servers locally
- CLI applications that bundle an MCP server
- Desktop applications (Claude Desktop, Cursor)
- When you need to pass secrets directly as environment variables without a network layer
When NOT to use stdio:
- Web servers or API endpoints (you'd spawn a new process per request)
- Any production deployment where the MCP server is remote
- Environments without filesystem access to the server binary
Transport Comparison Table
| Feature | HTTP (Streamable) | SSE | stdio |
|---|---|---|---|
| Remote deployment | ✅ | ✅ | ❌ |
| Serverless compatible | ✅ | ⚠️ | ❌ |
| Streaming support | ✅ | ✅ | ✅ |
| Session support | Optional | Persistent connection | Process lifetime |
| Authentication | Headers | Headers | Environment |
| Production recommended | ✅ | Legacy | Dev only |
Authentication
MCP authorization and authentication requirements depend on the server, transport, and deployment architecture. Remote MCP servers may use authorization mechanisms such as OAuth or bearer tokens, while local stdio servers commonly receive credentials through environment variables.
Bearer Token Authentication (HTTP and SSE)
import { createMCPClient } from 'ai';
import { StreamableHTTPClientTransport } from '@ai-sdk/mcp';
const client = await createMCPClient({
transport: new StreamableHTTPClientTransport(
new URL('https://your-mcp-server.com/mcp'),
{
requestInit: {
headers: {
Authorization: `Bearer ${process.env.MCP_API_KEY}`,
'X-Client-ID': 'my-app-v1',
},
},
},
),
});
Dynamic Token Refresh (OAuth 2.0)
For OAuth flows where tokens expire, implement a custom fetch wrapper:
import { createMCPClient } from 'ai';
import { StreamableHTTPClientTransport } from '@ai-sdk/mcp';
async function getAccessToken(): Promise<string> {
// Your token refresh logic here
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID!,
client_secret: process.env.CLIENT_SECRET!,
}),
});
const data = await response.json();
return data.access_token;
}
const transport = new StreamableHTTPClientTransport(
new URL('https://your-mcp-server.com/mcp'),
{
// Called before every request — ideal for injecting fresh tokens
requestInit: async () => ({
headers: {
Authorization: `Bearer ${await getAccessToken()}`,
},
}),
},
);
const client = await createMCPClient({ transport });
stdio Authentication
For stdio servers, pass secrets as environment variables:
const client = await createMCPClient({
transport: new StdioClientTransport({
command: 'npx',
args: ['-y', '@your-org/mcp-server'],
env: {
...process.env, // Inherit parent environment
API_KEY: process.env.INTERNAL_API_KEY!,
DATABASE_URL: process.env.DATABASE_URL!,
},
}),
});
Security note: Never log the full env object passed to stdio transports. It will contain your secrets in plaintext.
Session Management
MCP sessions track the negotiated capabilities between client and server across a connection. Proper lifecycle management prevents resource leaks and connection exhaustion.
Single-Request Pattern (Serverless)
For serverless functions, create and close the client within a single invocation:
import {
createMCPClient,
generateText,
stepCountIs,
} from 'ai';
import { StreamableHTTPClientTransport } from '@ai-sdk/mcp';
import { anthropic } from '@ai-sdk/anthropic';
export async function POST(req: Request) {
const { prompt } = await req.json();
const client = await createMCPClient({
transport: new StreamableHTTPClientTransport(
new URL(process.env.MCP_SERVER_URL!),
),
});
const model = anthropic(process.env.ANTHROPIC_MODEL!);
try {
const tools = await client.tools();
const result = await generateText({
model,
tools,
prompt,
stopWhen: stepCountIs(5),
});
return Response.json({ text: result.text });
} finally {
await client.close();
}
}
Connection Pool Pattern (Long-Running Servers)
For Express/Fastify servers with high request volume, avoid reconnecting on every request:
import { createMCPClient } from 'ai';
import { StreamableHTTPClientTransport } from '@ai-sdk/mcp';
class MCPClientPool {
private clients: Map<string, Awaited<ReturnType<typeof createMCPClient>>> = new Map();
async getClient(serverUrl: string) {
if (!this.clients.has(serverUrl)) {
const client = await createMCPClient({
transport: new StreamableHTTPClientTransport(new URL(serverUrl)),
});
this.clients.set(serverUrl, client);
}
return this.clients.get(serverUrl)!;
}
async closeAll() {
await Promise.all(
Array.from(this.clients.values()).map(client => client.close())
);
this.clients.clear();
}
}
const pool = new MCPClientPool();
// Graceful shutdown
process.on('SIGTERM', () => pool.closeAll());
process.on('SIGINT', () => pool.closeAll());
Tool Discovery
Tool discovery is the mechanism by which your application learns what capabilities an MCP server exposes. Call client.tools() after connecting:
const tools = await client.tools();
console.log(Object.keys(tools));
// ['search_database', 'create_issue', 'read_file', 'execute_query']
The returned object maps tool names to AI SDK-compatible tool definitions. Each definition includes the JSON Schema for inputs, the tool description, and a wrapped execute function that handles the MCP round-trip automatically.
Selective Tool Exposure
Don't blindly expose every tool to the model. Passing 50 tools to a model increases token usage and reduces reliability. Filter to only what a given request needs:
const allTools = await client.tools();
// Only expose database tools for this request
const dbTools = Object.fromEntries(
Object.entries(allTools).filter(([name]) => name.startsWith('db_'))
);
const result = await generateText({
model: anthropic(process.env.ANTHROPIC_MODEL!),
tools: dbTools,
prompt: 'Find all users who signed up this week',
});
Discovering Tools from Multiple Servers
const [dbClient, githubClient, slackClient] = await Promise.all([
createMCPClient({ transport: new StreamableHTTPClientTransport(new URL(process.env.DB_MCP_URL!)) }),
createMCPClient({ transport: new StreamableHTTPClientTransport(new URL(process.env.GITHUB_MCP_URL!)) }),
createMCPClient({ transport: new StreamableHTTPClientTransport(new URL(process.env.SLACK_MCP_URL!)) }),
]);
const [dbTools, githubTools, slackTools] = await Promise.all([
dbClient.tools(),
githubClient.tools(),
slackClient.tools(),
]);
// Merge — ensure no tool name collisions
const allTools = {
...dbTools,
...githubTools,
...slackTools,
};
Tip: Before deploying multi-server configurations to production, validate each server's tool schemas with MCPForge Verify. Schema inconsistencies between servers are a silent failure mode that won't surface until the model tries to call a malformed tool.
Using generateText() with MCP Tools
generateText() is the right choice when you need a complete response before proceeding — batch jobs, API endpoints that return JSON, or any non-streaming use case.
Basic Single-Step Example
import {
createMCPClient,
generateText,
stepCountIs,
} from 'ai';
import { StreamableHTTPClientTransport } from '@ai-sdk/mcp';
import { anthropic } from '@ai-sdk/anthropic';
async function runAgentTask(prompt: string) {
const client = await createMCPClient({
transport: new StreamableHTTPClientTransport(
new URL(process.env.MCP_SERVER_URL!),
),
});
const model = anthropic(process.env.ANTHROPIC_MODEL!);
try {
const tools = await client.tools();
const { text, steps, usage } = await generateText({
model,
tools,
prompt,
stopWhen: stepCountIs(10),
onStepFinish: ({ toolCalls }) => {
if (toolCalls.length > 0) {
console.log(
'Tools called:',
toolCalls.map(toolCall => toolCall.toolName),
);
}
},
});
console.log(`Completed in ${steps.length} steps`);
console.log(`Tokens used: ${usage.totalTokens}`);
return text;
} finally {
await client.close();
}
}
Controlling Multi-Step Tool Calls with stopWhen
By default, generateText() stops after the first step when the model generates text or executes a tool call.
For workflows that require multiple tool calls, use stopWhen with stepCountIs() to define the maximum number of steps.
For example:
stopWhen: stepCountIs(5)
allows the model to continue for up to five steps.
Use lower limits for simple workflows and higher limits only when a task genuinely requires multiple tool calls. Every additional step can increase token usage, latency, and the number of operations executed by MCP servers.
Always combine multi-step workflows with appropriate permissions, rate limits, logging, and cost monitoring.
Using streamText() with MCP Tools
streamText() is the right choice for user-facing interfaces. Tool calls stream as delta events, and partial text appears as the model generates it.
Next.js App Router Streaming Example
import {
createMCPClient,
stepCountIs,
streamText,
} from 'ai';
import { StreamableHTTPClientTransport } from '@ai-sdk/mcp';
import { anthropic } from '@ai-sdk/anthropic';
export const maxDuration = 60;
export async function POST(req: Request) {
const { messages } = await req.json();
const client = await createMCPClient({
transport: new StreamableHTTPClientTransport(
new URL(process.env.MCP_SERVER_URL!),
),
});
const model = anthropic(process.env.ANTHROPIC_MODEL!);
const tools = await client.tools();
const result = streamText({
model,
messages,
tools,
stopWhen: stepCountIs(10),
onFinish: async () => {
await client.close();
},
onError: async ({ error }) => {
await client.close();
console.error('Stream error:', error);
},
});
return result.toUIMessageStreamResponse();
}
Rendering MCP-Powered Chat Interfaces
MCP tools can be used in AI SDK chat applications through streamText() on the server and useChat() from @ai-sdk/react on the client.
The MCP client should remain server-side. The browser communicates with your application API route, while the API route connects to MCP servers, discovers tools, executes model calls, and streams the result back to the UI.
Never initialize createMCPClient() or expose MCP server credentials directly in browser code.
Using Anthropic Models with AI SDK MCP
Claude (Anthropic) is the model most commonly paired with MCP because Anthropic co-developed the protocol. The integration is seamless.
import { anthropic } from '@ai-sdk/anthropic';
import { StreamableHTTPClientTransport } from '@ai-sdk/mcp';
import {
createMCPClient,
generateText,
stepCountIs,
} from 'ai';
const client = await createMCPClient({
transport: new StreamableHTTPClientTransport(
new URL(process.env.MCP_SERVER_URL!),
),
});
const tools = await client.tools();
const result = await generateText({
model: anthropic(process.env.ANTHROPIC_MODEL!),
system: `You are a helpful assistant with access to tools.
Always confirm the result of tool calls before presenting them to the user.
If a tool call fails, explain what happened and suggest alternatives.`,
tools,
stopWhen: stepCountIs(15),
prompt: 'Analyze our database for users with failed payments this month and draft a re-engagement email template.',
});
await client.close();
console.log(result.text);
Cursor Integration
Cursor uses MCP to extend its coding assistant with project-specific tools. To point Cursor at an MCP server, configure .cursor/mcp.json in your project root:
{
"mcpServers": {
"my-project-server": {
"command": "node",
"args": ["./mcp-server/index.js"],
"env": {
"DATABASE_URL": "${DATABASE_URL}",
"API_KEY": "${API_KEY}"
}
},
"remote-tools": {
"url": "https://your-mcp-server.com/mcp",
"headers": {
"Authorization": "Bearer ${MCP_API_KEY}"
}
}
}
}
Cursor uses stdio transport for local servers and HTTP transport for remote URLs. When Cursor connects, it calls your server's tool list endpoint and makes those tools available to its AI agent in the IDE.
If you're building a Next.js MCP server intended for Cursor use, test it locally with stdio first, then deploy it behind HTTP for team sharing. Find production-ready servers to use with Cursor in the MCPForge Verified Directory.
Complete Production Example
Here's a production-grade Next.js API route that handles errors, logs tool calls, respects timeouts, and cleans up connections:
// app/api/agent/route.ts
import {
createMCPClient,
generateText,
stepCountIs,
type ModelMessage,
} from 'ai';
import { StreamableHTTPClientTransport } from '@ai-sdk/mcp';
import { anthropic } from '@ai-sdk/anthropic';
export const maxDuration = 120;
interface AgentRequest {
messages: ModelMessage[];
sessionId?: string;
}
export async function POST(req: Request) {
const { messages, sessionId }: AgentRequest = await req.json();
if (!messages?.length) {
return Response.json(
{ error: 'messages required' },
{ status: 400 },
);
}
const mcpServerUrl = process.env.MCP_SERVER_URL;
if (!mcpServerUrl) {
return Response.json(
{ error: 'MCP server not configured' },
{ status: 500 },
);
}
const model = anthropic(process.env.ANTHROPIC_MODEL!);
let client: Awaited<ReturnType<typeof createMCPClient>> | null = null;
try {
client = await createMCPClient({
transport: new StreamableHTTPClientTransport(
new URL(mcpServerUrl),
{
requestInit: {
headers: {
Authorization: `Bearer ${process.env.MCP_API_KEY}`,
'X-Session-ID': sessionId ?? 'anonymous',
},
},
},
),
});
const tools = await client.tools();
const toolNames = Object.keys(tools);
if (toolNames.length === 0) {
console.warn('[MCP] No tools discovered from server');
} else {
console.log(
`[MCP] Discovered ${toolNames.length} tools:`,
toolNames,
);
}
const result = await generateText({
model,
messages,
tools,
stopWhen: stepCountIs(10),
onStepFinish: ({
toolCalls,
usage,
}) => {
console.log(
JSON.stringify({
event: 'mcp_step',
sessionId,
toolsCalled: toolCalls.map(
toolCall => toolCall.toolName,
),
tokensUsed: usage.totalTokens,
timestamp: new Date().toISOString(),
}),
);
},
});
return Response.json({
text: result.text,
steps: result.steps.length,
usage: result.usage,
});
} catch (error) {
const message =
error instanceof Error
? error.message
: 'Unknown error';
console.error('[MCP] Agent error:', message);
if (
message.includes('ECONNREFUSED') ||
message.includes('fetch failed')
) {
return Response.json(
{
error:
'MCP server unavailable. Please try again.',
},
{ status: 503 },
);
}
return Response.json(
{ error: 'Agent request failed' },
{ status: 500 },
);
} finally {
if (client) {
await client.close().catch(error => {
console.error(
'[MCP] Failed to close client:',
error,
);
});
}
}
}
Common Mistakes and How to Avoid Them
1. Not Closing the Client
The most common source of resource leaks. Always use try/finally:
// ❌ Wrong — client leaks if generateText throws
const client = await createMCPClient({ transport });
const tools = await client.tools();
const result = await generateText({ tools, prompt });
await client.close();
// ✅ Correct
const client = await createMCPClient({ transport });
try {
const tools = await client.tools();
const result = await generateText({ tools, prompt });
return result;
} finally {
await client.close();
}
2. Not Setting stopWhen
Without stopWhen, the model makes at most one round of tool calls and then returns — even if the task requires multiple tool invocations. Every agentic workflow should set this explicitly.
3. Using SSE Transport in Serverless Functions
SSE transport maintains a persistent connection. Vercel Functions, AWS Lambda, and similar platforms terminate connections after a timeout. The connection drops, your tool calls hang, and you get cryptic timeout errors. Use HTTP (Streamable) transport instead.
4. Passing All Tools to Every Request
Tool selection is part of prompt engineering. Passing 40 tools to a model that only needs 3 wastes tokens and increases the chance of the model choosing the wrong tool. Filter your tool set per request type.
5. Ignoring Transport-Level Errors
If the MCP server returns a non-200 on the initialization handshake, createMCPClient() will throw. Wrap it in a try/catch and return a graceful error to the user rather than a 500.
6. Storing MCP API Keys in Client-Side Code
The MCP client runs server-side. Never import @ai-sdk/mcp in browser code. Your API keys will be exposed in the JavaScript bundle.
Troubleshooting
"Tool not found" or model doesn't use tools
- Call
client.tools()andconsole.log(Object.keys(tools))— if the list is empty, the server isn't advertising tools. - Check the MCP server logs for initialization errors.
- Confirm the server URL is correct and reachable from your deployment environment.
- Verify the tool descriptions in your server are clear. Models use descriptions to decide when to call a tool.
Connection refused / fetch failed
- Test the MCP server with curl to confirm that it is running:
curl -X POST https://your-server.com/mcp -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' - Check firewall rules allow your deployment IP.
- Verify TLS certificates are valid (self-signed certs will fail with default fetch).
Tool calls return errors
- Log the full
toolResultsarray inonStepFinish. - MCP tool errors are structured — the server returns an
isError: trueflag with a message. The model will see this and can adjust. - Validate your server's tool input schemas with MCPForge Verify. Malformed schemas cause the model to generate invalid inputs.
Unexpected token usage spikes
stopWhenthat's too high, combined with large tool results, will balloon token counts. Logusage.totalTokensper step.- Summarize large tool results before returning them from your MCP server. A database query that returns 10,000 rows should be summarized server-side.
- Use
maxTokensongenerateText()as a hard cap.
SSE disconnects mid-stream
- Check your server's keep-alive interval — SSE connections need a heartbeat (comment lines or empty events) every 15–30 seconds.
- Check load balancer timeout settings. AWS ALB defaults to 60 seconds.
- Migrate to HTTP transport if persistent connections are unreliable in your environment.
Security Considerations
MCP dramatically expands what an LLM can do. That expansion needs security guardrails.
Principle of Least Privilege for Tools
Every tool you expose to the model is a potential attack surface for prompt injection. A user who can craft the model's input might convince it to call delete_all_records instead of search_records. Expose only the tools each use case requires.
Input Validation on the MCP Server
Never trust tool inputs from the AI SDK without validation. Validate all inputs against your expected schema server-side, even though the AI SDK passes JSON Schema-validated inputs. The model can generate unexpected values.
// In your MCP server tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'execute_query') {
const { query } = request.params.arguments as { query: string };
// Validate — don't trust model-generated SQL directly
if (!isAllowedQuery(query)) {
return {
content: [{ type: 'text', text: 'Query not permitted' }],
isError: true,
};
}
// Execute
}
});
Rate Limiting at the MCP Server
Implement rate limiting on your MCP server, not just on your application. A runaway agent loop with stopWhen: 50 can generate hundreds of tool calls per minute. Rate limit by session ID or API key.
Audit Logging
Log every tool call — tool name, input, output, calling user, session ID, and timestamp. This is your audit trail for debugging and compliance.
Network Isolation
If your MCP server has access to sensitive internal systems (databases, internal APIs), place it in a private subnet and restrict inbound access to your application servers only. Never expose MCP servers to the public internet without authentication.
Production Deployment Checklist
Before taking an MCP-powered application to production, work through this checklist:
Server validation
- MCP server responds to initialize handshake correctly
- All tool schemas are valid JSON Schema
- Tool descriptions are clear and accurate
- Server validated with MCPForge Verify
Authentication
- All MCP server endpoints require authentication
- API keys are stored in environment variables, not code
- Token rotation is implemented (if using OAuth)
Error handling
- Connection errors return graceful 503 responses
- Tool errors are logged with full context
-
client.close()is called in all code paths
Performance
- Tool set is filtered per request type
-
stopWhenis calibrated to the use case - Token usage is monitored and alerted
- Large tool results are summarized server-side
Security
- Tool inputs are validated server-side
- Rate limiting is implemented
- Audit logging is enabled
- MCP server is network-isolated if accessing internal systems
Monitoring
- Tool call success/failure rate tracked
- p95 latency tracked per tool
- Step count distribution tracked
- Token cost tracked per session
For deeper guidance on running MCP in production environments, see our article on running MCP in production.
Best Practices Summary
Transport selection: Use Streamable HTTP for everything deployed remotely. Reserve stdio for local development. Only use SSE if the server doesn't support HTTP transport yet.
Lifecycle management: Create and close clients per request in serverless. Use a connection pool with a health check in long-running servers.
Tool governance: Discover tools once per application startup (or cache with TTL). Filter per request. Log every call.
Error resilience: Wrap every createMCPClient() call. Handle network errors separately from tool errors. Provide fallback behavior when the MCP server is unavailable.
Cost management: Set stopWhen deliberately. Monitor token usage per step. Summarize large tool results at the server level.
Security: Authenticate every connection. Validate every input. Rate limit. Audit log. Network-isolate sensitive servers.
Validation: Use MCPForge Verify before deploying any MCP server to production. Catch schema errors, capability mismatches, and transport issues before they reach users. Discover validated, production-ready MCP servers in the MCPForge Verified Directory.
Want to analyze your API security?
Import your OpenAPI spec and generate a Security Report automatically.
Key Takeaways
@ai-sdk/mcpis the official package connecting Vercel AI SDK to the MCP ecosystem- Use Streamable HTTP transport for all production remote servers; stdio is for local development only
- Always call
client.close()in afinallyblock — connection leaks are silent in production - Set
stopWhenexplicitly on everygenerateText()andstreamText()call - Filter your tool set per request — don't expose 40 tools when a task needs 3
- Authenticate at the transport layer with Bearer tokens or custom headers
- Validate MCP server schemas before deployment to catch integration errors early
- Every tool the model can call is a security surface — implement input validation, rate limiting, and audit logging on the server side