Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/test/mcp/src/multiplex.ts
3520 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import { Server, ServerOptions } from '@modelcontextprotocol/sdk/server/index.js';
6
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
7
import { Implementation, ListToolsRequestSchema, CallToolRequestSchema, ListToolsResult, Tool, CallToolResult, McpError, ErrorCode, CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
8
import { getServer as getAutomationServer } from './automation';
9
import { getServer as getPlaywrightServer } from './playwright';
10
import { ApplicationService } from './application';
11
import { createInMemoryTransportPair } from './inMemoryTransport';
12
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
13
import { Application } from '../../automation';
14
15
interface SubServerConfig {
16
subServer: Client;
17
excludeTools?: string[];
18
}
19
20
export async function getServer(): Promise<Server> {
21
const appService = new ApplicationService();
22
const automationServer = await getAutomationServer(appService);
23
const [automationServerTransport, automationClientTransport] = createInMemoryTransportPair();
24
const automationClient = new Client({ name: 'Automation Client', version: '1.0.0' });
25
await automationServer.connect(automationServerTransport);
26
await automationClient.connect(automationClientTransport);
27
28
const multiplexServer = new MultiplexServer(
29
[{ subServer: automationClient }],
30
{
31
name: 'VS Code Automation + Playwright Server',
32
version: '1.0.0',
33
title: 'Contains tools that can interact with a local build of VS Code. Used for verifying UI behavior.'
34
}
35
);
36
37
const closables: { close(): Promise<void> }[] = [];
38
const createPlaywrightServer = async (app: Application) => {
39
const playwrightServer = await getPlaywrightServer(app);
40
const [playwrightServerTransport, playwrightClientTransport] = createInMemoryTransportPair();
41
const playwrightClient = new Client({ name: 'Playwright Client', version: '1.0.0' });
42
await playwrightServer.connect(playwrightServerTransport);
43
await playwrightClient.connect(playwrightClientTransport);
44
await playwrightClient.notification({ method: 'notifications/initialized' });
45
46
// Add subserver with optional tool exclusions
47
multiplexServer.addSubServer({
48
subServer: playwrightClient,
49
excludeTools: [
50
// The page will always be opened in the context of the application,
51
// so navigation and tab management is not needed.
52
'browser_navigate',
53
'browser_navigate_back',
54
'browser_tabs'
55
]
56
});
57
multiplexServer.sendToolListChanged();
58
closables.push(
59
playwrightClient,
60
playwrightServer,
61
playwrightServerTransport,
62
playwrightClientTransport,
63
{
64
async close() {
65
multiplexServer.removeSubServer(playwrightClient);
66
multiplexServer.sendToolListChanged();
67
}
68
}
69
);
70
};
71
const disposePlaywrightServer = async () => {
72
while (closables.length) {
73
closables.pop()?.close();
74
}
75
};
76
appService.onApplicationChange(async app => {
77
if (app) {
78
await createPlaywrightServer(app);
79
} else {
80
await disposePlaywrightServer();
81
}
82
});
83
84
return multiplexServer.server;
85
}
86
87
/**
88
* High-level MCP server that provides a simpler API for working with resources, tools, and prompts.
89
* For advanced usage (like sending notifications or setting custom request handlers), use the underlying
90
* Server instance available via the `server` property.
91
*/
92
export class MultiplexServer {
93
/**
94
* The underlying Server instance, useful for advanced operations like sending notifications.
95
*/
96
readonly server: Server;
97
98
private readonly _subServerToToolSet = new Map<Client, Set<string>>();
99
private readonly _subServerToExcludedTools = new Map<Client, Set<string>>();
100
private readonly _subServers: Client[];
101
102
constructor(subServerConfigs: SubServerConfig[], serverInfo: Implementation, options?: ServerOptions) {
103
this.server = new Server(serverInfo, options);
104
this._subServers = [];
105
106
// Process configurations and set up subservers
107
for (const config of subServerConfigs) {
108
this._subServers.push(config.subServer);
109
if (config.excludeTools && config.excludeTools.length > 0) {
110
this._subServerToExcludedTools.set(config.subServer, new Set(config.excludeTools));
111
}
112
}
113
114
this.setToolRequestHandlers();
115
}
116
117
async start(): Promise<void> {
118
await this.server.sendToolListChanged();
119
}
120
121
/**
122
* Attaches to the given transport, starts it, and starts listening for messages.
123
*
124
* 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.
125
*/
126
async connect(transport: Transport): Promise<void> {
127
return await this.server.connect(transport);
128
}
129
130
/**
131
* Closes the connection.
132
*/
133
async close(): Promise<void> {
134
await this.server.close();
135
}
136
137
private _toolHandlersInitialized = false;
138
139
private setToolRequestHandlers() {
140
if (this._toolHandlersInitialized) {
141
return;
142
}
143
144
this.server.assertCanSetRequestHandler(
145
ListToolsRequestSchema.shape.method.value,
146
);
147
this.server.assertCanSetRequestHandler(
148
CallToolRequestSchema.shape.method.value,
149
);
150
151
this.server.registerCapabilities({
152
tools: {
153
listChanged: true
154
}
155
});
156
157
this.server.setRequestHandler(
158
ListToolsRequestSchema,
159
async (): Promise<ListToolsResult> => {
160
const tools: Tool[] = [];
161
for (const subServer of this._subServers) {
162
const result = await subServer.listTools();
163
const allToolNames = new Set(result.tools.map(t => t.name));
164
const excludedForThisServer = this._subServerToExcludedTools.get(subServer) || new Set();
165
const filteredTools = result.tools.filter(tool => !excludedForThisServer.has(tool.name));
166
this._subServerToToolSet.set(subServer, allToolNames);
167
tools.push(...filteredTools);
168
}
169
return { tools };
170
},
171
);
172
173
this.server.setRequestHandler(
174
CallToolRequestSchema,
175
async (request, extra): Promise<CallToolResult> => {
176
const toolName = request.params.name;
177
for (const subServer of this._subServers) {
178
const toolSet = this._subServerToToolSet.get(subServer);
179
const excludedForThisServer = this._subServerToExcludedTools.get(subServer) || new Set();
180
if (toolSet?.has(toolName)) {
181
// Check if tool is excluded for this specific subserver
182
if (excludedForThisServer.has(toolName)) {
183
throw new McpError(ErrorCode.InvalidParams, `Tool with ID ${toolName} is excluded`);
184
}
185
return await subServer.request(
186
{
187
method: 'tools/call',
188
params: request.params
189
},
190
CallToolResultSchema
191
);
192
}
193
}
194
throw new McpError(ErrorCode.InvalidParams, `Tool with ID ${toolName} not found`);
195
},
196
);
197
198
this._toolHandlersInitialized = true;
199
}
200
201
/**
202
* Checks if the server is connected to a transport.
203
* @returns True if the server is connected
204
*/
205
isConnected() {
206
return this.server.transport !== undefined;
207
}
208
209
/**
210
* Sends a tool list changed event to the client, if connected.
211
*/
212
sendToolListChanged() {
213
if (this.isConnected()) {
214
this.server.sendToolListChanged();
215
}
216
}
217
218
addSubServer(config: SubServerConfig) {
219
this._subServers.push(config.subServer);
220
if (config.excludeTools && config.excludeTools.length > 0) {
221
this._subServerToExcludedTools.set(config.subServer, new Set(config.excludeTools));
222
}
223
this.sendToolListChanged();
224
}
225
226
removeSubServer(subServer: Client) {
227
const index = this._subServers.indexOf(subServer);
228
if (index >= 0) {
229
const removed = this._subServers.splice(index, 1);
230
if (removed.length > 0) {
231
// Clean up excluded tools mapping
232
this._subServerToExcludedTools.delete(subServer);
233
this.sendToolListChanged();
234
}
235
} else {
236
throw new Error('SubServer not found.');
237
}
238
}
239
}
240
241