← All articles

CopilotKit MCP: Complete Developer Guide, Setup & Best Practices

July 5, 2026·24 min read·MCPForge

What Is CopilotKit?

CopilotKit is an open-source framework for building AI copilots and agents directly inside React applications. Unlike standalone chatbot SDKs, CopilotKit deeply integrates with your app's state, UI, and backend — letting AI agents read your application context, trigger UI actions, and call server-side functions as part of a single coherent workflow.

At its core, CopilotKit provides:

  • useCopilotReadable — expose app state as context the AI can read
  • useCopilotAction — define frontend actions the AI can invoke (open modals, update state, submit forms)
  • CopilotRuntime — a server-side runtime that orchestrates LLM calls, tool execution, and multi-step agent workflows
  • CopilotChat / CopilotSidebar — prebuilt React UI components for the chat interface

The framework was purpose-built for production React apps, not demos. It handles streaming, multi-turn conversation state, tool call rendering, and agent loop orchestration out of the box.


What Is MCP and Why Does It Matter Here?

The Model Context Protocol (MCP), developed by Anthropic, is an open standard that defines how AI models connect to external tools, data sources, and services. Think of it as a USB-C standard for AI integrations — one protocol, any tool.

MCP servers expose three primitives:

PrimitivePurposeExample
ToolsExecutable functions the AI can callRun SQL query, send email, create GitHub issue
ResourcesRead-only data the AI can accessFile contents, database records, API responses
PromptsReusable prompt templatesStructured task prompts with arguments

MCP uses JSON-RPC 2.0 over either stdio (local process) or HTTP with SSE or Streamable HTTP (networked). Tool discovery happens automatically — clients call tools/list and get back every available tool with its JSON Schema definition.


Why Combine CopilotKit with MCP?

Want to analyze your API security?

Import your OpenAPI spec and generate a Security Report automatically.

Before MCP, adding a new tool to a CopilotKit agent meant writing custom backend code, updating your runtime configuration, and redeploying. Every integration was bespoke.

With MCP, your CopilotKit runtime connects to any MCP-compliant server and automatically inherits all of its tools. You deploy a Postgres MCP server once — every CopilotKit agent in your organization can query your database without any additional code.

The practical advantages:

  • Zero-code tool additions — point CopilotKit at a new MCP server, tools appear automatically
  • Reusability — the same MCP server works with Claude Desktop, Cursor, CopilotKit, and any other MCP client
  • Separation of concerns — tool logic lives in the MCP server, agent orchestration lives in CopilotKit
  • Ecosystem leverage — thousands of production-ready MCP servers already exist for GitHub, Slack, Notion, Postgres, Stripe, and more
  • Standardized schema — JSON Schema tool definitions mean consistent validation and type safety

When MCP specifically outperforms custom integrations:

Custom integration:  1 tool = 1 custom implementation
MCP integration:     1 server = N tools, any client

If you are building a multi-agent system, a platform product, or an AI copilot that needs to grow its tool surface over time, MCP is the architectural decision that keeps your system maintainable.


Architecture: How CopilotKit + MCP Works

┌─────────────────────────────────────────────────────┐
│                  Browser / React App                │
│  ┌──────────────┐  ┌──────────────────────────────┐ │
│  │ CopilotChat  │  │  useCopilotReadable          │ │
│  │ UI Component │  │  useCopilotAction (frontend) │ │
│  └──────┬───────┘  └──────────────────────────────┘ │
└─────────┼───────────────────────────────────────────┘
          │ HTTP (streaming)
          ▼
┌─────────────────────────────────────────────────────┐
│              CopilotKit Runtime (Server)            │
│  ┌──────────────────────────────────────────────┐   │
│  │  LLM Adapter (OpenAI / Anthropic / Gemini)  │   │
│  └──────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────┐   │
│  │  MCP Client Layer                           │   │
│  │  - Discovers tools via tools/list           │   │
│  │  - Injects schemas into LLM context         │   │
│  │  - Executes tool calls via tools/call       │   │
│  └──────────────────────┬───────────────────────┘   │
└─────────────────────────┼───────────────────────────┘
                          │ JSON-RPC (SSE / HTTP)
          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
   ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
   │  GitHub MCP │ │ Postgres MCP│ │  Slack MCP  │
   │   Server   │ │   Server   │ │   Server   │
   └─────────────┘ └─────────────┘ └─────────────┘

