Deduplicated semantically identical MCP endpoints across provider-specific names while preserving provider priority and canonical direct names. Kept the first registration on sanitized tool-name collisions and logged both origins. Fixes #6786
98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
/**
|
|
* MCP (Model Context Protocol) Servers Capability
|
|
*
|
|
* Canonical shape for MCP server configurations, regardless of source format.
|
|
* All providers translate their native format to this shape.
|
|
*/
|
|
import { defineCapability } from ".";
|
|
import type { SourceMeta } from "./types";
|
|
|
|
/**
|
|
* Canonical MCP server configuration.
|
|
*/
|
|
export interface MCPServer {
|
|
/** Server name (unique key) */
|
|
name: string;
|
|
/** Whether this server is enabled (default: true) */
|
|
enabled?: boolean;
|
|
/** Connection timeout in milliseconds */
|
|
timeout?: number;
|
|
/** Command to run (for stdio transport) */
|
|
command?: string;
|
|
/** Command arguments */
|
|
args?: string[];
|
|
/** Environment variables */
|
|
env?: Record<string, string>;
|
|
/** Working directory for stdio transport */
|
|
cwd?: string;
|
|
/** URL (for HTTP/SSE transport) */
|
|
url?: string;
|
|
/** HTTP headers (for HTTP transport) */
|
|
headers?: Record<string, string>;
|
|
/** Authentication configuration */
|
|
auth?: {
|
|
type: "oauth" | "apikey";
|
|
credentialId?: string;
|
|
tokenUrl?: string;
|
|
clientId?: string;
|
|
clientSecret?: string;
|
|
resource?: string;
|
|
};
|
|
/** OAuth configuration (clientId, clientSecret, redirectUri, callbackPort, callbackPath, prompt) for servers requiring explicit client credentials */
|
|
oauth?: {
|
|
clientId?: string;
|
|
clientSecret?: string;
|
|
redirectUri?: string;
|
|
callbackPort?: number;
|
|
callbackPath?: string;
|
|
prompt?: string;
|
|
};
|
|
/** Transport type */
|
|
transport?: "stdio" | "sse" | "http";
|
|
/** Source metadata (added by loader) */
|
|
_source: SourceMeta;
|
|
}
|
|
|
|
/** Compare the transport inputs that determine which MCP endpoint gets connected. */
|
|
function isSameMCPConnection(left: MCPServer, right: MCPServer): boolean {
|
|
if (!Bun.deepEquals(left.auth, right.auth) || !Bun.deepEquals(left.oauth, right.oauth)) return false;
|
|
|
|
const leftTransport = left.transport ?? (left.command ? "stdio" : left.url ? "http" : "stdio");
|
|
const rightTransport = right.transport ?? (right.command ? "stdio" : right.url ? "http" : "stdio");
|
|
if (leftTransport !== rightTransport) return false;
|
|
|
|
if (leftTransport === "stdio") {
|
|
return (
|
|
left.command === right.command &&
|
|
Bun.deepEquals(left.args, right.args) &&
|
|
Bun.deepEquals(left.env, right.env) &&
|
|
left.cwd === right.cwd
|
|
);
|
|
}
|
|
|
|
return left.url === right.url && Bun.deepEquals(left.headers, right.headers);
|
|
}
|
|
|
|
export const mcpCapability = defineCapability<MCPServer>({
|
|
id: "mcps",
|
|
displayName: "MCP Servers",
|
|
description: "Model Context Protocol server configurations for external tool integrations",
|
|
key: server => server.name,
|
|
equivalent: isSameMCPConnection,
|
|
toExtensionId: server => `mcp:${server.name}`,
|
|
validate: server => {
|
|
if (!server.name) return "Missing server name";
|
|
if (!server.command && !server.url) return "Must have command or url";
|
|
|
|
// Validate transport-endpoint pairing
|
|
if (server.transport === "stdio" && !server.command) {
|
|
return "stdio transport requires command field";
|
|
}
|
|
if ((server.transport === "http" || server.transport === "sse") && !server.url) {
|
|
return "http/sse transport requires url field";
|
|
}
|
|
|
|
return undefined;
|
|
},
|
|
});
|