Skip to content

Commit 7d94875

Browse files
Add MCP client with VS Code-compatible configuration
- Added @ai-sdk/mcp package for Model Context Protocol support - Created MCPClientManager for managing multiple MCP server connections - Implemented config loader with VS Code-compatible mcp.json format - Added environment variable substitution syntax - Created DynamicToolRenderer component for rendering MCP tools in chat UI - Integrated dynamic tool rendering in message.tsx - Added comprehensive documentation in docs/MCP.md - Included mcp.json.example with HTTP transport example - Added mcp.json to .gitignore for security Addresses #1368
1 parent 22de923 commit 7d94875

File tree

12 files changed

+622
-1
lines changed

12 files changed

+622
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ yarn-error.log*
3030
.env.test.local
3131
.env.production.local
3232

33+
# MCP configuration (may contain sensitive tokens)
34+
mcp.json
35+
3336
# turbo
3437
.turbo
3538

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"use client";
2+
import type { UseChatHelpers } from "@ai-sdk/react";
3+
import type { ChatMessage } from "@/lib/types";
4+
import {
5+
Tool,
6+
ToolContent,
7+
ToolHeader,
8+
ToolInput,
9+
ToolOutput,
10+
} from "./elements/tool";
11+
12+
type DynamicToolPartType = "dynamic-tool-call" | "dynamic-tool-result";
13+
14+
type DynamicToolPart = {
15+
type: DynamicToolPartType;
16+
toolCallId: string;
17+
toolName: string;
18+
state:
19+
| "input-available"
20+
| "output-available"
21+
| "approval-requested"
22+
| "approval-responded"
23+
| "output-denied";
24+
input?: any;
25+
output?: any;
26+
approval?: {
27+
id: string;
28+
approved?: boolean;
29+
};
30+
};
31+
32+
type DynamicToolRendererProps = {
33+
part: DynamicToolPart;
34+
addToolApprovalResponse: UseChatHelpers<ChatMessage>["addToolApprovalResponse"];
35+
};
36+
37+
export function DynamicToolRenderer({
38+
part,
39+
addToolApprovalResponse,
40+
}: DynamicToolRendererProps) {
41+
const { toolCallId, toolName, state } = part;
42+
const approvalId = part.approval?.id;
43+
const isDenied =
44+
state === "output-denied" ||
45+
(state === "approval-responded" && part.approval?.approved === false);
46+
47+
// Format tool name for display
48+
const displayName = toolName.replace(/:/g, " › ");
49+
50+
if (isDenied) {
51+
return (
52+
<Tool className="w-full" defaultOpen={true}>
53+
<ToolHeader state="output-denied" type={toolName as any} />
54+
<ToolContent>
55+
<div className="px-4 py-3 text-muted-foreground text-sm">
56+
Tool execution was denied.
57+
</div>
58+
</ToolContent>
59+
</Tool>
60+
);
61+
}
62+
63+
if (state === "output-available") {
64+
return (
65+
<Tool className="w-full" defaultOpen={true}>
66+
<ToolHeader state={state} type={toolName as any} />
67+
<ToolContent>
68+
{part.input && <ToolInput input={part.input} />}
69+
{part.output && (
70+
<ToolOutput
71+
errorText={
72+
typeof part.output === "object" && "error" in part.output
73+
? String(part.output.error)
74+
: undefined
75+
}
76+
output={
77+
<div className="overflow-auto">
78+
<pre className="whitespace-pre-wrap break-words rounded bg-muted p-3 font-mono text-xs">
79+
{JSON.stringify(part.output, null, 2)}
80+
</pre>
81+
</div>
82+
}
83+
/>
84+
)}
85+
</ToolContent>
86+
</Tool>
87+
);
88+
}
89+
90+
if (state === "approval-responded") {
91+
return (
92+
<Tool className="w-full" defaultOpen={true}>
93+
<ToolHeader state={state} type={toolName as any} />
94+
<ToolContent>
95+
<ToolInput input={part.input} />
96+
</ToolContent>
97+
</Tool>
98+
);
99+
}
100+
101+
// input-available or approval-requested
102+
return (
103+
<Tool className="w-full" defaultOpen={true}>
104+
<ToolHeader state={state} type={toolName as any} />
105+
<ToolContent>
106+
{(state === "input-available" || state === "approval-requested") && (
107+
<ToolInput input={part.input} />
108+
)}
109+
{state === "approval-requested" && approvalId && (
110+
<div className="flex items-center justify-end gap-2 border-t px-4 py-3">
111+
<button
112+
className="rounded-md px-3 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground"
113+
onClick={() => {
114+
addToolApprovalResponse({
115+
id: approvalId,
116+
approved: false,
117+
reason: `User denied ${displayName}`,
118+
});
119+
}}
120+
type="button"
121+
>
122+
Deny
123+
</button>
124+
<button
125+
className="rounded-md bg-primary px-3 py-1.5 text-primary-foreground text-sm transition-colors hover:bg-primary/90"
126+
onClick={() => {
127+
addToolApprovalResponse({
128+
id: approvalId,
129+
approved: true,
130+
});
131+
}}
132+
type="button"
133+
>
134+
Allow
135+
</button>
136+
</div>
137+
)}
138+
</ToolContent>
139+
</Tool>
140+
);
141+
}