The CopilotKit runtime sits between your React frontend and your MCP servers. It discovers tools, injects their schemas into the LLM's system context, routes tool call responses back to the appropriate MCP server, and streams results back to the browser.


Prerequisites

Before starting, ensure you have:

  • Node.js 18+ (LTS recommended)
  • A React 18+ project (Next.js 14+ with App Router recommended)
  • An LLM API key (OpenAI, Anthropic, or compatible)
  • At least one MCP server running locally or remotely
  • Basic familiarity with TypeScript

Installation

1. Install CopilotKit packages

bash
npm install @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime

For Anthropic's Claude as your LLM:

bash
npm install @ai-sdk/anthropic

For OpenAI:

bash
npm install @ai-sdk/openai

2. Install MCP SDK (if building your own MCP server)

bash
npm install @modelcontextprotocol/sdk

Project Structure

For a Next.js App Router project:

my-app/
├── app/
│   ├── api/
│   │   └── copilotkit/
│   │       └── route.ts          ← CopilotKit runtime endpoint
│   ├── layout.tsx
│   └── page.tsx
├── components/
│   └── CopilotWrapper.tsx        ← CopilotKit provider setup
├── mcp-servers/
│   └── my-tools-server.ts        ← Optional: your own MCP server
└── .env.local

Step 1: Configure the CopilotKit Runtime with MCP

This is where CopilotKit connects to your MCP servers. Create your route handler:

typescript
// app/api/copilotkit/route.ts
import {
  CopilotRuntime,
  AnthropicAdapter,
  copilotRuntimeNextJSAppRouterEndpoint,
} from '@copilotkit/runtime';
import { experimental_createMCPClient } from 'ai';
import { NextRequest } from 'next/server';

export const POST = async (req: NextRequest) => {
  // Initialize MCP clients for each server
  const githubMCPClient = await experimental_createMCPClient({
    transport: {
      type: 'sse',
      url: process.env.GITHUB_MCP_SERVER_URL!, // e.g., http://localhost:3001/sse
      headers: {
        Authorization: `Bearer ${process.env.GITHUB_MCP_TOKEN}`,
      },
    },
  });

  const postgresMCPClient = await experimental_createMCPClient({
    transport: {
      type: 'sse',
      url: process.env.POSTGRES_MCP_SERVER_URL!,
    },
  });

  // Discover tools from all MCP servers
  const githubTools = await githubMCPClient.tools();
  const postgresTools = await postgresMCPClient.tools();

  const runtime = new CopilotRuntime({
    tools: {
      ...githubTools,
      ...postgresTools,
    },
  });

  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter: new AnthropicAdapter({
      model: 'claude-3-5-sonnet-20241022',
    }),
    endpoint: '/api/copilotkit',
  });

  return handleRequest(req);
};

Important: MCP client initialization happens per-request in this example for simplicity. In production, consider caching the MCP client connections and refreshing tools periodically rather than reconnecting on every request. See the production deployment section below.

Environment variables

bash
# .env.local
ANTHROPIC_API_KEY=sk-ant-...
GITHUB_MCP_SERVER_URL=http://localhost:3001/sse
GITHUB_MCP_TOKEN=ghp_...
POSTGRES_MCP_SERVER_URL=http://localhost:3002/sse

Step 2: Wrap Your React App with CopilotKit

typescript
// components/CopilotWrapper.tsx
'use client';

import { CopilotKit } from '@copilotkit/react-core';
import { ReactNode } from 'react';

export function CopilotWrapper({ children }: { children: ReactNode }) {
  return (
    <CopilotKit runtimeUrl="/api/copilotkit">
      {children}
    </CopilotKit>
  );
}
typescript
// app/layout.tsx
import { CopilotWrapper } from '@/components/CopilotWrapper';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <CopilotWrapper>
          {children}
        </CopilotWrapper>
      </body>
    </html>
  );
}

Step 3: Add the Chat Interface

typescript
// app/page.tsx
'use client';

import { CopilotSidebar } from '@copilotkit/react-ui';
import '@copilotkit/react-ui/styles.css';

