@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.
- package/README.md +39 -0
- package/build/agent/ProbeAgent.d.ts +7 -1
- package/build/agent/ProbeAgent.js +589 -4
- package/build/agent/engines/codex.js +347 -0
- package/build/agent/engines/enhanced-claude-code.js +556 -0
- package/build/agent/engines/enhanced-vercel.js +83 -0
- package/build/agent/engines/vercel.js +62 -0
- package/build/agent/index.js +2228 -174
- package/build/agent/mcp/built-in-server.js +790 -0
- package/build/agent/shared/Session.js +53 -0
- package/build/agent/shared/prompts.js +129 -0
- package/cjs/agent/ProbeAgent.cjs +13760 -10554
- package/cjs/index.cjs +13840 -10634
- package/index.d.ts +7 -1
- package/package.json +2 -1
- package/src/agent/ProbeAgent.d.ts +7 -1
- package/src/agent/ProbeAgent.js +589 -4
- package/src/agent/engines/codex.js +347 -0
- package/src/agent/engines/enhanced-claude-code.js +556 -0
- package/src/agent/engines/enhanced-vercel.js +83 -0
- package/src/agent/engines/vercel.js +62 -0
- package/src/agent/mcp/built-in-server.js +790 -0
- package/src/agent/shared/Session.js +53 -0
- package/src/agent/shared/prompts.js +129 -0
|
@@ -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
|
+
}
|