diff --git a/package-lock.json b/package-lock.json index 165e43ca1f5..d0d76f5c7b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "node-pty": "1.1.0-beta35", "open": "^10.1.2", "tas-client-umd": "0.2.0", + "undici": "^7.9.0", "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", diff --git a/package.json b/package.json index a5a82086a58..4caed473562 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "node-pty": "1.1.0-beta35", "open": "^10.1.2", "tas-client-umd": "0.2.0", + "undici": "^7.9.0", "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index 5ec3de8aca2..f953343470d 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -33,6 +33,7 @@ export interface IExtHostMpcService extends ExtHostMcpShape { export class ExtHostMcpService extends Disposable implements IExtHostMpcService { protected _proxy: MainThreadMcpShape; + protected _mcpHttpHandleType = McpHTTPHandle; private readonly _initialProviderPromises = new Set>(); private readonly _sseEventSources = this._register(new DisposableMap()); private readonly _unresolvedMcpServers = new Map(); private _mode: HttpModeT = { value: HttpMode.Unknown }; @@ -753,7 +754,7 @@ class McpHTTPHandle extends Disposable { let currentUrl = url; let response!: Response; for (let redirectCount = 0; redirectCount < MAX_FOLLOW_REDIRECTS; redirectCount++) { - response = await fetch(currentUrl, { + response = await this._fetchInternal(currentUrl, { ...init, signal: this._abortCtrl.signal, redirect: 'manual' @@ -790,6 +791,10 @@ class McpHTTPHandle extends Disposable { return response; } + + protected _fetchInternal(url: string, init?: CommonRequestInit): Promise { + return fetch(url, init); + } } interface MinimalRequestInit { @@ -798,6 +803,11 @@ interface MinimalRequestInit { body?: Uint8Array; } +export interface CommonRequestInit extends MinimalRequestInit { + signal?: AbortSignal; + redirect?: RequestRedirect; +} + function isJSON(str: string): boolean { try { JSON.parse(str); diff --git a/src/vs/workbench/api/node/extHostMcpNode.ts b/src/vs/workbench/api/node/extHostMcpNode.ts index 7fad0fc24ad..ca536ad0282 100644 --- a/src/vs/workbench/api/node/extHostMcpNode.ts +++ b/src/vs/workbench/api/node/extHostMcpNode.ts @@ -16,9 +16,11 @@ import { findExecutable } from '../../../base/node/processes.js'; import { LogLevel } from '../../../platform/log/common/log.js'; import { McpConnectionState, McpServerLaunch, McpServerTransportStdio, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; import { McpStdioStateHandler } from '../../contrib/mcp/node/mcpStdioStateHandler.js'; -import { ExtHostMcpService } from '../common/extHostMcp.js'; +import { CommonRequestInit, ExtHostMcpService, McpHTTPHandle } from '../common/extHostMcp.js'; export class NodeExtHostMpcService extends ExtHostMcpService { + protected override _mcpHttpHandleType = McpHTTPHandleNode; + private nodeServers = this._register(new DisposableMap()); protected override _startMcp(id: number, launch: McpServerLaunch, defaultCwd?: URI, errorOnUserInteraction?: boolean): void { @@ -134,6 +136,42 @@ export class NodeExtHostMpcService extends ExtHostMcpService { } } +class McpHTTPHandleNode extends McpHTTPHandle { + protected override async _fetchInternal(url: string, init?: CommonRequestInit): Promise { + const { fetch, Agent } = await import('undici'); + + const undiciInit = { ...init, dispatcher: undefined as any }; + + let httpUrl = url; + const uri = URI.parse(url); + + if (uri.scheme === 'unix' || uri.scheme === 'pipe') { + // By convention, we put the *socket path* as the URI path, and the *request path* in the fragment + // So, set the dispatcher with the socket path + undiciInit.dispatcher = new Agent({ + socketPath: uri.path, + }); + + // And then rewrite the URL to be http://localhost/ + httpUrl = uri + .with({ + scheme: 'http', + authority: 'localhost', // HTTP always wants a host (not that we're using it), but if we're using a socket or pipe then localhost is sorta right anyway + path: uri.fragment, + }) + .toString(true); + } + + const undiciResponse = await fetch(httpUrl, undiciInit); + + return new Response(undiciResponse.body as ReadableStream, { + status: undiciResponse.status, + statusText: undiciResponse.statusText, + headers: undiciResponse.headers + }); + } +} + const windowsShellScriptRe = /\.(bat|cmd)$/i; /**