export default function Home() {
  return (
    <main>
      <h1>My AI Copilot</h1>
      {/* Your existing app content */}
      <CopilotSidebar
        defaultOpen={false}
        instructions="You are a helpful assistant with access to GitHub and database tools. Use tools proactively to help the user."
        labels={{
          title: 'AI Assistant',
          initial: 'Hi! I can help you manage GitHub issues and query your database.',
        }}
      />
    </main>
  );
}

Step 4: Build an MCP Server (If You Need a Custom One)

If existing MCP servers don't cover your use case, here is a production-ready custom MCP server using SSE transport:

typescript
// mcp-servers/crm-server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express from 'express';
import { z } from 'zod';

const app = express();
const server = new McpServer({
  name: 'crm-mcp-server',
  version: '1.0.0',
});

// Define a tool: get customer by ID
server.tool(
  'get_customer',
  'Retrieve a customer record from the CRM by their ID',
  {
    customer_id: z.string().describe('The unique customer identifier'),
  },
  async ({ customer_id }) => {
    // In production: query your actual CRM API
    const customer = await fetchCustomerFromCRM(customer_id);
    
    if (!customer) {
      return {
        content: [{
          type: 'text',
          text: `No customer found with ID: ${customer_id}`,
        }],
        isError: true,
      };
    }

    return {
      content: [{
        type: 'text',
        text: JSON.stringify(customer, null, 2),
      }],
    };
  }
);

// Define a tool: create support ticket
server.tool(
  'create_ticket',
  'Create a new support ticket for a customer',
  {
    customer_id: z.string(),
    subject: z.string().max(200),
    priority: z.enum(['low', 'medium', 'high', 'critical']),
    description: z.string().max(2000),
  },
  async ({ customer_id, subject, priority, description }) => {
    const ticket = await createSupportTicket({ customer_id, subject, priority, description });
    return {
      content: [{
        type: 'text',
        text: `Ticket created successfully. Ticket ID: ${ticket.id}`,
      }],
    };
  }
);

// SSE transport setup
const transports: Map<string, SSEServerTransport> = new Map();

app.get('/sse', async (req, res) => {
  const transport = new SSEServerTransport('/messages', res);
  transports.set(transport.sessionId, transport);
  
  res.on('close', () => {
    transports.delete(transport.sessionId);
  });

  await server.connect(transport);
});

app.post('/messages', async (req, res) => {
  const { sessionId } = req.query as { sessionId: string };
  const transport = transports.get(sessionId);
  
  if (!transport) {
    res.status(404).json({ error: 'Session not found' });
    return;
  }

  await transport.handlePostMessage(req, res);
});

app.listen(3001, () => {
  console.log('CRM MCP Server running on http://localhost:3001');
});

Combining MCP Tools with Frontend CopilotKit Actions

One of CopilotKit's most powerful patterns is mixing MCP server tools (which run server-side) with frontend actions (which run in the browser). The AI decides which tool to call:

typescript
// In your React component
'use client';

import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core';
import { useState } from 'react';

export function CustomerDashboard() {
  const [selectedCustomerId, setSelectedCustomerId] = useState<string | null>(null);
  const [ticketModalOpen, setTicketModalOpen] = useState(false);

  // Expose current UI state as readable context
  useCopilotReadable({
    description: 'Currently selected customer ID in the dashboard',
    value: selectedCustomerId,
  });

  // Frontend action: open the ticket creation modal
  // This runs in the browser — no server round trip
  useCopilotAction({
    name: 'open_ticket_modal',
    description: 'Open the ticket creation modal for the current customer',
    parameters: [],
    handler: async () => {
      setTicketModalOpen(true);
    },
  });

  // Frontend action: navigate to a customer profile
  useCopilotAction({
    name: 'select_customer',
    description: 'Select a customer and navigate to their profile',
    parameters: [
      {
        name: 'customer_id',
        type: 'string',
        description: 'The customer ID to navigate to',
        required: true,
      },
    ],
    handler: async ({ customer_id }) => {
      setSelectedCustomerId(customer_id);
    },
  });

  return (
    <div>
      {/* Your UI */}
      {ticketModalOpen && <TicketModal onClose={() => setTicketModalOpen(false)} />}
    </div>
  );
}

