Connect MCP servers as a tool catalog in your assistant-ui app.
MCP is an open protocol for exposing tools, resources, and prompts to LLMs. One MCP server can publish many tools (file system, GitHub, Slack, your own service) and any MCP-aware client can use them. The AI SDK has a built-in MCP client; this page is the wiring guide for plugging it into an assistant-ui app.
How it works
client ──► /api/chat ──► MCP client ──► MCP server (HTTP, SSE, stdio)
│
└─ tools() ──► passed to streamText({ tools })The MCP client lives on the server inside your AI SDK route handler. It connects to one or more MCP servers, calls tools() to get a tool map, and hands that map to streamText. assistant-ui's existing tool-call UI (ToolFallback, or toolkit entries with render) renders the results.
If you use a "use generative" toolkit, spread defineMcpToolkit({ ... })
in the toolkit and use AISDKToolkit in your route. It opens the MCP clients,
merges their tools with your toolkit, and closes them for you.
Setup
Install the MCP client
npm install @ai-sdk/mcpFor stdio transports (local dev only), also install the official MCP SDK:
npm install @modelcontextprotocol/sdkConnect to an MCP server
Set the server URL and any auth token your server requires:
MCP_SERVER_URL=https://your-mcp-server.example/mcp
MCP_TOKEN=...Then inside your AI SDK route handler, create the client with the transport that matches your server. HTTP is the production transport; SSE is the legacy streaming transport; stdio spawns a local process and is dev-only.
import { createMCPClient } from "@ai-sdk/mcp";
const mcpClient = await createMCPClient({
transport: {
type: "http",
url: process.env.MCP_SERVER_URL!,
headers: { Authorization: `Bearer ${process.env.MCP_TOKEN}` },
},
});For stdio:
import { createMCPClient } from "@ai-sdk/mcp";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const mcpClient = await createMCPClient({
transport: new StdioClientTransport({
command: "node",
args: ["./mcp-server/dist/index.js"],
}),
});Define MCP servers in your toolkit
In a generative toolkit, spread defineMcpToolkit({ ... }) with one entry per
MCP server. The entry key names the server connection; the MCP server publishes
the actual tool names.
"use generative";
import { defineMcpToolkit, defineToolkit } from "@assistant-ui/react";
export default defineToolkit({
...defineMcpToolkit({
github: {
type: "http",
url: "https://mcp.example.com/mcp",
},
}),
});Use AISDKToolkit in the route instead of generativeTools when the toolkit
contains MCP entries:
import { AISDKToolkit } from "@assistant-ui/react-ai-sdk";
import { openai } from "@ai-sdk/openai";
import { streamText, convertToModelMessages } from "ai";
import type { UIMessage } from "ai";
import toolkit from "../../toolkit";
export async function POST(req: Request) {
const { messages, tools }: { messages: UIMessage[]; tools?: Record<string, any> } =
await req.json();
const aiToolkit = new AISDKToolkit({ toolkit });
const result = streamText({
model: openai("gpt-5.4-mini"),
messages: await convertToModelMessages(messages),
tools: await aiToolkit.tools({ frontend: tools }),
onFinish: async () => {
await aiToolkit.close();
},
});
return result.toUIMessageStreamResponse();
}Wire the tools into the route
For manual MCP client control, mcpClient.tools() returns an object shaped
exactly like the tools argument of streamText. Spread it in alongside any of
your own tools, and close the client when the response finishes:
import { createMCPClient } from "@ai-sdk/mcp";
import { openai } from "@ai-sdk/openai";
import { streamText, convertToModelMessages } from "ai";
import type { UIMessage } from "ai";
export const maxDuration = 60;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const mcpClient = await createMCPClient({
transport: {
type: "http",
url: process.env.MCP_SERVER_URL!,
headers: { Authorization: `Bearer ${process.env.MCP_TOKEN}` },
},
});
const tools = await mcpClient.tools();
const result = streamText({
model: openai("gpt-5.4-mini"),
messages: await convertToModelMessages(messages),
tools,
onFinish: async () => {
await mcpClient.close();
},
});
return result.toUIMessageStreamResponse();
}onFinish is the right place to call close(): it fires after the stream completes, so the connection stays open as long as the model is still calling tools.
Combine multiple MCP servers
Each server has its own client. Spread their tool maps together:
const githubClient = await createMCPClient({
transport: { type: "http", url: process.env.GITHUB_MCP_URL! },
});
const filesClient = await createMCPClient({
transport: { type: "http", url: process.env.FILES_MCP_URL! },
});
const tools = {
...(await githubClient.tools()),
...(await filesClient.tools()),
};
// remember to close both in onFinishIf two servers expose tools with the same name, the later spread wins. Rename or scope as needed.
Render results in the UI
Tool calls flow through the existing assistant-ui tool-call rendering. With no
setup, the bundled <ToolFallback> component renders the call name, arguments,
and result. To customize the appearance for a specific tool in a generative
toolkit, add an externalTool() renderer whose key matches the MCP tool name:
"use generative";
import { defineMcpToolkit, defineToolkit, externalTool } from "@assistant-ui/react";
type Args = { repo: string; number: number };
type Result = { title: string; state: string; url: string };
export default defineToolkit({
...defineMcpToolkit({
github: { type: "http", url: "https://mcp.example.com/mcp" },
}),
github_get_issue: {
execute: externalTool(),
render: ({ args, result }: { args: Args; result?: Result }) => (
<div className="rounded border p-3">
<div className="font-mono text-sm">{args.repo}#{args.number}</div>
{result && (
<a href={result.url} className="underline">
{result.title} ({result.state})
</a>
)}
</div>
),
},
});import type { Toolkit } from "@assistant-ui/react-native";
import { Linking, Pressable, Text, View } from "react-native";
type Args = { repo: string; number: number };
type Result = { title: string; state: string; url: string };
export const toolkit = {
github_get_issue: {
type: "backend",
render: ({ args, result }: { args: Args; result?: Result }) => (
<View style={{ borderWidth: 1, borderRadius: 6, padding: 12 }}>
<Text style={{ fontFamily: "Menlo", fontSize: 13 }}>
{args.repo}#{args.number}
</Text>
{result && (
<Pressable onPress={() => Linking.openURL(result.url)}>
<Text style={{ textDecorationLine: "underline" }}>
{result.title} ({result.state})
</Text>
</Pressable>
)}
</View>
),
},
} satisfies Toolkit;import type { Toolkit } from "@assistant-ui/react-ink";
import { Box, Text } from "ink";
type Args = { repo: string; number: number };
type Result = { title: string; state: string; url: string };
export const toolkit = {
github_get_issue: {
type: "backend",
render: ({ args, result }: { args: Args; result?: Result }) => (
<Box borderStyle="round" paddingX={1} flexDirection="column">
<Text>
{args.repo}#{args.number}
</Text>
{result && (
<Text>
{result.title} ({result.state}) — {result.url}
</Text>
)}
</Box>
),
},
} satisfies Toolkit;Register the toolkit once with Tools({ toolkit }). Renderer keys such as
github_get_issue must match the tool names your MCP server publishes.
"use client";
import { AssistantRuntimeProvider, Tools, useAui } from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
import type { ReactNode } from "react";
import { toolkit } from "./GitHubIssueToolUI";
export function MyRuntimeProvider({ children }: { children: ReactNode }) {
const runtime = useChatRuntime({ api: "/api/chat" });
const aui = useAui({ tools: Tools({ toolkit }) });
return (
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
{children}
</AssistantRuntimeProvider>
);
}Run and verify
Start the app and trigger a tool call (e.g., ask the assistant to do something the MCP server can do). Confirm:
- The tool call appears in the chat with the expected arguments.
- The result renders (either via your custom
ToolUIor the fallback). - No connection leaks: the MCP client closes after each response. If you see open connections accumulating, check
onFinish.
Notes
- Server-side only. The MCP client uses Node APIs (sockets, optionally child processes). Never instantiate it in client code.
- Per-request lifecycle. A fresh client per request keeps connection state simple. For high-throughput servers, pool clients yourself with care: the AI SDK's
tools()call assumes the connection is alive whenstreamTextruns. - Sampling. If your MCP server uses
sampling/createMessage(lets the server ask the LLM mid-call), assistant-cloud users can instrument it viainstrumentMcpSamplingfor observability. This is independent of the wiring above. - Transport choice. HTTP for any networked server. SSE only if the server doesn't speak HTTP. stdio is for local development against an MCP server in your monorepo.