components/message.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { cn, sanitizeText } from "@/lib/utils";
88
import { useDataStream } from "./data-stream-provider";
99
import { DocumentToolResult } from "./document";
1010
import { DocumentPreview } from "./document-preview";
11+
import { DynamicToolRenderer } from "./dynamic-tool-renderer";
1112
import { MessageContent } from "./elements/message";
1213
import { Response } from "./elements/response";
1314
import {
@@ -339,6 +340,22 @@ const PurePreviewMessage = ({
339340
);
340341
}
341342

343+
// Handle dynamic tools from MCP servers
344+
if (
345+
type.startsWith("tool-") &&
346+
!["tool-getWeather", "tool-createDocument", "tool-updateDocument", "tool-requestSuggestions"].includes(type)
347+
) {
348+
// This is a dynamic tool
349+
const dynamicPart = part as any;
350+
return (
351+
<DynamicToolRenderer
352+
addToolApprovalResponse={addToolApprovalResponse}
353+
key={dynamicPart.toolCallId}
354+
part={dynamicPart}
355+
/>
356+
);
357+
}
358+
342359
return null;
343360
})}
344361

docs/MCP.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# MCP (Model Context Protocol) Integration
2+
3+
This project supports connecting to MCP servers using VS Code-compatible configuration.
4+
5+
## Setup
6+
7+
1. Copy the example configuration:
8+
```bash
9+
cp mcp.json.example mcp.json
10+
```
11+
12+
2. Edit `mcp.json` to configure your MCP servers:
13+
```json
14+
{
15+
"servers": {
16+
"github": {
17+
"command": "docker",
18+
"args": ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
19+
"env": {
20+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}"
21+
},
22+
"type": "stdio"
23+
}
24+
},
25+
"inputs": [
26+
{
27+
"id": "github_token",
28+
"type": "promptString",
29+
"description": "GitHub Personal Access Token",
30+
"password": true
31+
}
32+
]
33+
}
34+
```
35+
36+
3. Set input values in your application code before connecting to servers.
37+
38+
## Usage
39+
40+
### Server-side
41+
42+
```typescript
43+
import { MCPClientManager, loadMCPConfig } from "@/lib/mcp";
44+
45+
// Load configuration
46+
const config = await loadMCPConfig("mcp.json");
47+
48+
// Create manager
49+
const mcpManager = new MCPClientManager(config);
50+
51+
// Set input values (e.g., from user prompts or environment)
52+
mcpManager.setInputValue("github_token", process.env.GITHUB_TOKEN);
53+
54+
// Connect to a server
55+
await mcpManager.connect("github");
56+
57+
// Get all tools from connected servers
58+
const tools = await mcpManager.getAllTools();
59+
60+
// Use tools with AI SDK
61+
import { generateText } from "ai";
62+
63+
const result = await generateText({
64+
model: yourModel,
65+
prompt: "List my GitHub repositories",
66+
tools,
67+
});
68+
```
69+
70+
### Client-side
71+
72+
Dynamic tools from MCP servers are automatically rendered in the chat UI using the `DynamicToolRenderer` component.
73+
74+
## Supported Transports
75+
76+
**Currently supported (beta):**
77+
- `http`: HTTP transport for remote MCP servers
78+
- `sse`: Server-Sent Events transport
79+
80+
**Coming in AI SDK 6 stable:**
81+
- `stdio`: Local process communication (Docker, npx, Node.js)
82+
83+
### HTTP Transport Example
84+
```json
85+
{
86+
"servers": {
87+
"my-server": {
88+
"url": "https://your-mcp-server.com/mcp",
89+
"env": {
90+
"Authorization": "Bearer ${input:api_token}"
91+
},
92+
"type": "http"
93+
}
94+
}
95+
}
96+
```
97+
98+
### stdio Transport (Future)
99+
Once AI SDK 6 stable is released with full stdio support:
100+
```json
101+
{
102+
"servers": {
103+
"github": {
104+
"command": "docker",
105+
"args": ["run", "-i", "--rm", "ghcr.io/github/github-mcp-server"],
106+
"type": "stdio"
107+
}
108+
}
109+
}
110+
```
111+
112+
## Configuration Format
113+
114+
The configuration format is compatible with VS Code's MCP settings:
115+
116+
- `servers`: Object mapping server names to configurations
117+
- `command`: Command to execute (e.g., "docker", "npx", "node")
118+
- `args`: Array of command arguments
119+
- `env`: Optional environment variables (supports `${input:id}` substitution)
120+
- `type`: Transport type ("stdio")
121+
122+
- `inputs`: Optional array of input prompts
123+
- `id`: Unique identifier referenced in env vars
124+
- `type`: Input type ("promptString")
125+
- `description`: Human-readable description
126+
- `password`: Whether to hide input (boolean)
127+
128+
## Examples
129+
130+
### Filesystem MCP Server
131+
```json
132+
{
133+
"servers": {
134+
"filesystem": {
135+
"command": "npx",
136+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/directory"],
137+
"type": "stdio"
138+
}
139+
}
140+
}
141+
```
142+
143+
### Multiple Servers
144+
```json
145+
{
146+
"servers": {
147+
"github": {
148+
"command": "docker",
149+
"args": ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
150+
"env": {
151+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}"
152+
},
153+
"type": "stdio"
154+
},
155+
"database": {
156+
"command": "npx",
157+
"args": ["-y", "@modelcontextprotocol/server-postgres"],
158+
"env": {
159+
"DATABASE_URL": "${input:db_url}"
160+
},
161+
"type": "stdio"
162+
}
163+
},
164+
"inputs": [
165+
{
166+
"id": "github_token",
167+
"type": "promptString",
168+
"description": "GitHub Personal Access Token",
169+
"password": true
170+
},
171+
{
172+
"id": "db_url",
173+
"type": "promptString",
174+
"description": "Database Connection URL",
175+
"password": true
176+
}
177+
]
178+
}
179+
```
180+
181+
## Security Notes
182+
183+
- Never commit `mcp.json` with actual tokens/credentials
184+
- Use environment variables or secure input prompts for sensitive data
185+
- The `mcp.json` file is automatically ignored by git

0 commit comments

Comments
 (0)