Now the AI has access to:

  • get_customer — MCP tool, queries your CRM API server-side
  • create_ticket — MCP tool, creates a real ticket
  • open_ticket_modal — frontend action, opens the React modal
  • select_customer — frontend action, updates React state

A user can say "Look up customer 12345 and open a high priority ticket for their billing issue" — the AI will call get_customer, then open_ticket_modal, all in sequence.


AI Agent Workflows with CopilotKit + MCP

For more complex, multi-step agentic workflows, CopilotKit integrates with LangGraph and other agent frameworks. MCP tools slot directly into these agent graphs:

typescript
// Using CopilotKit with a LangGraph agent that has MCP tools
import { CopilotRuntime } from '@copilotkit/runtime';
import { LangGraphAdapter } from '@copilotkit/runtime';

const runtime = new CopilotRuntime({
  remoteActions: [
    {
      url: process.env.LANGGRAPH_AGENT_URL!, // Your deployed LangGraph agent
    },
  ],
});

In your LangGraph agent, MCP tools are bound directly to the graph nodes:

python
# agent/graph.py (Python LangGraph example)
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
from langchain_anthropic import ChatAnthropic

async def create_agent():
    client = MultiServerMCPClient(
        {
            "github": {
                "url": "http://github-mcp-server:3001/sse",
                "transport": "sse",
            },
            "postgres": {
                "url": "http://postgres-mcp-server:3002/sse", 
                "transport": "sse",
            },
        }
    )
    
    tools = await client.get_tools()
    model = ChatAnthropic(model="claude-3-5-sonnet-20241022")
    
    agent = create_react_agent(model, tools)
    return agent

This pattern is powerful for research agents, code review pipelines, customer support bots, and any workflow requiring multiple sequential tool calls with reasoning between steps.


Claude Desktop Integration

If your team uses Claude Desktop alongside CopilotKit, you can point it at the same MCP servers your CopilotKit runtime uses. This means your AI copilot and Claude Desktop share the same tool ecosystem.

json
// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
{
  "mcpServers": {
    "crm-server": {
      "command": "node",
      "args": ["/path/to/mcp-servers/crm-server.js"],
      "env": {
        "CRM_API_KEY": "your-api-key"
      }
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..."
      }
    }
  }
}

For SSE-based servers you've already deployed for CopilotKit:

json
{
  "mcpServers": {
    "crm-server": {
      "command": "npx",
      "args": ["mcp-remote", "http://your-crm-mcp-server.com/sse"],
      "env": {
        "MCP_BEARER_TOKEN": "your-bearer-token"
      }
    }
  }
}

Restart Claude Desktop after saving. Tools appear immediately in the Claude Desktop interface.


Cursor Integration

Cursor IDE supports MCP for AI-assisted development. Connect the same MCP servers:

json
// .cursor/mcp.json (project-level) or ~/.cursor/mcp.json (global)
{
  "mcpServers": {
    "crm-server": {
      "command": "node",
      "args": ["./mcp-servers/crm-server.js"]
    },
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres"],
      "env": {
        "POSTGRES_CONNECTION_STRING": "postgresql://user:pass@localhost:5432/mydb"
      }
    }
  }
}

With this in place, Cursor's AI composer can call your actual database or CRM while helping you write code — the same server your CopilotKit runtime calls in production.


Authentication and Security

This is where most production CopilotKit + MCP setups get it wrong. Here is what actually matters:

Authenticating MCP Server Connections

typescript
// Passing credentials to MCP servers in your CopilotKit runtime
const mcpClient = await experimental_createMCPClient({
  transport: {
    type: 'sse',
    url: process.env.PRIVATE_MCP_SERVER_URL!,
    headers: {
      Authorization: `Bearer ${process.env.MCP_SERVER_TOKEN}`,
      'X-Service-Name': 'copilotkit-runtime',
      'X-Request-ID': crypto.randomUUID(), // For tracing
    },
  },
});

Validating User Identity Before Tool Calls

Your CopilotKit route handler receives the original HTTP request. Use this to validate the user before initializing MCP clients:

typescript
// app/api/copilotkit/route.ts
import { getServerSession } from 'next-auth';

