-
Notifications
You must be signed in to change notification settings - Fork 6.2k
Add MCP Client with VS Code-Compatible Configuration #1370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- 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 vercel#1368
|
@blacksheep-git is attempting to deploy a commit to the v0-evals-vtest314 Team on Vercel. A member of the Team first needs to authorize it. |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements MCP (Model Context Protocol) client support with VS Code-compatible configuration, enabling the application to connect to multiple MCP servers and expose their tools to the AI chat interface. The implementation currently supports HTTP and SSE transports in beta, with stdio transport prepared for when AI SDK 6 stable is released.
- Added
@ai-sdk/mcppackage dependency (v1.0.0) for MCP client functionality - Implemented MCPClientManager for managing multiple MCP server connections with environment variable substitution
- Created DynamicToolRenderer component to render tools from MCP servers in the chat UI
Reviewed changes
Copilot reviewed 9 out of 12 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Added @ai-sdk/mcp v1.0.0 dependency |
| pnpm-lock.yaml | Updated lockfile with new MCP SDK dependencies and transitive packages |
| lib/mcp/types.ts | Defined TypeScript types for MCP configuration (servers, inputs, transport types) |
| lib/mcp/config.ts | Implemented configuration loading and validation functions |
| lib/mcp/client.ts | Core MCPClientManager class for connection pooling, environment substitution, and tool aggregation |
| lib/mcp/index.ts | Public API exports for MCP module |
| components/dynamic-tool-renderer.tsx | React component for rendering dynamic MCP tools with approval flow |
| components/message.tsx | Integrated dynamic tool renderer for MCP tools in chat messages |
| mcp.json.example | Example configuration file showing HTTP transport setup |
| docs/MCP.md | Comprehensive documentation covering setup, usage, and examples |
| .gitignore | Added mcp.json to prevent credential exposure |
| next-env.d.ts | Auto-generated Next.js type reference path update |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export type MCPServerConfig = { | ||
| // stdio transport (will be available in AI SDK 6 stable) | ||
| command?: string; | ||
| args?: string[]; | ||
| // http/sse transport (currently supported in beta) | ||
| url?: string; | ||
| env?: Record<string, string>; | ||
| type: "stdio" | "http" | "sse"; | ||
| }; |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MCPServerConfig type makes all properties optional (command, args, url), but the validation logic and connect method expect specific properties based on transport type. Consider using discriminated unions to enforce that http/sse require 'url' and stdio requires 'command'. This would make the type system catch configuration errors at compile time rather than runtime.
| export type MCPServerConfig = { | |
| // stdio transport (will be available in AI SDK 6 stable) | |
| command?: string; | |
| args?: string[]; | |
| // http/sse transport (currently supported in beta) | |
| url?: string; | |
| env?: Record<string, string>; | |
| type: "stdio" | "http" | "sse"; | |
| }; | |
| export type MCPServerConfig = | |
| | { | |
| // stdio transport (will be available in AI SDK 6 stable) | |
| type: "stdio"; | |
| command: string; | |
| args?: string[]; | |
| env?: Record<string, string>; | |
| // stdio transport does not use URL | |
| url?: never; | |
| } | |
| | { | |
| // http/sse transport (currently supported in beta) | |
| type: "http" | "sse"; | |
| url: string; | |
| env?: Record<string, string>; | |
| // http/sse transport does not use command/args | |
| command?: never; | |
| args?: never; | |
| }; |
| if (!Array.isArray(serverConfig.args)) { | ||
| throw new Error( | ||
| `Invalid MCP config: server '${serverName}' 'args' must be an array for stdio transport` | ||
| ); | ||
| } |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validation logic checks if args is an array, but it doesn't verify that args exists. This will pass validation even when args is undefined for stdio transport. Consider changing the condition to check if args exists first, or require args to be present for stdio transport.
| : path.join(process.cwd(), configPath); | ||
|
|
||
| const content = await fs.readFile(absolutePath, "utf-8"); | ||
| const config = JSON.parse(content) as MCPConfig; |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loadMCPConfig function doesn't call validateMCPConfig. Consider either calling validation automatically within loadMCPConfig or documenting that callers must manually validate the configuration after loading it.
| const config = JSON.parse(content) as MCPConfig; | |
| const config = JSON.parse(content) as MCPConfig; | |
| validateMCPConfig(config); |
| const client = await createMCPClient({ | ||
| transport: { | ||
| type: serverConfig.type as "http" | "sse", | ||
| url: (serverConfig as any).url || "", |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using type assertion 'as any' bypasses type safety. The serverConfig should already have a 'url' property for http/sse transports based on the validation. Consider using a more specific type assertion or restructuring to avoid the need for 'any'.
| const client = await createMCPClient({ | |
| transport: { | |
| type: serverConfig.type as "http" | "sse", | |
| url: (serverConfig as any).url || "", | |
| const transportType: "http" | "sse" = | |
| serverConfig.type === "http" ? "http" : "sse"; | |
| const url = | |
| "url" in serverConfig && typeof serverConfig.url === "string" | |
| ? serverConfig.url | |
| : ""; | |
| const client = await createMCPClient({ | |
| transport: { | |
| type: transportType, | |
| url, |
| const match = value.match(/^\$\{input:(.+)\}$/); | ||
| if (match) { | ||
| const inputId = match[1]; | ||
| const inputValue = this.inputValues.get(inputId); | ||
| if (!inputValue) { | ||
| throw new Error( | ||
| `Missing input value for '${inputId}'. Call setInputValue() first.` | ||
| ); | ||
| } | ||
| result[key] = inputValue; | ||
| } else { | ||
| result[key] = value; | ||
| } |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The substituteEnvVars method only matches the exact pattern '${input:id}' (full string match with ^ and
| const match = value.match(/^\$\{input:(.+)\}$/); | |
| if (match) { | |
| const inputId = match[1]; | |
| const inputValue = this.inputValues.get(inputId); | |
| if (!inputValue) { | |
| throw new Error( | |
| `Missing input value for '${inputId}'. Call setInputValue() first.` | |
| ); | |
| } | |
| result[key] = inputValue; | |
| } else { | |
| result[key] = value; | |
| } | |
| const substitutedValue = value.replace( | |
| /\$\{input:([^}]+)\}/g, | |
| (fullMatch, inputId: string) => { | |
| const inputValue = this.inputValues.get(inputId); | |
| if (inputValue === undefined) { | |
| throw new Error( | |
| `Missing input value for '${inputId}'. Call setInputValue() first.` | |
| ); | |
| } | |
| return inputValue; | |
| } | |
| ); | |
| result[key] = substitutedValue; |
| await Promise.all( | ||
| Array.from(this.clients.keys()).map((name) => this.disconnect(name)) | ||
| ); |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The disconnectAll method doesn't handle errors from individual disconnect operations. If one client.close() fails, it could prevent cleanup of other clients. Consider using Promise.allSettled instead of Promise.all to ensure all disconnect attempts are made even if some fail, and potentially log or collect errors.
| await Promise.all( | |
| Array.from(this.clients.keys()).map((name) => this.disconnect(name)) | |
| ); | |
| const disconnectPromises = Array.from(this.clients.keys()).map((name) => | |
| this.disconnect(name) | |
| ); | |
| const results = await Promise.allSettled(disconnectPromises); | |
| const failures = results.filter( | |
| (result): result is PromiseRejectedResult => result.status === "rejected" | |
| ); | |
| if (failures.length > 0) { | |
| throw new AggregateError( | |
| failures.map((failure) => failure.reason), | |
| "Failed to disconnect from one or more MCP clients" | |
| ); | |
| } |
| <ToolHeader state="output-denied" type={toolName as any} /> | ||
| <ToolContent> | ||
| <div className="px-4 py-3 text-muted-foreground text-sm"> | ||
| Tool execution was denied. | ||
| </div> | ||
| </ToolContent> | ||
| </Tool> | ||
| ); | ||
| } | ||
|
|
||
| if (state === "output-available") { | ||
| return ( | ||
| <Tool className="w-full" defaultOpen={true}> | ||
| <ToolHeader state={state} type={toolName as any} /> | ||
| <ToolContent> | ||
| {part.input && <ToolInput input={part.input} />} | ||
| {part.output && ( | ||
| <ToolOutput | ||
| errorText={ | ||
| typeof part.output === "object" && "error" in part.output | ||
| ? String(part.output.error) | ||
| : undefined | ||
| } | ||
| output={ | ||
| <div className="overflow-auto"> | ||
| <pre className="whitespace-pre-wrap break-words rounded bg-muted p-3 font-mono text-xs"> | ||
| {JSON.stringify(part.output, null, 2)} | ||
| </pre> | ||
| </div> | ||
| } | ||
| /> | ||
| )} | ||
| </ToolContent> | ||
| </Tool> | ||
| ); | ||
| } | ||
|
|
||
| if (state === "approval-responded") { | ||
| return ( | ||
| <Tool className="w-full" defaultOpen={true}> | ||
| <ToolHeader state={state} type={toolName as any} /> | ||
| <ToolContent> | ||
| <ToolInput input={part.input} /> | ||
| </ToolContent> | ||
| </Tool> | ||
| ); | ||
| } | ||
|
|
||
| // input-available or approval-requested | ||
| return ( | ||
| <Tool className="w-full" defaultOpen={true}> | ||
| <ToolHeader state={state} type={toolName as any} /> |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple uses of 'as any' type assertions bypass type safety. The ToolHeader component's 'type' prop expects a specific type, but dynamic tool names may not match that type. Consider updating ToolHeader to accept dynamic tool names, or create a separate header component for dynamic tools.
| <Tool className="w-full" defaultOpen={true}> | ||
| <ToolHeader state={state} type={toolName as any} /> | ||
| <ToolContent> | ||
| <ToolInput input={part.input} /> |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When state is "approval-responded", the component renders the input but doesn't check if part.input exists. This could result in ToolInput receiving undefined, which may cause an error. Consider adding a conditional check similar to line 68.
| <ToolInput input={part.input} /> | |
| {part.input && <ToolInput input={part.input} />} |
| !["tool-getWeather", "tool-createDocument", "tool-updateDocument", "tool-requestSuggestions"].includes(type) | ||
| ) { | ||
| // This is a dynamic tool | ||
| const dynamicPart = part as any; |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using 'as any' bypasses type safety. The part should already have the expected structure based on the type check. Consider defining a proper type for dynamic tool parts and using a type guard function instead of 'as any'.
| part, | ||
| addToolApprovalResponse, | ||
| }: DynamicToolRendererProps) { | ||
| const { toolCallId, toolName, state } = part; |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable toolCallId.
| const { toolCallId, toolName, state } = part; | |
| const { toolName, state } = part; |
Summary
Implements MCP (Model Context Protocol) client with VS Code-compatible configuration format, enabling connection to multiple MCP servers and exposing their tools to the AI chat.
Changes
Core MCP Implementation
MCPClientManager (
lib/mcp/client.ts): Manages multiple MCP server connections with support for:${input:id}syntax)Configuration (
lib/mcp/config.ts,lib/mcp/types.ts):mcp.jsonformatUI Integration
DynamicToolRenderer (
components/dynamic-tool-renderer.tsx):Message Component (
components/message.tsx):Documentation & Examples
docs/MCP.md)mcp.json.example)Transport Support
Currently Supported (Beta):
Ready for AI SDK 6 Stable:
Configuration Example
{ "servers": { "my-server": { "url": "https://your-mcp-server.com/mcp", "env": { "Authorization": "Bearer ${input:api_token}" }, "type": "http" } }, "inputs": [ { "id": "api_token", "type": "promptString", "description": "API Token", "password": true } ] }Security
mcp.jsonto.gitignoreto prevent accidental credential commitsTesting
Future Enhancements
Once AI SDK 6 stable releases with stdio transport support, the implementation will automatically support:
Closes #1368