feat: support streameable http transport (#3413)

* feat: support Stremeable Http transport

* feat: add http to rpc method
This commit is contained in:
Alejandro Peral Taboada 2025-05-21 19:17:26 +02:00 committed by GitHub
parent 0870c65fa5
commit 8356e058c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 44 additions and 41 deletions

View File

@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Support Streameable Http Transport for MCPs

View File

@ -134,14 +134,6 @@ const baseConfig = {
aliasResolverPlugin, aliasResolverPlugin,
/* add to the end of plugins array */ /* add to the end of plugins array */
esbuildProblemMatcherPlugin, esbuildProblemMatcherPlugin,
{
name: "alias-plugin",
setup(build) {
build.onResolve({ filter: /^pkce-challenge$/ }, (args) => {
return { path: require.resolve("pkce-challenge/dist/index.browser.js") }
})
},
},
], ],
format: "cjs", format: "cjs",
sourcesContent: false, sourcesContent: false,

34
package-lock.json generated
View File

@ -19,7 +19,7 @@
"@grpc/grpc-js": "^1.9.15", "@grpc/grpc-js": "^1.9.15",
"@grpc/reflection": "^1.0.4", "@grpc/reflection": "^1.0.4",
"@mistralai/mistralai": "^1.5.0", "@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.7.0", "@modelcontextprotocol/sdk": "^1.11.1",
"@opentelemetry/api": "^1.4.1", "@opentelemetry/api": "^1.4.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.39.1", "@opentelemetry/exporter-trace-otlp-http": "^0.39.1",
"@opentelemetry/resources": "^1.30.1", "@opentelemetry/resources": "^1.30.1",
@ -7600,17 +7600,17 @@
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.7.0", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.7.0.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz",
"integrity": "sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA==", "integrity": "sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"content-type": "^1.0.5", "content-type": "^1.0.5",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-spawn": "^7.0.3",
"eventsource": "^3.0.2", "eventsource": "^3.0.2",
"express": "^5.0.1", "express": "^5.0.1",
"express-rate-limit": "^7.5.0", "express-rate-limit": "^7.5.0",
"pkce-challenge": "^4.1.0", "pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0", "raw-body": "^3.0.0",
"zod": "^3.23.8", "zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1" "zod-to-json-schema": "^3.24.1"
@ -21465,10 +21465,9 @@
} }
}, },
"node_modules/pkce-challenge": { "node_modules/pkce-challenge": {
"version": "4.1.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
"integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">=16.20.0" "node": ">=16.20.0"
} }
@ -31509,16 +31508,17 @@
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==" "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="
}, },
"@modelcontextprotocol/sdk": { "@modelcontextprotocol/sdk": {
"version": "1.7.0", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.7.0.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz",
"integrity": "sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA==", "integrity": "sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==",
"requires": { "requires": {
"content-type": "^1.0.5", "content-type": "^1.0.5",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-spawn": "^7.0.3",
"eventsource": "^3.0.2", "eventsource": "^3.0.2",
"express": "^5.0.1", "express": "^5.0.1",
"express-rate-limit": "^7.5.0", "express-rate-limit": "^7.5.0",
"pkce-challenge": "^4.1.0", "pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0", "raw-body": "^3.0.0",
"zod": "^3.23.8", "zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1" "zod-to-json-schema": "^3.24.1"
@ -41248,9 +41248,9 @@
"dev": true "dev": true
}, },
"pkce-challenge": { "pkce-challenge": {
"version": "4.1.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
"integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==" "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="
}, },
"pony-cause": { "pony-cause": {
"version": "1.1.1", "version": "1.1.1",

View File

@ -341,7 +341,7 @@
"@grpc/grpc-js": "^1.9.15", "@grpc/grpc-js": "^1.9.15",
"@grpc/reflection": "^1.0.4", "@grpc/reflection": "^1.0.4",
"@mistralai/mistralai": "^1.5.0", "@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.7.0", "@modelcontextprotocol/sdk": "^1.11.1",
"@opentelemetry/api": "^1.4.1", "@opentelemetry/api": "^1.4.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.39.1", "@opentelemetry/exporter-trace-otlp-http": "^0.39.1",
"@opentelemetry/resources": "^1.30.1", "@opentelemetry/resources": "^1.30.1",

View File

@ -30,6 +30,7 @@ import { arePathsEqual } from "@utils/path"
import { secondsToMs } from "@utils/time" import { secondsToMs } from "@utils/time"
import { GlobalFileNames } from "@core/storage/disk" import { GlobalFileNames } from "@core/storage/disk"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { ExtensionMessage } from "@shared/ExtensionMessage" import { ExtensionMessage } from "@shared/ExtensionMessage"
// Default timeout for internal MCP data requests in milliseconds; is not the same as the user facing timeout stored as DEFAULT_MCP_TIMEOUT_SECONDS // Default timeout for internal MCP data requests in milliseconds; is not the same as the user facing timeout stored as DEFAULT_MCP_TIMEOUT_SECONDS
@ -38,10 +39,10 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 5000
export type McpConnection = { export type McpConnection = {
server: McpServer server: McpServer
client: Client client: Client
transport: StdioClientTransport | SSEClientTransport transport: StdioClientTransport | SSEClientTransport | StreamableHTTPClientTransport
} }
export type McpTransportType = "stdio" | "sse" export type McpTransportType = "stdio" | "sse" | "http"
export type McpServerConfig = z.infer<typeof ServerConfigSchema> export type McpServerConfig = z.infer<typeof ServerConfigSchema>
@ -54,22 +55,23 @@ const BaseConfigSchema = z.object({
}) })
const SseConfigSchema = BaseConfigSchema.extend({ const SseConfigSchema = BaseConfigSchema.extend({
transportType: z.literal("sse"),
url: z.string().url(), url: z.string().url(),
}).transform((config) => ({ })
...config,
transportType: "sse" as const,
}))
const StdioConfigSchema = BaseConfigSchema.extend({ const StdioConfigSchema = BaseConfigSchema.extend({
transportType: z.literal("stdio"),
command: z.string(), command: z.string(),
args: z.array(z.string()).optional(), args: z.array(z.string()).optional(),
env: z.record(z.string()).optional(), env: z.record(z.string()).optional(),
}).transform((config) => ({ })
...config,
transportType: "stdio" as const,
}))
const ServerConfigSchema = z.union([StdioConfigSchema, SseConfigSchema]) const StreamableHTTPConfigSchema = BaseConfigSchema.extend({
transportType: z.literal("http"),
url: z.string().url(),
})
const ServerConfigSchema = z.discriminatedUnion("transportType", [StdioConfigSchema, SseConfigSchema, StreamableHTTPConfigSchema])
const McpSettingsSchema = z.object({ const McpSettingsSchema = z.object({
mcpServers: z.record(ServerConfigSchema), mcpServers: z.record(ServerConfigSchema),
@ -183,7 +185,7 @@ export class McpHub {
private async connectToServerRPC( private async connectToServerRPC(
name: string, name: string,
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>, config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema> | z.infer<typeof StreamableHTTPConfigSchema>,
): Promise<void> { ): Promise<void> {
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand) // Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
this.connections = this.connections.filter((conn) => conn.server.name !== name) this.connections = this.connections.filter((conn) => conn.server.name !== name)
@ -200,10 +202,12 @@ export class McpHub {
}, },
) )
let transport: StdioClientTransport | SSEClientTransport let transport: StdioClientTransport | SSEClientTransport | StreamableHTTPClientTransport
if (config.transportType === "sse") { if (config.transportType === "sse") {
transport = new SSEClientTransport(new URL(config.url), {}) transport = new SSEClientTransport(new URL(config.url), {})
} else if (config.transportType === "http") {
transport = new StreamableHTTPClientTransport(new URL(config.url), {})
} else { } else {
transport = new StdioClientTransport({ transport = new StdioClientTransport({
command: config.command, command: config.command,
@ -297,7 +301,7 @@ export class McpHub {
private async connectToServer( private async connectToServer(
name: string, name: string,
config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema>, config: z.infer<typeof StdioConfigSchema> | z.infer<typeof SseConfigSchema> | z.infer<typeof StreamableHTTPConfigSchema>,
): Promise<void> { ): Promise<void> {
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand) // Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
this.connections = this.connections.filter((conn) => conn.server.name !== name) this.connections = this.connections.filter((conn) => conn.server.name !== name)
@ -314,10 +318,12 @@ export class McpHub {
}, },
) )
let transport: StdioClientTransport | SSEClientTransport let transport: StdioClientTransport | SSEClientTransport | StreamableHTTPClientTransport
if (config.transportType === "sse") { if (config.transportType === "sse") {
transport = new SSEClientTransport(new URL(config.url), {}) transport = new SSEClientTransport(new URL(config.url), {})
} else if (config.transportType === "http") {
transport = new StreamableHTTPClientTransport(new URL(config.url), {})
} else { } else {
transport = new StdioClientTransport({ transport = new StdioClientTransport({
command: config.command, command: config.command,