export const POST = async (req: NextRequest) => {
  // Validate the user session first
  const session = await getServerSession();
  
  if (!session?.user) {
    return new Response('Unauthorized', { status: 401 });
  }

  // Scope MCP tool access based on user role
  const userRole = session.user.role;
  
  const mcpClient = await experimental_createMCPClient({
    transport: {
      type: 'sse',
      url: process.env.MCP_SERVER_URL!,
      headers: {
        Authorization: `Bearer ${process.env.MCP_SERVICE_TOKEN}`,
        'X-User-ID': session.user.id,
        'X-User-Role': userRole,
      },
    },
  });

  let tools = await mcpClient.tools();

  // Allowlist tools based on user role
  if (userRole !== 'admin') {
    const allowedTools = ['get_customer', 'create_ticket'];
    tools = Object.fromEntries(
      Object.entries(tools).filter(([name]) => allowedTools.includes(name))
    );
  }

  const runtime = new CopilotRuntime({ tools });
  // ...
};

MCP Server-Side Security

Your MCP server should never trust the client blindly:

typescript
// In your MCP server tool handler
server.tool(
  'delete_customer',
  'Delete a customer record',
  { customer_id: z.string() },
  async ({ customer_id }, { requestContext }) => {
    const userRole = requestContext?.headers?.['x-user-role'];
    
    if (userRole !== 'admin') {
      return {
        content: [{ type: 'text', text: 'Permission denied: admin role required' }],
        isError: true,
      };
    }

    // Validate the customer_id format to prevent injection
    if (!/^[a-zA-Z0-9-]{8,36}$/.test(customer_id)) {
      return {
        content: [{ type: 'text', text: 'Invalid customer ID format' }],
        isError: true,
      };
    }

    await deleteCustomer(customer_id);
    return {
      content: [{ type: 'text', text: `Customer ${customer_id} deleted` }],
    };
  }
);

Security Checklist

  • All MCP server URLs use HTTPS in production
  • Bearer tokens for MCP server connections stored in environment variables, never hardcoded
  • CopilotKit runtime route validates user session before connecting to MCP servers
  • User role information passed to MCP server in request headers
  • MCP server validates all tool arguments before execution
  • Rate limiting applied at the MCP server level (per user/session)
  • Tool allowlists enforced based on user permissions
  • Audit logging enabled for all tool calls with user ID and timestamp
  • MCP servers not directly accessible from public internet (behind API gateway)

Validating Your MCP Server Before Integration

Before wiring an MCP server into CopilotKit production, validate it properly. Protocol compliance issues — missing capability declarations, malformed JSON Schema, incorrect JSON-RPC handshake — cause silent failures that are painful to debug.

MCPForge Verify runs your MCP server through a full protocol compliance check: it validates the initialize handshake, tools/list response schema, tool argument schemas, error handling, and transport configuration. Use it before any production deployment.

For discovering battle-tested, pre-validated MCP servers compatible with CopilotKit, browse the MCPForge Verified Directory — servers listed there have already passed protocol compliance checks.


Production Deployment

Connection Pooling for MCP Clients

Do not create a new MCP client connection on every request in production. Instead, cache connections and refresh the tool list on a schedule:

typescript
// lib/mcp-client-pool.ts
import { experimental_createMCPClient } from 'ai';

interface MCPClientPool {
  tools: Record<string, unknown>;
  lastRefreshed: number;
  client: Awaited<ReturnType<typeof experimental_createMCPClient>>;
}

const pools = new Map<string, MCPClientPool>();
const REFRESH_INTERVAL_MS = 5 * 60 * 1000; // Refresh tool list every 5 minutes

export async function getMCPTools(
  serverName: string,
  serverUrl: string,
  headers?: Record<string, string>
): Promise<Record<string, unknown>> {
  const existing = pools.get(serverName);
  const now = Date.now();

  if (existing && now - existing.lastRefreshed < REFRESH_INTERVAL_MS) {
    return existing.tools;
  }

  const client = await experimental_createMCPClient({
    transport: { type: 'sse', url: serverUrl, headers },
  });

  const tools = await client.tools();

  pools.set(serverName, {
    tools,
    lastRefreshed: now,
    client,
  });

  return tools;
}

Health Checks

Implement health check endpoints on your MCP servers and monitor them:

