Get rid of dependency on playwright-mcp (#292432)

This commit is contained in:
Tyler James Leonhardt 2026-02-02 23:16:17 -08:00 committed by GitHub
parent 2c2c45a9b5
commit c8d90ab45f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 82 additions and 654 deletions

2
.vscode/mcp.json vendored
View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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,

View File

@ -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[] = [];

View File

@ -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<string, { streamId: string; message: JSONRPCMessage }> = 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<string> {
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<void> }
): Promise<string> {
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;
}
}

View File

@ -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<void> {
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<void> {
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<void>((resolve, reject) => {
this._stream.write(message, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
/**
* Closes the transport and the underlying stream.
*/
async close(): Promise<void> {
if (this._closed) {
return;
}
this._closed = true;
// End the stream, which will trigger the 'end' event on the peer
return new Promise<void>((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;
}
}

View File

@ -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<Server> {
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<void> }[] = [];
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<Client, Set<string>>();
private readonly _subServerToExcludedTools = new Map<Client, Set<string>>();
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<void> {
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<void> {
return await this.server.connect(transport);
}
/**
* Closes the connection.
*/
async close(): Promise<void> {
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<ListToolsResult> => {
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<CallToolResult> => {
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.');
}
}
}

View File

@ -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<Server> {
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;
}

View File

@ -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();