diff --git a/package-lock.json b/package-lock.json index 3bbff3b71..34caaf974 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "mongodb-connection-string-url": "^3.0.2", "mongodb-data-service": "^22.30.1", "mongodb-log-writer": "^2.4.1", - "mongodb-mcp-server": "^0.3.1-prerelease.1", + "mongodb-mcp-server": "^1.0.0", "mongodb-query-parser": "^4.4.2", "mongodb-schema": "^12.6.2", "node-machine-id": "1.1.12", @@ -125,7 +125,7 @@ "xvfb-maybe": "^0.2.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >= 24.7.0", + "node": "^20.19.0 || ^22.12.0 || >= 23.0.0", "npm": ">=10.1.0", "vscode": "^1.101.1" } @@ -7719,15 +7719,6 @@ "node": ">=12" } }, - "node_modules/@mongodb-js/oidc-mock-provider/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/@mongodb-js/oidc-plugin": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-2.0.3.tgz", @@ -20685,6 +20676,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/meow/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -21151,6 +21152,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/mongodb": { "version": "6.19.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.19.0.tgz", @@ -21347,9 +21358,9 @@ } }, "node_modules/mongodb-mcp-server": { - "version": "0.3.1-prerelease.1", - "resolved": "https://registry.npmjs.org/mongodb-mcp-server/-/mongodb-mcp-server-0.3.1-prerelease.1.tgz", - "integrity": "sha512-FslY1fIgME1eaSA4umEUZUOkd2gcF+l4zTsr+RGwr1TEnGN/gF5lF5m8B0OwqjgKyxTIzBVB3+ZllZDsYBuWUA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mongodb-mcp-server/-/mongodb-mcp-server-1.0.0.tgz", + "integrity": "sha512-0pPyYQd2ciwotlMPzvRUwx8suIY2HvVsUOyeR2NN9Zk3kM84hcVR1h2MBOMgMdESfte4jkpXeohtLatlTVd/Gg==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.4", @@ -21372,7 +21383,7 @@ "oauth4webapi": "^3.8.0", "openapi-fetch": "^0.14.0", "ts-levenshtein": "^1.0.7", - "yargs-parser": "^22.0.0", + "yargs-parser": "^21.1.1", "zod": "^3.25.76" }, "bin": { @@ -21477,15 +21488,6 @@ "node": ">=10" } }, - "node_modules/mongodb-mcp-server/node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, "node_modules/mongodb-ns": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/mongodb-ns/-/mongodb-ns-2.4.2.tgz", @@ -22444,15 +22446,6 @@ "node": ">=12" } }, - "node_modules/mongodb-runner/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/mongodb-schema": { "version": "12.6.2", "resolved": "https://registry.npmjs.org/mongodb-schema/-/mongodb-schema-12.6.2.tgz", @@ -22526,15 +22519,6 @@ "node": ">=12" } }, - "node_modules/mongodb-schema/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "optional": true, - "engines": { - "node": ">=12" - } - }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -29116,12 +29100,12 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-unparser": { @@ -29148,6 +29132,16 @@ "node": ">=8" } }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", diff --git a/package.json b/package.json index 4f4593963..f9a7c2308 100644 --- a/package.json +++ b/package.json @@ -1418,7 +1418,7 @@ "mongodb-connection-string-url": "^3.0.2", "mongodb-data-service": "^22.30.1", "mongodb-log-writer": "^2.4.1", - "mongodb-mcp-server": "^0.3.1-prerelease.1", + "mongodb-mcp-server": "^1.0.0", "mongodb-query-parser": "^4.4.2", "mongodb-schema": "^12.6.2", "node-machine-id": "1.1.12", diff --git a/src/mcp/mcpConfig.ts b/src/mcp/mcpConfig.ts new file mode 100644 index 000000000..3de49bd72 --- /dev/null +++ b/src/mcp/mcpConfig.ts @@ -0,0 +1,72 @@ +import { + type UserConfig, + configurableProperties, + defaultUserConfig, +} from 'mongodb-mcp-server'; +import * as vscode from 'vscode'; +import { createLogger } from '../logging'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { contributes } = require('../../package.json'); + +const logger = createLogger('mcp-config'); + +export function getMCPConfigFromVSCodeSettings( + packageJsonConfiguredProperties: Record = contributes + ?.configuration?.properties ?? {}, + retrieveMCPConfiguration: () => vscode.WorkspaceConfiguration = (): vscode.WorkspaceConfiguration => + vscode.workspace.getConfiguration('mdb.mcp'), +): Partial { + // We're attempting to: + // 1. Use only the config values for MCP server exposed by VSCode config + // 2. Use only the config values that are relevant for MCP server (all mcp + // config exposed by VSCode does contain some irrelevant config as well, + // such as `server`) + const vscConfiguredProperties = Object.keys(packageJsonConfiguredProperties) + .filter((key) => key.startsWith('mdb.mcp')) + .map((key) => key.replace(/^mdb\.mcp\./, '')) + .filter((property) => configurableProperties.has(property)); + + logger.debug('Will retrieve MCP config for the following properties', { + vscConfiguredProperties, + }); + + const mcpConfiguration = retrieveMCPConfiguration(); + return Object.fromEntries( + vscConfiguredProperties.map((property) => { + const configuredValue = mcpConfiguration.get(property); + return [ + property, + // Most of the MCP config, if not all, consists of non-null configs and it is + // possible for a VSCode config to have a null value edited directly in the + // settings file which is why to safeguard against incorrect values we map + // them at-least to the expected defaults. + mcpConfigValues(property, configuredValue), + ]; + }), + ); +} + +// eslint-disable-next-line complexity +function mcpConfigValues(property: string, configuredValue: unknown): unknown { + switch (property) { + case 'apiBaseUrl': + case 'apiClientId': + case 'apiClientSecret': + case 'exportsPath': { + const trimmedValue = String(configuredValue).trim(); + return typeof configuredValue === 'string' && !!trimmedValue + ? trimmedValue + : defaultUserConfig[property]; + } + case 'disabledTools': + return Array.isArray(configuredValue) + ? configuredValue + : defaultUserConfig.disabledTools; + case 'readOnly': + case 'indexCheck': + case 'exportTimeoutMs': + case 'exportCleanupIntervalMs': + default: + return configuredValue ?? defaultUserConfig[property]; + } +} diff --git a/src/mcp/mcpController.ts b/src/mcp/mcpController.ts index e060bcd7d..da1105e9f 100644 --- a/src/mcp/mcpController.ts +++ b/src/mcp/mcpController.ts @@ -18,6 +18,7 @@ import { createLogger } from '../logging'; import type { MCPConnectParams } from './mcpConnectionManager'; import { MCPConnectionManager } from './mcpConnectionManager'; import { createMCPConnectionErrorHandler } from './mcpConnectionErrorHandler'; +import { getMCPConfigFromVSCodeSettings } from './mcpConfig'; export type McpServerStartupConfig = 'enabled' | 'disabled'; @@ -64,6 +65,9 @@ export class MCPController { this.context = context; this.connectionController = connectionController; this.getTelemetryAnonymousId = getTelemetryAnonymousId; + } + + public async activate(): Promise { this.context.subscriptions.push( vscode.lm.registerMcpServerDefinitionProvider('mongodb', { onDidChangeMcpServerDefinitions: this.didChangeEmitter.event, @@ -75,9 +79,7 @@ export class MCPController { }, }), ); - } - public async activate(): Promise { this.connectionController.addEventListener( 'ACTIVE_CONNECTION_CHANGED', () => { @@ -102,14 +104,32 @@ export class MCPController { }; registerGlobalSecretToRedact(token, 'password'); + const vscodeConfiguredMCPConfig = getMCPConfigFromVSCodeSettings(); + const mcpConfig: UserConfig = { ...defaultUserConfig, + ...vscodeConfiguredMCPConfig, + transport: 'http', httpPort: 0, httpHeaders: headers, - disabledTools: ['connect'], - loggers: ['mcp'], + disabledTools: Array.from( + new Set([ + 'connect', + ...(vscodeConfiguredMCPConfig.disabledTools ?? []), + ]), + ), + loggers: Array.from( + new Set(['mcp', ...(vscodeConfiguredMCPConfig.loggers ?? [])]), + ), }; + logger.info('Starting MCP server with config', { + ...mcpConfig, + httpHeaders: '', + apiClientId: '', + apiClientSecret: '', + }); + const createConnectionManager: ConnectionManagerFactoryFn = async ({ logger, }) => { diff --git a/src/test/suite/mcp/mcpConfig.test.ts b/src/test/suite/mcp/mcpConfig.test.ts new file mode 100644 index 000000000..72e15a8c5 --- /dev/null +++ b/src/test/suite/mcp/mcpConfig.test.ts @@ -0,0 +1,156 @@ +import type * as vscode from 'vscode'; +import { expect } from 'chai'; +import { getMCPConfigFromVSCodeSettings } from '../../../mcp/mcpConfig'; + +const vscMCPConfig = { + 'mdb.mcp.apiBaseUrl': 'https://cloud.mongodb.com/', + 'mdb.mcp.apiClientId': '', + 'mdb.mcp.apiClientSecret': '', + 'mdb.mcp.disabledTools': ['connect'], + 'mdb.mcp.readOnly': true, // note that we changed it to true + 'mdb.mcp.indexCheck': null, // note that this is null + 'mdb.mcp.server': 'ask', + 'mdb.mcp.exportsPath': '', // note that this is not modified + 'mdb.mcp.exportTimeoutMs': null, // note that this is set to null + 'mdb.mcp.exportCleanupIntervalMs': 0, // not that this is set to 0 +} as const; + +const getDefaultVSCodeConfigForMCP = (): vscode.WorkspaceConfiguration => + ({ + get(key: string) { + return vscMCPConfig[`mdb.mcp.${key}`]; + }, + has(key: string) { + return `mdb.mcp.${key}` in vscMCPConfig; + }, + }) as unknown as vscode.WorkspaceConfiguration; + +suite('MCPConfig test suite', () => { + test('normal calls with package.json properties should return expected MCP config from the configured VSCode config', () => { + const output = getMCPConfigFromVSCodeSettings( + undefined, + getDefaultVSCodeConfigForMCP, + ); + expect(Object.keys(output)).to.not.contain('server'); + expect(output.apiBaseUrl).to.equal('https://cloud.mongodb.com/'); + expect(output.apiClientId).to.be.undefined; + expect(output.apiClientSecret).to.be.undefined; + expect(output.disabledTools).to.deep.equal(['connect']); + expect(output.exportCleanupIntervalMs).to.equal(0); + expect(output.exportTimeoutMs).to.equal(300000); + expect(output.exportsPath?.endsWith('exports')).to.be.true; + expect(output.indexCheck).to.be.false; + expect(output.readOnly).to.be.true; + }); + + test('should return empty object if packageJsonConfiguredProperties resolves to empty object', () => { + expect( + getMCPConfigFromVSCodeSettings({}, getDefaultVSCodeConfigForMCP), + ).to.deep.equal({}); + }); + + suite('mcpConfigValues edge cases', () => { + test('should handle non-string values for string properties', () => { + const mockConfig = { + 'mdb.mcp.apiBaseUrl': 42, + 'mdb.mcp.apiClientId': true, + 'mdb.mcp.apiClientSecret': {}, + 'mdb.mcp.exportsPath': [], + }; + + const getMockConfig = (): vscode.WorkspaceConfiguration => + ({ + get(key: string) { + return mockConfig[`mdb.mcp.${key}`]; + }, + has(key: string) { + return `mdb.mcp.${key}` in mockConfig; + }, + }) as unknown as vscode.WorkspaceConfiguration; + + const output = getMCPConfigFromVSCodeSettings(undefined, getMockConfig); + + // All should fall back to defaults since they're not valid strings + expect(output.apiBaseUrl).to.equal('https://cloud.mongodb.com/'); + expect(output.apiClientId).to.be.undefined; + expect(output.apiClientSecret).to.be.undefined; + expect(output.exportsPath?.endsWith('exports')).to.be.true; + }); + + test('should handle whitespace-only string values', () => { + const mockConfig = { + 'mdb.mcp.apiBaseUrl': ' ', + 'mdb.mcp.apiClientId': '\t\t', + 'mdb.mcp.apiClientSecret': '\n\n', + 'mdb.mcp.exportsPath': '', + }; + + const getMockConfig = (): vscode.WorkspaceConfiguration => + ({ + get(key: string) { + return mockConfig[`mdb.mcp.${key}`]; + }, + has(key: string) { + return `mdb.mcp.${key}` in mockConfig; + }, + }) as unknown as vscode.WorkspaceConfiguration; + + const output = getMCPConfigFromVSCodeSettings(undefined, getMockConfig); + + // All should fall back to defaults since trimmed values are empty + expect(output.apiBaseUrl).to.equal('https://cloud.mongodb.com/'); + expect(output.apiClientId).to.be.undefined; + expect(output.apiClientSecret).to.be.undefined; + expect(output.exportsPath?.endsWith('exports')).to.be.true; + }); + + test('should properly trim and return valid string values', () => { + const mockConfig = { + 'mdb.mcp.apiBaseUrl': ' https://custom.mongodb.com ', + 'mdb.mcp.apiClientId': '\tcustom-client-id\t', + 'mdb.mcp.apiClientSecret': '\ncustom-secret\n', + 'mdb.mcp.exportsPath': ' /custom/path ', + }; + + const getMockConfig = (): vscode.WorkspaceConfiguration => + ({ + get(key: string) { + return mockConfig[`mdb.mcp.${key}`]; + }, + has(key: string) { + return `mdb.mcp.${key}` in mockConfig; + }, + }) as unknown as vscode.WorkspaceConfiguration; + + const output = getMCPConfigFromVSCodeSettings(undefined, getMockConfig); + + // Should return the trimmed original values + expect(output.apiBaseUrl).to.equal('https://custom.mongodb.com'); + expect(output.apiClientId).to.equal('custom-client-id'); + expect(output.apiClientSecret).to.equal('custom-secret'); + expect(output.exportsPath).to.equal('/custom/path'); + }); + + test('should handle non-array values for disabledTools', () => { + const mockConfig = { + 'mdb.mcp.disabledTools': 'not-an-array', + }; + + const getMockConfig = (): vscode.WorkspaceConfiguration => + ({ + get(key: string) { + return mockConfig[`mdb.mcp.${key}`]; + }, + has(key: string) { + return `mdb.mcp.${key}` in mockConfig; + }, + }) as unknown as vscode.WorkspaceConfiguration; + + const output = getMCPConfigFromVSCodeSettings(undefined, getMockConfig); + + // Should fall back to default disabledTools array + expect(output.disabledTools).to.be.an('array'); + expect(output.disabledTools).to.deep.equal([]); + }); + }); +});