@probelabs/probe 0.6.0-rc166 → 0.6.0-rc168

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.
@@ -0,0 +1,790 @@
1
+ /**
2
+ * Built-in MCP Server for Probe
3
+ * Runs in the same process as ProbeAgent, eliminating spawn overhead
4
+ */
5
+
6
+ import { createServer } from 'http';
7
+ import { EventEmitter } from 'events';
8
+ import { randomUUID } from 'crypto';
9
+ import { Server as MCPServer } from '@modelcontextprotocol/sdk/server/index.js';
10
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
11
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
12
+ import {
13
+ CallToolRequestSchema,
14
+ ListToolsRequestSchema,
15
+ isInitializeRequest
16
+ } from '@modelcontextprotocol/sdk/types.js';
17
+
18
+ /**
19
+ * Simple in-memory event store for resumability
20
+ */
21
+ class InMemoryEventStore {
22
+ constructor() {
23
+ this.events = new Map();
24
+ }
25
+
26
+ generateEventId(streamId) {
27
+ return `${streamId}_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
28
+ }
29
+
30
+ getStreamIdFromEventId(eventId) {
31
+ const parts = eventId.split('_');
32
+ return parts.length > 0 ? parts[0] : '';
33
+ }
34
+
35
+ async storeEvent(streamId, message) {
36
+ const eventId = this.generateEventId(streamId);
37
+ this.events.set(eventId, { streamId, message });
38
+ return eventId;
39
+ }
40
+
41
+ async replayEventsAfter(lastEventId, { send }) {
42
+ if (!lastEventId || !this.events.has(lastEventId)) {
43
+ return '';
44
+ }
45
+
46
+ const streamId = this.getStreamIdFromEventId(lastEventId);
47
+ if (!streamId) {
48
+ return '';
49
+ }
50
+
51
+ let foundLastEvent = false;
52
+ const sortedEvents = [...this.events.entries()].sort((a, b) => a[0].localeCompare(b[0]));
53
+
54
+ for (const [eventId, { streamId: eventStreamId, message }] of sortedEvents) {
55
+ if (eventStreamId !== streamId) {
56
+ continue;
57
+ }
58
+
59
+ if (eventId === lastEventId) {
60
+ foundLastEvent = true;
61
+ continue;
62
+ }
63
+
64
+ if (foundLastEvent) {
65
+ await send(eventId, message);
66
+ }
67
+ }
68
+
69
+ return streamId;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Built-in MCP Server that runs in-process
75
+ */
76
+ export class BuiltInMCPServer extends EventEmitter {
77
+ constructor(agent, options = {}) {
78
+ super();
79
+ this.agent = agent;
80
+ this.port = options.port || 0; // 0 = ephemeral port
81
+ this.host = options.host || '127.0.0.1';
82
+ this.httpServer = null;
83
+ this.mcpServer = null;
84
+ this.sseTransports = new Map(); // Map of sessionId -> SSEServerTransport (deprecated)
85
+ this.streamableTransports = new Map(); // Map of sessionId -> StreamableHTTPServerTransport
86
+ this.connections = new Set();
87
+ this.debug = options.debug || false;
88
+ }
89
+
90
+ /**
91
+ * Start the built-in MCP server
92
+ */
93
+ async start() {
94
+ // Create HTTP server for SSE/HTTP transport
95
+ this.httpServer = createServer();
96
+
97
+ // Handle SSE connections
98
+ this.httpServer.on('request', (req, res) => {
99
+ this.handleRequest(req, res);
100
+ });
101
+
102
+ // Create MCP server
103
+ this.mcpServer = new MCPServer({
104
+ name: 'probe-builtin',
105
+ version: '1.0.0'
106
+ }, {
107
+ capabilities: {
108
+ tools: {}
109
+ }
110
+ });
111
+
112
+ // Register MCP handlers
113
+ this.registerHandlers();
114
+
115
+ // Start listening on ephemeral port
116
+ return new Promise((resolve, reject) => {
117
+ this.httpServer.listen(this.port, this.host, async () => {
118
+ const address = this.httpServer.address();
119
+ this.port = address.port;
120
+
121
+ if (this.debug) {
122
+ console.log(`[MCP] Built-in server started at http://${this.host}:${this.port}`);
123
+ console.log(`[MCP] SSE endpoint: http://${this.host}:${this.port}/sse`);
124
+ console.log(`[MCP] Messages endpoint: http://${this.host}:${this.port}/messages`);
125
+ }
126
+
127
+ this.emit('ready', { host: this.host, port: this.port });
128
+ resolve({ host: this.host, port: this.port });
129
+ });
130
+
131
+ this.httpServer.on('error', reject);
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Handle HTTP requests (SSE and JSON-RPC)
137
+ */
138
+ handleRequest(req, res) {
139
+ const { method, url } = req;
140
+
141
+ if (this.debug) {
142
+ console.log(`[MCP] Request: ${method} ${url}`);
143
+ }
144
+
145
+ // CORS headers for local development
146
+ res.setHeader('Access-Control-Allow-Origin', '*');
147
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
148
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
149
+
150
+ if (method === 'OPTIONS') {
151
+ res.writeHead(204);
152
+ res.end();
153
+ return;
154
+ }
155
+
156
+ // Handle SSE endpoint (GET) - create new transport per connection
157
+ if (url === '/sse' && method === 'GET') {
158
+ if (this.debug) {
159
+ console.log('[MCP] Routing to handleSSEConnection');
160
+ }
161
+ this.handleSSEConnection(req, res);
162
+ return;
163
+ }
164
+
165
+ // Handle /messages endpoint (POST) - route to existing transport
166
+ if (url.startsWith('/messages') && method === 'POST') {
167
+ this.handleSSEMessage(req, res);
168
+ return;
169
+ }
170
+
171
+ // Handle JSON-RPC endpoint
172
+ if (url === '/rpc' && method === 'POST') {
173
+ this.handleJSONRPC(req, res);
174
+ return;
175
+ }
176
+
177
+ // Handle Streamable HTTP protocol (GET/POST/DELETE on /mcp)
178
+ if (url === '/mcp') {
179
+ this.handleStreamableHTTP(req, res);
180
+ return;
181
+ }
182
+
183
+ // Health check
184
+ if (url === '/health') {
185
+ res.writeHead(200, { 'Content-Type': 'application/json' });
186
+ res.end(JSON.stringify({
187
+ status: 'ok',
188
+ server: 'probe-builtin-mcp',
189
+ tools: this.getToolCount()
190
+ }));
191
+ return;
192
+ }
193
+
194
+ // 404 for unknown endpoints
195
+ res.writeHead(404);
196
+ res.end('Not Found');
197
+ }
198
+
199
+ /**
200
+ * Handle SSE connection (GET /sse) - creates new transport
201
+ */
202
+ async handleSSEConnection(req, res) {
203
+ if (this.debug) {
204
+ console.log('[MCP] New SSE connection request');
205
+ }
206
+
207
+ // Create new SSEServerTransport for this connection
208
+ const transport = new SSEServerTransport('/messages', res);
209
+
210
+ // Store transport by sessionId
211
+ this.sseTransports.set(transport.sessionId, transport);
212
+
213
+ // Clean up on connection close
214
+ res.on('close', () => {
215
+ if (this.debug) {
216
+ console.log('[MCP] SSE connection closed, sessionId:', transport.sessionId);
217
+ }
218
+ this.sseTransports.delete(transport.sessionId);
219
+ });
220
+
221
+ // Connect MCP server to this transport
222
+ try {
223
+ await this.mcpServer.connect(transport);
224
+ if (this.debug) {
225
+ console.log('[MCP] MCP server connected to SSE transport, sessionId:', transport.sessionId);
226
+ }
227
+ } catch (error) {
228
+ if (this.debug) {
229
+ console.error('[MCP] Error connecting MCP server to transport:', error);
230
+ }
231
+ this.sseTransports.delete(transport.sessionId);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Handle SSE message (POST /messages?sessionId=...) - routes to existing transport
237
+ */
238
+ async handleSSEMessage(req, res) {
239
+ // Parse URL to get sessionId from query parameter
240
+ const url = new URL(req.url, `http://${req.headers.host}`);
241
+ const sessionId = url.searchParams.get('sessionId');
242
+
243
+ if (!sessionId) {
244
+ res.writeHead(400, { 'Content-Type': 'application/json' });
245
+ res.end(JSON.stringify({
246
+ jsonrpc: '2.0',
247
+ error: {
248
+ code: -32000,
249
+ message: 'Bad Request: sessionId query parameter is required'
250
+ },
251
+ id: null
252
+ }));
253
+ return;
254
+ }
255
+
256
+ // Find transport for this session
257
+ const transport = this.sseTransports.get(sessionId);
258
+ if (!transport) {
259
+ res.writeHead(400, { 'Content-Type': 'application/json' });
260
+ res.end(JSON.stringify({
261
+ jsonrpc: '2.0',
262
+ error: {
263
+ code: -32000,
264
+ message: `Bad Request: No transport found for sessionId: ${sessionId}`
265
+ },
266
+ id: null
267
+ }));
268
+ return;
269
+ }
270
+
271
+ // Read request body
272
+ let body = '';
273
+ req.on('data', chunk => {
274
+ body += chunk.toString();
275
+ });
276
+
277
+ req.on('end', async () => {
278
+ try {
279
+ const message = JSON.parse(body);
280
+ await transport.handlePostMessage(req, res, message);
281
+ } catch (error) {
282
+ res.writeHead(500, { 'Content-Type': 'application/json' });
283
+ res.end(JSON.stringify({
284
+ jsonrpc: '2.0',
285
+ error: {
286
+ code: -32603,
287
+ message: 'Internal error',
288
+ data: error.message
289
+ },
290
+ id: null
291
+ }));
292
+ }
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Handle Streamable HTTP protocol (GET/POST/DELETE on /mcp)
298
+ */
299
+ async handleStreamableHTTP(req, res) {
300
+ const { method } = req;
301
+
302
+ if (this.debug) {
303
+ console.log(`[MCP] Streamable HTTP ${method} request`);
304
+ }
305
+
306
+ try {
307
+ // Parse request body for POST requests
308
+ let body = null;
309
+ if (method === 'POST') {
310
+ body = await this.parseRequestBody(req);
311
+ }
312
+
313
+ // Check for existing session ID in header
314
+ const sessionId = req.headers['mcp-session-id'];
315
+ let transport;
316
+
317
+ if (sessionId && this.streamableTransports.has(sessionId)) {
318
+ // Reuse existing transport
319
+ transport = this.streamableTransports.get(sessionId);
320
+ if (this.debug) {
321
+ console.log(`[MCP] Reusing existing transport for session: ${sessionId}`);
322
+ }
323
+ } else if (!sessionId && method === 'POST' && body && isInitializeRequest(body)) {
324
+ // New session - create transport for initialization request
325
+ if (this.debug) {
326
+ console.log('[MCP] Creating new Streamable HTTP transport for initialization');
327
+ }
328
+
329
+ const eventStore = new InMemoryEventStore();
330
+ transport = new StreamableHTTPServerTransport({
331
+ sessionIdGenerator: () => randomUUID(),
332
+ eventStore, // Enable resumability
333
+ onsessioninitialized: (newSessionId) => {
334
+ // Store the transport by session ID
335
+ if (this.debug) {
336
+ console.log(`[MCP] Streamable HTTP session initialized: ${newSessionId}`);
337
+ }
338
+ this.streamableTransports.set(newSessionId, transport);
339
+ },
340
+ onsessionclosed: (closedSessionId) => {
341
+ // Remove transport when session is closed
342
+ if (this.debug) {
343
+ console.log(`[MCP] Streamable HTTP session closed: ${closedSessionId}`);
344
+ }
345
+ this.streamableTransports.delete(closedSessionId);
346
+ }
347
+ });
348
+
349
+ // Set up onclose handler
350
+ transport.onclose = () => {
351
+ const sid = transport.sessionId;
352
+ if (sid && this.streamableTransports.has(sid)) {
353
+ if (this.debug) {
354
+ console.log(`[MCP] Transport closed for session ${sid}`);
355
+ }
356
+ this.streamableTransports.delete(sid);
357
+ }
358
+ };
359
+
360
+ // Connect the transport to the MCP server
361
+ await this.mcpServer.connect(transport);
362
+ } else {
363
+ // Invalid request - no session ID or not an initialization request
364
+ res.writeHead(400, { 'Content-Type': 'application/json' });
365
+ res.end(JSON.stringify({
366
+ jsonrpc: '2.0',
367
+ error: {
368
+ code: -32000,
369
+ message: 'Bad Request: No valid session ID provided or not an initialization request'
370
+ },
371
+ id: null
372
+ }));
373
+ return;
374
+ }
375
+
376
+ // Handle the request with the transport
377
+ await transport.handleRequest(req, res, body);
378
+ } catch (error) {
379
+ if (this.debug) {
380
+ console.error('[MCP] Error handling Streamable HTTP request:', error);
381
+ }
382
+
383
+ if (!res.headersSent) {
384
+ res.writeHead(500, { 'Content-Type': 'application/json' });
385
+ res.end(JSON.stringify({
386
+ jsonrpc: '2.0',
387
+ error: {
388
+ code: -32603,
389
+ message: 'Internal server error',
390
+ data: error.message
391
+ },
392
+ id: null
393
+ }));
394
+ }
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Parse request body as JSON
400
+ */
401
+ async parseRequestBody(req) {
402
+ return new Promise((resolve, reject) => {
403
+ let body = '';
404
+ req.on('data', chunk => {
405
+ body += chunk.toString();
406
+ });
407
+ req.on('end', () => {
408
+ try {
409
+ const parsed = body ? JSON.parse(body) : null;
410
+ resolve(parsed);
411
+ } catch (error) {
412
+ reject(error);
413
+ }
414
+ });
415
+ req.on('error', reject);
416
+ });
417
+ }
418
+
419
+ /**
420
+ * Handle Server-Sent Events connection (DEPRECATED - use handleSSEConnection instead)
421
+ */
422
+ handleSSE(req, res) {
423
+ res.writeHead(200, {
424
+ 'Content-Type': 'text/event-stream',
425
+ 'Cache-Control': 'no-cache',
426
+ 'Connection': 'keep-alive'
427
+ });
428
+
429
+ // Send initial connection event
430
+ res.write('event: connected\n');
431
+ res.write(`data: ${JSON.stringify({ type: 'connected', server: 'probe-builtin' })}\n\n`);
432
+
433
+ // Store connection
434
+ this.connections.add(res);
435
+
436
+ // Clean up on close
437
+ req.on('close', () => {
438
+ this.connections.delete(res);
439
+ });
440
+ }
441
+
442
+ /**
443
+ * Handle JSON-RPC requests
444
+ */
445
+ async handleJSONRPC(req, res) {
446
+ let body = '';
447
+
448
+ req.on('data', chunk => {
449
+ body += chunk.toString();
450
+ });
451
+
452
+ req.on('end', async () => {
453
+ try {
454
+ const request = JSON.parse(body);
455
+ const response = await this.processRequest(request);
456
+
457
+ res.writeHead(200, { 'Content-Type': 'application/json' });
458
+ res.end(JSON.stringify(response));
459
+ } catch (error) {
460
+ res.writeHead(400, { 'Content-Type': 'application/json' });
461
+ res.end(JSON.stringify({
462
+ jsonrpc: '2.0',
463
+ error: {
464
+ code: -32700,
465
+ message: 'Parse error',
466
+ data: error.message
467
+ },
468
+ id: null
469
+ }));
470
+ }
471
+ });
472
+ }
473
+
474
+ /**
475
+ * Handle MCP protocol messages
476
+ */
477
+ async handleMCPProtocol(req, res) {
478
+ let body = '';
479
+
480
+ req.on('data', chunk => {
481
+ body += chunk.toString();
482
+ });
483
+
484
+ req.on('end', async () => {
485
+ try {
486
+ const message = JSON.parse(body);
487
+
488
+ // Process through MCP server handlers
489
+ let response;
490
+
491
+ if (message.method === 'tools/list') {
492
+ response = await this.handleListTools();
493
+ } else if (message.method === 'tools/call') {
494
+ response = await this.handleCallTool(message.params);
495
+ } else {
496
+ response = {
497
+ error: {
498
+ code: -32601,
499
+ message: 'Method not found'
500
+ }
501
+ };
502
+ }
503
+
504
+ res.writeHead(200, { 'Content-Type': 'application/json' });
505
+ res.end(JSON.stringify(response));
506
+ } catch (error) {
507
+ res.writeHead(500, { 'Content-Type': 'application/json' });
508
+ res.end(JSON.stringify({
509
+ error: {
510
+ code: -32603,
511
+ message: 'Internal error',
512
+ data: error.message
513
+ }
514
+ }));
515
+ }
516
+ });
517
+ }
518
+
519
+ /**
520
+ * Process JSON-RPC request
521
+ */
522
+ async processRequest(request) {
523
+ const { jsonrpc, method, params, id } = request;
524
+
525
+ try {
526
+ let result;
527
+
528
+ switch (method) {
529
+ case 'tools/list':
530
+ result = await this.handleListTools();
531
+ break;
532
+
533
+ case 'tools/call':
534
+ result = await this.handleCallTool(params);
535
+ break;
536
+
537
+ default:
538
+ return {
539
+ jsonrpc: '2.0',
540
+ error: {
541
+ code: -32601,
542
+ message: 'Method not found'
543
+ },
544
+ id
545
+ };
546
+ }
547
+
548
+ return {
549
+ jsonrpc: '2.0',
550
+ result,
551
+ id
552
+ };
553
+ } catch (error) {
554
+ return {
555
+ jsonrpc: '2.0',
556
+ error: {
557
+ code: -32603,
558
+ message: 'Internal error',
559
+ data: error.message
560
+ },
561
+ id
562
+ };
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Register MCP protocol handlers
568
+ */
569
+ registerHandlers() {
570
+ // Handle list tools request
571
+ this.mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
572
+ return this.handleListTools();
573
+ });
574
+
575
+ // Handle tool execution
576
+ this.mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
577
+ return this.handleCallTool(request.params);
578
+ });
579
+ }
580
+
581
+ /**
582
+ * Handle list tools request
583
+ */
584
+ async handleListTools() {
585
+ const tools = [];
586
+
587
+ // Get tools from agent
588
+ if (this.agent && this.agent.allowedTools) {
589
+ const toolDefs = {
590
+ search: {
591
+ description: 'Search for code patterns using semantic search',
592
+ inputSchema: {
593
+ type: 'object',
594
+ properties: {
595
+ query: { type: 'string', description: 'Search query' },
596
+ path: { type: 'string', description: 'Directory to search', default: '.' },
597
+ maxResults: { type: 'integer', default: 10 }
598
+ },
599
+ required: ['query']
600
+ }
601
+ },
602
+ extract: {
603
+ description: 'Extract code from specific file location',
604
+ inputSchema: {
605
+ type: 'object',
606
+ properties: {
607
+ path: { type: 'string', description: 'File path with optional line number' }
608
+ },
609
+ required: ['path']
610
+ }
611
+ },
612
+ listFiles: {
613
+ description: 'List files in a directory',
614
+ inputSchema: {
615
+ type: 'object',
616
+ properties: {
617
+ path: { type: 'string', description: 'Directory path' },
618
+ pattern: { type: 'string', description: 'File pattern' }
619
+ },
620
+ required: ['path']
621
+ }
622
+ },
623
+ searchFiles: {
624
+ description: 'Search for files by name pattern',
625
+ inputSchema: {
626
+ type: 'object',
627
+ properties: {
628
+ pattern: { type: 'string', description: 'File name pattern' },
629
+ path: { type: 'string', description: 'Directory to search' }
630
+ },
631
+ required: ['pattern']
632
+ }
633
+ },
634
+ query: {
635
+ description: 'Query code using AST patterns',
636
+ inputSchema: {
637
+ type: 'object',
638
+ properties: {
639
+ query: { type: 'string', description: 'AST query' },
640
+ path: { type: 'string', description: 'Directory to search' }
641
+ },
642
+ required: ['query']
643
+ }
644
+ }
645
+ };
646
+
647
+ for (const [name, def] of Object.entries(toolDefs)) {
648
+ if (this.agent.allowedTools.isEnabled(name)) {
649
+ tools.push({
650
+ name: `mcp__probe__${name}`,
651
+ description: def.description,
652
+ inputSchema: def.inputSchema
653
+ });
654
+ }
655
+ }
656
+ }
657
+
658
+ return { tools };
659
+ }
660
+
661
+ /**
662
+ * Handle tool execution
663
+ */
664
+ async handleCallTool(params) {
665
+ const { name, arguments: args } = params;
666
+
667
+ // Extract tool name from MCP format
668
+ const toolName = name.replace('mcp__probe__', '');
669
+
670
+ // Check if tool is enabled
671
+ if (!this.agent.allowedTools.isEnabled(toolName)) {
672
+ throw new Error(`Tool ${name} is not enabled`);
673
+ }
674
+
675
+ // Get tool implementation
676
+ const tool = this.agent.toolImplementations[toolName];
677
+ if (!tool) {
678
+ throw new Error(`Tool ${name} not found`);
679
+ }
680
+
681
+ try {
682
+ // Execute tool directly (no spawning!)
683
+ const result = await tool.execute(args);
684
+
685
+ return {
686
+ content: [{
687
+ type: 'text',
688
+ text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
689
+ }]
690
+ };
691
+ } catch (error) {
692
+ return {
693
+ content: [{
694
+ type: 'text',
695
+ text: `Error executing ${name}: ${error.message}`
696
+ }],
697
+ isError: true
698
+ };
699
+ }
700
+ }
701
+
702
+ /**
703
+ * Get the number of available tools
704
+ */
705
+ getToolCount() {
706
+ if (!this.agent || !this.agent.allowedTools) {
707
+ return 0;
708
+ }
709
+
710
+ const tools = ['search', 'extract', 'listFiles', 'searchFiles', 'query'];
711
+ return tools.filter(name => this.agent.allowedTools.isEnabled(name)).length;
712
+ }
713
+
714
+ /**
715
+ * Broadcast message to all SSE connections
716
+ */
717
+ broadcast(event, data) {
718
+ const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
719
+
720
+ for (const connection of this.connections) {
721
+ connection.write(message);
722
+ }
723
+ }
724
+
725
+ /**
726
+ * Stop the server
727
+ */
728
+ async stop() {
729
+ // Close all Streamable HTTP transports
730
+ for (const [sessionId, transport] of this.streamableTransports.entries()) {
731
+ try {
732
+ await transport.close();
733
+ if (this.debug) {
734
+ console.log(`[MCP] Closed Streamable HTTP transport for session: ${sessionId}`);
735
+ }
736
+ } catch (error) {
737
+ if (this.debug) {
738
+ console.error(`[MCP] Error closing Streamable HTTP transport ${sessionId}:`, error);
739
+ }
740
+ }
741
+ }
742
+ this.streamableTransports.clear();
743
+
744
+ // Close all SSE transports
745
+ for (const [sessionId, transport] of this.sseTransports.entries()) {
746
+ try {
747
+ await transport.close();
748
+ if (this.debug) {
749
+ console.log(`[MCP] Closed SSE transport for session: ${sessionId}`);
750
+ }
751
+ } catch (error) {
752
+ if (this.debug) {
753
+ console.error(`[MCP] Error closing SSE transport ${sessionId}:`, error);
754
+ }
755
+ }
756
+ }
757
+ this.sseTransports.clear();
758
+
759
+ // Close all SSE connections
760
+ for (const connection of this.connections) {
761
+ connection.end();
762
+ }
763
+ this.connections.clear();
764
+
765
+ // Close HTTP server
766
+ if (this.httpServer) {
767
+ return new Promise((resolve) => {
768
+ this.httpServer.close(() => {
769
+ if (this.debug) {
770
+ console.log('[MCP] Built-in server stopped');
771
+ }
772
+ resolve();
773
+ });
774
+ });
775
+ }
776
+ }
777
+
778
+ /**
779
+ * Get server configuration for MCP clients
780
+ */
781
+ getConfig() {
782
+ return {
783
+ transport: 'http',
784
+ url: `http://${this.host}:${this.port}/mcp`,
785
+ // Alternative transports:
786
+ // sse: `http://${this.host}:${this.port}/sse`,
787
+ // rpc: `http://${this.host}:${this.port}/rpc`
788
+ };
789
+ }
790
+ }