@vibebrowser/mcp 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -12
- package/dist/browser-cli.d.ts +4 -0
- package/dist/browser-cli.d.ts.map +1 -0
- package/dist/browser-cli.js +1597 -0
- package/dist/browser-cli.js.map +1 -0
- package/dist/browser-main.d.ts +3 -0
- package/dist/browser-main.d.ts.map +1 -0
- package/dist/browser-main.js +10 -0
- package/dist/browser-main.js.map +1 -0
- package/dist/cli.d.ts +0 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +106 -13
- package/dist/cli.js.map +1 -1
- package/dist/connection.d.ts +15 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +118 -24
- package/dist/connection.js.map +1 -1
- package/dist/ollama.d.ts +1 -1
- package/dist/ollama.js +4 -4
- package/dist/ollama.js.map +1 -1
- package/dist/relay.d.ts +19 -10
- package/dist/relay.d.ts.map +1 -1
- package/dist/relay.js +272 -98
- package/dist/relay.js.map +1 -1
- package/dist/server.d.ts +56 -11
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +488 -68
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +30 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +15 -0
- package/dist/version.js.map +1 -0
- package/docs/chrome-devtools-relay.md +351 -0
- package/docs/eval.md +294 -0
- package/docs/openclaw-local-browser.md +264 -0
- package/openclaw/vibebrowser/SKILL.md +153 -0
- package/package.json +11 -2
package/dist/server.js
CHANGED
|
@@ -3,37 +3,156 @@
|
|
|
3
3
|
*
|
|
4
4
|
* MCP server that bridges AI clients with the Vibe browser extension.
|
|
5
5
|
*/
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
6
7
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
8
|
+
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
|
|
7
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
-
import {
|
|
10
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
11
|
+
import { CallToolRequestSchema, isInitializeRequest, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
12
|
import { ExtensionConnection } from './connection.js';
|
|
10
|
-
import { DEFAULT_WS_PORT } from './types.js';
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
+
import { DEFAULT_HTTP_PATH, DEFAULT_HTTP_PORT, DEFAULT_WS_PORT, } from './types.js';
|
|
14
|
+
import { getPackageVersion } from './version.js';
|
|
15
|
+
const SERVER_NAME = 'vibebrowser-mcp';
|
|
16
|
+
const SERVER_VERSION = getPackageVersion();
|
|
13
17
|
const STARTUP_TOOLS_REFRESH_TIMEOUT_MS = 4_000;
|
|
14
18
|
const STARTUP_TOOLS_EVENT_WAIT_TIMEOUT_MS = 1_500;
|
|
15
19
|
/**
|
|
16
20
|
* Vibe MCP Server
|
|
17
21
|
*
|
|
18
|
-
* Exposes Vibe browser tools to MCP clients via stdio transport.
|
|
22
|
+
* Exposes Vibe browser tools to MCP clients via stdio or streamable HTTP transport.
|
|
19
23
|
*/
|
|
20
24
|
export class VibeMcpServer {
|
|
21
|
-
server;
|
|
22
25
|
connection;
|
|
23
26
|
config;
|
|
27
|
+
sessions = new Map();
|
|
28
|
+
stdioServer = null;
|
|
29
|
+
httpApp = null;
|
|
30
|
+
httpServer = null;
|
|
31
|
+
shutdownStarted = false;
|
|
24
32
|
constructor(config = {}) {
|
|
25
33
|
this.config = {
|
|
26
34
|
port: config.port ?? DEFAULT_WS_PORT,
|
|
27
35
|
host: config.host ?? '127.0.0.1',
|
|
28
36
|
debug: config.debug ?? false,
|
|
37
|
+
transport: config.transport ?? 'stdio',
|
|
38
|
+
httpPort: config.httpPort ?? DEFAULT_HTTP_PORT,
|
|
39
|
+
httpPath: normalizeHttpPath(config.httpPath ?? DEFAULT_HTTP_PATH),
|
|
40
|
+
allowedHosts: config.allowedHosts,
|
|
29
41
|
remoteUuid: config.remoteUuid,
|
|
42
|
+
sessionId: config.sessionId,
|
|
30
43
|
remoteRelayUrl: config.remoteRelayUrl,
|
|
31
44
|
};
|
|
32
45
|
const remoteConfig = this.config.remoteUuid
|
|
33
46
|
? { uuid: this.config.remoteUuid, relayUrl: this.config.remoteRelayUrl }
|
|
34
47
|
: undefined;
|
|
35
|
-
this.connection = new ExtensionConnection(this.config.port, this.config.debug, remoteConfig);
|
|
36
|
-
this.
|
|
48
|
+
this.connection = new ExtensionConnection(this.config.port, this.config.debug, remoteConfig, this.config.remoteUuid ? undefined : { sessionId: this.config.sessionId });
|
|
49
|
+
this.setupConnectionEvents();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Start the MCP server
|
|
53
|
+
*/
|
|
54
|
+
async start() {
|
|
55
|
+
await this.connection.start();
|
|
56
|
+
if (this.config.remoteUuid) {
|
|
57
|
+
this.log(`Connected to remote relay for UUID ${this.config.remoteUuid}`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.log(`Waiting for Vibe extension connection on port ${this.config.port}...`);
|
|
61
|
+
}
|
|
62
|
+
if (this.config.transport === 'http') {
|
|
63
|
+
await this.startHttpServer();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
await this.startStdioServer();
|
|
67
|
+
}
|
|
68
|
+
this.registerProcessHandlers();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Shutdown the server
|
|
72
|
+
*/
|
|
73
|
+
async stop() {
|
|
74
|
+
if (this.httpServer) {
|
|
75
|
+
await new Promise((resolve, reject) => {
|
|
76
|
+
this.httpServer.close((error) => {
|
|
77
|
+
if (error) {
|
|
78
|
+
reject(error);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
resolve();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
this.httpServer = null;
|
|
85
|
+
this.httpApp = null;
|
|
86
|
+
}
|
|
87
|
+
for (const [sessionId, session] of this.sessions) {
|
|
88
|
+
try {
|
|
89
|
+
await session.transport.close();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// ignore shutdown cleanup errors
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await session.server.close();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// ignore shutdown cleanup errors
|
|
99
|
+
}
|
|
100
|
+
this.sessions.delete(sessionId);
|
|
101
|
+
}
|
|
102
|
+
if (this.stdioServer) {
|
|
103
|
+
try {
|
|
104
|
+
await this.stdioServer.close();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// ignore shutdown cleanup errors
|
|
108
|
+
}
|
|
109
|
+
this.stdioServer = null;
|
|
110
|
+
}
|
|
111
|
+
await this.connection.stop();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Return the configured MCP endpoint URL in HTTP mode.
|
|
115
|
+
*/
|
|
116
|
+
getHttpUrl() {
|
|
117
|
+
if (this.config.transport !== 'http') {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const host = this.config.host.includes(':') && !this.config.host.startsWith('[')
|
|
121
|
+
? `[${this.config.host}]`
|
|
122
|
+
: this.config.host;
|
|
123
|
+
return `http://${host}:${this.config.httpPort}${this.config.httpPath}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Return the configured transport mode.
|
|
127
|
+
*/
|
|
128
|
+
getTransportMode() {
|
|
129
|
+
return this.config.transport;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Set up extension connection events
|
|
133
|
+
*/
|
|
134
|
+
setupConnectionEvents() {
|
|
135
|
+
this.connection.on('connected', () => {
|
|
136
|
+
this.log('Extension connected');
|
|
137
|
+
});
|
|
138
|
+
this.connection.on('disconnected', () => {
|
|
139
|
+
this.log('Extension disconnected');
|
|
140
|
+
this.notifyToolListChanged();
|
|
141
|
+
});
|
|
142
|
+
this.connection.on('tools_updated', (tools) => {
|
|
143
|
+
this.log(`Received ${tools.length} tools from extension`);
|
|
144
|
+
this.notifyToolListChanged();
|
|
145
|
+
});
|
|
146
|
+
this.connection.on('extension_disconnected', () => {
|
|
147
|
+
this.log('Extension disconnected from relay');
|
|
148
|
+
this.notifyToolListChanged();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create a configured MCP server instance.
|
|
153
|
+
*/
|
|
154
|
+
createProtocolServer() {
|
|
155
|
+
const server = new Server({
|
|
37
156
|
name: SERVER_NAME,
|
|
38
157
|
version: SERVER_VERSION,
|
|
39
158
|
}, {
|
|
@@ -43,18 +162,9 @@ export class VibeMcpServer {
|
|
|
43
162
|
},
|
|
44
163
|
},
|
|
45
164
|
});
|
|
46
|
-
|
|
47
|
-
this.setupConnectionEvents();
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Set up MCP request handlers
|
|
51
|
-
*/
|
|
52
|
-
setupHandlers() {
|
|
53
|
-
// List available tools
|
|
54
|
-
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
165
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
55
166
|
if (this.connection.getTools().length === 0 && this.connection.isExtensionConnected()) {
|
|
56
167
|
try {
|
|
57
|
-
// Keep startup tool discovery under client startup budgets (e.g. Codex 10s).
|
|
58
168
|
await this.connection.refreshTools(STARTUP_TOOLS_REFRESH_TIMEOUT_MS);
|
|
59
169
|
}
|
|
60
170
|
catch (error) {
|
|
@@ -65,23 +175,23 @@ export class VibeMcpServer {
|
|
|
65
175
|
if (this.connection.getTools().length === 0 && this.connection.isExtensionConnected()) {
|
|
66
176
|
await this.connection.waitForToolsUpdate(STARTUP_TOOLS_EVENT_WAIT_TIMEOUT_MS);
|
|
67
177
|
}
|
|
68
|
-
const tools = this.connection.getTools();
|
|
69
178
|
return {
|
|
70
|
-
tools:
|
|
179
|
+
tools: this.connection.getTools().map((tool) => ({
|
|
71
180
|
name: tool.name,
|
|
72
181
|
description: tool.description,
|
|
73
182
|
inputSchema: tool.inputSchema,
|
|
74
183
|
})),
|
|
75
184
|
};
|
|
76
185
|
});
|
|
77
|
-
|
|
78
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
186
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
79
187
|
const { name, arguments: args } = request.params;
|
|
80
188
|
try {
|
|
81
|
-
const
|
|
189
|
+
const preparedArgs = this.withDefaultPageStateFormat(name, toRecord(args));
|
|
190
|
+
const result = await this.connection.callTool(name, preparedArgs);
|
|
191
|
+
const enriched = await this.withFallbackPageContent(name, preparedArgs, result);
|
|
82
192
|
return {
|
|
83
|
-
content:
|
|
84
|
-
isError:
|
|
193
|
+
content: enriched.content,
|
|
194
|
+
isError: enriched.isError,
|
|
85
195
|
};
|
|
86
196
|
}
|
|
87
197
|
catch (error) {
|
|
@@ -92,59 +202,257 @@ export class VibeMcpServer {
|
|
|
92
202
|
};
|
|
93
203
|
}
|
|
94
204
|
});
|
|
205
|
+
return server;
|
|
206
|
+
}
|
|
207
|
+
withDefaultPageStateFormat(name, args) {
|
|
208
|
+
const tool = this.findToolByName(name);
|
|
209
|
+
if (!tool) {
|
|
210
|
+
return args;
|
|
211
|
+
}
|
|
212
|
+
if (args.pageStateFormat !== undefined || args.page_state_format !== undefined) {
|
|
213
|
+
return args;
|
|
214
|
+
}
|
|
215
|
+
const properties = tool.inputSchema.properties ?? {};
|
|
216
|
+
if (Object.prototype.hasOwnProperty.call(properties, 'pageStateFormat')) {
|
|
217
|
+
return { ...args, pageStateFormat: 'markdown' };
|
|
218
|
+
}
|
|
219
|
+
if (Object.prototype.hasOwnProperty.call(properties, 'page_state_format')) {
|
|
220
|
+
return { ...args, page_state_format: 'markdown' };
|
|
221
|
+
}
|
|
222
|
+
return args;
|
|
223
|
+
}
|
|
224
|
+
async withFallbackPageContent(name, args, result) {
|
|
225
|
+
if (result.isError) {
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
if (!shouldFallbackToSnapshot(name)) {
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
const primaryText = firstToolText(result);
|
|
232
|
+
if (looksLikePageContentText(primaryText)) {
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
const snapshotText = await this.takeMarkdownSnapshot(extractPageId(args, result));
|
|
236
|
+
if (!snapshotText) {
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
...result,
|
|
241
|
+
content: [
|
|
242
|
+
{ type: 'text', text: snapshotText },
|
|
243
|
+
...result.content,
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async takeMarkdownSnapshot(pageId) {
|
|
248
|
+
if (this.connection.getTools().length === 0 && this.connection.isExtensionConnected()) {
|
|
249
|
+
try {
|
|
250
|
+
await this.connection.refreshTools(STARTUP_TOOLS_REFRESH_TIMEOUT_MS);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// ignore and continue with cached tools
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const snapshotTool = this.findToolByName('take_md_snapshot');
|
|
257
|
+
if (!snapshotTool) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
const callArgs = {};
|
|
261
|
+
const properties = snapshotTool.inputSchema.properties ?? {};
|
|
262
|
+
if (typeof pageId === 'number' && Number.isFinite(pageId)) {
|
|
263
|
+
if (Object.prototype.hasOwnProperty.call(properties, 'pageId')) {
|
|
264
|
+
callArgs.pageId = pageId;
|
|
265
|
+
}
|
|
266
|
+
else if (Object.prototype.hasOwnProperty.call(properties, 'tabId')) {
|
|
267
|
+
callArgs.tabId = pageId;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (Object.prototype.hasOwnProperty.call(properties, 'pageStateFormat')) {
|
|
271
|
+
callArgs.pageStateFormat = 'markdown';
|
|
272
|
+
}
|
|
273
|
+
else if (Object.prototype.hasOwnProperty.call(properties, 'page_state_format')) {
|
|
274
|
+
callArgs.page_state_format = 'markdown';
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const snapshot = await this.connection.callTool(snapshotTool.name, callArgs, STARTUP_TOOLS_REFRESH_TIMEOUT_MS);
|
|
278
|
+
const text = firstToolText(snapshot);
|
|
279
|
+
return text || undefined;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
findToolByName(name) {
|
|
286
|
+
const needle = normalizeToolName(name);
|
|
287
|
+
return this.connection.getTools().find((tool) => normalizeToolName(tool.name) === needle);
|
|
95
288
|
}
|
|
96
289
|
/**
|
|
97
|
-
*
|
|
290
|
+
* Start stdio MCP transport.
|
|
98
291
|
*/
|
|
99
|
-
|
|
100
|
-
this.
|
|
101
|
-
|
|
292
|
+
async startStdioServer() {
|
|
293
|
+
const server = this.createProtocolServer();
|
|
294
|
+
const transport = new StdioServerTransport();
|
|
295
|
+
await server.connect(transport);
|
|
296
|
+
this.stdioServer = server;
|
|
297
|
+
this.log('MCP server started on stdio');
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Start streamable HTTP MCP transport.
|
|
301
|
+
*/
|
|
302
|
+
async startHttpServer() {
|
|
303
|
+
this.httpApp = createMcpExpressApp({
|
|
304
|
+
host: this.config.host,
|
|
305
|
+
allowedHosts: this.config.allowedHosts,
|
|
102
306
|
});
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
307
|
+
const healthHandler = (_req, res) => {
|
|
308
|
+
res.statusCode = 200;
|
|
309
|
+
res.setHeader('content-type', 'application/json');
|
|
310
|
+
res.end(JSON.stringify({
|
|
311
|
+
name: SERVER_NAME,
|
|
312
|
+
version: SERVER_VERSION,
|
|
313
|
+
transport: 'http',
|
|
314
|
+
mcpPath: this.config.httpPath,
|
|
315
|
+
extensionConnected: this.connection.isExtensionConnected(),
|
|
316
|
+
cachedTools: this.connection.getTools().length,
|
|
317
|
+
}));
|
|
318
|
+
};
|
|
319
|
+
this.httpApp.get('/health', healthHandler);
|
|
320
|
+
this.httpApp.get('/', healthHandler);
|
|
321
|
+
this.httpApp.post(this.config.httpPath, async (req, res) => {
|
|
322
|
+
await this.handleHttpRequest(req, res, req.body);
|
|
106
323
|
});
|
|
107
|
-
this.
|
|
108
|
-
this.
|
|
109
|
-
this.notifyToolListChanged();
|
|
324
|
+
this.httpApp.get(this.config.httpPath, async (req, res) => {
|
|
325
|
+
await this.handleHttpRequest(req, res);
|
|
110
326
|
});
|
|
111
|
-
this.
|
|
112
|
-
this.
|
|
113
|
-
|
|
327
|
+
this.httpApp.delete(this.config.httpPath, async (req, res) => {
|
|
328
|
+
await this.handleHttpRequest(req, res);
|
|
329
|
+
});
|
|
330
|
+
this.httpServer = await new Promise((resolve, reject) => {
|
|
331
|
+
const server = this.httpApp.listen(this.config.httpPort, this.config.host, () => resolve(server));
|
|
332
|
+
server.once('error', reject);
|
|
114
333
|
});
|
|
334
|
+
this.log(`MCP server started on ${this.getHttpUrl()}`);
|
|
115
335
|
}
|
|
116
336
|
/**
|
|
117
|
-
*
|
|
337
|
+
* Handle a streamable HTTP request.
|
|
118
338
|
*/
|
|
119
|
-
async
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
339
|
+
async handleHttpRequest(req, res, parsedBody) {
|
|
340
|
+
try {
|
|
341
|
+
const transport = await this.resolveHttpTransport(req, res, parsedBody);
|
|
342
|
+
if (!transport) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
124
346
|
}
|
|
125
|
-
|
|
126
|
-
|
|
347
|
+
catch (error) {
|
|
348
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
349
|
+
this.log(`Error handling HTTP MCP request: ${message}`);
|
|
350
|
+
if (!res.headersSent) {
|
|
351
|
+
res.statusCode = 500;
|
|
352
|
+
res.setHeader('content-type', 'application/json');
|
|
353
|
+
res.end(JSON.stringify({
|
|
354
|
+
jsonrpc: '2.0',
|
|
355
|
+
error: {
|
|
356
|
+
code: -32603,
|
|
357
|
+
message: 'Internal server error',
|
|
358
|
+
},
|
|
359
|
+
id: null,
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
127
362
|
}
|
|
128
|
-
// Connect MCP server to stdio transport
|
|
129
|
-
const transport = new StdioServerTransport();
|
|
130
|
-
await this.server.connect(transport);
|
|
131
|
-
this.log('MCP server started on stdio');
|
|
132
|
-
// Handle process termination
|
|
133
|
-
process.on('SIGINT', () => this.shutdown());
|
|
134
|
-
process.on('SIGTERM', () => this.shutdown());
|
|
135
|
-
process.stdin.on('close', () => this.shutdown());
|
|
136
363
|
}
|
|
137
364
|
/**
|
|
138
|
-
*
|
|
365
|
+
* Resolve or create the HTTP session transport for a request.
|
|
366
|
+
*/
|
|
367
|
+
async resolveHttpTransport(req, res, parsedBody) {
|
|
368
|
+
const sessionId = getSessionId(req);
|
|
369
|
+
if (sessionId) {
|
|
370
|
+
const existing = this.sessions.get(sessionId);
|
|
371
|
+
if (!existing) {
|
|
372
|
+
writeJsonRpcError(res, 404, 'Session not found');
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
return existing.transport;
|
|
376
|
+
}
|
|
377
|
+
if (req.method === 'POST' && parsedBody && isInitializeRequest(parsedBody)) {
|
|
378
|
+
const session = await this.createHttpSession();
|
|
379
|
+
return session.transport;
|
|
380
|
+
}
|
|
381
|
+
writeJsonRpcError(res, 400, 'Bad Request: No valid session ID provided');
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Create a new streamable HTTP session.
|
|
386
|
+
*/
|
|
387
|
+
async createHttpSession() {
|
|
388
|
+
const server = this.createProtocolServer();
|
|
389
|
+
const transport = new StreamableHTTPServerTransport({
|
|
390
|
+
sessionIdGenerator: () => randomUUID(),
|
|
391
|
+
onsessioninitialized: (sessionId) => {
|
|
392
|
+
this.sessions.set(sessionId, { server, transport });
|
|
393
|
+
this.log(`HTTP session initialized: ${sessionId}`);
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
transport.onclose = () => {
|
|
397
|
+
const sessionId = transport.sessionId;
|
|
398
|
+
if (sessionId) {
|
|
399
|
+
this.sessions.delete(sessionId);
|
|
400
|
+
this.log(`HTTP session closed: ${sessionId}`);
|
|
401
|
+
}
|
|
402
|
+
// Do not call server.close() here: server.close() closes the transport,
|
|
403
|
+
// which re-enters transport.onclose() and can recurse until stack overflow.
|
|
404
|
+
};
|
|
405
|
+
transport.onerror = (error) => {
|
|
406
|
+
const sessionId = transport.sessionId ?? 'unknown';
|
|
407
|
+
this.log(`HTTP transport error (${sessionId}): ${error.message}`);
|
|
408
|
+
};
|
|
409
|
+
await server.connect(transport);
|
|
410
|
+
return { server, transport };
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Notify all connected transports that the tool list changed.
|
|
139
414
|
*/
|
|
415
|
+
notifyToolListChanged() {
|
|
416
|
+
if (this.stdioServer) {
|
|
417
|
+
this.sendToolListChanged(this.stdioServer);
|
|
418
|
+
}
|
|
419
|
+
for (const { server } of this.sessions.values()) {
|
|
420
|
+
this.sendToolListChanged(server);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
sendToolListChanged(server) {
|
|
424
|
+
if (!server.transport) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
server.sendToolListChanged().catch((error) => {
|
|
428
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
429
|
+
this.log(`Failed to send tools/list_changed: ${message}`);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Handle process termination.
|
|
434
|
+
*/
|
|
435
|
+
registerProcessHandlers() {
|
|
436
|
+
const onSignal = () => {
|
|
437
|
+
void this.shutdown();
|
|
438
|
+
};
|
|
439
|
+
process.on('SIGINT', onSignal);
|
|
440
|
+
process.on('SIGTERM', onSignal);
|
|
441
|
+
if (this.config.transport === 'stdio') {
|
|
442
|
+
process.stdin.on('close', onSignal);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
140
445
|
async shutdown() {
|
|
446
|
+
if (this.shutdownStarted) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
this.shutdownStarted = true;
|
|
141
450
|
this.log('Shutting down...');
|
|
142
451
|
try {
|
|
143
|
-
await this.
|
|
144
|
-
await this.server.close();
|
|
452
|
+
await this.stop();
|
|
145
453
|
}
|
|
146
|
-
catch
|
|
147
|
-
//
|
|
454
|
+
catch {
|
|
455
|
+
// ignore shutdown errors
|
|
148
456
|
}
|
|
149
457
|
process.exit(0);
|
|
150
458
|
}
|
|
@@ -156,15 +464,6 @@ export class VibeMcpServer {
|
|
|
156
464
|
console.error(`[${SERVER_NAME}] ${message}`);
|
|
157
465
|
}
|
|
158
466
|
}
|
|
159
|
-
notifyToolListChanged() {
|
|
160
|
-
if (!this.server.transport) {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
this.server.sendToolListChanged().catch((error) => {
|
|
164
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
165
|
-
this.log(`Failed to send tools/list_changed: ${message}`);
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
467
|
}
|
|
169
468
|
/**
|
|
170
469
|
* Create and start the MCP server
|
|
@@ -174,4 +473,125 @@ export async function createServer(config) {
|
|
|
174
473
|
await server.start();
|
|
175
474
|
return server;
|
|
176
475
|
}
|
|
476
|
+
function toRecord(value) {
|
|
477
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
478
|
+
return {};
|
|
479
|
+
}
|
|
480
|
+
return { ...value };
|
|
481
|
+
}
|
|
482
|
+
function normalizeToolName(value) {
|
|
483
|
+
return value.replace(/[-\s]/g, '_').toLowerCase();
|
|
484
|
+
}
|
|
485
|
+
function shouldFallbackToSnapshot(name) {
|
|
486
|
+
const normalized = normalizeToolName(name);
|
|
487
|
+
return new Set([
|
|
488
|
+
'open',
|
|
489
|
+
'navigate',
|
|
490
|
+
'new_page',
|
|
491
|
+
'create_new_tab',
|
|
492
|
+
'navigate_page',
|
|
493
|
+
'navigate_to_url',
|
|
494
|
+
'click',
|
|
495
|
+
'fill',
|
|
496
|
+
'fill_form',
|
|
497
|
+
'type_text',
|
|
498
|
+
'press_key',
|
|
499
|
+
'hover',
|
|
500
|
+
'drag',
|
|
501
|
+
'scroll_page',
|
|
502
|
+
'media_control',
|
|
503
|
+
]).has(normalized);
|
|
504
|
+
}
|
|
505
|
+
function firstToolText(result) {
|
|
506
|
+
const textItem = result.content.find((entry) => entry.type === 'text');
|
|
507
|
+
if (!textItem || !('text' in textItem)) {
|
|
508
|
+
return '';
|
|
509
|
+
}
|
|
510
|
+
return typeof textItem.text === 'string' ? textItem.text : '';
|
|
511
|
+
}
|
|
512
|
+
function looksLikePageContentText(text) {
|
|
513
|
+
const trimmed = text.trim();
|
|
514
|
+
if (!trimmed) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
if (/Error retrieving page content/i.test(trimmed) || /page content extraction failed/i.test(trimmed)) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
return /Page State Format:/i.test(trimmed)
|
|
521
|
+
|| /#\s*(?:Markdown Snapshot|Accessibility Snapshot|HTML Snapshot):/i.test(trimmed)
|
|
522
|
+
|| /```(?:markdown|text|html)/i.test(trimmed);
|
|
523
|
+
}
|
|
524
|
+
function extractPageId(args, result) {
|
|
525
|
+
const direct = firstNumber(args, ['pageId', 'tabId']);
|
|
526
|
+
if (direct !== undefined) {
|
|
527
|
+
return direct;
|
|
528
|
+
}
|
|
529
|
+
const text = firstToolText(result);
|
|
530
|
+
const parsedJson = parseMaybeJsonText(text);
|
|
531
|
+
if (parsedJson && typeof parsedJson === 'object') {
|
|
532
|
+
const parsedId = firstNumber(parsedJson, ['pageId', 'tabId', 'id']);
|
|
533
|
+
if (parsedId !== undefined) {
|
|
534
|
+
return parsedId;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const createdMatch = /new background page \(ID:\s*(\d+)\)/i.exec(text);
|
|
538
|
+
if (createdMatch) {
|
|
539
|
+
return Number.parseInt(createdMatch[1], 10);
|
|
540
|
+
}
|
|
541
|
+
const tabMatch = /\bTab ID:\s*(\d+)\b/i.exec(text);
|
|
542
|
+
if (tabMatch) {
|
|
543
|
+
return Number.parseInt(tabMatch[1], 10);
|
|
544
|
+
}
|
|
545
|
+
const pageMatch = /\bPage ID:\s*(\d+)\b/i.exec(text);
|
|
546
|
+
if (pageMatch) {
|
|
547
|
+
return Number.parseInt(pageMatch[1], 10);
|
|
548
|
+
}
|
|
549
|
+
return undefined;
|
|
550
|
+
}
|
|
551
|
+
function parseMaybeJsonText(text) {
|
|
552
|
+
const trimmed = text.trim();
|
|
553
|
+
if (!trimmed) {
|
|
554
|
+
return undefined;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
return JSON.parse(trimmed);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function firstNumber(record, keys) {
|
|
564
|
+
for (const key of keys) {
|
|
565
|
+
const value = record[key];
|
|
566
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
567
|
+
return value;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
function normalizeHttpPath(path) {
|
|
573
|
+
if (!path || path === '/') {
|
|
574
|
+
return DEFAULT_HTTP_PATH;
|
|
575
|
+
}
|
|
576
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
577
|
+
}
|
|
578
|
+
function getSessionId(req) {
|
|
579
|
+
const value = req.headers['mcp-session-id'];
|
|
580
|
+
if (Array.isArray(value)) {
|
|
581
|
+
return value[0] ?? null;
|
|
582
|
+
}
|
|
583
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
584
|
+
}
|
|
585
|
+
function writeJsonRpcError(res, statusCode, message) {
|
|
586
|
+
res.statusCode = statusCode;
|
|
587
|
+
res.setHeader('content-type', 'application/json');
|
|
588
|
+
res.end(JSON.stringify({
|
|
589
|
+
jsonrpc: '2.0',
|
|
590
|
+
error: {
|
|
591
|
+
code: -32000,
|
|
592
|
+
message,
|
|
593
|
+
},
|
|
594
|
+
id: null,
|
|
595
|
+
}));
|
|
596
|
+
}
|
|
177
597
|
//# sourceMappingURL=server.js.map
|