From c8d90ab45fd1612379bbdf2208f042a4d4e58ed3 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:16:17 -0800 Subject: [PATCH] Get rid of dependency on playwright-mcp (#292432) --- .vscode/mcp.json | 2 +- test/mcp/README.md | 112 ++++++---- test/mcp/package.json | 7 +- test/mcp/src/application.ts | 12 +- test/mcp/src/automationTools/windows.ts | 2 +- test/mcp/src/inMemoryEventStore.ts | 81 ------- test/mcp/src/inMemoryTransport.ts | 198 ---------------- test/mcp/src/multiplex.ts | 286 ------------------------ test/mcp/src/playwright.ts | 24 -- test/mcp/src/stdio.ts | 12 +- 10 files changed, 82 insertions(+), 654 deletions(-) delete mode 100644 test/mcp/src/inMemoryEventStore.ts delete mode 100644 test/mcp/src/inMemoryTransport.ts delete mode 100644 test/mcp/src/multiplex.ts delete mode 100644 test/mcp/src/playwright.ts diff --git a/.vscode/mcp.json b/.vscode/mcp.json index f798c6131b2..08be33e5196 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,6 +1,6 @@ { "servers": { - "vscode-playwright-mcp": { + "vscode-automation-mcp": { "type": "stdio", "command": "npm", // Look at the [README](../test/mcp/README.md) to see what arguments are supported diff --git a/test/mcp/README.md b/test/mcp/README.md index 5c02ee0f09d..db2f09e59fc 100644 --- a/test/mcp/README.md +++ b/test/mcp/README.md @@ -1,17 +1,17 @@ # Code - OSS Development MCP Server -This directory contains a Model Context Protocol (MCP) server that provides Playwright browser automation capabilities for Code - OSS development and testing. The MCP server exposes Code - OSS's Playwright testing infrastructure through a standardized interface, allowing AI assistants and other tools to interact with browsers programmatically. +This directory contains a Model Context Protocol (MCP) server that provides VS Code automation capabilities for Code - OSS development and testing. The MCP server exposes Code - OSS's testing infrastructure through a standardized interface, allowing AI assistants and other tools to interact with VS Code programmatically. ## What is MCP? -The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that enables AI assistants to securely connect to external data sources and tools. This MCP server specifically provides browser automation capabilities using Playwright, making it possible for AI assistants to: +The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that enables AI assistants to securely connect to external data sources and tools. This MCP server specifically provides VS Code automation capabilities, making it possible for AI assistants to: -- Navigate web pages -- Interact with UI elements (click, type, hover, etc.) -- Take screenshots and capture page content -- Evaluate JavaScript in browser contexts -- Handle file uploads and downloads -- Manage browser tabs and windows +- Start and stop VS Code instances +- Interact with editors, terminals, and UI elements +- Run commands and keybindings +- Navigate the explorer, search, debug, and other viewlets +- Manage extensions, settings, and keybindings +- Work with notebooks and chat features ## Quick Start - Stdio @@ -19,14 +19,12 @@ Firstly, make sure you install all dependencies (`npm i`) at the root of the rep Then, open the Command Palette and run: ``` -MCP: List Servers → vscode-playwright-mcp → Start Server +MCP: List Servers → vscode-automation-mcp → Start Server ``` or open [mcp.json](../../.vscode/mcp.json) and start it from there. That's it! It should automatically compile everything needed. -Then you can use `/playwright` to ask specific questions. - ## Arguments Open the [mcp.json](../../.vscode/mcp.json) and modify the `args`: @@ -41,7 +39,7 @@ Open the [mcp.json](../../.vscode/mcp.json) and modify the `args`: You can modify the mcp.json to debug the server: ```JSON -"vscode-playwright-mcp": { +"vscode-automation-mcp": { "type": "stdio", "command": "node", "args": ["./out/stdio.js"], @@ -57,26 +55,44 @@ You can modify the mcp.json to debug the server: ## What the Server Provides -The MCP server exposes a comprehensive set of browser automation tools through the MCP protocol: +The MCP server exposes a comprehensive set of VS Code automation tools through the MCP protocol: -### Element Interaction -- Click on elements (single, double, right-click) -- Type text into input fields -- Hover over elements -- Drag and drop between elements -- Select options in dropdowns +### Application Management +- Start, stop, and restart VS Code instances +- Open workspaces and folders -### Content Capture & Analysis -- Take screenshots (full page or specific elements) -- Capture accessibility snapshots for better element targeting -- Get page console messages -- Monitor network requests +### Editor Tools +- Open, close, and navigate files +- Get and set editor content +- Manage selections and cursors -### Advanced Features -- Evaluate JavaScript code in browser contexts -- Handle file uploads -- Wait for specific content or time delays -- Handle browser dialogs and alerts +### Terminal Tools +- Create and manage terminal instances +- Send commands to terminals +- Read terminal output + +### Debug Tools +- Start and stop debug sessions +- Manage breakpoints +- Step through code + +### Search Tools +- Search for files and text +- Navigate search results + +### Extension Tools +- Install and manage extensions +- View extension information + +### UI Interaction +- Quick access and command palette +- Explorer and activity bar +- Source control management +- Status bar interactions +- Problems panel +- Settings and keybindings editors +- Notebook support +- Chat features ## Development @@ -103,22 +119,30 @@ npm start ``` test/mcp/ ├── src/ -│ ├── main.ts # Express server and MCP endpoint handlers -│ ├── playwright.ts # Code - OSS Playwright integration -│ ├── inMemoryEventStore.ts # Session management for resumability -│ └── utils.ts # Utility functions +│ ├── stdio.ts # Entry point for stdio transport +│ ├── automation.ts # MCP server with automation tools +│ ├── application.ts # VS Code application lifecycle management +│ ├── options.ts # Command-line options parsing +│ ├── utils.ts # Utility functions +│ └── automationTools/ # Tool implementations organized by feature +│ ├── index.ts # Tool registration +│ ├── core.ts # Core application tools +│ ├── editor.ts # Editor tools +│ ├── terminal.ts # Terminal tools +│ ├── debug.ts # Debug tools +│ └── ... # Other feature-specific tools ├── package.json # Dependencies and scripts -├── tsconfig.json # TypeScript configuration -└── README.md # This file +├── tsconfig.json # TypeScript configuration +└── README.md # This file ``` -### Key Features +### Architecture -- **Session Management**: Supports multiple concurrent MCP sessions with proper cleanup -- **Resumability**: Built-in event store for connection resumption -- **Code - OSS Integration**: Uses Code - OSS's existing Playwright test infrastructure -- **CORS Support**: Configured for cross-origin requests -- **Error Handling**: Comprehensive error handling and logging +The server uses a simple architecture: +- **stdio.ts** - Entry point that creates the MCP server and connects via stdio transport +- **automation.ts** - Creates the MCP server and registers all automation tools +- **application.ts** - Manages VS Code application lifecycle (start, stop, restart) +- **automationTools/** - Modular tool implementations organized by VS Code feature area ## Troubleshooting @@ -126,10 +150,10 @@ test/mcp/ - Ensure Code - OSS has been built and run at least once (via F5 or `code.sh`) - Verify all dependencies are installed with `npm install` -### Browser Automation Issues +### Automation Issues - Ensure Code - OSS has been built and run at least once (via F5 or `code.sh`) -- Check the server logs for Playwright-related errors -- Verify the test repository is properly cloned +- Check the server logs for errors +- Verify the workspace path is correct ## Contributing diff --git a/test/mcp/package.json b/test/mcp/package.json index b32637f5fd7..f4829b826d4 100644 --- a/test/mcp/package.json +++ b/test/mcp/package.json @@ -9,20 +9,15 @@ "watch-automation": "cd ../automation && npm run watch", "watch-mcp": "node ../../node_modules/typescript/bin/tsc --watch --preserveWatchOutput", "watch": "npm-run-all2 -lp watch-automation watch-mcp", - "start-stdio": "echo 'Starting vscode-playwright-mcp... For customization and troubleshooting, see ./test/mcp/README.md' && npm ci && npm run -s compile && node ./out/stdio.js" + "start-stdio": "echo 'Starting vscode-automation-mcp... For customization and troubleshooting, see ./test/mcp/README.md' && npm ci && npm run -s compile && node ./out/stdio.js" }, "dependencies": { "@modelcontextprotocol/sdk": "1.25.2", - "@playwright/mcp": "^0.0.40", - "cors": "^2.8.5", - "express": "^5.2.1", "minimist": "^1.2.8", "ncp": "^2.0.0", "node-fetch": "^2.6.7" }, "devDependencies": { - "@types/cors": "^2.8.19", - "@types/express": "^5.0.3", "@types/ncp": "2.0.1", "@types/node": "22.x", "@types/node-fetch": "^2.5.10", diff --git a/test/mcp/src/application.ts b/test/mcp/src/application.ts index 0af8959a237..1eff6a914ad 100644 --- a/test/mcp/src/application.ts +++ b/test/mcp/src/application.ts @@ -3,13 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as playwright from 'playwright'; 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'; import * as vscodetest from '@vscode/test-electron'; -import { createApp, retry } from './utils'; +import { createApp, retry, parseVersion } from './utils'; import { opts } from './options'; const rootPath = path.join(__dirname, '..', '..', '..'); @@ -61,11 +60,6 @@ function fail(errorMessage): void { let quality: Quality; let version: string | undefined; -function parseVersion(version: string): { major: number; minor: number; patch: number } { - const [, major, minor, patch] = /^(\d+)\.(\d+)\.(\d+)/.exec(version)!; - return { major: parseInt(major), minor: parseInt(minor), patch: parseInt(patch) }; -} - function parseQuality(): Quality { if (process.env.VSCODE_DEV === '1') { return Quality.Dev; @@ -245,10 +239,6 @@ export async function getApplication({ recordVideo, workspacePath }: { recordVid await setup(); const application = createApp({ - // Pass the alpha version of Playwright down... This is a hack since Playwright MCP - // doesn't play nice with Playwright Test: https://github.com/microsoft/playwright-mcp/issues/917 - // eslint-disable-next-line local/code-no-any-casts - playwright: playwright as any, quality, version: parseVersion(version ?? '0.0.0'), codePath: opts.build, diff --git a/test/mcp/src/automationTools/windows.ts b/test/mcp/src/automationTools/windows.ts index de166b8d4da..ab6076ec234 100644 --- a/test/mcp/src/automationTools/windows.ts +++ b/test/mcp/src/automationTools/windows.ts @@ -18,7 +18,7 @@ function textResponse(text: string) { /** * Window Management Tools for multi-window support. - * These tools are thin wrappers around PlaywrightDriver methods. + * These tools provide Playwright-based window interactions through the automation driver. */ export function applyWindowTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { const tools: RegisteredTool[] = []; diff --git a/test/mcp/src/inMemoryEventStore.ts b/test/mcp/src/inMemoryEventStore.ts deleted file mode 100644 index 7688fefbbcf..00000000000 --- a/test/mcp/src/inMemoryEventStore.ts +++ /dev/null @@ -1,81 +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 { EventStore } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'; - -/** - * Simple in-memory implementation of the EventStore interface for resumability - * This is primarily intended for examples and testing, not for production use - * where a persistent storage solution would be more appropriate. - */ -export class InMemoryEventStore implements EventStore { - private events: Map = new Map(); - - /** - * Generates a unique event ID for a given stream ID - */ - private generateEventId(streamId: string): string { - return `${streamId}_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`; - } - - /** - * Extracts the stream ID from an event ID - */ - private getStreamIdFromEventId(eventId: string): string { - const parts = eventId.split('_'); - return parts.length > 0 ? parts[0] : ''; - } - - /** - * Stores an event with a generated event ID - * Implements EventStore.storeEvent - */ - async storeEvent(streamId: string, message: JSONRPCMessage): Promise { - const eventId = this.generateEventId(streamId); - this.events.set(eventId, { streamId, message }); - return eventId; - } - - /** - * Replays events that occurred after a specific event ID - * Implements EventStore.replayEventsAfter - */ - async replayEventsAfter(lastEventId: string, - { send }: { send: (eventId: string, message: JSONRPCMessage) => Promise } - ): Promise { - if (!lastEventId || !this.events.has(lastEventId)) { - return ''; - } - - // Extract the stream ID from the event ID - const streamId = this.getStreamIdFromEventId(lastEventId); - if (!streamId) { - return ''; - } - - let foundLastEvent = false; - - // Sort events by eventId for chronological ordering - const sortedEvents = [...this.events.entries()].sort((a, b) => a[0].localeCompare(b[0])); - - for (const [eventId, { streamId: eventStreamId, message }] of sortedEvents) { - // Only include events from the same stream - if (eventStreamId !== streamId) { - continue; - } - - // Start sending events after we find the lastEventId - if (eventId === lastEventId) { - foundLastEvent = true; - continue; - } - - if (foundLastEvent) { - await send(eventId, message); - } - } - return streamId; - } -} diff --git a/test/mcp/src/inMemoryTransport.ts b/test/mcp/src/inMemoryTransport.ts deleted file mode 100644 index aa9c1a7d3b9..00000000000 --- a/test/mcp/src/inMemoryTransport.ts +++ /dev/null @@ -1,198 +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 { JSONRPCMessage, MessageExtraInfo } from '@modelcontextprotocol/sdk/types.js'; -import { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js'; -import { Duplex } from 'stream'; - -/** - * Creates a pair of in-memory transports that are connected to each other. - * Messages sent on one transport are received on the other transport. - * This uses actual Node.js streams to simulate real stdio behavior. - * - * @returns A tuple of [serverTransport, clientTransport] where the server - * and client can communicate with each other through these transports. - */ -export function createInMemoryTransportPair(): [InMemoryTransport, InMemoryTransport] { - // Create two duplex streams that are connected to each other - const serverStream = new Duplex({ objectMode: true, allowHalfOpen: false }); - const clientStream = new Duplex({ objectMode: true, allowHalfOpen: false }); - - // Cross-connect the streams: server writes go to client reads and vice versa - // Server stream implementation - serverStream._write = (chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void) => { - // When server writes, client should receive it - clientStream.push(chunk); - callback(); - }; - - serverStream._read = () => { - // Signal that we're ready to read - no action needed for cross-connected streams - }; - - // Client stream implementation - clientStream._write = (chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void) => { - // When client writes, server should receive it - serverStream.push(chunk); - callback(); - }; - - clientStream._read = () => { - // Signal that we're ready to read - no action needed for cross-connected streams - }; - - // Handle stream ending properly - serverStream.on('end', () => { - if (!clientStream.destroyed) { - clientStream.push(null); - } - }); - - clientStream.on('end', () => { - if (!serverStream.destroyed) { - serverStream.push(null); - } - }); - - const serverTransport = new InMemoryTransport(serverStream); - const clientTransport = new InMemoryTransport(clientStream); - - return [serverTransport, clientTransport]; -} - -/** - * An in-memory transport implementation that allows two MCP endpoints to communicate - * using Node.js streams, similar to how StdioTransport works. This provides more - * realistic behavior than direct message passing. - */ -export class InMemoryTransport implements Transport { - private _stream: Duplex; - private _started = false; - private _closed = false; - private _sessionId: string; - - // Transport callbacks - public onclose?: () => void; - public onerror?: (error: Error) => void; - public onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void; - - constructor(stream: Duplex) { - this._stream = stream; - this._sessionId = `memory-${Math.random().toString(36).substring(2, 15)}`; - - // Set up stream event handlers - this._stream.on('data', (data: any) => { - if (this._started && !this._closed) { - try { - // Expect data to be a JSON-RPC message object - const message = typeof data === 'string' ? JSON.parse(data) : data; - const extra: MessageExtraInfo | undefined = undefined; - this.onmessage?.(message, extra); - } catch (error) { - this.onerror?.(error instanceof Error ? error : new Error(String(error))); - } - } - }); - - this._stream.on('error', (error: Error) => { - this.onerror?.(error); - }); - - this._stream.on('end', () => { - this._closed = true; - this.onclose?.(); - }); - - this._stream.on('close', () => { - this._closed = true; - this.onclose?.(); - }); - } - - /** - * Starts the transport. This must be called before sending or receiving messages. - */ - async start(): Promise { - if (this._started) { - return; - } - - if (this._closed) { - throw new Error('Cannot start a closed transport'); - } - - this._started = true; - } - - /** - * Sends a JSON-RPC message through the stream. - */ - async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise { - if (!this._started) { - throw new Error('Transport not started'); - } - - if (this._closed) { - throw new Error('Transport is closed'); - } - - // Write the message to the stream - similar to how StdioTransport works - return new Promise((resolve, reject) => { - this._stream.write(message, (error) => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }); - } - - /** - * Closes the transport and the underlying stream. - */ - async close(): Promise { - if (this._closed) { - return; - } - - this._closed = true; - - // End the stream, which will trigger the 'end' event on the peer - return new Promise((resolve) => { - this._stream.end(() => { - resolve(); - }); - }); - } - - /** - * Gets the session ID for this transport connection. - */ - get sessionId(): string { - return this._sessionId; - } - - /** - * Sets the protocol version (optional implementation). - */ - setProtocolVersion?(version: string): void { - // No-op for in-memory transport - } - - /** - * Checks if the transport is currently connected and started. - */ - get isConnected(): boolean { - return this._started && !this._closed && !this._stream.destroyed; - } - - /** - * Checks if the transport has been closed. - */ - get isClosed(): boolean { - return this._closed || this._stream.destroyed; - } -} diff --git a/test/mcp/src/multiplex.ts b/test/mcp/src/multiplex.ts deleted file mode 100644 index 5fe84030d89..00000000000 --- a/test/mcp/src/multiplex.ts +++ /dev/null @@ -1,286 +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 { Server, ServerOptions } from '@modelcontextprotocol/sdk/server/index.js'; -import { Transport } from '@modelcontextprotocol/sdk/shared/transport.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 { ApplicationService } from './application'; -import { createInMemoryTransportPair } from './inMemoryTransport'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { Application } from '../../automation'; -import { opts } from './options'; - -interface SubServerConfig { - subServer: Client; - excludeTools?: string[]; -} - -export async function getServer(): Promise { - const appService = new ApplicationService(); - const automationServer = await getAutomationServer(appService); - const [automationServerTransport, automationClientTransport] = createInMemoryTransportPair(); - const automationClient = new Client({ name: 'Automation Client', version: '1.0.0' }); - await automationServer.connect(automationServerTransport); - await automationClient.connect(automationClientTransport); - - const multiplexServer = new MultiplexServer( - [{ subServer: automationClient }], - { - 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.' - } - ); - - 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' }); - - // Add subserver with optional tool exclusions - multiplexServer.addSubServer({ - subServer: playwrightClient, - excludeTools: [ - // Playwright MCP doesn't properly support Electron's multi-window model. - // It uses browserContext.pages() which doesn't track Electron windows correctly. - // We provide vscode_automation_window_* alternatives that use ElectronApplication.windows(). - - // Navigation not needed - VS Code opens its own windows - 'browser_navigate', - 'browser_navigate_back', - 'browser_tabs', - - // Page interaction tools - replaced by vscode_automation_window_* - 'browser_click', // → vscode_automation_window_click - 'browser_type', // → vscode_automation_window_type - 'browser_hover', // → vscode_automation_window_hover - 'browser_drag', // → vscode_automation_window_drag - 'browser_select_option', // → vscode_automation_window_select_option - 'browser_fill_form', // → vscode_automation_window_fill_form - 'browser_press_key', // → vscode_automation_window_press_key - - // Mouse operations - replaced by vscode_automation_window_mouse_* - 'browser_mouse_move_xy', // → vscode_automation_window_mouse_move - 'browser_mouse_click_xy', // → vscode_automation_window_mouse_click - 'browser_mouse_drag_xy', // → vscode_automation_window_mouse_drag - - // Content capture - replaced by vscode_automation_window_* - 'browser_snapshot', // → vscode_automation_window_snapshot - 'browser_take_screenshot', // → vscode_automation_window_screenshot - 'browser_evaluate', // → vscode_automation_window_evaluate - - // Console/debugging - replaced by vscode_automation_window_* - 'browser_console_messages', // → vscode_automation_window_console_messages - - // Wait/timing - replaced by vscode_automation_window_* - 'browser_wait_for', // → vscode_automation_window_wait_for_text / wait_for_time - - // Verification - replaced by vscode_automation_window_* - 'browser_verify_element_visible', // → vscode_automation_window_verify_element_visible - 'browser_verify_text_visible', // → vscode_automation_window_verify_text_visible - 'browser_verify_list_visible', // (no direct replacement - use multiple verify_text_visible) - 'browser_verify_value', // → vscode_automation_window_get_input_value - - // Other page-dependent tools (not typically needed for VS Code testing) - 'browser_close', - 'browser_resize', - 'browser_network_requests', - 'browser_file_upload', - 'browser_handle_dialog', - 'browser_pdf_save', - 'browser_generate_locator' - ] - }); - multiplexServer.sendToolListChanged(); - closables.push( - playwrightClient, - playwrightServer, - playwrightServerTransport, - playwrightClientTransport, - { - async close() { - multiplexServer.removeSubServer(playwrightClient); - multiplexServer.sendToolListChanged(); - } - } - ); - }; - const disposePlaywrightServer = async () => { - while (closables.length) { - closables.pop()?.close(); - } - }; - appService.onApplicationChange(async app => { - if (app) { - await createPlaywrightServer(app); - } else { - await disposePlaywrightServer(); - } - }); - - if (opts.autostart) { - await appService.getOrCreateApplication(); - } - return multiplexServer.server; -} - -/** - * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. - * For advanced usage (like sending notifications or setting custom request handlers), use the underlying - * Server instance available via the `server` property. - */ -export class MultiplexServer { - /** - * The underlying Server instance, useful for advanced operations like sending notifications. - */ - readonly server: Server; - - private readonly _subServerToToolSet = new Map>(); - private readonly _subServerToExcludedTools = new Map>(); - private readonly _subServers: Client[]; - - constructor(subServerConfigs: SubServerConfig[], serverInfo: Implementation, options?: ServerOptions) { - this.server = new Server(serverInfo, options); - this._subServers = []; - - // Process configurations and set up subservers - for (const config of subServerConfigs) { - this._subServers.push(config.subServer); - if (config.excludeTools && config.excludeTools.length > 0) { - this._subServerToExcludedTools.set(config.subServer, new Set(config.excludeTools)); - } - } - - this.setToolRequestHandlers(); - } - - async start(): Promise { - await this.server.sendToolListChanged(); - } - - /** - * Attaches to the given transport, starts it, and starts listening for messages. - * - * The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward. - */ - async connect(transport: Transport): Promise { - return await this.server.connect(transport); - } - - /** - * Closes the connection. - */ - async close(): Promise { - await this.server.close(); - } - - private _toolHandlersInitialized = false; - - private setToolRequestHandlers() { - if (this._toolHandlersInitialized) { - return; - } - - this.server.assertCanSetRequestHandler( - ListToolsRequestSchema.shape.method.value, - ); - this.server.assertCanSetRequestHandler( - CallToolRequestSchema.shape.method.value, - ); - - this.server.registerCapabilities({ - tools: { - listChanged: true - } - }); - - this.server.setRequestHandler( - ListToolsRequestSchema, - async (): Promise => { - const tools: Tool[] = []; - for (const subServer of this._subServers) { - const result = await subServer.listTools(); - const allToolNames = new Set(result.tools.map(t => t.name)); - const excludedForThisServer = this._subServerToExcludedTools.get(subServer) || new Set(); - const filteredTools = result.tools.filter(tool => !excludedForThisServer.has(tool.name)); - this._subServerToToolSet.set(subServer, allToolNames); - tools.push(...filteredTools); - } - return { tools }; - }, - ); - - this.server.setRequestHandler( - CallToolRequestSchema, - async (request, extra): Promise => { - const toolName = request.params.name; - for (const subServer of this._subServers) { - const toolSet = this._subServerToToolSet.get(subServer); - const excludedForThisServer = this._subServerToExcludedTools.get(subServer) || new Set(); - if (toolSet?.has(toolName)) { - // Check if tool is excluded for this specific subserver - if (excludedForThisServer.has(toolName)) { - throw new McpError(ErrorCode.InvalidParams, `Tool with ID ${toolName} is excluded`); - } - return await subServer.request( - { - method: 'tools/call', - params: request.params - }, - CallToolResultSchema - ); - } - } - throw new McpError(ErrorCode.InvalidParams, `Tool with ID ${toolName} not found`); - }, - ); - - this._toolHandlersInitialized = true; - } - - /** - * Checks if the server is connected to a transport. - * @returns True if the server is connected - */ - isConnected() { - return this.server.transport !== undefined; - } - - /** - * Sends a tool list changed event to the client, if connected. - */ - sendToolListChanged() { - if (this.isConnected()) { - this.server.sendToolListChanged(); - } - } - - addSubServer(config: SubServerConfig) { - this._subServers.push(config.subServer); - if (config.excludeTools && config.excludeTools.length > 0) { - this._subServerToExcludedTools.set(config.subServer, new Set(config.excludeTools)); - } - this.sendToolListChanged(); - } - - removeSubServer(subServer: Client) { - const index = this._subServers.indexOf(subServer); - if (index >= 0) { - const removed = this._subServers.splice(index, 1); - if (removed.length > 0) { - // Clean up excluded tools mapping - this._subServerToExcludedTools.delete(subServer); - this.sendToolListChanged(); - } - } else { - throw new Error('SubServer not found.'); - } - } -} diff --git a/test/mcp/src/playwright.ts b/test/mcp/src/playwright.ts deleted file mode 100644 index 5a1a229e3e9..00000000000 --- a/test/mcp/src/playwright.ts +++ /dev/null @@ -1,24 +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 { createConnection } from '@playwright/mcp'; -import { getApplication } from './application'; -import { Application } from '../../automation'; -import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; - -export async function getServer(app?: Application): Promise { - const application = app ?? await getApplication(); - const connection = await createConnection( - { - capabilities: ['core', 'pdf', 'vision'] - }, - // eslint-disable-next-line local/code-no-any-casts - () => Promise.resolve(application.code.driver.browserContext as any) - ); - application.code.driver.browserContext.on('close', async () => { - await connection.close(); - }); - return connection; -} diff --git a/test/mcp/src/stdio.ts b/test/mcp/src/stdio.ts index 97387ad449e..ea8193baa2f 100644 --- a/test/mcp/src/stdio.ts +++ b/test/mcp/src/stdio.ts @@ -3,11 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { getServer } from './multiplex'; +import { getServer } from './automation'; +import { ApplicationService } from './application'; +import { opts } from './options'; const transport: StdioServerTransport = new StdioServerTransport(); (async () => { - const server = await getServer(); + const appService = new ApplicationService(); + const server = await getServer(appService); + + if (opts.autostart) { + await appService.getOrCreateApplication(); + } + await server.connect(transport); })().catch(err => { transport.close();