Skip to content

Commit 0b72e6a

Browse files
chore: tests for VSCodeConnectionManager and MCPController
1 parent 0f2ed1b commit 0b72e6a

File tree

4 files changed

+547
-28
lines changed

4 files changed

+547
-28
lines changed

src/mcp/mcpController.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ import * as vscode from 'vscode';
22
import { defaultUserConfig, StreamableHttpRunner } from 'mongodb-mcp-server';
33
import type ConnectionController from '../connectionController';
44
import { VSCodeMCPConnectionManager } from './vsCodeMCPConnectionManager';
5-
import type { CreateConnectionManagerFn, UserConfig } from 'mongodb-mcp-server';
5+
import type {
6+
ConnectionManagerFactoryFn,
7+
UserConfig,
8+
} from 'mongodb-mcp-server';
69

710
type mcpServerStartupConfig = 'ask' | 'enabled' | 'disabled';
811

12+
export type MCPServerInfo = {
13+
runner: StreamableHttpRunner;
14+
headers: Record<string, string>;
15+
};
16+
917
export class MCPController {
1018
private didChangeEmitter = new vscode.EventEmitter<void>();
11-
private server?: {
12-
runner: StreamableHttpRunner;
13-
headers: Record<string, string>;
14-
};
19+
private server?: MCPServerInfo;
1520
private mcpConnectionManager?: VSCodeMCPConnectionManager;
1621

1722
constructor(
@@ -54,7 +59,7 @@ export class MCPController {
5459
disabledTools: ['connect'],
5560
};
5661

57-
const createConnectionManager: CreateConnectionManagerFn = async ({
62+
const createConnectionManager: ConnectionManagerFactoryFn = async ({
5863
logger,
5964
}) => {
6065
const connectionManager = (this.mcpConnectionManager =
@@ -208,7 +213,7 @@ ${jsonConfig}`,
208213
const mongoClientOptions =
209214
this.connectionController.getMongoClientConnectionOptions();
210215
await this.mcpConnectionManager?.updateConnection({
211-
connectionId,
216+
connectionId: connectionId ?? undefined,
212217
connectionString: mongoClientOptions?.url,
213218
connectOptions: mongoClientOptions?.options,
214219
});

src/mcp/vsCodeMCPConnectionManager.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {
22
ConnectionManager,
33
type AnyConnectionState,
4-
type CompositeLogger,
54
type ConnectionStateDisconnected,
5+
type LoggerBase,
66
} from 'mongodb-mcp-server';
77
import {
88
NodeDriverServiceProvider,
@@ -22,7 +22,7 @@ export class VSCodeMCPConnectionManager extends ConnectionManager {
2222
private activeConnectionId: string | null = null;
2323
private activeConnectionProvider: ServiceProvider | null = null;
2424

25-
constructor(private readonly logger: CompositeLogger) {
25+
constructor(private readonly logger: LoggerBase) {
2626
super();
2727
}
2828

@@ -41,14 +41,14 @@ export class VSCodeMCPConnectionManager extends ConnectionManager {
4141
connectParams: VSCodeMCPConnectParams,
4242
): Promise<AnyConnectionState> {
4343
try {
44-
const serviceProvider = (this.activeConnectionProvider =
45-
await NodeDriverServiceProvider.connect(
46-
connectParams.connectionString,
47-
connectParams.connectOptions,
48-
));
44+
const serviceProvider = await NodeDriverServiceProvider.connect(
45+
connectParams.connectionString,
46+
connectParams.connectOptions,
47+
);
4948
await serviceProvider.runCommand('admin', { hello: 1 });
5049
this.activeConnectionId = connectParams.connectionId;
51-
return this.changeState('connection-succeeded', {
50+
this.activeConnectionProvider = serviceProvider;
51+
return this.changeState('connection-success', {
5252
tag: 'connected',
5353
serviceProvider,
5454
});
@@ -58,7 +58,7 @@ export class VSCodeMCPConnectionManager extends ConnectionManager {
5858
context: 'VSCodeMCPConnectionManager.connect',
5959
message: error instanceof Error ? error.message : String(error),
6060
});
61-
return this.changeState('connection-errored', {
61+
return this.changeState('connection-error', {
6262
tag: 'errored',
6363
errorReason: error instanceof Error ? error.message : String(error),
6464
});
@@ -78,16 +78,18 @@ export class VSCodeMCPConnectionManager extends ConnectionManager {
7878
} finally {
7979
this.activeConnectionId = null;
8080
this.activeConnectionProvider = null;
81+
return this.changeState('connection-close', {
82+
tag: 'disconnected',
83+
});
8184
}
82-
return Promise.resolve({ tag: 'disconnected' });
8385
}
8486

8587
async updateConnection({
8688
connectionId,
8789
connectionString,
8890
connectOptions,
8991
}: {
90-
connectionId: string | null;
92+
connectionId: string | undefined;
9193
connectionString: string | undefined;
9294
connectOptions: DevtoolsConnectOptions | undefined;
9395
}): Promise<void> {
@@ -96,21 +98,23 @@ export class VSCodeMCPConnectionManager extends ConnectionManager {
9698
await this.disconnect();
9799
}
98100

99-
if (this.activeConnectionId === connectionId) {
100-
return;
101-
}
101+
const connectionWasDisconnected =
102+
!connectionId || !connectionString || !connectOptions;
102103

103-
if (!connectionString || !connectOptions || !connectionId) {
104-
this.changeState('connection-errored', {
105-
tag: 'errored',
106-
errorReason:
107-
'MongoDB MCP server cannot establish connection without the required connection string and MongoClientOptions',
108-
});
104+
if (
105+
this.activeConnectionId === connectionId ||
106+
connectionWasDisconnected
107+
) {
109108
return;
110109
}
111110

112111
if (isAtlasStream(connectionString)) {
113-
this.changeState('connection-errored', {
112+
this.logger.warning({
113+
id: MCPLogIds.UpdateConnectionError,
114+
context: 'VSCodeMCPConnectionManager.updateConnection',
115+
message: 'updateConnection called for an AtlasStreams connection',
116+
});
117+
this.changeState('connection-error', {
114118
tag: 'errored',
115119
errorReason:
116120
'MongoDB MCP server do not support connecting to Atlas Streams',
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import type { SinonStub } from 'sinon';
2+
import sinon from 'sinon';
3+
import { expect } from 'chai';
4+
import { afterEach, beforeEach } from 'mocha';
5+
import * as vscode from 'vscode';
6+
import type { ExtensionContext } from 'vscode';
7+
import * as MCPServer from 'mongodb-mcp-server';
8+
import { ExtensionContextStub } from '../stubs';
9+
import type { MCPServerInfo } from '../../../mcp/mcpController';
10+
import { MCPController } from '../../../mcp/mcpController';
11+
import ConnectionController from '../../../connectionController';
12+
import { StatusView } from '../../../views';
13+
import { StorageController } from '../../../storage';
14+
import { TelemetryService } from '../../../telemetry';
15+
import { TEST_DATABASE_URI } from '../dbTestHelper';
16+
17+
const sandbox = sinon.createSandbox();
18+
suite('MCPController test suite', function () {
19+
this.timeout(300000);
20+
let extensionContext: ExtensionContext;
21+
let connectionController: ConnectionController;
22+
let mcpController: MCPController;
23+
24+
beforeEach(() => {
25+
extensionContext = new ExtensionContextStub();
26+
const testStorageController = new StorageController(extensionContext);
27+
const testTelemetryService = new TelemetryService(
28+
testStorageController,
29+
extensionContext,
30+
);
31+
connectionController = new ConnectionController({
32+
statusView: new StatusView(extensionContext),
33+
storageController: testStorageController,
34+
telemetryService: testTelemetryService,
35+
});
36+
37+
mcpController = new MCPController(extensionContext, connectionController);
38+
});
39+
40+
afterEach(async () => {
41+
sandbox.restore();
42+
sandbox.reset();
43+
connectionController.clearAllConnections();
44+
await vscode.workspace.getConfiguration('mdb').update('mcp.server', null);
45+
});
46+
47+
test('should register mcp server definition provider', function () {
48+
// At-least one from our mcp controller
49+
expect(extensionContext.subscriptions.length).to.be.greaterThanOrEqual(1);
50+
});
51+
52+
suite('#activate', function () {
53+
test('should subscribe to ACTIVE_CONNECTION_CHANGED event', async function () {
54+
const addEventListenerSpy = sandbox.spy(
55+
connectionController,
56+
'addEventListener',
57+
);
58+
await mcpController.activate();
59+
expect(addEventListenerSpy).to.be.called;
60+
expect(addEventListenerSpy.args[0]).to.contain(
61+
'ACTIVE_CONNECTION_CHANGED',
62+
);
63+
});
64+
});
65+
66+
suite('#startServer', function () {
67+
test('should initialize HTTP transport and start it', async function () {
68+
await mcpController.startServer();
69+
const serverInfo = (mcpController as any).server as
70+
| MCPServerInfo
71+
| undefined;
72+
expect(serverInfo).to.not.be.undefined;
73+
expect(serverInfo?.runner).to.be.instanceOf(
74+
MCPServer.StreamableHttpRunner,
75+
);
76+
expect(serverInfo?.headers?.authorization).to.not.be.undefined;
77+
});
78+
});
79+
80+
suite('when mcp server start is enabled from config', function () {
81+
test('it should start mcp server without any confirmation', async function () {
82+
await vscode.workspace
83+
.getConfiguration('mdb')
84+
.update('mcp.server', 'enabled');
85+
86+
const showInformationSpy = sandbox.spy(
87+
vscode.window,
88+
'showInformationMessage',
89+
);
90+
const startServerSpy = sandbox.spy(mcpController, 'startServer');
91+
// listen to connection events
92+
await mcpController.activate();
93+
// add a new connection to trigger connection change
94+
await connectionController.addNewConnectionStringAndConnect({
95+
connectionString: TEST_DATABASE_URI,
96+
});
97+
98+
expect(showInformationSpy).to.not.be.called;
99+
expect(startServerSpy).to.be.calledOnce;
100+
});
101+
});
102+
103+
suite('when mcp server start is disabled from config', function () {
104+
test('it should not start mcp server and ask for no confirmation', async function () {
105+
await vscode.workspace
106+
.getConfiguration('mdb')
107+
.update('mcp.server', 'disabled');
108+
109+
const showInformationSpy = sandbox.spy(
110+
vscode.window,
111+
'showInformationMessage',
112+
);
113+
const startServerSpy = sandbox.spy(mcpController, 'startServer');
114+
// listen to connection events
115+
await mcpController.activate();
116+
// add a new connection to trigger connection change
117+
await connectionController.addNewConnectionStringAndConnect({
118+
connectionString: TEST_DATABASE_URI,
119+
});
120+
121+
expect(showInformationSpy).to.not.be.called;
122+
expect(startServerSpy).to.not.be.called;
123+
});
124+
});
125+
126+
suite('when mcp server start is not configured', function () {
127+
test('it should ask before starting the mcp server, and update the configuration with the chosen value', async function () {
128+
const updateStub = sandbox.stub();
129+
const fakeGetConfiguration = sandbox.fake.returns({
130+
get: () => null,
131+
update: updateStub,
132+
});
133+
sandbox.replace(
134+
vscode.workspace,
135+
'getConfiguration',
136+
fakeGetConfiguration,
137+
);
138+
139+
const showInformationStub: SinonStub = sandbox.stub(
140+
vscode.window,
141+
'showInformationMessage',
142+
);
143+
showInformationStub.resolves('Yes');
144+
const startServerSpy = sandbox.spy(mcpController, 'startServer');
145+
// listen to connection events
146+
await mcpController.activate();
147+
// add a new connection to trigger connection change
148+
await connectionController.addNewConnectionStringAndConnect({
149+
connectionString: TEST_DATABASE_URI,
150+
});
151+
expect(showInformationStub).to.be.calledOnce;
152+
expect(updateStub).to.be.calledWith('mcp.server', 'enabled', true);
153+
expect(startServerSpy).to.be.called;
154+
});
155+
156+
test('it should ask before starting the mcp server, and when denied, should not start the server', async function () {
157+
const updateStub = sandbox.stub();
158+
const fakeGetConfiguration = sandbox.fake.returns({
159+
get: () => null,
160+
update: updateStub,
161+
});
162+
sandbox.replace(
163+
vscode.workspace,
164+
'getConfiguration',
165+
fakeGetConfiguration,
166+
);
167+
168+
const showInformationStub: SinonStub = sandbox.stub(
169+
vscode.window,
170+
'showInformationMessage',
171+
);
172+
showInformationStub.resolves('No');
173+
const startServerSpy = sandbox.spy(mcpController, 'startServer');
174+
// listen to connection events
175+
await mcpController.activate();
176+
// add a new connection to trigger connection change
177+
await connectionController.addNewConnectionStringAndConnect({
178+
connectionString: TEST_DATABASE_URI,
179+
});
180+
expect(showInformationStub).to.be.calledOnce;
181+
expect(updateStub).to.be.calledWith('mcp.server', 'disabled', true);
182+
expect(startServerSpy).to.not.be.called;
183+
});
184+
});
185+
186+
suite('when an MCP server is already running', function () {
187+
test('it should notify the connection manager of the connection changed event', async function () {
188+
// We want to connect as soon as connection changes
189+
await vscode.workspace
190+
.getConfiguration('mdb')
191+
.update('mcp.server', 'enabled');
192+
193+
// Start the controller and list to events
194+
await mcpController.activate();
195+
196+
// Add a connection
197+
await connectionController.addNewConnectionStringAndConnect({
198+
connectionString: TEST_DATABASE_URI,
199+
});
200+
201+
const connectionChangedSpy = sandbox.spy(
202+
mcpController as any,
203+
'onActiveConnectionChanged',
204+
);
205+
206+
await connectionController.disconnect();
207+
expect(connectionChangedSpy).to.be.calledOnce;
208+
});
209+
});
210+
});

0 commit comments

Comments
 (0)