Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
aea317c
chore: disable MCP auto start and instead request
himanshusinghs Sep 23, 2025
f2364e1
fix: bind DataService.disconnect to correct instance
himanshusinghs Sep 23, 2025
6c48e71
chore: enable readOnly mode by default
himanshusinghs Sep 23, 2025
aea2eaa
chore: remove unused type
himanshusinghs Sep 23, 2025
c5a65da
chore: fix tests with correct expectations
himanshusinghs Sep 23, 2025
f6b6b00
Update src/test/suite/mcp/mcpController.test.ts
himanshusinghs Sep 23, 2025
c94498f
chore: apply copilot suggestions
himanshusinghs Sep 23, 2025
adcab74
chore: no exclusive tests
himanshusinghs Sep 23, 2025
df9c0f3
chore: add missing hosting_mode for telemetry data
himanshusinghs Sep 23, 2025
e4be477
chore: remove unused async wrapper
himanshusinghs Sep 23, 2025
1b14b66
chore: avoid relying on internals for tests
himanshusinghs Sep 25, 2025
58c880f
chore: use pre-build logger
himanshusinghs Sep 25, 2025
ee2e851
chore: clarify the comment
himanshusinghs Sep 25, 2025
5e23a07
chore: disable eslint just for the lines
himanshusinghs Sep 25, 2025
f03b737
chore: type the returned value of getMCPAutoStartConfig
himanshusinghs Sep 25, 2025
13cbb37
Merge remote-tracking branch 'origin/main' into fix/VSCODE-704-flip-m…
himanshusinghs Sep 25, 2025
a0cc53e
chore: coerce unknown stored values to known value
himanshusinghs Sep 25, 2025
fedd5d1
chore: handle multiple mcp client connections
himanshusinghs Sep 26, 2025
03957d1
chore: remove unused imports
himanshusinghs Sep 26, 2025
0cff996
Merge remote-tracking branch 'origin/main' into fix/VSCODE-704-flip-m…
himanshusinghs Sep 26, 2025
98a4187
chore: bump mongodb-mcp-server
himanshusinghs Sep 26, 2025
f638bc6
chore: add tests to verify multi client handling
himanshusinghs Sep 26, 2025
d3d6448
chore: disable eslint just for the lines
himanshusinghs Sep 26, 2025
90be6f3
chore: rename clientConnectionManager to mcpConnectionManager
himanshusinghs Sep 29, 2025
955a371
chore: add test for separate client states
himanshusinghs Sep 30, 2025
0a4f818
chore: add test validating that MCP server shutdown closes Connection…
himanshusinghs Sep 30, 2025
5f77f59
chore: log when prompted and remove one line func
himanshusinghs Sep 30, 2025
5a4e165
Merge remote-tracking branch 'origin/main' into fix/VSCODE-704-flip-m…
himanshusinghs Sep 30, 2025
1613071
chore: use typed autoStartConfig retriever
himanshusinghs Oct 1, 2025
6f161a8
chore: update test to reflect non-coercion
himanshusinghs Oct 1, 2025
8f8606f
chore: do not show popup on extension activate
himanshusinghs Oct 1, 2025
a1954c8
chore: correctly place the log
himanshusinghs Oct 1, 2025
2786c47
chore: refactor log placement a little
himanshusinghs Oct 1, 2025
d09a052
chore: remove super long timeout
himanshusinghs Oct 2, 2025
d2187f3
chore: add clarifying comments and use debug notifications
himanshusinghs Oct 2, 2025
4f39d31
chore: make createConnectionManager a static func
himanshusinghs Oct 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1320,7 +1320,7 @@
},
"mdb.mcp.readOnly": {
"type": "boolean",
"default": false,
"default": true,
"description": "When set to true, MongoDB MCP server will only allow read, connect, and metadata operation types, disabling create/update/delete operations."
},
"mdb.mcp.indexCheck": {
Expand All @@ -1331,11 +1331,16 @@
"mdb.mcp.server": {
"type": "string",
"enum": [
"ask",
"enabled",
"disabled"
"prompt",
"autoStartEnabled",
"autoStartDisabled"
],
"enumItemLabels": [
"Ask",
"Auto Start Enabled",
"Auto Start Disabled"
],
"default": "ask",
"default": "prompt",
"description": "Controls whether MongoDB MCP server starts automatically with the extension and connects to the active connection. If automatic startup is disabled, the server can still be started using the 'MongoDB: Start MCP Server' command."
},
"mdb.mcp.exportsPath": {
Expand Down
4 changes: 3 additions & 1 deletion src/connectionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,9 @@ export default class ConnectionController {
return false;
}

const originalDisconnect = this._activeDataService.disconnect.bind(this);
const originalDisconnect = this._activeDataService.disconnect.bind(
this._activeDataService,
);
this._activeDataService = null;

try {
Expand Down
4 changes: 4 additions & 0 deletions src/mcp/mcpConnectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export class MCPConnectionManager extends ConnectionManager {
this.getTelemetryAnonymousId = getTelemetryAnonymousId;
}

setLogger(logger: LoggerBase): void {
this.logger = logger;
}

connect(): Promise<AnyConnectionState> {
return Promise.reject(
new Error(
Expand Down
245 changes: 151 additions & 94 deletions src/mcp/mcpController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ import type { MCPConnectParams } from './mcpConnectionManager';
import { MCPConnectionManager } from './mcpConnectionManager';
import { createMCPConnectionErrorHandler } from './mcpConnectionErrorHandler';
import { getMCPConfigFromVSCodeSettings } from './mcpConfig';
import { DEFAULT_TELEMETRY_APP_NAME } from '../connectionController';

export type McpServerStartupConfig = 'enabled' | 'disabled';
export type MCPServerStartupConfig =
| 'prompt'
| 'autoStartEnabled'
| 'autoStartDisabled';

class VSCodeMCPLogger extends LoggerBase {
private readonly _logger = createLogger('mcp-server');
Expand Down Expand Up @@ -51,11 +55,11 @@ type MCPControllerConfig = {
export class MCPController {
private context: vscode.ExtensionContext;
private connectionController: ConnectionController;
private getTelemetryAnonymousId: () => string;

private didChangeEmitter = new vscode.EventEmitter<void>();
private server?: MCPServerInfo;
private mcpConnectionManager?: MCPConnectionManager;
private mcpConnectionManager: MCPConnectionManager;
private vsCodeMCPLogger: LoggerBase;

constructor({
context,
Expand All @@ -64,10 +68,17 @@ export class MCPController {
}: MCPControllerConfig) {
this.context = context;
this.connectionController = connectionController;
this.getTelemetryAnonymousId = getTelemetryAnonymousId;
this.vsCodeMCPLogger = new VSCodeMCPLogger(Keychain.root);
this.mcpConnectionManager = new MCPConnectionManager({
logger: this.vsCodeMCPLogger,
getTelemetryAnonymousId,
});
}

public async activate(): Promise<void> {
await this.migrateOldConfigToNewConfig(this.getMCPAutoStartConfig());
await this.switchConnectionManagerToCurrentConnection();

this.context.subscriptions.push(
vscode.lm.registerMcpServerDefinitionProvider('mongodb', {
onDidChangeMcpServerDefinitions: this.didChangeEmitter.event,
Expand All @@ -87,16 +98,83 @@ export class MCPController {
},
);

if (this.shouldStartMCPServer()) {
if (this.shouldPromptForAutoStart()) {
void this.promptForMCPAutoStart();
}

if (this.getMCPAutoStartConfig() === 'autoStartEnabled') {
await this.startServer();
void this.notifyOnFirstStart();
}
}

private async migrateOldConfigToNewConfig(oldConfig: unknown): Promise<void> {
try {
switch (oldConfig) {
// The previous logic would set the mdb.mcp.server to 'enabled' on
// extension activate (with a notification) so we're assuming that this
// value is not the result of explicit user action and hence mapping it
// to 'prompt'
case 'ask':
case 'enabled': {
await this.setMCPAutoStartConfig('prompt');
break;
}

// In the previous logic only 'disabled' value would've represented an
// explicit user action which is why we preserve that and map it to new
// disabled value.
case 'disabled': {
await this.setMCPAutoStartConfig('autoStartDisabled');
break;
}

// Any other value is possible only if user explicitly mentioned or when
// if the values were already migrated it so we don't alter them.
default: {
break;
}
}
} catch (error) {
logger.error('Error when migrating old config to the new config', error);
}
}

private async promptForMCPAutoStart(): Promise<void> {
try {
const promptResponse = await vscode.window.showInformationMessage(
'Would you like to automatically start the MongoDB MCP server? When started, the MongoDB MCP Server will automatically connect to your active MongoDB instance.',
'Yes',
'Not now',
);

switch (promptResponse) {
case 'Yes': {
await this.setMCPAutoStartConfig('autoStartEnabled');
await this.startServer();
break;
}

case 'Not now': {
await this.setMCPAutoStartConfig('autoStartDisabled');
break;
}

default:
break;
}
} catch (error) {
logger.error('Error when prompting for MCP auto start', error);
}
}

public async startServer(): Promise<void> {
try {
// Stop an already running server if any
await this.stopServer();
if (this.server) {
logger.info(
'MCP server start requested. An MCP server is already running, will not start a new server.',
);
return;
}

const token = crypto.randomUUID();
const headers: Record<string, string> = {
Expand Down Expand Up @@ -130,16 +208,11 @@ export class MCPController {
apiClientSecret: '<redacted>',
});

const createConnectionManager: ConnectionManagerFactoryFn = async ({
const createConnectionManager: ConnectionManagerFactoryFn = ({
logger,
}) => {
const connectionManager = (this.mcpConnectionManager =
new MCPConnectionManager({
logger,
getTelemetryAnonymousId: this.getTelemetryAnonymousId,
}));
await this.switchConnectionManagerToCurrentConnection();
return connectionManager;
this.mcpConnectionManager.setLogger(logger);
return Promise.resolve(this.mcpConnectionManager);
};

const runner = new StreamableHttpRunner({
Expand All @@ -149,6 +222,9 @@ export class MCPController {
this.connectionController,
),
additionalLoggers: [new VSCodeMCPLogger(Keychain.root)],
telemetryProperties: {
hosting_mode: DEFAULT_TELEMETRY_APP_NAME,
},
});
await runner.start();

Expand All @@ -165,66 +241,18 @@ export class MCPController {
}

public async stopServer(): Promise<void> {
try {
await this.server?.runner.close();
this.server = undefined;
this.didChangeEmitter.fire();
} catch (error) {
logger.error('Error when attempting to close the MCP server', error);
}
}

private async notifyOnFirstStart(): Promise<void> {
try {
if (!this.server) {
// Server was never started so no need to notify
logger.info(
'MCP server stop requested. No MCP server running, nothing to stop.',
);
return;
}

const serverStartConfig = this.getMCPAutoStartConfig();

// If the config value is one of the following values means they are
// intentional (either set by user or by this function itself) and we
// should not notify in that case.
const shouldNotNotify =
serverStartConfig === 'enabled' || serverStartConfig === 'disabled';

if (shouldNotNotify) {
return;
}

// We set the auto start already to enabled to not prompt user again for
// this on the next boot. We do it this way because chances are that the
// user might not act on the notification in which case the final update
// will never happen.
await this.setMCPAutoStartConfig('enabled');
let selectedServerStartConfig: McpServerStartupConfig = 'enabled';

const prompt = await vscode.window.showInformationMessage(
'MongoDB MCP server started automatically and will connect to your active connection. Would you like to keep or disable automatic startup?',
'Keep',
'Disable',
);

switch (prompt) {
case 'Keep':
default:
// The default happens only when users explicity dismiss the
// notification.
selectedServerStartConfig = 'enabled';
break;
case 'Disable': {
selectedServerStartConfig = 'disabled';
await this.stopServer();
}
}

await this.setMCPAutoStartConfig(selectedServerStartConfig);
await this.server.runner.close();
this.server = undefined;
this.didChangeEmitter.fire();
} catch (error) {
logger.error(
'Error while attempting to emit MCP server started notification',
error,
);
logger.error('Error when attempting to close the MCP server', error);
}
}

Expand Down Expand Up @@ -299,43 +327,72 @@ ${jsonConfig}`,
}

private async onActiveConnectionChanged(): Promise<void> {
if (!this.server) {
return;
logger.info(
'Active connection changed, will switch connection manager to new connection',
{
connectionId: this.connectionController.getActiveConnectionId(),
shouldPromptForAutoStart: this.shouldPromptForAutoStart(),
serverStarted: !!this.server,
},
);

if (
this.connectionController.getActiveConnectionId() &&
this.shouldPromptForAutoStart()
) {
void this.promptForMCPAutoStart();
}

await this.switchConnectionManagerToCurrentConnection();
}

private async switchConnectionManagerToCurrentConnection(): Promise<void> {
const connectionId = this.connectionController.getActiveConnectionId();
const mongoClientOptions =
this.connectionController.getMongoClientConnectionOptions();

const connectParams: MCPConnectParams | undefined =
connectionId && mongoClientOptions
? {
connectionId: connectionId,
connectionString: mongoClientOptions.url,
connectOptions: mongoClientOptions.options,
}
: undefined;
await this.mcpConnectionManager?.updateConnection(connectParams);
try {
const connectionId = this.connectionController.getActiveConnectionId();
const mongoClientOptions =
this.connectionController.getMongoClientConnectionOptions();

const connectParams: MCPConnectParams | undefined =
connectionId && mongoClientOptions
? {
connectionId: connectionId,
connectionString: mongoClientOptions.url,
connectOptions: mongoClientOptions.options,
}
: undefined;
await this.mcpConnectionManager.updateConnection(connectParams);
} catch (error) {
logger.error('Error when attempting to switch connection', error);
}
}

private shouldStartMCPServer(): boolean {
return this.getMCPAutoStartConfig() !== 'disabled';
private shouldPromptForAutoStart(): boolean {
const storedConfig = this.getMCPAutoStartConfig();
return storedConfig === 'prompt';
}

private getMCPAutoStartConfig(): McpServerStartupConfig | undefined {
private getMCPAutoStartConfig(): unknown {
return vscode.workspace
.getConfiguration('mdb')
.get<McpServerStartupConfig>('mcp.server');
.getConfiguration()
.get<unknown>('mdb.mcp.server', 'prompt');
}

private async setMCPAutoStartConfig(
config: McpServerStartupConfig,
config: MCPServerStartupConfig,
): Promise<void> {
await vscode.workspace
.getConfiguration('mdb')
.update('mcp.server', config, true);
.getConfiguration()
.update('mdb.mcp.server', config, true);
}

public get _test_isServerRunning(): boolean {
return (
this.server !== undefined &&
this.server.runner instanceof StreamableHttpRunner
);
}

public get _test_mcpConnectionManager(): MCPConnectionManager {
return this.mcpConnectionManager;
}
}
6 changes: 4 additions & 2 deletions src/test/suite/connectionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,10 @@ suite('Connection Controller Test Suite', function () {
// The number of times we expect to re-render connections on the sidebar:
// - connection attempt started
// - connection attempt finished
// - disconnect
const expectedTimesToFire = 3;
// - disconnect from our call in the tests
// - disconnect from on('close') listener on DataService that gets called as
// a result of our disconnect call
const expectedTimesToFire = 4;
let connectionsDidChangeEventFiredCount = 0;

testConnectionController.addEventListener('CONNECTIONS_DID_CHANGE', () => {
Expand Down
Loading
Loading