@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/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 { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
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
- const SERVER_NAME = 'vibe-mcp';
12
- const SERVER_VERSION = '0.1.0';
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.server = new Server({
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
- this.setupHandlers();
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: tools.map((tool) => ({
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
- // Call a tool
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 result = await this.connection.callTool(name, args ?? {});
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: result.content,
84
- isError: result.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
- * Set up extension connection events
290
+ * Start stdio MCP transport.
98
291
  */
99
- setupConnectionEvents() {
100
- this.connection.on('connected', () => {
101
- this.log('Extension connected');
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
- this.connection.on('disconnected', () => {
104
- this.log('Extension disconnected');
105
- this.notifyToolListChanged();
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.connection.on('tools_updated', (tools) => {
108
- this.log(`Received ${tools.length} tools from extension`);
109
- this.notifyToolListChanged();
324
+ this.httpApp.get(this.config.httpPath, async (req, res) => {
325
+ await this.handleHttpRequest(req, res);
110
326
  });
111
- this.connection.on('extension_disconnected', () => {
112
- this.log('Extension disconnected from relay');
113
- this.notifyToolListChanged();
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
- * Start the MCP server
337
+ * Handle a streamable HTTP request.
118
338
  */
119
- async start() {
120
- // Start connection to extension (local relay or remote)
121
- await this.connection.start();
122
- if (this.config.remoteUuid) {
123
- this.log(`Connected to remote relay for UUID ${this.config.remoteUuid}`);
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
- else {
126
- this.log(`Waiting for Vibe extension connection on port ${this.config.port}...`);
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
- * Shutdown the server
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.connection.stop();
144
- await this.server.close();
452
+ await this.stop();
145
453
  }
146
- catch (error) {
147
- // Ignore errors during shutdown
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