Skip to content

Commit e02b22d

Browse files
committed
Cleaned up session support in MCP server (#DH-21947)
1 parent c324604 commit e02b22d

2 files changed

Lines changed: 209 additions & 88 deletions

File tree

src/mcp/McpServer.spec.ts

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,39 @@
11
import * as http from 'http';
22
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
33
import type { McpTool, McpToolSpec } from '../types';
4+
import type { OutputChannelWithHistory } from '../util';
5+
6+
/* eslint-disable @typescript-eslint/naming-convention */
7+
// HTTP headers and MCP protocol headers must use their spec-defined names
48

59
vi.mock('vscode');
610

711
// Mock all tool creators to return minimal stub tools
812
vi.mock('./tools', () => ({
9-
createAddRemoteFileSourcesTool: vi.fn(() => makeStubTool('addRemoteFileSources')),
13+
createAddRemoteFileSourcesTool: vi.fn(() =>
14+
makeStubTool('addRemoteFileSources')
15+
),
1016
createConnectToServerTool: vi.fn(() => makeStubTool('connectToServer')),
1117
createGetColumnStatsTool: vi.fn(() => makeStubTool('getColumnStats')),
1218
createGetLogsTool: vi.fn(() => makeStubTool('getLogs')),
1319
createGetTableDataTool: vi.fn(() => makeStubTool('getTableData')),
1420
createGetTableStatsTool: vi.fn(() => makeStubTool('getTableStats')),
1521
createListConnectionsTool: vi.fn(() => makeStubTool('listConnections')),
1622
createListVariablesTool: vi.fn(() => makeStubTool('listVariables')),
17-
createListRemoteFileSourcesTool: vi.fn(() => makeStubTool('listRemoteFileSources')),
23+
createListRemoteFileSourcesTool: vi.fn(() =>
24+
makeStubTool('listRemoteFileSources')
25+
),
1826
createListServersTool: vi.fn(() => makeStubTool('listServers')),
1927
createOpenFilesInEditorTool: vi.fn(() => makeStubTool('openFilesInEditor')),
2028
createOpenVariablePanelsTool: vi.fn(() => makeStubTool('openVariablePanels')),
21-
createRemoveRemoteFileSourcesTool: vi.fn(() => makeStubTool('removeRemoteFileSources')),
29+
createRemoveRemoteFileSourcesTool: vi.fn(() =>
30+
makeStubTool('removeRemoteFileSources')
31+
),
2232
createRunCodeFromUriTool: vi.fn(() => makeStubTool('runCodeFromUri')),
2333
createRunCodeTool: vi.fn(() => makeStubTool('runCode')),
24-
createSetEditorConnectionTool: vi.fn(() => makeStubTool('setEditorConnection')),
34+
createSetEditorConnectionTool: vi.fn(() =>
35+
makeStubTool('setEditorConnection')
36+
),
2537
createShowOutputPanelTool: vi.fn(() => makeStubTool('showOutputPanel')),
2638
}));
2739

@@ -37,6 +49,7 @@ function makeStubTool(name: string): McpTool<McpToolSpec> {
3749
title: name,
3850
description: `Stub tool: ${name}`,
3951
inputSchema: {},
52+
outputSchema: {},
4053
},
4154
handler: vi.fn().mockResolvedValue({
4255
content: [{ type: 'text', text: `stub result for ${name}` }],
@@ -45,7 +58,7 @@ function makeStubTool(name: string): McpTool<McpToolSpec> {
4558
}
4659

4760
/** Create a minimal mock for OutputChannelWithHistory */
48-
function makeMockOutputChannel() {
61+
function makeMockOutputChannel(): OutputChannelWithHistory {
4962
return {
5063
appendLine: vi.fn(),
5164
append: vi.fn(),
@@ -55,15 +68,19 @@ function makeMockOutputChannel() {
5568
hide: vi.fn(),
5669
replace: vi.fn(),
5770
name: 'mock',
58-
};
71+
} as unknown as OutputChannelWithHistory;
5972
}
6073

6174
// Helper: send an HTTP POST to the MCP server and collect the full response
6275
function postToMcp(
6376
port: number,
6477
body: unknown,
6578
headers: Record<string, string> = {}
66-
): Promise<{ status: number; headers: http.IncomingMessage['headers']; body: string }> {
79+
): Promise<{
80+
status: number;
81+
headers: http.IncomingMessage['headers'];
82+
body: string;
83+
}> {
6784
return new Promise((resolve, reject) => {
6885
const payload = JSON.stringify(body);
6986
const req = http.request(
@@ -85,7 +102,11 @@ function postToMcp(
85102
let data = '';
86103
res.on('data', chunk => (data += chunk));
87104
res.on('end', () =>
88-
resolve({ status: res.statusCode ?? 0, headers: res.headers, body: data })
105+
resolve({
106+
status: res.statusCode ?? 0,
107+
headers: res.headers,
108+
body: data,
109+
})
89110
);
90111
}
91112
);
@@ -129,7 +150,7 @@ describe('McpServer', () => {
129150
{} as any, // panelService
130151
{} as any, // pythonDiagnostics
131152
{} as any, // pythonWorkspace
132-
{} as any // serverManager
153+
{} as any // serverManager
133154
);
134155

135156
port = await server.start();
@@ -149,8 +170,8 @@ describe('McpServer', () => {
149170
expect(res.status).toBe(200);
150171
// The session ID is returned either in the response header or body
151172
const sessionId =
152-
res.headers['mcp-session-id'] as string | undefined ??
153-
(() => {
173+
(res.headers['mcp-session-id'] as string | undefined) ??
174+
((): string | undefined => {
154175
try {
155176
return JSON.parse(res.body)?.sessionId as string | undefined;
156177
} catch {
@@ -172,10 +193,10 @@ describe('McpServer', () => {
172193
expect(body.error.code).toBe(-32600);
173194
});
174195

175-
it('should reject GET requests with 405', async () => {
196+
it('should reject unsupported methods (PUT, DELETE, etc.) with 405', async () => {
176197
const res = await new Promise<{ status: number }>((resolve, reject) => {
177198
const req = http.request(
178-
{ hostname: '127.0.0.1', port, path: '/mcp', method: 'GET' },
199+
{ hostname: '127.0.0.1', port, path: '/mcp', method: 'PUT' },
179200
res => resolve({ status: res.statusCode ?? 0 })
180201
);
181202
req.on('error', reject);
@@ -301,10 +322,26 @@ describe('McpServer', () => {
301322

302323
// Then fire requests on both sessions in parallel
303324
const [res1a, res1b, res2a, res2b] = await Promise.all([
304-
postToMcp(port, { ...LIST_TOOLS_REQUEST, id: 21 }, { 'mcp-session-id': session1 }),
305-
postToMcp(port, { ...LIST_TOOLS_REQUEST, id: 22 }, { 'mcp-session-id': session1 }),
306-
postToMcp(port, { ...LIST_TOOLS_REQUEST, id: 23 }, { 'mcp-session-id': session2 }),
307-
postToMcp(port, { ...LIST_TOOLS_REQUEST, id: 24 }, { 'mcp-session-id': session2 }),
325+
postToMcp(
326+
port,
327+
{ ...LIST_TOOLS_REQUEST, id: 21 },
328+
{ 'mcp-session-id': session1 }
329+
),
330+
postToMcp(
331+
port,
332+
{ ...LIST_TOOLS_REQUEST, id: 22 },
333+
{ 'mcp-session-id': session1 }
334+
),
335+
postToMcp(
336+
port,
337+
{ ...LIST_TOOLS_REQUEST, id: 23 },
338+
{ 'mcp-session-id': session2 }
339+
),
340+
postToMcp(
341+
port,
342+
{ ...LIST_TOOLS_REQUEST, id: 24 },
343+
{ 'mcp-session-id': session2 }
344+
),
308345
]);
309346

310347
for (const res of [res1a, res1b, res2a, res2b]) {
@@ -327,7 +364,8 @@ describe('McpServer', () => {
327364
const toolsRes = await postToMcp(port, LIST_TOOLS_REQUEST, {
328365
'mcp-session-id': sessionId,
329366
});
330-
const tools: { name: string }[] = JSON.parse(toolsRes.body).result?.tools ?? [];
367+
const tools: { name: string }[] =
368+
JSON.parse(toolsRes.body).result?.tools ?? [];
331369
expect(tools.some(t => t.name === 'addRemoteFileSources')).toBe(true);
332370
});
333371

@@ -338,7 +376,8 @@ describe('McpServer', () => {
338376
const toolsRes = await postToMcp(port, LIST_TOOLS_REQUEST, {
339377
'mcp-session-id': sessionId,
340378
});
341-
const tools: { name: string }[] = JSON.parse(toolsRes.body).result?.tools ?? [];
379+
const tools: { name: string }[] =
380+
JSON.parse(toolsRes.body).result?.tools ?? [];
342381
expect(tools.some(t => t.name === 'listServers')).toBe(true);
343382
});
344383

@@ -349,7 +388,8 @@ describe('McpServer', () => {
349388
const toolsRes = await postToMcp(port, LIST_TOOLS_REQUEST, {
350389
'mcp-session-id': sessionId,
351390
});
352-
const tools: { name: string }[] = JSON.parse(toolsRes.body).result?.tools ?? [];
391+
const tools: { name: string }[] =
392+
JSON.parse(toolsRes.body).result?.tools ?? [];
353393
expect(tools.some(t => t.name === 'runCode')).toBe(true);
354394
});
355395
});

0 commit comments

Comments
 (0)