@probelabs/probe 0.6.0-rc165 → 0.6.0-rc167

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,464 @@
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 { Server as MCPServer } from '@modelcontextprotocol/sdk/server/index.js';
9
+ import {
10
+ CallToolRequestSchema,
11
+ ListToolsRequestSchema
12
+ } from '@modelcontextprotocol/sdk/types.js';
13
+
14
+ /**
15
+ * Built-in MCP Server that runs in-process
16
+ */
17
+ export class BuiltInMCPServer extends EventEmitter {
18
+ constructor(agent, options = {}) {
19
+ super();
20
+ this.agent = agent;
21
+ this.port = options.port || 0; // 0 = ephemeral port
22
+ this.host = options.host || '127.0.0.1';
23
+ this.httpServer = null;
24
+ this.mcpServer = null;
25
+ this.connections = new Set();
26
+ this.debug = options.debug || false;
27
+ }
28
+
29
+ /**
30
+ * Start the built-in MCP server
31
+ */
32
+ async start() {
33
+ // Create HTTP server for SSE/HTTP transport
34
+ this.httpServer = createServer();
35
+
36
+ // Handle SSE connections
37
+ this.httpServer.on('request', (req, res) => {
38
+ this.handleRequest(req, res);
39
+ });
40
+
41
+ // Create MCP server
42
+ this.mcpServer = new MCPServer({
43
+ name: 'probe-builtin',
44
+ version: '1.0.0'
45
+ }, {
46
+ capabilities: {
47
+ tools: {}
48
+ }
49
+ });
50
+
51
+ // Register MCP handlers
52
+ this.registerHandlers();
53
+
54
+ // Start listening on ephemeral port
55
+ return new Promise((resolve, reject) => {
56
+ this.httpServer.listen(this.port, this.host, () => {
57
+ const address = this.httpServer.address();
58
+ this.port = address.port;
59
+
60
+ if (this.debug) {
61
+ console.log(`[MCP] Built-in server started at http://${this.host}:${this.port}`);
62
+ }
63
+
64
+ this.emit('ready', { host: this.host, port: this.port });
65
+ resolve({ host: this.host, port: this.port });
66
+ });
67
+
68
+ this.httpServer.on('error', reject);
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Handle HTTP requests (SSE and JSON-RPC)
74
+ */
75
+ handleRequest(req, res) {
76
+ const { method, url } = req;
77
+
78
+ // CORS headers for local development
79
+ res.setHeader('Access-Control-Allow-Origin', '*');
80
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
81
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
82
+
83
+ if (method === 'OPTIONS') {
84
+ res.writeHead(204);
85
+ res.end();
86
+ return;
87
+ }
88
+
89
+ // Handle SSE endpoint
90
+ if (url === '/sse' && method === 'GET') {
91
+ this.handleSSE(req, res);
92
+ return;
93
+ }
94
+
95
+ // Handle JSON-RPC endpoint
96
+ if (url === '/rpc' && method === 'POST') {
97
+ this.handleJSONRPC(req, res);
98
+ return;
99
+ }
100
+
101
+ // Handle stdio-like protocol over HTTP
102
+ if (url === '/mcp' && method === 'POST') {
103
+ this.handleMCPProtocol(req, res);
104
+ return;
105
+ }
106
+
107
+ // Health check
108
+ if (url === '/health') {
109
+ res.writeHead(200, { 'Content-Type': 'application/json' });
110
+ res.end(JSON.stringify({
111
+ status: 'ok',
112
+ server: 'probe-builtin-mcp',
113
+ tools: this.getToolCount()
114
+ }));
115
+ return;
116
+ }
117
+
118
+ // 404 for unknown endpoints
119
+ res.writeHead(404);
120
+ res.end('Not Found');
121
+ }
122
+
123
+ /**
124
+ * Handle Server-Sent Events connection
125
+ */
126
+ handleSSE(req, res) {
127
+ res.writeHead(200, {
128
+ 'Content-Type': 'text/event-stream',
129
+ 'Cache-Control': 'no-cache',
130
+ 'Connection': 'keep-alive'
131
+ });
132
+
133
+ // Send initial connection event
134
+ res.write('event: connected\n');
135
+ res.write(`data: ${JSON.stringify({ type: 'connected', server: 'probe-builtin' })}\n\n`);
136
+
137
+ // Store connection
138
+ this.connections.add(res);
139
+
140
+ // Clean up on close
141
+ req.on('close', () => {
142
+ this.connections.delete(res);
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Handle JSON-RPC requests
148
+ */
149
+ async handleJSONRPC(req, res) {
150
+ let body = '';
151
+
152
+ req.on('data', chunk => {
153
+ body += chunk.toString();
154
+ });
155
+
156
+ req.on('end', async () => {
157
+ try {
158
+ const request = JSON.parse(body);
159
+ const response = await this.processRequest(request);
160
+
161
+ res.writeHead(200, { 'Content-Type': 'application/json' });
162
+ res.end(JSON.stringify(response));
163
+ } catch (error) {
164
+ res.writeHead(400, { 'Content-Type': 'application/json' });
165
+ res.end(JSON.stringify({
166
+ jsonrpc: '2.0',
167
+ error: {
168
+ code: -32700,
169
+ message: 'Parse error',
170
+ data: error.message
171
+ },
172
+ id: null
173
+ }));
174
+ }
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Handle MCP protocol messages
180
+ */
181
+ async handleMCPProtocol(req, res) {
182
+ let body = '';
183
+
184
+ req.on('data', chunk => {
185
+ body += chunk.toString();
186
+ });
187
+
188
+ req.on('end', async () => {
189
+ try {
190
+ const message = JSON.parse(body);
191
+
192
+ // Process through MCP server handlers
193
+ let response;
194
+
195
+ if (message.method === 'tools/list') {
196
+ response = await this.handleListTools();
197
+ } else if (message.method === 'tools/call') {
198
+ response = await this.handleCallTool(message.params);
199
+ } else {
200
+ response = {
201
+ error: {
202
+ code: -32601,
203
+ message: 'Method not found'
204
+ }
205
+ };
206
+ }
207
+
208
+ res.writeHead(200, { 'Content-Type': 'application/json' });
209
+ res.end(JSON.stringify(response));
210
+ } catch (error) {
211
+ res.writeHead(500, { 'Content-Type': 'application/json' });
212
+ res.end(JSON.stringify({
213
+ error: {
214
+ code: -32603,
215
+ message: 'Internal error',
216
+ data: error.message
217
+ }
218
+ }));
219
+ }
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Process JSON-RPC request
225
+ */
226
+ async processRequest(request) {
227
+ const { jsonrpc, method, params, id } = request;
228
+
229
+ try {
230
+ let result;
231
+
232
+ switch (method) {
233
+ case 'tools/list':
234
+ result = await this.handleListTools();
235
+ break;
236
+
237
+ case 'tools/call':
238
+ result = await this.handleCallTool(params);
239
+ break;
240
+
241
+ default:
242
+ return {
243
+ jsonrpc: '2.0',
244
+ error: {
245
+ code: -32601,
246
+ message: 'Method not found'
247
+ },
248
+ id
249
+ };
250
+ }
251
+
252
+ return {
253
+ jsonrpc: '2.0',
254
+ result,
255
+ id
256
+ };
257
+ } catch (error) {
258
+ return {
259
+ jsonrpc: '2.0',
260
+ error: {
261
+ code: -32603,
262
+ message: 'Internal error',
263
+ data: error.message
264
+ },
265
+ id
266
+ };
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Register MCP protocol handlers
272
+ */
273
+ registerHandlers() {
274
+ // Handle list tools request
275
+ this.mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
276
+ return this.handleListTools();
277
+ });
278
+
279
+ // Handle tool execution
280
+ this.mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
281
+ return this.handleCallTool(request.params);
282
+ });
283
+ }
284
+
285
+ /**
286
+ * Handle list tools request
287
+ */
288
+ async handleListTools() {
289
+ const tools = [];
290
+
291
+ // Get tools from agent
292
+ if (this.agent && this.agent.allowedTools) {
293
+ const toolDefs = {
294
+ search: {
295
+ description: 'Search for code patterns using semantic search',
296
+ inputSchema: {
297
+ type: 'object',
298
+ properties: {
299
+ query: { type: 'string', description: 'Search query' },
300
+ path: { type: 'string', description: 'Directory to search', default: '.' },
301
+ maxResults: { type: 'integer', default: 10 }
302
+ },
303
+ required: ['query']
304
+ }
305
+ },
306
+ extract: {
307
+ description: 'Extract code from specific file location',
308
+ inputSchema: {
309
+ type: 'object',
310
+ properties: {
311
+ path: { type: 'string', description: 'File path with optional line number' }
312
+ },
313
+ required: ['path']
314
+ }
315
+ },
316
+ listFiles: {
317
+ description: 'List files in a directory',
318
+ inputSchema: {
319
+ type: 'object',
320
+ properties: {
321
+ path: { type: 'string', description: 'Directory path' },
322
+ pattern: { type: 'string', description: 'File pattern' }
323
+ },
324
+ required: ['path']
325
+ }
326
+ },
327
+ searchFiles: {
328
+ description: 'Search for files by name pattern',
329
+ inputSchema: {
330
+ type: 'object',
331
+ properties: {
332
+ pattern: { type: 'string', description: 'File name pattern' },
333
+ path: { type: 'string', description: 'Directory to search' }
334
+ },
335
+ required: ['pattern']
336
+ }
337
+ },
338
+ query: {
339
+ description: 'Query code using AST patterns',
340
+ inputSchema: {
341
+ type: 'object',
342
+ properties: {
343
+ query: { type: 'string', description: 'AST query' },
344
+ path: { type: 'string', description: 'Directory to search' }
345
+ },
346
+ required: ['query']
347
+ }
348
+ }
349
+ };
350
+
351
+ for (const [name, def] of Object.entries(toolDefs)) {
352
+ if (this.agent.allowedTools.isEnabled(name)) {
353
+ tools.push({
354
+ name: `mcp__probe__${name}`,
355
+ description: def.description,
356
+ inputSchema: def.inputSchema
357
+ });
358
+ }
359
+ }
360
+ }
361
+
362
+ return { tools };
363
+ }
364
+
365
+ /**
366
+ * Handle tool execution
367
+ */
368
+ async handleCallTool(params) {
369
+ const { name, arguments: args } = params;
370
+
371
+ // Extract tool name from MCP format
372
+ const toolName = name.replace('mcp__probe__', '');
373
+
374
+ // Check if tool is enabled
375
+ if (!this.agent.allowedTools.isEnabled(toolName)) {
376
+ throw new Error(`Tool ${name} is not enabled`);
377
+ }
378
+
379
+ // Get tool implementation
380
+ const tool = this.agent.toolImplementations[toolName];
381
+ if (!tool) {
382
+ throw new Error(`Tool ${name} not found`);
383
+ }
384
+
385
+ try {
386
+ // Execute tool directly (no spawning!)
387
+ const result = await tool.execute(args);
388
+
389
+ return {
390
+ content: [{
391
+ type: 'text',
392
+ text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
393
+ }]
394
+ };
395
+ } catch (error) {
396
+ return {
397
+ content: [{
398
+ type: 'text',
399
+ text: `Error executing ${name}: ${error.message}`
400
+ }],
401
+ isError: true
402
+ };
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Get the number of available tools
408
+ */
409
+ getToolCount() {
410
+ if (!this.agent || !this.agent.allowedTools) {
411
+ return 0;
412
+ }
413
+
414
+ const tools = ['search', 'extract', 'listFiles', 'searchFiles', 'query'];
415
+ return tools.filter(name => this.agent.allowedTools.isEnabled(name)).length;
416
+ }
417
+
418
+ /**
419
+ * Broadcast message to all SSE connections
420
+ */
421
+ broadcast(event, data) {
422
+ const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
423
+
424
+ for (const connection of this.connections) {
425
+ connection.write(message);
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Stop the server
431
+ */
432
+ async stop() {
433
+ // Close all SSE connections
434
+ for (const connection of this.connections) {
435
+ connection.end();
436
+ }
437
+ this.connections.clear();
438
+
439
+ // Close HTTP server
440
+ if (this.httpServer) {
441
+ return new Promise((resolve) => {
442
+ this.httpServer.close(() => {
443
+ if (this.debug) {
444
+ console.log('[MCP] Built-in server stopped');
445
+ }
446
+ resolve();
447
+ });
448
+ });
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Get server configuration for MCP clients
454
+ */
455
+ getConfig() {
456
+ return {
457
+ transport: 'http',
458
+ url: `http://${this.host}:${this.port}/mcp`,
459
+ // Alternative transports:
460
+ // sse: `http://${this.host}:${this.port}/sse`,
461
+ // rpc: `http://${this.host}:${this.port}/rpc`
462
+ };
463
+ }
464
+ }
@@ -154,6 +154,31 @@ User: Find all markdown files in the docs directory, but only at the top level.
154
154
  </examples>
155
155
  `;
156
156
 
157
+ // Define the readImage tool XML definition
158
+ export const readImageToolDefinition = `
159
+ ## readImage
160
+ Description: Read and load an image file so it can be viewed by the AI. Use this when you need to analyze, describe, or work with image content. Images from user messages are automatically loaded, but use this tool to explicitly read images mentioned in tool outputs or when you need to examine specific image files.
161
+
162
+ Parameters:
163
+ - path: (required) The path to the image file to read. Supports png, jpg, jpeg, webp, bmp, and svg formats.
164
+
165
+ Usage Example:
166
+
167
+ <examples>
168
+
169
+ User: Can you describe what's in screenshot.png?
170
+ <readImage>
171
+ <path>screenshot.png</path>
172
+ </readImage>
173
+
174
+ User: Analyze the diagram in docs/architecture.svg
175
+ <readImage>
176
+ <path>docs/architecture.svg</path>
177
+ </readImage>
178
+
179
+ </examples>
180
+ `;
181
+
157
182
  /**
158
183
  * Enhanced XML parser that handles thinking tags and attempt_complete shorthand
159
184
  * This function removes any <thinking></thinking> tags from the input string