typescript
// In your Express MCP server
app.get('/health', async (req, res) => {
  try {
    // Check that the server can still reach its dependencies
    await db.query('SELECT 1');
    res.json({ status: 'healthy', timestamp: new Date().toISOString() });
  } catch (error) {
    res.status(503).json({ status: 'unhealthy', error: String(error) });
  }
});

Docker Deployment

dockerfile
# Dockerfile for your MCP server
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
EXPOSE 3001
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD wget -q -O- http://localhost:3001/health || exit 1
CMD ["node", "dist/crm-server.js"]
yaml
# docker-compose.yml
version: '3.8'
services:
  crm-mcp-server:
    build: ./mcp-servers/crm
    ports:
      - '3001:3001'
    environment:
      CRM_API_URL: ${CRM_API_URL}
      CRM_API_KEY: ${CRM_API_KEY}
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'wget', '-q', '-O-', 'http://localhost:3001/health']
      interval: 30s
      timeout: 10s
      retries: 3

  nextjs-app:
    build: .
    ports:
      - '3000:3000'
    environment:
      CRM_MCP_SERVER_URL: http://crm-mcp-server:3001/sse
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
    depends_on:
      crm-mcp-server:
        condition: service_healthy

For a deeper look at scaling and operating MCP servers in production environments, see Running MCP in Production.


Performance Optimization

Tool Schema Caching

Tool schemas rarely change. Cache them aggressively:

typescript
import { unstable_cache } from 'next/cache';

const getCachedMCPTools = unstable_cache(
  async (serverUrl: string) => {
    const client = await experimental_createMCPClient({
      transport: { type: 'sse', url: serverUrl },
    });
    return client.tools();
  },
  ['mcp-tools'],
  { revalidate: 300 } // Cache for 5 minutes
);

Limit Tool Context Size

If you connect many MCP servers, the aggregated tool schemas can consume significant LLM context tokens. Be selective:

typescript
// Only include tools relevant to the current page or user role
const allTools = await getMCPTools('crm-server', process.env.CRM_MCP_URL!);

// Filter to only the tools this user needs
const relevantTools = Object.fromEntries(
  Object.entries(allTools).filter(([name]) =>
    userPermissions.allowedTools.includes(name)
  )
);

Parallel Tool Discovery

When connecting to multiple MCP servers, discover tools in parallel:

typescript
const [githubTools, postgresTools, slackTools] = await Promise.all([
  getMCPTools('github', process.env.GITHUB_MCP_URL!),
  getMCPTools('postgres', process.env.POSTGRES_MCP_URL!),
  getMCPTools('slack', process.env.SLACK_MCP_URL!),
]);

const allTools = { ...githubTools, ...postgresTools, ...slackTools };

Common Mistakes

1. Using stdio transport for deployed applications

Stdio only works when the MCP server runs in the same process or on the same machine. The moment your CopilotKit runtime is deployed (Vercel, Railway, AWS), stdio breaks. Always use SSE or Streamable HTTP for deployed setups.

2. Exposing MCP servers directly to the internet

Your MCP server URLs should never be publicly accessible without authentication. Run them behind an API gateway or load balancer, require Bearer tokens, and never expose them from a public Docker port without authentication middleware.

3. Initializing MCP clients on every request without caching

Each MCP client initialization involves a tools/list round-trip. At scale, this adds hundreds of milliseconds to every request. Use the connection pooling pattern shown above.

4. Not validating tool arguments in the MCP server

The LLM can hallucinate tool arguments. Your MCP server is the last line of defense. Always validate with Zod or equivalent before executing any database query, API call, or system operation.

5. Naming conflicts between MCP tool sources

If two MCP servers expose a tool with the same name, the second will silently overwrite the first when you spread them into a single object. Use namespacing:

typescript
const githubTools = await githubClient.tools();
const gitlabTools = await gitlabClient.tools();

// Namespace to avoid collisions
const namespacedGithubTools = Object.fromEntries(
  Object.entries(githubTools).map(([k, v]) => [`github__${k}`, v])
);
const namespacedGitlabTools = Object.fromEntries(
  Object.entries(gitlabTools).map(([k, v]) => [`gitlab__${k}`, v])
);

6. Missing error handling in tool handlers

An unhandled exception in an MCP tool handler will crash the tool call and may crash the server. Always wrap handlers:

