Skip to content

Commit 19f66d3

Browse files
committed
feat(inspector): implement Streamable HTTP server for MCP Inspector
1 parent 5cb8c0a commit 19f66d3

2 files changed

Lines changed: 109 additions & 3 deletions

File tree

src/main.ts

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import './polyfill.js';
88

99
import {exec} from 'node:child_process';
10+
import {randomUUID} from 'node:crypto';
11+
import {createServer} from 'node:http';
1012
import path from 'node:path';
1113
import process from 'node:process';
1214

@@ -32,6 +34,7 @@ import {lifecycleService} from './services/index.js';
3234
import {
3335
McpServer,
3436
StdioServerTransport,
37+
StreamableHTTPServerTransport,
3538
type CallToolResult,
3639
SetLevelRequestSchema,
3740
} from './third_party/index.js';
@@ -372,7 +375,7 @@ let extensionHotReloadInProgress: Promise<void> | null = null;
372375
/** Shared result from the hot-reload check — set by the winner, consumed by waiters. */
373376
let hotReloadResult: CallToolResult | null = null;
374377

375-
function registerTool(tool: ToolDefinition): void {
378+
function registerTool(targetServer: McpServer, tool: ToolDefinition): void {
376379
if (
377380
tool.annotations.conditions?.includes('computerVision') &&
378381
!config.experimentalVision
@@ -386,7 +389,7 @@ function registerTool(tool: ToolDefinition): void {
386389
) {
387390
return;
388391
}
389-
server.registerTool(
392+
targetServer.registerTool(
390393
tool.name,
391394
{
392395
description: tool.description,
@@ -700,7 +703,7 @@ function registerTool(tool: ToolDefinition): void {
700703
}
701704

702705
for (const tool of tools) {
703-
registerTool(tool);
706+
registerTool(server, tool);
704707
}
705708

706709
await loadIssueDescriptions();
@@ -724,4 +727,106 @@ try {
724727

725728
logDisclaimers();
726729

730+
// ── Inspector HTTP Server (Streamable HTTP transport) ────────────────
731+
// Exposes the same tools on a separate Streamable HTTP endpoint so
732+
// MCP Inspector (browser-based) can connect to this running server
733+
// instance without spawning a second process. Each Inspector browser
734+
// session gets its own McpServer + StreamableHTTPServerTransport that
735+
// share the same module-level state (connection, mutexes, etc.).
736+
const INSPECTOR_HTTP_PORT = 6274;
737+
738+
function startInspectorServer(): void {
739+
const sessions = new Map<string, {transport: StreamableHTTPServerTransport; mcpServer: McpServer}>();
740+
741+
const httpServer = createServer(async (req, res) => {
742+
// CORS headers — Inspector runs in a browser on a different origin
743+
res.setHeader('Access-Control-Allow-Origin', '*');
744+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
745+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id, mcp-protocol-version');
746+
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id');
747+
748+
if (req.method === 'OPTIONS') {
749+
res.writeHead(204);
750+
res.end();
751+
return;
752+
}
753+
754+
const url = new URL(req.url ?? '/', `http://localhost:${INSPECTOR_HTTP_PORT}`);
755+
if (url.pathname !== '/mcp') {
756+
res.writeHead(404);
757+
res.end('Not Found');
758+
return;
759+
}
760+
761+
try {
762+
const sessionId = typeof req.headers['mcp-session-id'] === 'string'
763+
? req.headers['mcp-session-id']
764+
: undefined;
765+
766+
// Route to existing session
767+
if (sessionId) {
768+
const entry = sessions.get(sessionId);
769+
if (entry) {
770+
await entry.transport.handleRequest(req, res);
771+
return;
772+
}
773+
// Unknown session — per spec, 404
774+
res.writeHead(404, {'Content-Type': 'application/json'});
775+
res.end(JSON.stringify({
776+
jsonrpc: '2.0',
777+
error: {code: -32000, message: 'Session not found'},
778+
}));
779+
return;
780+
}
781+
782+
// New session: create a dedicated McpServer + transport pair
783+
const inspectorTransport = new StreamableHTTPServerTransport({
784+
sessionIdGenerator: () => randomUUID(),
785+
onsessioninitialized: (sid: string) => {
786+
sessions.set(sid, {transport: inspectorTransport, mcpServer: inspectorMcp});
787+
logger(`[inspector] New session: ${sid.substring(0, 8)}…`);
788+
},
789+
onsessionclosed: (sid: string) => {
790+
sessions.delete(sid);
791+
logger(`[inspector] Session ${sid.substring(0, 8)}… closed`);
792+
},
793+
});
794+
const inspectorMcp = new McpServer(
795+
{name: 'vscode_devtools', title: 'VS Code DevTools MCP server', version: VERSION},
796+
{capabilities: {logging: {}}},
797+
);
798+
inspectorMcp.server.setRequestHandler(SetLevelRequestSchema, () => ({}));
799+
800+
for (const tool of tools) {
801+
registerTool(inspectorMcp, tool);
802+
}
803+
804+
await inspectorMcp.connect(inspectorTransport);
805+
await inspectorTransport.handleRequest(req, res);
806+
} catch (err) {
807+
logger(`[inspector] Request error: ${err instanceof Error ? err.message : String(err)}`);
808+
if (!res.headersSent) {
809+
res.writeHead(500, {'Content-Type': 'application/json'});
810+
res.end(JSON.stringify({
811+
jsonrpc: '2.0',
812+
error: {code: -32603, message: 'Internal error'},
813+
}));
814+
}
815+
}
816+
});
817+
818+
httpServer.listen(INSPECTOR_HTTP_PORT, () => {
819+
logger(`[inspector] MCP Inspector endpoint ready at http://localhost:${INSPECTOR_HTTP_PORT}/mcp`);
820+
});
821+
822+
httpServer.on('error', (err: NodeJS.ErrnoException) => {
823+
if (err.code === 'EADDRINUSE') {
824+
logger(`[inspector] ⚠ Port ${INSPECTOR_HTTP_PORT} in use — Inspector HTTP endpoint not available`);
825+
} else {
826+
logger(`[inspector] HTTP server error: ${err.message}`);
827+
}
828+
});
829+
}
830+
831+
startInspectorServer();
727832

src/third_party/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export {default as debug} from 'debug';
1515
export type {Debugger} from 'debug';
1616
export {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
1717
export {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
18+
export {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
1819
export {
1920
type CallToolResult,
2021
SetLevelRequestSchema,

0 commit comments

Comments
 (0)