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';
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' });
multiplexServer.addSubServer({
subServer: playwrightClient,
excludeTools: [
'browser_navigate',
'browser_navigate_back',
'browser_tabs'
]
});
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();
}
});
return multiplexServer.server;
}
export class MultiplexServer {
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 = [];
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();
}
async connect(transport: Transport): Promise<void> {
return await this.server.connect(transport);
}
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)) {
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;
}
isConnected() {
return this.server.transport !== undefined;
}
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) {
this._subServerToExcludedTools.delete(subServer);
this.sendToolListChanged();
}
} else {
throw new Error('SubServer not found.');
}
}
}