mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-19 08:08:39 +01:00
Get rid of dependency on playwright-mcp (#292432)
This commit is contained in:
committed by
GitHub
parent
2c2c45a9b5
commit
c8d90ab45f
@@ -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,
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user