typescript
server.tool('risky_operation', '...', { id: z.string() }, async ({ id }) => {
  try {
    const result = await riskyOperation(id);
    return { content: [{ type: 'text', text: JSON.stringify(result) }] };
  } catch (error) {
    console.error('risky_operation failed:', error);
    return {
      content: [{ type: 'text', text: `Operation failed: ${error instanceof Error ? error.message : 'Unknown error'}` }],
      isError: true,
    };
  }
});

Troubleshooting

Tools not appearing in CopilotKit

  1. Verify your MCP server is running: curl http://localhost:3001/health
  2. Test the SSE endpoint directly: curl -N http://localhost:3001/sse
  3. Check the tools/list response manually using MCP Inspector (npx @modelcontextprotocol/inspector)
  4. Run your server URL through MCPForge Verify to catch protocol issues
  5. Check your CopilotKit runtime logs for MCP connection errors

SSE connection drops immediately

  • Ensure you are not behind a reverse proxy that buffers SSE responses (nginx needs proxy_buffering off)
  • Add appropriate keep-alive headers
  • Check that the MCP server does not send Connection: close
nginx
# nginx config for MCP SSE endpoint
location /sse {
  proxy_pass http://mcp-server:3001;
  proxy_http_version 1.1;
  proxy_set_header Connection '';
  proxy_buffering off;
  proxy_cache off;
  proxy_read_timeout 3600s;
  chunked_transfer_encoding on;
}

Tool calls return unexpected errors

  • Check MCP server logs for the actual error
  • Verify Zod schema matches what the LLM is sending
  • Add argument logging at the top of each tool handler during debugging
  • Test tool calls manually using MCP Inspector

CopilotKit runtime timeout on tool execution

Long-running tools (>30s) can timeout. Solutions:

  • Return a ticket/job ID immediately and poll for results
  • Increase the CopilotKit runtime timeout in your route handler
  • Use streaming responses for incremental results

CopilotKit vs Traditional AI SDK Integration

DimensionCopilotKit + MCPDirect LLM SDK + Custom Tools
Tool additionAdd MCP server, tools auto-discoveredWrite new function, redeploy
Frontend integrationNative React hooks, state bindingManual implementation
Multi-agentLangGraph/CoAgents support built-inDIY orchestration
Tool reusabilityAny MCP client (Claude, Cursor, etc.)Only your app
StreamingBuilt-inManual implementation
Schema validationZod via MCP SDKManual
Auth handlingPer-request context availableVaries by implementation
Setup complexityMedium (more moving parts)Low initially, high at scale
Best forProduction apps, growing tool surfaceSimple, fixed-tool chatbots

Production Readiness Checklist

Infrastructure
  ☐ MCP servers deployed with Docker / Kubernetes
  ☐ Health check endpoints implemented and monitored
  ☐ MCP servers behind API gateway (not public-facing)
  ☐ HTTPS enforced on all MCP server URLs
  ☐ Restart policies configured (restart: unless-stopped)

Security  
  ☐ Bearer tokens stored in environment variables
  ☐ User session validated before MCP client initialization
  ☐ Tool allowlists enforced per user role
  ☐ All tool arguments validated server-side with Zod
  ☐ Rate limiting enabled per user/session
  ☐ Audit logging for all tool calls

Performance
  ☐ MCP client connection pooling implemented
  ☐ Tool schemas cached (5-minute TTL minimum)
  ☐ Parallel tool discovery for multiple servers
  ☐ Tool namespace filtered to relevant subset per user

Reliability
  ☐ Tool handler errors caught and returned as isError: true
  ☐ MCP server timeouts configured appropriately
  ☐ Graceful degradation when MCP servers are unavailable
  ☐ Alerting on MCP server health check failures

Validation
  ☐ MCP server validated with MCPForge Verify before go-live
  ☐ Tool schemas tested with MCP Inspector
  ☐ Load tested with expected concurrent user count

