Overview
This example demonstrates how to connect LLMs to MCP tools using the Vercel AI SDK. We’ll create a simple chat component that connects to MCP servers and executes tools dynamically. We’ll use a simple effect on mount to connect to servers. However, you can easily let users add their own servers by implementing a server management UI or allowing server URLs to be configured through environment variables or user settings.Installation
Install the required dependencies:Copy
Ask AI
npm install ai @ai-sdk/react @ai-sdk/openai @modelcontextprotocol/sdk @smithery/sdk
Example
Client-side Component
Create a chat component that connects to MCP servers and handles tool execution:Copy
Ask AI
'use client';
import { useState, useCallback, useRef, useEffect } from 'react';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
import { createMCPClient } from '@/lib/mcp/create-client';
import { createSmitheryUrl } from "@smithery/sdk/shared/config.js";
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
import type { Tool as MCPTool } from "@modelcontextprotocol/sdk/types.js";
export default function Home() {
const [text, setText] = useState<string>('');
const [client, setClient] = useState<Client | null>(null);
const [allTools, setAllTools] = useState<MCPTool[]>([]);
// Connect to MCP server on mount
// This is for demo - in your application you might want to let users dynamically add their servers
useEffect(() => {
const connectToMCP = async () => {
try {
// Replace with your server details
const serverUrl = 'https://server.smithery.ai/{server_id}';
const apiKey = '{your_api_key}';
const profile = '{your_profile}';
const connectionUrl = createSmitheryUrl(serverUrl, {
apiKey,
profile,
});
const result = await createMCPClient(new URL(connectionUrl));
if (!result.ok) {
throw result.error;
}
const { client } = result.value;
setClient(client);
// Get available tools
try {
const toolsResult = await client.listTools();
setAllTools(toolsResult.tools || []);
} catch (toolsError) {
console.warn('Failed to list tools:', toolsError);
setAllTools([]);
}
} catch (error) {
console.error('Failed to connect to MCP server:', error);
}
};
connectToMCP();
// Cleanup on unmount
return () => {
if (client) {
client.close().catch(console.error);
}
};
}, []);
// Use a ref to store the latest handler to avoid stale closures
const handleToolCallRef = useRef<((params: any) => Promise<void>) | null>(null);
// Stable wrapper that always calls the latest version
const handleToolCall = useCallback(async (params: any) => {
return handleToolCallRef.current?.(params);
}, []);
// Configure useChat with MCP tool handling
const { messages, sendMessage, status, addToolResult } = useChat({
transport: new DefaultChatTransport({
api: '/api/chat',
}),
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
onToolCall: handleToolCall,
});
// Update the ref with the latest implementation
handleToolCallRef.current = async ({ toolCall }: any) => {
// Check if it's a dynamic tool first for proper type narrowing
if (toolCall.dynamic) {
return;
}
try {
if (!client) {
throw new Error('MCP client not connected');
}
// Execute the tool using the MCP client
const result = await client.callTool({
name: toolCall.toolName,
arguments: toolCall.input || {}
});
// Extract text content from the result
let output = 'Tool executed successfully';
if (result.content && Array.isArray(result.content)) {
const textContent = result.content
.filter(item => item.type === 'text')
.map(item => item.text)
.join('\n');
output = textContent || output;
}
// Add the MCP result back to the conversation
addToolResult({
toolCallId: toolCall.toolCallId,
tool: toolCall.toolName,
output,
});
} catch (error) {
// Add error result
addToolResult({
toolCallId: toolCall.toolCallId,
tool: toolCall.toolName,
output: {
error: 'Tool execution failed',
message: error instanceof Error ? error.message : 'Unknown error'
},
});
}
};
const handleSendMessage = useCallback(async () => {
if (!text.trim() || status === 'streaming') return;
// Pass the current tools state to the API
sendMessage({ text }, {
body: {
tools: allTools,
}
});
setText('');
}, [text, status, sendMessage, allTools]);
return (
<div className="flex flex-col h-screen max-w-4xl mx-auto">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4">
{messages.map((message) => (
<div key={message.id} className="mb-4">
<div className="font-semibold mb-2">
{message.role === 'user' ? 'You' : 'Assistant'}:
</div>
{message.parts.map((part, i) => {
switch (part.type) {
case 'text':
return (
<div key={`${message.id}-${i}`} className="prose">
{part.text}
</div>
);
default:
// Handle tool call parts generically
if (part.type.startsWith('tool-') && 'toolCallId' in part) {
const toolPart = part as any;
const callId = toolPart.toolCallId;
const toolName = part.type.replace('tool-', '');
if ('state' in toolPart) {
switch (toolPart.state) {
case 'input-streaming':
return (
<div key={callId} className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
Preparing {toolName}...
</div>
);
case 'input-available':
return (
<div key={callId} className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
Executing {toolName}...
</div>
);
case 'output-available':
return (
<div key={callId} className="p-3 bg-green-50 border border-green-200 rounded-lg">
<strong>{toolName} result:</strong> {toolPart.output}
</div>
);
case 'output-error':
return (
<div key={callId} className="p-3 bg-red-50 border border-red-200 rounded-lg">
Error in {toolName}: {toolPart.errorText}
</div>
);
}
}
}
return null;
}
})}
</div>
))}
{status === 'streaming' && (
<div className="mb-4">
<div className="font-semibold mb-2">Assistant:</div>
<div className="animate-pulse">Thinking...</div>
</div>
)}
</div>
{/* Input */}
<div className="p-4 border-t">
<div className="flex gap-2">
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Ask anything..."
className="flex-1 p-3 border border-gray-300 rounded-lg"
disabled={status === 'streaming'}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}}
/>
<button
onClick={handleSendMessage}
disabled={!text.trim() || status === 'streaming'}
className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"
>
Send
</button>
</div>
</div>
</div>
);
}
API Route
Create an API route that handles MCP tool integration:Copy
Ask AI
import { streamText, UIMessage, convertToModelMessages, jsonSchema } from 'ai';
import { openai } from '@ai-sdk/openai';
import type { Tool as MCPTool } from "@modelcontextprotocol/sdk/types.js";
import type { JSONSchema7 } from "json-schema";
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const {
model,
messages,
tools,
}: {
messages: UIMessage[];
model?: string;
tools?: MCPTool[];
} = await req.json();
// Convert MCP tools to AI SDK format
const aiTools = Object.fromEntries(
(tools || []).map(tool => [
tool.name,
{
description: tool.description || tool.name,
inputSchema: jsonSchema(tool.inputSchema as JSONSchema7),
},
]),
);
const result = streamText({
model: openai(model || 'gpt-4o-mini'),
messages: convertToModelMessages(messages),
system: 'You are a helpful assistant with access to various tools through MCP (Model Context Protocol). Use the available tools to help users with their requests.',
tools: aiTools,
});
return result.toUIMessageStreamResponse();
}
Advanced Configuration
Multiple MCP Servers
You can connect to multiple MCP servers and aggregate their tools:Copy
Ask AI
const [clients, setClients] = useState<Client[]>([]);
const [allTools, setAllTools] = useState<MCPTool[]>([]);
useEffect(() => {
const connectToMultipleMCP = async () => {
const urls = [
'https://server1.example.com/mcp',
'https://server2.example.com/mcp',
];
const connectedClients: Client[] = [];
const allTools: MCPTool[] = [];
for (const url of urls) {
try {
const result = await createMCPClient(new URL(url));
if (result.ok) {
const { client } = result.value;
connectedClients.push(client);
const toolsResult = await client.listTools();
allTools.push(...(toolsResult.tools || []));
}
} catch (error) {
console.error(`Failed to connect to ${url}:`, error);
}
}
setClients(connectedClients);
setAllTools(allTools);
};
connectToMultipleMCP();
}, []);