diff --git a/test/mcp/README.md b/test/mcp/README.md index aef4bc58435..a3c8a896c2d 100644 --- a/test/mcp/README.md +++ b/test/mcp/README.md @@ -27,28 +27,6 @@ That's it! It should automatically compile everything needed. Then you can use `/playwright` to ask specific questions. -## Quick Start - HTTP - -Getting started with the MCP server is simple - just run the pre-configured Code - OSS task: - -1. Launch the MCP Server - - In Code - OSS, open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and run: - - ``` - Tasks: Run Task → Launch MCP Server - ``` - -2. Start the MCP Server - - Open the Command Palette and run: - ``` - MCP: List Servers → vscode-playwright-mcp → Start Server - ``` - or open [mcp.json](../../.vscode/mcp.json) and start it from there. - -That's it! Your AI assistant can now use browser automation capabilities through MCP. - ## Arguments Open the [mcp.json](../../.vscode/mcp.json) and modify the `args`: @@ -149,11 +127,6 @@ test/mcp/ - Check that port 33418 is not already in use - Verify all dependencies are installed with `npm install` -### Connection Issues -- Confirm the server is running on `http://localhost:33418/mcp` -- Check your AI assistant's MCP configuration -- Look for CORS-related errors in browser console - ### Browser Automation Issues - Ensure Code - OSS has been built and run at least once - Check the server logs for Playwright-related errors diff --git a/test/mcp/package.json b/test/mcp/package.json index 14e89b3106c..37098b708d8 100644 --- a/test/mcp/package.json +++ b/test/mcp/package.json @@ -9,7 +9,6 @@ "watch-automation": "cd ../automation && npm run watch", "watch-mcp": "node ../../node_modules/typescript/bin/tsc --watch --preserveWatchOutput", "watch": "npm-run-all -lp watch-automation watch-mcp", - "start-http": "npm run -s compile && node ./out/main.js", "start-stdio": "npm run -s compile && node ./out/stdio.js" }, "dependencies": { diff --git a/test/mcp/src/application.ts b/test/mcp/src/application.ts index 2552fafd347..5f01cc93d6c 100644 --- a/test/mcp/src/application.ts +++ b/test/mcp/src/application.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as playwright from 'playwright'; -import { getDevElectronPath, Quality, ConsoleLogger, FileLogger, Logger, MultiLogger, getBuildElectronPath, getBuildVersion, measureAndLog } from '../../automation'; +import { getDevElectronPath, Quality, ConsoleLogger, FileLogger, Logger, MultiLogger, getBuildElectronPath, getBuildVersion, measureAndLog, Application } from '../../automation'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; @@ -281,7 +281,6 @@ async function ensureStableCode(): Promise { } async function setup(): Promise { - logger.log('Test data path:', testDataPath); logger.log('Preparing smoketest setup...'); if (!opts.web && !opts.remote && opts.build) { @@ -329,3 +328,55 @@ export async function getApplication() { }); return application; } + +export class ApplicationService { + private _application: Application | undefined; + private _closing: Promise | undefined; + private _listeners: ((app: Application | undefined) => void)[] = []; + + onApplicationChange(listener: (app: Application | undefined) => void): void { + this._listeners.push(listener); + } + + removeApplicationChangeListener(listener: (app: Application | undefined) => void): void { + const index = this._listeners.indexOf(listener); + if (index >= 0) { + this._listeners.splice(index, 1); + } + } + + get application(): Application | undefined { + return this._application; + } + + async getOrCreateApplication(): Promise { + if (this._closing) { + await this._closing; + } + if (!this._application) { + this._application = await getApplication(); + this._application.code.driver.browserContext.on('close', () => { + this._closing = (async () => { + if (this._application) { + this._application.code.driver.browserContext.removeAllListeners(); + await this._application.stop(); + this._application = undefined; + this._runAllListeners(); + } + })(); + }); + this._runAllListeners(); + } + return this._application; + } + + private _runAllListeners() { + for (const listener of this._listeners) { + try { + listener(this._application); + } catch (error) { + console.error('Error occurred in application change listener:', error); + } + } + } +} diff --git a/test/mcp/src/automation.ts b/test/mcp/src/automation.ts index 5f779ef221c..5b52e14a0a8 100644 --- a/test/mcp/src/automation.ts +++ b/test/mcp/src/automation.ts @@ -4,25 +4,47 @@ *--------------------------------------------------------------------------------------------*/ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { getApplication } from './application'; +import { ApplicationService } from './application'; import { applyAllTools } from './automationTools/index.js'; -import { Application } from '../../automation'; import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; -export async function getServer(app?: Application): Promise { +export async function getServer(appService: ApplicationService): Promise { const server = new McpServer({ name: 'VS Code Automation Server', version: '1.0.0', title: 'An MCP Server that can interact with a local build of VS Code. Used for verifying UI behavior.' }, { capabilities: { logging: {} } }); - const application = app ?? await getApplication(); + server.tool( + 'vscode_automation_start', + 'Start VS Code Build', + {}, + async () => { + const app = await appService.getOrCreateApplication(); + return { + content: [{ + type: 'text' as const, + text: app ? `VS Code started successfully` : `Failed to start VS Code` + }] + }; + } + ); // Apply all VS Code automation tools using the modular structure - applyAllTools(server, application); + const registeredTools = applyAllTools(server, appService); + const app = appService.application; + if (app) { + registeredTools.forEach(t => t.enable()); + } else { + registeredTools.forEach(t => t.disable()); + } - application.code.driver.browserContext.on('close', async () => { - await server.close(); + appService.onApplicationChange(app => { + if (app) { + registeredTools.forEach(t => t.enable()); + } else { + registeredTools.forEach(t => t.disable()); + } }); return server.server; diff --git a/test/mcp/src/automationTools/activityBar.ts b/test/mcp/src/automationTools/activityBar.ts index 651b0762b54..8b06471dda9 100644 --- a/test/mcp/src/automationTools/activityBar.ts +++ b/test/mcp/src/automationTools/activityBar.ts @@ -3,13 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; /** * Activity Bar Tools */ -export function applyActivityBarTools(server: McpServer, app: Application) { +export function applyActivityBarTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + // Doesn't seem particularly useful // server.tool( // 'vscode_automation_activitybar_wait_for_position', @@ -29,4 +31,6 @@ export function applyActivityBarTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/core.ts b/test/mcp/src/automationTools/core.ts index 4a00011b9a4..879f58ef7c0 100644 --- a/test/mcp/src/automationTools/core.ts +++ b/test/mcp/src/automationTools/core.ts @@ -3,27 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; /** * Core Application Management Tools */ -export function applyCoreTools(server: McpServer, app: Application) { - // A dummy start tool just so that the model has something to hold on to. - server.tool( - 'vscode_automation_start', - 'Start VS Code Build', - {}, - async () => { - return { - content: [{ - type: 'text' as const, - text: `VS Code started successfully` - }] - }; - } - ); +export function applyCoreTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + // Playwright keeps using this as a start... maybe it needs some massaging // server.tool( // 'vscode_automation_restart', @@ -166,4 +154,6 @@ export function applyCoreTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/debug.ts b/test/mcp/src/automationTools/debug.ts index 8e98e875970..b9dfb2951f3 100644 --- a/test/mcp/src/automationTools/debug.ts +++ b/test/mcp/src/automationTools/debug.ts @@ -3,18 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; import { z } from 'zod'; /** * Debug Tools */ -export function applyDebugTools(server: McpServer, app: Application) { - server.tool( +export function applyDebugTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + tools.push(server.tool( 'vscode_automation_debug_open', 'Open the debug viewlet', async () => { + const app = await appService.getOrCreateApplication(); await app.workbench.debug.openDebugViewlet(); return { content: [{ @@ -23,9 +25,9 @@ export function applyDebugTools(server: McpServer, app: Application) { }] }; } - ); + )); - server.tool( + tools.push(server.tool( 'vscode_automation_debug_set_breakpoint', 'Set a breakpoint on a specific line', { @@ -33,6 +35,7 @@ export function applyDebugTools(server: McpServer, app: Application) { }, async (args) => { const { lineNumber } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.debug.setBreakpointOnLine(lineNumber); return { content: [{ @@ -41,12 +44,13 @@ export function applyDebugTools(server: McpServer, app: Application) { }] }; } - ); + )); - server.tool( + tools.push(server.tool( 'vscode_automation_debug_start', 'Start debugging', async () => { + const app = await appService.getOrCreateApplication(); const result = await app.workbench.debug.startDebugging(); return { content: [{ @@ -55,7 +59,7 @@ export function applyDebugTools(server: McpServer, app: Application) { }] }; } - ); + )); // Playwright can probably figure this out // server.tool( @@ -131,4 +135,6 @@ export function applyDebugTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/editor.ts b/test/mcp/src/automationTools/editor.ts index f94c111fec4..0af80f18650 100644 --- a/test/mcp/src/automationTools/editor.ts +++ b/test/mcp/src/automationTools/editor.ts @@ -3,14 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; import { z } from 'zod'; /** * Editor Management Tools */ -export function applyEditorTools(server: McpServer, app: Application) { +export function applyEditorTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; // Playwright can probably figure this one out // server.tool( // 'vscode_automation_editor_open_file', @@ -31,7 +32,7 @@ export function applyEditorTools(server: McpServer, app: Application) { // ); // This one is critical as Playwright had trouble typing in monaco - server.tool( + tools.push(server.tool( 'vscode_automation_editor_type_text', 'Type text in the currently active editor', { @@ -40,6 +41,7 @@ export function applyEditorTools(server: McpServer, app: Application) { }, async (args) => { const { text, filename } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.editor.waitForTypeInEditor(filename, text); return { content: [{ @@ -48,7 +50,7 @@ export function applyEditorTools(server: McpServer, app: Application) { }] }; } - ); + )); // Doesn't seem particularly useful // server.tool( @@ -161,10 +163,11 @@ export function applyEditorTools(server: McpServer, app: Application) { // ); // Editor File Management Tools - server.tool( + tools.push(server.tool( 'vscode_automation_editor_new_untitled_file', 'Create a new untitled file', async () => { + const app = await appService.getOrCreateApplication(); await app.workbench.editors.newUntitledFile(); return { content: [{ @@ -173,12 +176,13 @@ export function applyEditorTools(server: McpServer, app: Application) { }] }; } - ); + )); - server.tool( + tools.push(server.tool( 'vscode_automation_editor_save_file', 'Save the currently active file', async () => { + const app = await appService.getOrCreateApplication(); await app.workbench.editors.saveOpenedFile(); return { content: [{ @@ -187,7 +191,7 @@ export function applyEditorTools(server: McpServer, app: Application) { }] }; } - ); + )); // Playwright can probably figure this out // server.tool( @@ -247,4 +251,6 @@ export function applyEditorTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/explorer.ts b/test/mcp/src/automationTools/explorer.ts index ebf3c7e2072..777d7e3dfcb 100644 --- a/test/mcp/src/automationTools/explorer.ts +++ b/test/mcp/src/automationTools/explorer.ts @@ -3,13 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; /** * Explorer and File Management Tools */ -export function applyExplorerTools(server: McpServer, app: Application) { +export function applyExplorerTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + // Playwright can probably figure this out // server.tool( // 'vscode_automation_explorer_open', @@ -24,4 +26,6 @@ export function applyExplorerTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/extensions.ts b/test/mcp/src/automationTools/extensions.ts index d84c17b1eef..e379a7d90e4 100644 --- a/test/mcp/src/automationTools/extensions.ts +++ b/test/mcp/src/automationTools/extensions.ts @@ -3,14 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; import { z } from 'zod'; /** * Extensions Tools */ -export function applyExtensionsTools(server: McpServer, app: Application) { +export function applyExtensionsTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + // Playwright can probably figure this out // server.tool( // 'vscode_automation_extensions_search', @@ -30,7 +32,7 @@ export function applyExtensionsTools(server: McpServer, app: Application) { // } // ); - server.tool( + tools.push(server.tool( 'vscode_automation_extensions_open', 'Open an extension by ID', { @@ -38,6 +40,7 @@ export function applyExtensionsTools(server: McpServer, app: Application) { }, async (args) => { const { extensionId } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.extensions.openExtension(extensionId); return { content: [{ @@ -46,7 +49,7 @@ export function applyExtensionsTools(server: McpServer, app: Application) { }] }; } - ); + )); // Playwright can probably figure this out // server.tool( @@ -67,7 +70,7 @@ export function applyExtensionsTools(server: McpServer, app: Application) { // } // ); - server.tool( + tools.push(server.tool( 'vscode_automation_extensions_install', 'Install an extension by ID', { @@ -76,6 +79,7 @@ export function applyExtensionsTools(server: McpServer, app: Application) { }, async (args) => { const { extensionId, waitUntilEnabled = true } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.extensions.installExtension(extensionId, waitUntilEnabled); return { content: [{ @@ -84,5 +88,7 @@ export function applyExtensionsTools(server: McpServer, app: Application) { }] }; } - ); + )); + + return tools; } diff --git a/test/mcp/src/automationTools/index.ts b/test/mcp/src/automationTools/index.ts index ee24c3f81a7..8bcbc3705b8 100644 --- a/test/mcp/src/automationTools/index.ts +++ b/test/mcp/src/automationTools/index.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; // Import all tool modules import { applyCoreTools } from './core.js'; @@ -25,66 +24,73 @@ import { applyNotebookTools } from './notebook.js'; import { applyLocalizationTools } from './localization.js'; import { applyTaskTools } from './task.js'; import { applyProfilerTools } from './profiler.js'; +import { ApplicationService } from '../application'; /** * Apply all VS Code automation tools to the MCP server * @param server - The MCP server instance - * @param app - The VS Code application instance + * @param appService - The application service instance + * @returns The registered tools from the server */ -export function applyAllTools(server: McpServer, app: Application): void { +export function applyAllTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + let tools: RegisteredTool[] = []; + // Core Application Management Tools - applyCoreTools(server, app); + tools = tools.concat(applyCoreTools(server, appService)); // Editor Management Tools - applyEditorTools(server, app); + tools = tools.concat(applyEditorTools(server, appService)); // Terminal Management Tools - applyTerminalTools(server, app); + tools = tools.concat(applyTerminalTools(server, appService)); // Debug Tools - applyDebugTools(server, app); + tools = tools.concat(applyDebugTools(server, appService)); // Search Tools - applySearchTools(server, app); + tools = tools.concat(applySearchTools(server, appService)); // Extensions Tools - applyExtensionsTools(server, app); + tools = tools.concat(applyExtensionsTools(server, appService)); // Command Palette and Quick Access Tools - applyQuickAccessTools(server, app); + tools = tools.concat(applyQuickAccessTools(server, appService)); // Explorer and File Management Tools - applyExplorerTools(server, app); + tools = tools.concat(applyExplorerTools(server, appService)); // Activity Bar Tools - applyActivityBarTools(server, app); + tools = tools.concat(applyActivityBarTools(server, appService)); // Source Control Management Tools - applySCMTools(server, app); + tools = tools.concat(applySCMTools(server, appService)); // Status Bar Tools - applyStatusBarTools(server, app); + tools = tools.concat(applyStatusBarTools(server, appService)); // Problems Panel Tools - applyProblemsTools(server, app); + tools = tools.concat(applyProblemsTools(server, appService)); // Settings Editor Tools - applySettingsTools(server, app); + tools = tools.concat(applySettingsTools(server, appService)); // Keybindings Editor Tools - applyKeybindingsTools(server, app); + tools = tools.concat(applyKeybindingsTools(server, appService)); // Notebook Tools - applyNotebookTools(server, app); + tools = tools.concat(applyNotebookTools(server, appService)); // Localization Tools - applyLocalizationTools(server, app); + tools = tools.concat(applyLocalizationTools(server, appService)); // Task Tools - applyTaskTools(server, app); + tools = tools.concat(applyTaskTools(server, appService)); // Profiler Tools - applyProfilerTools(server, app); + tools = tools.concat(applyProfilerTools(server, appService)); + + // Return all registered tools + return tools; } // Re-export individual tool functions for selective use diff --git a/test/mcp/src/automationTools/keybindings.ts b/test/mcp/src/automationTools/keybindings.ts index c2d8375633e..28908119e0d 100644 --- a/test/mcp/src/automationTools/keybindings.ts +++ b/test/mcp/src/automationTools/keybindings.ts @@ -3,13 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; /** * Keybindings Editor Tools */ -export function applyKeybindingsTools(server: McpServer, app: Application) { +export function applyKeybindingsTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + // Seems too niche // server.tool( // 'vscode_automation_keybindings_update', @@ -31,4 +33,6 @@ export function applyKeybindingsTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/localization.ts b/test/mcp/src/automationTools/localization.ts index 207a5a86618..bff17b43e70 100644 --- a/test/mcp/src/automationTools/localization.ts +++ b/test/mcp/src/automationTools/localization.ts @@ -3,13 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; /** * Localization Tools */ -export function applyLocalizationTools(server: McpServer, app: Application) { +export function applyLocalizationTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + // Seems too niche // server.tool( // 'vscode_automation_localization_get_locale_info', @@ -39,4 +41,6 @@ export function applyLocalizationTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/notebook.ts b/test/mcp/src/automationTools/notebook.ts index 37e30c8f792..68ce82ca6eb 100644 --- a/test/mcp/src/automationTools/notebook.ts +++ b/test/mcp/src/automationTools/notebook.ts @@ -3,18 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; import { z } from 'zod'; /** * Notebook Tools */ -export function applyNotebookTools(server: McpServer, app: Application) { - server.tool( +export function applyNotebookTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + + tools.push(server.tool( 'vscode_automation_notebook_open', 'Open a notebook', async () => { + const app = await appService.getOrCreateApplication(); await app.workbench.notebook.openNotebook(); return { content: [{ @@ -23,7 +26,7 @@ export function applyNotebookTools(server: McpServer, app: Application) { }] }; } - ); + )); // Playwright can probably figure this one out // server.tool( @@ -55,10 +58,11 @@ export function applyNotebookTools(server: McpServer, app: Application) { // } // ); - server.tool( + tools.push(server.tool( 'vscode_automation_notebook_edit_cell', 'Enter edit mode for the current cell', async () => { + const app = await appService.getOrCreateApplication(); await app.workbench.notebook.editCell(); return { content: [{ @@ -67,7 +71,7 @@ export function applyNotebookTools(server: McpServer, app: Application) { }] }; } - ); + )); // Seems too niche // server.tool( @@ -84,7 +88,7 @@ export function applyNotebookTools(server: McpServer, app: Application) { // } // ); - server.tool( + tools.push(server.tool( 'vscode_automation_notebook_type_in_editor', 'Type text in the notebook cell editor', { @@ -92,6 +96,7 @@ export function applyNotebookTools(server: McpServer, app: Application) { }, async (args) => { const { text } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.notebook.waitForTypeInEditor(text); return { content: [{ @@ -100,7 +105,7 @@ export function applyNotebookTools(server: McpServer, app: Application) { }] }; } - ); + )); // Playwright can probably figure this one out // server.tool( @@ -238,4 +243,6 @@ export function applyNotebookTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/problems.ts b/test/mcp/src/automationTools/problems.ts index 827dc33c075..81c6f297e23 100644 --- a/test/mcp/src/automationTools/problems.ts +++ b/test/mcp/src/automationTools/problems.ts @@ -3,18 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; import { z } from 'zod'; /** * Problems Panel Tools */ -export function applyProblemsTools(server: McpServer, app: Application) { - server.tool( +export function applyProblemsTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + + tools.push(server.tool( 'vscode_automation_problems_show', 'Show the problems view', async () => { + const app = await appService.getOrCreateApplication(); await app.workbench.problems.showProblemsView(); return { content: [{ @@ -23,12 +26,13 @@ export function applyProblemsTools(server: McpServer, app: Application) { }] }; } - ); + )); - server.tool( + tools.push(server.tool( 'vscode_automation_problems_hide', 'Hide the problems view', async () => { + const app = await appService.getOrCreateApplication(); await app.workbench.problems.hideProblemsView(); return { content: [{ @@ -37,7 +41,7 @@ export function applyProblemsTools(server: McpServer, app: Application) { }] }; } - ); + )); // Playwright can probably figure this one out // server.tool( @@ -54,7 +58,7 @@ export function applyProblemsTools(server: McpServer, app: Application) { // } // ); - server.tool( + tools.push(server.tool( 'vscode_automation_problems_get_selector_in_view', 'Get CSS selector for problems of a specific severity in the problems view', { @@ -68,6 +72,7 @@ export function applyProblemsTools(server: McpServer, app: Application) { }; // This is a static method that returns a selector, not an async operation + const app = await appService.getOrCreateApplication(); const selector = (app.workbench.problems.constructor as any).getSelectorInProblemsView(severityMap[severity]); return { content: [{ @@ -76,7 +81,7 @@ export function applyProblemsTools(server: McpServer, app: Application) { }] }; } - ); + )); // Seems too niche // server.tool( @@ -102,4 +107,6 @@ export function applyProblemsTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/profiler.ts b/test/mcp/src/automationTools/profiler.ts index b5a19ab5013..c47f8bb00cb 100644 --- a/test/mcp/src/automationTools/profiler.ts +++ b/test/mcp/src/automationTools/profiler.ts @@ -3,15 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; /** * Profiler Tools * Note: Due to MCP limitations, these tools provide information about profiler methods * but cannot execute them directly as they require function parameters */ -export function applyProfilerTools(server: McpServer, app: Application) { +export function applyProfilerTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + // Seems too niche // server.tool( // 'vscode_automation_profiler_info', @@ -31,4 +33,6 @@ export function applyProfilerTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/quickAccess.ts b/test/mcp/src/automationTools/quickAccess.ts index 4592cc7e3be..aaed3d45ecf 100644 --- a/test/mcp/src/automationTools/quickAccess.ts +++ b/test/mcp/src/automationTools/quickAccess.ts @@ -3,15 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; import { z } from 'zod'; /** * Command Palette and Quick Access Tools */ -export function applyQuickAccessTools(server: McpServer, app: Application) { - server.tool( +export function applyQuickAccessTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + + tools.push(server.tool( 'vscode_automation_command_run', 'Run a command by name through the command palette', { @@ -20,6 +22,7 @@ export function applyQuickAccessTools(server: McpServer, app: Application) { }, async (args) => { const { command, exactMatch } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.quickaccess.runCommand(command, { exactLabelMatch: exactMatch }); return { content: [{ @@ -28,9 +31,9 @@ export function applyQuickAccessTools(server: McpServer, app: Application) { }] }; } - ); + )); - server.tool( + tools.push(server.tool( 'vscode_automation_quick_open_file', 'Open quick file search and select a file', { @@ -39,6 +42,7 @@ export function applyQuickAccessTools(server: McpServer, app: Application) { }, async (args) => { const { fileName, exactFileName } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.quickaccess.openFileQuickAccessAndWait(fileName, exactFileName || fileName); return { content: [{ @@ -47,7 +51,7 @@ export function applyQuickAccessTools(server: McpServer, app: Application) { }] }; } - ); + )); // Playwright can probably figure this one out // server.tool( @@ -87,4 +91,6 @@ export function applyQuickAccessTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/scm.ts b/test/mcp/src/automationTools/scm.ts index 39b90659168..55feec02b69 100644 --- a/test/mcp/src/automationTools/scm.ts +++ b/test/mcp/src/automationTools/scm.ts @@ -3,14 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; import { z } from 'zod'; /** - * Source Control Management (SCM) Tools + * Source Control Management Tools */ -export function applySCMTools(server: McpServer, app: Application) { +export function applySCMTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + // Playwright can probably figure this one out // server.tool( // 'vscode_automation_scm_open', @@ -118,7 +120,7 @@ export function applySCMTools(server: McpServer, app: Application) { // } // ); - server.tool( + tools.push(server.tool( 'vscode_automation_scm_commit', 'Commit staged changes with a message', { @@ -126,6 +128,7 @@ export function applySCMTools(server: McpServer, app: Application) { }, async (args) => { const { message } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.scm.commit(message); return { content: [{ @@ -134,5 +137,7 @@ export function applySCMTools(server: McpServer, app: Application) { }] }; } - ); + )); + + return tools; } diff --git a/test/mcp/src/automationTools/search.ts b/test/mcp/src/automationTools/search.ts index 5d92a2de84c..040ce94b68b 100644 --- a/test/mcp/src/automationTools/search.ts +++ b/test/mcp/src/automationTools/search.ts @@ -3,14 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; import { z } from 'zod'; /** * Search Tools */ -export function applySearchTools(server: McpServer, app: Application) { +export function applySearchTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + // Playwright can probably figure this one out // server.tool( // 'vscode_automation_search_open', @@ -26,7 +28,7 @@ export function applySearchTools(server: McpServer, app: Application) { // } // ); - server.tool( + tools.push(server.tool( 'vscode_automation_search_for_text', 'Search for text in files', { @@ -34,6 +36,7 @@ export function applySearchTools(server: McpServer, app: Application) { }, async (args) => { const { searchText } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.search.openSearchViewlet(); await app.workbench.search.searchFor(searchText); return { @@ -43,7 +46,7 @@ export function applySearchTools(server: McpServer, app: Application) { }] }; } - ); + )); // Seems too niche // server.tool( @@ -93,4 +96,6 @@ export function applySearchTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/settings.ts b/test/mcp/src/automationTools/settings.ts index 0cefde6889e..dad9f77c6fc 100644 --- a/test/mcp/src/automationTools/settings.ts +++ b/test/mcp/src/automationTools/settings.ts @@ -3,14 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; import { z } from 'zod'; /** * Settings Editor Tools */ -export function applySettingsTools(server: McpServer, app: Application) { +export function applySettingsTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + // I don't think we need this and the batch version // server.tool( // 'vscode_automation_settings_add_user_setting', @@ -31,7 +33,7 @@ export function applySettingsTools(server: McpServer, app: Application) { // } // ); - server.tool( + tools.push(server.tool( 'vscode_automation_settings_add_user_settings', 'Add multiple user settings at once', { @@ -39,6 +41,7 @@ export function applySettingsTools(server: McpServer, app: Application) { }, async (args) => { const { settings } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.settingsEditor.addUserSettings(settings); return { content: [{ @@ -47,12 +50,13 @@ export function applySettingsTools(server: McpServer, app: Application) { }] }; } - ); + )); - server.tool( + tools.push(server.tool( 'vscode_automation_settings_clear_user_settings', 'Clear all user settings', async () => { + const app = await appService.getOrCreateApplication(); await app.workbench.settingsEditor.clearUserSettings(); return { content: [{ @@ -61,7 +65,7 @@ export function applySettingsTools(server: McpServer, app: Application) { }] }; } - ); + )); // Playwright can probably figure this one out // server.tool( @@ -111,4 +115,6 @@ export function applySettingsTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/statusbar.ts b/test/mcp/src/automationTools/statusbar.ts index 96ac1085acf..2944d2c9faf 100644 --- a/test/mcp/src/automationTools/statusbar.ts +++ b/test/mcp/src/automationTools/statusbar.ts @@ -3,13 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; /** * Status Bar Tools */ -export function applyStatusBarTools(server: McpServer, app: Application) { +export function applyStatusBarTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + // Seems too niche // server.tool( // 'vscode_automation_statusbar_wait_for_element', @@ -128,4 +130,6 @@ export function applyStatusBarTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/automationTools/task.ts b/test/mcp/src/automationTools/task.ts index eceabc153ca..270d596fc18 100644 --- a/test/mcp/src/automationTools/task.ts +++ b/test/mcp/src/automationTools/task.ts @@ -3,14 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; import { z } from 'zod'; /** * Task Tools */ -export function applyTaskTools(server: McpServer, app: Application) { +export function applyTaskTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + // Seems too niche // server.tool( // 'vscode_automation_task_assert_tasks', @@ -45,7 +47,7 @@ export function applyTaskTools(server: McpServer, app: Application) { // } // ); - server.tool( + tools.push(server.tool( 'vscode_automation_task_configure', 'Configure a task with specific properties', { @@ -66,6 +68,7 @@ export function applyTaskTools(server: McpServer, app: Application) { }, async (args) => { const { properties } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.task.configureTask(properties); return { content: [{ @@ -74,5 +77,7 @@ export function applyTaskTools(server: McpServer, app: Application) { }] }; } - ); + )); + + return tools; } diff --git a/test/mcp/src/automationTools/terminal.ts b/test/mcp/src/automationTools/terminal.ts index b16e922d95e..eaab27effdf 100644 --- a/test/mcp/src/automationTools/terminal.ts +++ b/test/mcp/src/automationTools/terminal.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Application } from '../../../automation'; +import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ApplicationService } from '../application'; import { z } from 'zod'; /** * Terminal Management Tools */ -export function applyTerminalTools(server: McpServer, app: Application) { - server.tool( +export function applyTerminalTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { + const tools: RegisteredTool[] = []; + tools.push(server.tool( 'vscode_automation_terminal_create', 'Create a new terminal', { @@ -19,6 +20,7 @@ export function applyTerminalTools(server: McpServer, app: Application) { }, async (args) => { const { expectedLocation } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.terminal.createTerminal(expectedLocation); return { content: [{ @@ -27,9 +29,9 @@ export function applyTerminalTools(server: McpServer, app: Application) { }] }; } - ); + )); - server.tool( + tools.push(server.tool( 'vscode_automation_terminal_run_command', 'Run a command in the terminal', { @@ -38,6 +40,7 @@ export function applyTerminalTools(server: McpServer, app: Application) { }, async (args) => { const { command, skipEnter } = args; + const app = await appService.getOrCreateApplication(); await app.workbench.terminal.runCommandInTerminal(command, skipEnter); return { content: [{ @@ -46,7 +49,7 @@ export function applyTerminalTools(server: McpServer, app: Application) { }] }; } - ); + )); // Playwright can probably figure this out // server.tool( @@ -72,10 +75,11 @@ export function applyTerminalTools(server: McpServer, app: Application) { // } // ); - server.tool( + tools.push(server.tool( 'vscode_automation_terminal_get_groups', 'Get current terminal groups information', async () => { + const app = await appService.getOrCreateApplication(); const groups = await app.workbench.terminal.getTerminalGroups(); return { content: [{ @@ -84,7 +88,7 @@ export function applyTerminalTools(server: McpServer, app: Application) { }] }; } - ); + )); // Seems too niche and redundant with runCommand tool // server.tool( @@ -134,4 +138,6 @@ export function applyTerminalTools(server: McpServer, app: Application) { // }; // } // ); + + return tools; } diff --git a/test/mcp/src/main.ts b/test/mcp/src/main.ts deleted file mode 100644 index f6568a7dc7a..00000000000 --- a/test/mcp/src/main.ts +++ /dev/null @@ -1,268 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as express from 'express'; -import { Request, Response } from 'express'; -import * as cors from 'cors'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; -import { randomUUID } from 'node:crypto'; -import { InMemoryEventStore } from './inMemoryEventStore'; -import { getServer } from './playwright'; -import { getServer as getAutomationServer } from './automation'; -import { getApplication } from './application'; -import { Application } from '../../automation'; -import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; - -interface McpServerConfig { - name: string; - port: number; - getServerInstance: (app: Application) => Promise; -} - -const serverConfigs: McpServerConfig[] = [ - { - name: 'Playwright', - port: 33418, - getServerInstance: getServer - }, - { - name: 'Automation', - port: 33458, - getServerInstance: getAutomationServer - } -]; - -// Store all transports by server name -const allTransports: { [serverName: string]: { [sessionId: string]: StreamableHTTPServerTransport } } = {}; - -// Shared application instance -let sharedApplication: Application | undefined = undefined; - -// Initialize shared application -async function getSharedApplication(): Promise { - if (!sharedApplication) { - sharedApplication = await getApplication(); - } - return sharedApplication; -} - -// Check if there are any active transports across all servers -function hasActiveTransports(): boolean { - for (const serverName in allTransports) { - if (Object.keys(allTransports[serverName]).length > 0) { - return true; - } - } - return false; -} - -// Close shared application if no active transports remain -async function closeSharedApplicationIfUnused(): Promise { - if (sharedApplication && !hasActiveTransports()) { - try { - console.log('No active transports remaining, closing shared application...'); - await sharedApplication.stop(); - sharedApplication = undefined; - } catch (error) { - console.error('Error closing shared application:', error); - } - } -} - -// Common CORS configuration -const corsConfig = cors({ - origin: '*', // Allow all origins - exposedHeaders: ['Mcp-Session-Id'] -}); - -// Factory function to create MCP handlers -function createMcpHandlers(serverName: string, getServerInstance: (app: Application) => Promise) { - // Initialize transport storage for this server - allTransports[serverName] = {}; - const transports = allTransports[serverName]; - - const postHandler = async (req: Request, res: Response) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (sessionId) { - console.log(`Received ${serverName} MCP request for session: ${sessionId}`); - } else { - console.log(`${serverName} request body:`, req.body); - } - - try { - let transport: StreamableHTTPServerTransport; - if (sessionId && transports[sessionId]) { - // Reuse existing transport - transport = transports[sessionId]; - } else if (!sessionId && isInitializeRequest(req.body)) { - // New initialization request - const eventStore = new InMemoryEventStore(); - transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - eventStore, // Enable resumability - onsessioninitialized: (sessionId: any) => { - console.log(`${serverName} session initialized with ID: ${sessionId}`); - transports[sessionId] = transport; - } - }); - - // Set up onclose handler to clean up transport when closed - transport.onclose = async () => { - const sid = transport.sessionId; - if (sid && transports[sid]) { - console.log(`${serverName} transport closed for session ${sid}, removing from transports map`); - delete transports[sid]; - // Close shared application if no more transports are active - await closeSharedApplicationIfUnused(); - } - }; - - // Connect the transport to the MCP server - const sharedApp = await getSharedApplication(); - const server = await getServerInstance(sharedApp); - server.onclose = async () => { - const sid = transport.sessionId; - if (sid && transports[sid]) { - console.log(`${serverName} transport closed for session ${sid}, removing from transports map`); - delete transports[sid]; - // Close shared application if no more transports are active - await closeSharedApplicationIfUnused(); - } - }; - await server.connect(transport); - - await transport.handleRequest(req, res, req.body); - return; // Already handled - } else { - // Invalid request - no session ID or not initialization request - res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Bad Request: No valid session ID provided', - }, - id: null, - }); - return; - } - - // Handle the request with existing transport - await transport.handleRequest(req, res, req.body); - } catch (error) { - console.error(`Error handling ${serverName} MCP request:`, error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal server error', - }, - id: null, - }); - } - } - }; - - const getHandler = async (req: Request, res: Response) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); - return; - } - - const lastEventId = req.headers['last-event-id'] as string | undefined; - if (lastEventId) { - console.log(`${serverName} client reconnecting with Last-Event-ID: ${lastEventId}`); - } else { - console.log(`Establishing new ${serverName} SSE stream for session ${sessionId}`); - } - - const transport = transports[sessionId]; - await transport.handleRequest(req, res); - }; - - const deleteHandler = async (req: Request, res: Response) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send('Invalid or missing session ID'); - return; - } - - console.log(`Received ${serverName} session termination request for session ${sessionId}`); - - try { - const transport = transports[sessionId]; - await transport.handleRequest(req, res); - } catch (error) { - console.error(`Error handling ${serverName} session termination:`, error); - if (!res.headersSent) { - res.status(500).send('Error processing session termination'); - } - } - }; - - return { postHandler, getHandler, deleteHandler }; -} - -// Create and start all servers -const servers: { name: string; app: express.Application; port: number }[] = []; - -for (const config of serverConfigs) { - const app = express(); - app.use(express.json()); - app.use(corsConfig); - - const { postHandler, getHandler, deleteHandler } = createMcpHandlers(config.name, config.getServerInstance); - - app.post('/mcp', postHandler); - app.get('/mcp', getHandler); - app.delete('/mcp', deleteHandler); - - servers.push({ name: config.name, app, port: config.port }); -} - -// Start all servers -for (const server of servers) { - server.app.listen(server.port, (error: any) => { - if (error) { - console.error(`Failed to start ${server.name} MCP server:`, error); - process.exit(1); - } - console.log(`${server.name} MCP available at http://localhost:${server.port}/mcp`); - }); -} - -// Handle server shutdown -process.on('SIGINT', async () => { - console.log('Shutting down servers...'); - - // Close all active transports for all servers - for (const serverName in allTransports) { - const transports = allTransports[serverName]; - for (const sessionId in transports) { - try { - console.log(`Closing ${serverName} transport for session ${sessionId}`); - await transports[sessionId].close(); - delete transports[sessionId]; - } catch (error) { - console.error(`Error closing ${serverName} transport for session ${sessionId}:`, error); - } - } - } - - // Close the shared application if it exists - if (sharedApplication) { - try { - console.log('Closing shared application...'); - await sharedApplication.stop(); - sharedApplication = undefined; - } catch (error) { - console.error('Error closing shared application:', error); - } - } - - console.log('Server shutdown complete'); - process.exit(0); -}); diff --git a/test/mcp/src/multiplex.ts b/test/mcp/src/multiplex.ts index 5e844f1113b..1a438cf7649 100644 --- a/test/mcp/src/multiplex.ts +++ b/test/mcp/src/multiplex.ts @@ -2,16 +2,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { z, ZodRawShape, AnyZodObject, ZodTypeAny } from 'zod'; import { Server, ServerOptions } from '@modelcontextprotocol/sdk/server/index.js'; -import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; -import { Implementation, ListToolsRequestSchema, CallToolRequestSchema, ListToolsResult, Tool, CallToolResult, McpError, ErrorCode, ToolAnnotations, ServerRequest, ServerNotification, CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; +import { Implementation, ListToolsRequestSchema, CallToolRequestSchema, ListToolsResult, Tool, CallToolResult, McpError, ErrorCode, CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; import { getServer as getAutomationServer } from './automation'; import { getServer as getPlaywrightServer } from './playwright'; -import { getApplication } from './application'; +import { ApplicationService } from './application'; import { createInMemoryTransportPair } from './inMemoryTransport'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { Application } from '../../automation'; interface SubServer { client: Client; @@ -19,43 +18,61 @@ interface SubServer { } export async function getServer(): Promise { - const app = await getApplication(); - const automationServer = await getAutomationServer(app); - const playwrightServer = await getPlaywrightServer(app); - - // Create in-memory transport pairs for internal communication + const appService = new ApplicationService(); + const automationServer = await getAutomationServer(appService); const [automationServerTransport, automationClientTransport] = createInMemoryTransportPair(); - const [playwrightServerTransport, playwrightClientTransport] = createInMemoryTransportPair(); - - const automationClient = new Client({ - name: 'Automation Client', - version: '1.0.0' - }); - const playwrightClient = new Client({ - name: 'Playwright Client', - version: '1.0.0' - }); - + const automationClient = new Client({ name: 'Automation Client', version: '1.0.0' }); await automationServer.connect(automationServerTransport); await automationClient.connect(automationClientTransport); - await playwrightServer.connect(playwrightServerTransport); - await playwrightClient.connect(playwrightClientTransport); - await playwrightClient.notification({ - method: 'notifications/initialized', - }); - return new MultiplexServer( - [ - // Prefixes could change in the future... be careful. - { client: playwrightClient, prefix: 'browser_' }, - { client: automationClient, prefix: 'vscode_automation_' } - ], + const multiplexServer = new MultiplexServer( + [{ client: automationClient, prefix: 'vscode_automation_' }], { name: 'VS Code Automation + Playwright Server', version: '1.0.0', title: 'Contains tools that can interact with a local build of VS Code. Used for verifying UI behavior.' } - ).server; + ); + + const closables: { close(): Promise }[] = []; + const createPlaywrightServer = async (app: Application) => { + const playwrightServer = await getPlaywrightServer(app); + const [playwrightServerTransport, playwrightClientTransport] = createInMemoryTransportPair(); + const playwrightClient = new Client({ name: 'Playwright Client', version: '1.0.0' }); + await playwrightServer.connect(playwrightServerTransport); + await playwrightClient.connect(playwrightClientTransport); + await playwrightClient.notification({ method: 'notifications/initialized' }); + // Prefixes could change in the future... be careful. + const playwrightSubServer = { client: playwrightClient, prefix: 'browser_' }; + multiplexServer.addSubServer(playwrightSubServer); + multiplexServer.sendToolListChanged(); + closables.push( + playwrightClient, + playwrightServer, + playwrightServerTransport, + playwrightClientTransport, + { + async close() { + multiplexServer.removeSubServer(playwrightSubServer); + multiplexServer.sendToolListChanged(); + } + } + ); + }; + const disposePlaywrightServer = async () => { + while (closables.length) { + closables.pop()?.close(); + } + }; + appService.onApplicationChange(async app => { + if (app) { + await createPlaywrightServer(app); + } else { + await disposePlaywrightServer(); + } + }); + + return multiplexServer.server; } /** @@ -164,46 +181,21 @@ export class MultiplexServer { this.server.sendToolListChanged(); } } + + addSubServer(subServer: SubServer) { + this.subServers.push(subServer); + this.sendToolListChanged(); + } + + removeSubServer(subServer: SubServer) { + const index = this.subServers.indexOf(subServer); + if (index >= 0) { + const removed = this.subServers.splice(index); + if (removed) { + this.sendToolListChanged(); + } + } else { + throw new Error('SubServer not found.'); + } + } } - -/** - * Callback for a tool handler registered with Server.tool(). - * - * Parameters will include tool arguments, if applicable, as well as other request handler context. - * - * The callback should return: - * - `structuredContent` if the tool has an outputSchema defined - * - `content` if the tool does not have an outputSchema - * - Both fields are optional but typically one should be provided - */ -export type ToolCallback = - Args extends ZodRawShape - ? ( - args: z.objectOutputType, - extra: RequestHandlerExtra, - ) => CallToolResult | Promise - : (extra: RequestHandlerExtra) => CallToolResult | Promise; - -export type RegisteredTool = { - title?: string; - description?: string; - inputSchema?: AnyZodObject; - outputSchema?: AnyZodObject; - annotations?: ToolAnnotations; - callback: ToolCallback; - enabled: boolean; - enable(): void; - disable(): void; - update( - updates: { - name?: string | null; - title?: string; - description?: string; - paramsSchema?: InputArgs; - outputSchema?: OutputArgs; - annotations?: ToolAnnotations; - callback?: ToolCallback; - enabled?: boolean; - }): void; - remove(): void; -};