From 39a3d3cd73d83afec014c395a37aa0f1602ac693 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 19 Mar 2025 16:30:11 -0700 Subject: [PATCH] mcp: support envFile for mcp.json (#244059) Fixes #243709 Note it needs #244029 to support ${workspaceFolder} --- src/vs/base/common/envfile.ts | 100 ++++++++++++++ src/vs/base/test/common/envfile.test.ts | 130 ++++++++++++++++++ .../platform/mcp/common/mcpPlatformTypes.ts | 1 + src/vs/workbench/api/common/extHostMcp.ts | 3 +- src/vs/workbench/api/node/extHostMpcNode.ts | 36 +++-- .../common/discovery/configMcpDiscovery.ts | 1 + .../discovery/nativeMcpDiscoveryAdapters.ts | 1 + .../mcp/common/mcpConfigPathsService.ts | 3 + .../contrib/mcp/common/mcpConfiguration.ts | 5 + .../contrib/mcp/common/mcpRegistry.ts | 1 - .../workbench/contrib/mcp/common/mcpTypes.ts | 4 +- .../mcp/test/common/mcpRegistry.test.ts | 2 + .../test/common/mcpServerConnection.test.ts | 1 + 13 files changed, 274 insertions(+), 14 deletions(-) create mode 100644 src/vs/base/common/envfile.ts create mode 100644 src/vs/base/test/common/envfile.test.ts diff --git a/src/vs/base/common/envfile.ts b/src/vs/base/common/envfile.ts new file mode 100644 index 00000000000..9cb40d93507 --- /dev/null +++ b/src/vs/base/common/envfile.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Parses a standard .env/.envrc file into a map of the environment variables + * it defines. + * + * todo@connor4312: this can go away (if only used in Node.js targets) and be + * replaced with `util.parseEnv`. However, currently calling that makes the + * extension host crash. + */ +export function parseEnvFile(src: string) { + const result = new Map(); + + // Normalize line breaks + const normalizedSrc = src.replace(/\r\n?/g, '\n'); + const lines = normalizedSrc.split('\n'); + + for (let line of lines) { + // Skip empty lines and comments + line = line.trim(); + if (!line || line.startsWith('#')) { + continue; + } + + // Parse the line into key and value + const [key, value] = parseLine(line); + if (key) { + result.set(key, value); + } + } + + return result; + + function parseLine(line: string): [string, string] | [null, null] { + // Handle export prefix + if (line.startsWith('export ')) { + line = line.substring(7).trim(); + } + + // Find the key-value separator + const separatorIndex = findIndexOutsideQuotes(line, c => c === '=' || c === ':'); + if (separatorIndex === -1) { + return [null, null]; + } + + const key = line.substring(0, separatorIndex).trim(); + let value = line.substring(separatorIndex + 1).trim(); + + // Handle comments and remove them + const commentIndex = findIndexOutsideQuotes(value, c => c === '#'); + if (commentIndex !== -1) { + value = value.substring(0, commentIndex).trim(); + } + + // Process quoted values + if (value.length >= 2) { + const firstChar = value[0]; + const lastChar = value[value.length - 1]; + + if ((firstChar === '"' && lastChar === '"') || + (firstChar === '\'' && lastChar === '\'') || + (firstChar === '`' && lastChar === '`')) { + // Remove surrounding quotes + value = value.substring(1, value.length - 1); + + // Handle escaped characters in double quotes + if (firstChar === '"') { + value = value.replace(/\\n/g, '\n').replace(/\\r/g, '\r'); + } + } + } + + return [key, value]; + } + + function findIndexOutsideQuotes(text: string, predicate: (char: string) => boolean): number { + let inQuote = false; + let quoteChar = ''; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + + if (inQuote) { + if (char === quoteChar && text[i - 1] !== '\\') { + inQuote = false; + } + } else if (char === '"' || char === '\'' || char === '`') { + inQuote = true; + quoteChar = char; + } else if (predicate(char)) { + return i; + } + } + + return -1; + } +} diff --git a/src/vs/base/test/common/envfile.test.ts b/src/vs/base/test/common/envfile.test.ts new file mode 100644 index 00000000000..753a8c6b534 --- /dev/null +++ b/src/vs/base/test/common/envfile.test.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { parseEnvFile } from '../../common/envfile.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; +import * as assert from 'assert'; + +/* +Test cases from https://github.com/motdotla/dotenv/blob/master/tests/.env + + Copyright (c) 2015, Scott Motte + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +const example = ` +BASIC=basic + +# previous line intentionally left blank +AFTER_LINE=after_line +EMPTY= +EMPTY_SINGLE_QUOTES='' +EMPTY_DOUBLE_QUOTES="" +EMPTY_BACKTICKS=\`\` +SINGLE_QUOTES='single_quotes' +SINGLE_QUOTES_SPACED=' single quotes ' +DOUBLE_QUOTES="double_quotes" +DOUBLE_QUOTES_SPACED=" double quotes " +DOUBLE_QUOTES_INSIDE_SINGLE='double "quotes" work inside single quotes' +DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET="{ port: $MONGOLAB_PORT}" +SINGLE_QUOTES_INSIDE_DOUBLE="single 'quotes' work inside double quotes" +BACKTICKS_INSIDE_SINGLE='\`backticks\` work inside single quotes' +BACKTICKS_INSIDE_DOUBLE="\`backticks\` work inside double quotes" +BACKTICKS=\`backticks\` +BACKTICKS_SPACED=\` backticks \` +DOUBLE_QUOTES_INSIDE_BACKTICKS=\`double "quotes" work inside backticks\` +SINGLE_QUOTES_INSIDE_BACKTICKS=\`single 'quotes' work inside backticks\` +DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS=\`double "quotes" and single 'quotes' work inside backticks\` +EXPAND_NEWLINES="expand\\nnew\\nlines" +DONT_EXPAND_UNQUOTED=dontexpand\\nnewlines +DONT_EXPAND_SQUOTED='dontexpand\\nnewlines' +# COMMENTS=work +INLINE_COMMENTS=inline comments # work #very #well +INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work +INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work +INLINE_COMMENTS_BACKTICKS=\`inline comments outside of #backticks\` # work +INLINE_COMMENTS_SPACE=inline comments start with a#number sign. no space required. +EQUAL_SIGNS=equals== +RETAIN_INNER_QUOTES={"foo": "bar"} +RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}' +RETAIN_INNER_QUOTES_AS_BACKTICKS=\`{"foo": "bar's"}\` +TRIM_SPACE_FROM_UNQUOTED= some spaced out string +USERNAME=therealnerdybeast@example.tld + SPACED_KEY = parsed +`; + +suite('parseEnvFile', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('parses', () => { + const parsed = parseEnvFile(example); + assert.strictEqual(parsed.get('BASIC'), 'basic'); + assert.strictEqual(parsed.get('AFTER_LINE'), 'after_line'); + assert.strictEqual(parsed.get('EMPTY'), ''); + assert.strictEqual(parsed.get('EMPTY_SINGLE_QUOTES'), ''); + assert.strictEqual(parsed.get('EMPTY_DOUBLE_QUOTES'), ''); + assert.strictEqual(parsed.get('EMPTY_BACKTICKS'), ''); + assert.strictEqual(parsed.get('SINGLE_QUOTES'), 'single_quotes'); + assert.strictEqual(parsed.get('SINGLE_QUOTES_SPACED'), ' single quotes '); + assert.strictEqual(parsed.get('DOUBLE_QUOTES'), 'double_quotes'); + assert.strictEqual(parsed.get('DOUBLE_QUOTES_SPACED'), ' double quotes '); + assert.strictEqual(parsed.get('DOUBLE_QUOTES_INSIDE_SINGLE'), 'double "quotes" work inside single quotes'); + assert.strictEqual(parsed.get('DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET'), '{ port: $MONGOLAB_PORT}'); + assert.strictEqual(parsed.get('SINGLE_QUOTES_INSIDE_DOUBLE'), "single 'quotes' work inside double quotes"); + assert.strictEqual(parsed.get('BACKTICKS_INSIDE_SINGLE'), '`backticks` work inside single quotes'); + assert.strictEqual(parsed.get('BACKTICKS_INSIDE_DOUBLE'), '`backticks` work inside double quotes'); + assert.strictEqual(parsed.get('BACKTICKS'), 'backticks'); + assert.strictEqual(parsed.get('BACKTICKS_SPACED'), ' backticks '); + assert.strictEqual(parsed.get('DOUBLE_QUOTES_INSIDE_BACKTICKS'), 'double "quotes" work inside backticks'); + assert.strictEqual(parsed.get('SINGLE_QUOTES_INSIDE_BACKTICKS'), "single 'quotes' work inside backticks"); + assert.strictEqual(parsed.get('DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS'), "double \"quotes\" and single 'quotes' work inside backticks"); + assert.strictEqual(parsed.get('EXPAND_NEWLINES'), 'expand\nnew\nlines'); + assert.strictEqual(parsed.get('DONT_EXPAND_UNQUOTED'), 'dontexpand\\nnewlines'); + assert.strictEqual(parsed.get('DONT_EXPAND_SQUOTED'), 'dontexpand\\nnewlines'); + assert.strictEqual(parsed.get('COMMENTS'), undefined); + assert.strictEqual(parsed.get('INLINE_COMMENTS'), 'inline comments'); + assert.strictEqual(parsed.get('INLINE_COMMENTS_SINGLE_QUOTES'), 'inline comments outside of #singlequotes'); + assert.strictEqual(parsed.get('INLINE_COMMENTS_DOUBLE_QUOTES'), 'inline comments outside of #doublequotes'); + assert.strictEqual(parsed.get('INLINE_COMMENTS_BACKTICKS'), 'inline comments outside of #backticks'); + assert.strictEqual(parsed.get('INLINE_COMMENTS_SPACE'), 'inline comments start with a'); + assert.strictEqual(parsed.get('EQUAL_SIGNS'), 'equals=='); + assert.strictEqual(parsed.get('RETAIN_INNER_QUOTES'), '{"foo": "bar"}'); + assert.strictEqual(parsed.get('RETAIN_INNER_QUOTES_AS_STRING'), '{"foo": "bar"}'); + assert.strictEqual(parsed.get('RETAIN_INNER_QUOTES_AS_BACKTICKS'), '{"foo": "bar\'s"}'); + assert.strictEqual(parsed.get('TRIM_SPACE_FROM_UNQUOTED'), 'some spaced out string'); + assert.strictEqual(parsed.get('USERNAME'), 'therealnerdybeast@example.tld'); + assert.strictEqual(parsed.get('SPACED_KEY'), 'parsed'); + const payload = parseEnvFile('BUFFER=true'); + assert.strictEqual(payload.get('BUFFER'), 'true'); + const expectedPayload = Object.entries({ SERVER: 'localhost', PASSWORD: 'password', DB: 'tests' }); + const RPayload = parseEnvFile('SERVER=localhost\rPASSWORD=password\rDB=tests\r'); + assert.deepStrictEqual([...RPayload], expectedPayload); + const NPayload = parseEnvFile('SERVER=localhost\nPASSWORD=password\nDB=tests\n'); + assert.deepStrictEqual([...NPayload], expectedPayload); + const RNPayload = parseEnvFile('SERVER=localhost\r\nPASSWORD=password\r\nDB=tests\r\n'); + assert.deepStrictEqual([...RNPayload], expectedPayload); + }); +}); diff --git a/src/vs/platform/mcp/common/mcpPlatformTypes.ts b/src/vs/platform/mcp/common/mcpPlatformTypes.ts index 47df96028fb..b16d59e2934 100644 --- a/src/vs/platform/mcp/common/mcpPlatformTypes.ts +++ b/src/vs/platform/mcp/common/mcpPlatformTypes.ts @@ -17,6 +17,7 @@ export interface IMcpConfigurationStdio { command: string; args?: readonly string[]; env?: Record; + envFile?: string; } export interface IMcpConfigurationSSE { diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index eead33f21fe..fcac30516e6 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -108,7 +108,8 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService cwd: item.cwd, args: item.args, command: item.command, - env: item.env + env: item.env, + envFile: undefined, } }); } diff --git a/src/vs/workbench/api/node/extHostMpcNode.ts b/src/vs/workbench/api/node/extHostMpcNode.ts index a1d3cf5aef2..4a4e1e71612 100644 --- a/src/vs/workbench/api/node/extHostMpcNode.ts +++ b/src/vs/workbench/api/node/extHostMpcNode.ts @@ -4,14 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; -import { mapValues } from '../../../base/common/objects.js'; +import { readFile } from 'fs/promises'; +import { homedir } from 'os'; +import { PassThrough } from 'stream'; +import { parseEnvFile } from '../../../base/common/envfile.js'; import { URI } from '../../../base/common/uri.js'; import { StreamSplitter } from '../../../base/node/nodeStreams.js'; import { McpConnectionState, McpServerLaunch, McpServerTransportStdio, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; import { ExtHostMcpService } from '../common/extHostMcp.js'; import { IExtHostRpcService } from '../common/extHostRpcService.js'; -import { homedir } from 'os'; -import { PassThrough } from 'stream'; export class NodeExtHostMpcService extends ExtHostMcpService { constructor( @@ -47,19 +48,35 @@ export class NodeExtHostMpcService extends ExtHostMcpService { const nodeServer = this.nodeServers.get(id); if (nodeServer) { this._proxy.$onDidPublishLog(id, '[Client Says] ' + message.toString()); - nodeServer.child.stdin.write(message + '\n'); } else { super.$sendMessage(id, message); } } - private startNodeMpc(id: number, launch: McpServerTransportStdio): void { - const onError = (err: Error) => this._proxy.$onDidChangeState(id, { + private async startNodeMpc(id: number, launch: McpServerTransportStdio) { + const onError = (err: Error | string) => this._proxy.$onDidChangeState(id, { state: McpConnectionState.Kind.Error, - message: err.message, + message: typeof err === 'string' ? err : err.message, }); + // MCP servers are run on the same authority where they are defined, so + // reading the envfile based on its path off the filesystem here is fine. + const env = { ...process.env }; + if (launch.envFile) { + try { + for (const [key, value] of parseEnvFile(await readFile(launch.envFile, 'utf-8'))) { + env[key] = value; + } + } catch (e) { + onError(`Failed to read envFile '${launch.envFile}': ${e.message}`); + return; + } + } + for (const [key, value] of Object.entries(launch.env)) { + env[key] = value === null ? undefined : String(value); + } + const abortCtrl = new AbortController(); let child: ChildProcessWithoutNullStreams; try { @@ -67,10 +84,7 @@ export class NodeExtHostMpcService extends ExtHostMcpService { stdio: 'pipe', cwd: launch.cwd ? URI.revive(launch.cwd).fsPath : homedir(), signal: abortCtrl.signal, - env: { - ...process.env, - ...mapValues(launch.env, v => typeof v === 'number' ? String(v) : (v === null ? undefined : v)), - }, + env, }); } catch (e) { onError(e); diff --git a/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts index 4445cec8c94..6acb7dc9ee2 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts @@ -109,6 +109,7 @@ export class ConfigMcpDiscovery extends Disposable implements IMcpDiscovery { args: value.args || [], command: value.command, env: value.env || {}, + envFile: value.envFile, cwd: undefined, }, variableReplacement: { diff --git a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts index 8eb01b9c722..86d59549f93 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts @@ -63,6 +63,7 @@ export class ClaudeDesktopMpcDiscoveryAdapter implements NativeMpcDiscoveryAdapt args: server.args || [], command: server.command, env: server.env || {}, + envFile: undefined, cwd: homedir, } }; diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfigPathsService.ts b/src/vs/workbench/contrib/mcp/common/mcpConfigPathsService.ts index fe3a41b8424..250d2019a3b 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpConfigPathsService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpConfigPathsService.ts @@ -13,6 +13,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { ILabelService } from '../../../../platform/label/common/label.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; +import { IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; @@ -28,6 +29,8 @@ export interface IMcpConfigPath { scope: StorageScope; /** Configuration target that correspond to this file */ target: ConfigurationTarget; + /** Associated workspace folder, for workspace folder values */ + folder?: IWorkspaceFolder; /** Order in which the configuration should be displayed */ order: number; /** Config's remote authority */ diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts index 46864f954c1..d6603fd3db4 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts @@ -54,6 +54,11 @@ export const mcpStdioServerSchema: IJSONSchema = { type: 'string' }, }, + envFile: { + type: 'string', + description: localize('app.mcp.envFile.command', "Path to a file containing environment variables for the server."), + examples: ['${workspaceFolder}/.env'], + }, env: { description: localize('app.mcp.env.command', "Environment variables passed to the server."), additionalProperties: { diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts index a6fbe676b74..a5ca2d3e84e 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts @@ -284,7 +284,6 @@ export class McpRegistry extends Disposable implements IMcpRegistry { } } - // resolve variables requiring user input await this._configurationResolverService.resolveWithInteraction(folder, expr, section, undefined, target); diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index f986370c355..31887c1d364 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -261,6 +261,7 @@ export interface McpServerTransportStdio { readonly command: string; readonly args: readonly string[]; readonly env: Record; + readonly envFile: string | undefined; } /** @@ -280,7 +281,7 @@ export type McpServerLaunch = export namespace McpServerLaunch { export type Serialized = | { type: McpServerTransportType.SSE; uri: UriComponents; headers: [string, string][] } - | { type: McpServerTransportType.Stdio; cwd: UriComponents | undefined; command: string; args: readonly string[]; env: Record }; + | { type: McpServerTransportType.Stdio; cwd: UriComponents | undefined; command: string; args: readonly string[]; env: Record; envFile: string | undefined }; export function toSerialized(launch: McpServerLaunch): McpServerLaunch.Serialized { return launch; @@ -297,6 +298,7 @@ export namespace McpServerLaunch { command: launch.command, args: launch.args, env: launch.env, + envFile: launch.envFile, }; } } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts index f9257ac29eb..5a33d37f585 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts @@ -158,6 +158,7 @@ suite('Workbench - MCP - Registry', () => { command: 'test-command', args: [], env: {}, + envFile: undefined, cwd: URI.parse('file:///test') } }; @@ -196,6 +197,7 @@ suite('Workbench - MCP - Registry', () => { env: { PATH: '${input:testInteractive}' }, + envFile: undefined, cwd: URI.parse('file:///test') }, variableReplacement: { diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts index 83b9933c3f2..ab27e7e25a2 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts @@ -97,6 +97,7 @@ suite('Workbench - MCP - ServerConnection', () => { command: 'test-command', args: [], env: {}, + envFile: undefined, cwd: URI.parse('file:///test') } };