77import './polyfill.js' ;
88
99import { exec } from 'node:child_process' ;
10+ import { randomUUID } from 'node:crypto' ;
11+ import { createServer } from 'node:http' ;
1012import path from 'node:path' ;
1113import process from 'node:process' ;
1214
@@ -32,6 +34,7 @@ import {lifecycleService} from './services/index.js';
3234import {
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. */
373376let 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
702705for ( const tool of tools ) {
703- registerTool ( tool ) ;
706+ registerTool ( server , tool ) ;
704707}
705708
706709await loadIssueDescriptions ( ) ;
@@ -724,4 +727,106 @@ try {
724727
725728logDisclaimers ( ) ;
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
0 commit comments