Skip to content

Conversation

@blacksheep-git
Copy link
Contributor

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:

    • Dynamic server connection/disconnection
    • Environment variable substitution (${input:id} syntax)
    • Tool aggregation from multiple servers
    • Connection pooling and lifecycle management
  • Configuration (lib/mcp/config.ts, lib/mcp/types.ts):

    • VS Code-compatible mcp.json format
    • Validation and parsing
    • Support for HTTP, SSE, and stdio (future) transports

UI Integration

  • DynamicToolRenderer (components/dynamic-tool-renderer.tsx):

    • Generic renderer for any MCP tool
    • Displays tool input/output in collapsible UI
    • Supports tool approval flow
    • Follows existing Tool/ToolHeader/ToolContent pattern
  • Message Component (components/message.tsx):

    • Integrated dynamic tool rendering
    • Handles tools from MCP servers alongside static tools

Documentation & Examples

  • Comprehensive MCP guide (docs/MCP.md)
  • Example configuration (mcp.json.example)
  • Security best practices
  • Usage examples for server and client

Transport Support

Currently Supported (Beta):

  • ✅ HTTP transport for remote MCP servers
  • ✅ SSE (Server-Sent Events) transport

Ready for AI SDK 6 Stable:

  • 🔄 stdio transport (Docker, npx, Node.js) - code is prepared but awaits stable SDK release

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

  • Added mcp.json to .gitignore to prevent accidental credential commits
  • Environment variable substitution for secure token handling
  • Input prompts marked as password fields

Testing

  • ✅ TypeScript compilation successful
  • ✅ Production build successful
  • ✅ All existing functionality preserved

Future Enhancements

Once AI SDK 6 stable releases with stdio transport support, the implementation will automatically support:

  • Docker-based MCP servers
  • npx package MCP servers
  • Local Node.js MCP servers

Closes #1368

- 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
Copilot AI review requested due to automatic review settings December 23, 2025 04:49
@vercel
Copy link
Contributor

vercel bot commented Dec 23, 2025

@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.

@socket-security
Copy link

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​ai-sdk/​mcp@​1.0.0971007398100

View full report

Copy link
Contributor

Copilot AI left a 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/mcp package 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.

Comment on lines +1 to +9
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";
};
Copy link

Copilot AI Dec 23, 2025

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.

Suggested change
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;
};

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +40
if (!Array.isArray(serverConfig.args)) {
throw new Error(
`Invalid MCP config: server '${serverName}' 'args' must be an array for stdio transport`
);
}
Copy link

Copilot AI Dec 23, 2025

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.

Copilot uses AI. Check for mistakes.
: path.join(process.cwd(), configPath);

const content = await fs.readFile(absolutePath, "utf-8");
const config = JSON.parse(content) as MCPConfig;
Copy link

Copilot AI Dec 23, 2025

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.

Suggested change
const config = JSON.parse(content) as MCPConfig;
const config = JSON.parse(content) as MCPConfig;
validateMCPConfig(config);

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +76
const client = await createMCPClient({
transport: {
type: serverConfig.type as "http" | "sse",
url: (serverConfig as any).url || "",
Copy link

Copilot AI Dec 23, 2025

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'.

Suggested change
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,

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +45
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;
}
Copy link

Copilot AI Dec 23, 2025

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 $). This means environment variables that contain the pattern as part of a larger string won't be substituted. Consider whether this limitation is intentional or if partial substitutions should be supported (e.g., "Bearer ${input:token}" should work).

Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +132
await Promise.all(
Array.from(this.clients.keys()).map((name) => this.disconnect(name))
);
Copy link

Copilot AI Dec 23, 2025

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.

Suggested change
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"
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +104
<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} />
Copy link

Copilot AI Dec 23, 2025

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.

Copilot uses AI. Check for mistakes.
<Tool className="w-full" defaultOpen={true}>
<ToolHeader state={state} type={toolName as any} />
<ToolContent>
<ToolInput input={part.input} />
Copy link

Copilot AI Dec 23, 2025

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.

Suggested change
<ToolInput input={part.input} />
{part.input && <ToolInput input={part.input} />}

Copilot uses AI. Check for mistakes.
!["tool-getWeather", "tool-createDocument", "tool-updateDocument", "tool-requestSuggestions"].includes(type)
) {
// This is a dynamic tool
const dynamicPart = part as any;
Copy link

Copilot AI Dec 23, 2025

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'.

Copilot uses AI. Check for mistakes.
part,
addToolApprovalResponse,
}: DynamicToolRendererProps) {
const { toolCallId, toolName, state } = part;
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable toolCallId.

Suggested change
const { toolCallId, toolName, state } = part;
const { toolName, state } = part;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add MCP Client with VS Code-Compatible Configuration

1 participant