← All articles

Vercel AI SDK MCP: Complete Guide to Tools, Resources, OAuth & Production

July 6, 2026·22 min read·MCPForge

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

  1. The application creates an MCP client.
  2. The MCP client initializes a connection with the server.
  3. The client discovers available tools, resources, prompts, and capabilities.
  4. client.tools() converts MCP tools into AI SDK-compatible tool definitions.
  5. The application passes those tools to generateText() or streamText().
  6. The model selects a tool.
  7. The AI SDK executes the MCP-backed tool.
  8. The MCP server returns the result.
  9. The result is added to the model context.
  10. 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 MCPWith MCP
Custom tool adapters per SDKOne server, any MCP-compatible client
Schema changes require app deploysServer updates propagate automatically
Tools coupled to model providerProvider-agnostic tool layer
No standard discovery mechanismTool discovery built into the protocol
Authentication reinvented per toolSingle 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:

bash
npm install ai @ai-sdk/mcp

If you're using a specific model provider, install its adapter:

bash
# 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.

bash
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.

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.

typescript
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.

typescript
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.

typescript
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

FeatureHTTP (Streamable)SSEstdio
Remote deployment
Serverless compatible⚠️
Streaming support
Session supportOptionalPersistent connectionProcess lifetime
AuthenticationHeadersHeadersEnvironment
Production recommendedLegacyDev 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)

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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

typescript
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

typescript
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

typescript
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.

typescript
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:

json
{
  "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:

typescript
// 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:

typescript
// ❌ 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

  1. Call client.tools() and console.log(Object.keys(tools)) — if the list is empty, the server isn't advertising tools.
  2. Check the MCP server logs for initialization errors.
  3. Confirm the server URL is correct and reachable from your deployment environment.
  4. Verify the tool descriptions in your server are clear. Models use descriptions to decide when to call a tool.

Connection refused / fetch failed

  1. 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}'
  2. Check firewall rules allow your deployment IP.
  3. Verify TLS certificates are valid (self-signed certs will fail with default fetch).

Tool calls return errors

  1. Log the full toolResults array in onStepFinish.
  2. MCP tool errors are structured — the server returns an isError: true flag with a message. The model will see this and can adjust.
  3. Validate your server's tool input schemas with MCPForge Verify. Malformed schemas cause the model to generate invalid inputs.

Unexpected token usage spikes

  1. stopWhen that's too high, combined with large tool results, will balloon token counts. Log usage.totalTokens per step.
  2. Summarize large tool results before returning them from your MCP server. A database query that returns 10,000 rows should be summarized server-side.
  3. Use maxTokens on generateText() as a hard cap.

SSE disconnects mid-stream

  1. Check your server's keep-alive interval — SSE connections need a heartbeat (comment lines or empty events) every 15–30 seconds.
  2. Check load balancer timeout settings. AWS ALB defaults to 60 seconds.
  3. 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.

typescript
// 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
  • stopWhen is 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/mcp is 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 a finally block — connection leaks are silent in production
  • Set stopWhen explicitly on every generateText() and streamText() 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

Frequently Asked Questions

What is @ai-sdk/mcp and what does it do?

@ai-sdk/mcp provides stable Model Context Protocol client support for the Vercel AI SDK. createMCPClient() can connect to MCP servers, convert discovered MCP tools into AI SDK-compatible tools, list and read resources, discover resource templates, retrieve prompts, handle OAuth authentication flows, and respond to elicitation requests. MCP tools can be passed directly to generateText() and streamText(), while resources and prompts are accessed through dedicated MCP client methods.

Does AI SDK MCP work with OpenAI models, or only Claude?

It works with any model supported by the Vercel AI SDK, including OpenAI GPT-4o, Anthropic Claude, Google Gemini, Mistral, and others. MCP handles tool exposure; the AI SDK handles model routing. The two concerns are fully decoupled.

What is the difference between HTTP transport, SSE transport, and stdio transport in MCP?

HTTP transport (Streamable HTTP) uses standard HTTP POST requests and is the recommended choice for remote, production-deployed MCP servers. SSE transport uses Server-Sent Events for streaming and is compatible with older MCP server implementations. stdio transport spawns a local child process and communicates over stdin/stdout — ideal for CLI tools and local development, but not suitable for remote deployment.

Can I use multiple MCP servers at the same time with the Vercel AI SDK?

Yes. You can instantiate multiple MCP clients, create a separate MCP client for each server, and merge the resulting tool sets before passing them to generateText() or streamText(). Tools from different servers are namespaced to avoid collisions.

How do I authenticate requests to a remote MCP server using AI SDK MCP?

Pass an Authorization header with a Bearer token inside the headers option of the HTTP or SSE transport configuration. For OAuth 2.0 flows, implement token refresh logic in a custom fetch wrapper and inject the refreshed token per-request before it reaches the MCP client.

Is AI SDK MCP production-ready?

Yes. MCP support is stable in AI SDK 6 and available through createMCPClient(). The AI SDK MCP client supports tools, resources, prompts, OAuth authentication, and elicitation. Production readiness still depends on the reliability, authentication, permissions, monitoring, and security controls of the MCP servers your application connects to.

How do I troubleshoot 'tool not found' errors when using MCP with AI SDK?

Call client.tools() and log the returned object immediately after connecting. If the tool is missing, the MCP server is not advertising it correctly. Check the server's tool registration logic and confirm the MCP server is running the correct version. Also, confirm the transport URL is correct and the server returned a 200 on the initialization handshake.

Does MCP with Vercel AI SDK support streaming tool results?

Yes. Use streamText() instead of generateText(). Tool calls are streamed as delta events, and tool results are sent back to the model in the same streaming session. This enables real-time UI updates during multi-step agentic workflows.

How should I manage MCP client lifecycle to avoid connection leaks?

Always call client.close() in a finally block after your request completes, or implement a connection pool with a maximum idle timeout. For serverless functions, create and close the client within the same function invocation. Persistent connections are useful only in long-running server processes where the overhead of reconnecting is significant.

What is the MCP specification version supported by @ai-sdk/mcp?

The MCP specification version supported by AI SDK depends on the installed @ai-sdk/mcp release. Check the package changelog and AI SDK MCP documentation before relying on features introduced in a specific MCP specification revision. Streamable HTTP is the modern transport for remote MCP servers, while SSE remains relevant for compatibility with older MCP server implementations.

Check your MCP security posture

Generate a Security Score, detect risky tools, and review permissions before exposing APIs to AI agents.