Key Takeaways

  • CopilotKit + MCP is the production-grade pattern for React AI copilots that need to grow their tool surface without constant redeployment.
  • The CopilotKit runtime lives server-side and connects to MCP servers via SSE — never expose MCP servers directly to the browser.
  • Validate user sessions in the CopilotKit route handler before initializing MCP clients, and enforce role-based tool allowlists.
  • Cache MCP tool schemas aggressively — tool lists rarely change and re-fetching on every request kills performance at scale.
  • Validate your MCP server with MCPForge Verify before any production integration to catch protocol compliance issues early.
  • Mix MCP server tools (server-side execution) with useCopilotAction (browser-side execution) for the most capable copilot experience.
  • Namespace tool names when aggregating from multiple MCP servers to avoid silent collisions.

Frequently Asked Questions

Can CopilotKit connect to multiple MCP servers simultaneously?

Yes. CopilotKit's MCP integration supports multiple server connections in parallel. You define each server in the `mcpServers` configuration object with a unique key, and CopilotKit will aggregate tools and resources from all connected servers into a single unified tool namespace available to your AI agents.

Does CopilotKit MCP work with models other than Claude?

Yes. CopilotKit is model-agnostic. You can use OpenAI GPT-4o, Anthropic Claude, Google Gemini, or any LLM that supports function/tool calling. The MCP layer handles tool discovery and execution independently of which LLM is processing the conversation. You configure the model in your CopilotKit runtime separately from your MCP server connections.

What is the difference between MCP tools and CopilotKit useCopilotAction?

useCopilotAction defines tools on the frontend that execute in the browser — useful for UI manipulation like opening modals or updating React state. MCP tools execute server-side or on external MCP servers and can call APIs, databases, or file systems. When combined, CopilotKit agents have access to both local UI actions and remote MCP tool capabilities simultaneously.

Is stdio or SSE transport better for CopilotKit MCP integration?

For local development, stdio transport is simpler and requires no network configuration. For production deployments and any server-side CopilotKit runtime, SSE (Server-Sent Events) or Streamable HTTP transport is required because stdio cannot cross process or network boundaries. Always use SSE or HTTP for deployed CopilotKit applications.

How do I secure MCP tool calls made through CopilotKit?

Secure MCP tool calls by: (1) running MCP servers behind your API gateway, (2) passing Bearer tokens in the Authorization header when calling SSE endpoints, (3) validating every incoming tool argument server-side, (4) using allowlists to restrict which tools individual users can invoke, and (5) enabling rate limiting per user or session at the MCP server level.

Why is my CopilotKit agent not discovering MCP tools?

The most common causes are: the MCP server is not running or not reachable from the CopilotKit runtime, the server URL or transport type is misconfigured, the MCP server is not returning a valid tools/list response, or CORS is blocking SSE connections. Run your MCP server URL through MCPForge Verify at mcpforge.dev/verify to diagnose protocol-level issues quickly.

Can I use CopilotKit MCP with Next.js App Router?

Yes, and it is the most common production setup. You create a CopilotKit runtime route handler in `app/api/copilotkit/route.ts`, configure your MCP servers there, and use CopilotKit React hooks in your client components. The runtime runs server-side, so it can safely reach MCP servers with private credentials without exposing them to the browser.

What happens if an MCP server goes offline while CopilotKit is running?

By default, if an MCP server becomes unreachable, tool calls to that server will fail with an error that surfaces to the AI agent. The agent may retry or report the failure depending on your agent configuration. Implement health checks and circuit breaker patterns in production — and consider graceful degradation so your copilot continues functioning with remaining available MCP servers.

Does CopilotKit support MCP Resources and Prompts, or only Tools?

CopilotKit primarily integrates with MCP Tools for agent action execution. MCP Resources can be fetched and injected as context into your CopilotKit runtime manually. MCP Prompts are less commonly used in CopilotKit workflows today, but you can call the MCP server's prompts/get endpoint directly and inject the result as a system message in your CopilotKit runtime configuration.

How do I test MCP server compatibility before integrating with CopilotKit?

Use MCPForge Verify (mcpforge.dev/verify) to validate your MCP server's protocol compliance, tool schema correctness, and transport configuration before wiring it into CopilotKit. This catches JSON-RPC handshake issues, malformed tool definitions, and missing capability declarations that would silently break tool discovery in CopilotKit.

Check your MCP security posture

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

Related Articles

What Is Model Context Protocol (MCP)?

OpenAPI to MCP: Complete Guide

How to Connect Claude to Any API Using MCP

Coming soon

GitHub MCP Server Explained

Coming soon