@probelabs/probe 0.6.0-rc88 → 0.6.0-rc89

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.
Binary file
@@ -23,6 +23,14 @@ export interface ProbeAgentOptions {
23
23
  debug?: boolean;
24
24
  /** Optional telemetry tracer instance */
25
25
  tracer?: any;
26
+ /** Enable MCP (Model Context Protocol) tool integration */
27
+ enableMcp?: boolean;
28
+ /** Path to MCP configuration file */
29
+ mcpConfigPath?: string;
30
+ /** MCP configuration object (overrides mcpConfigPath) */
31
+ mcpConfig?: any;
32
+ /** @deprecated Use mcpConfig instead */
33
+ mcpServers?: any[];
26
34
  }
27
35
 
28
36
  /**
@@ -19,14 +19,15 @@ import {
19
19
  parseXmlToolCallWithThinking
20
20
  } from './tools.js';
21
21
  import { createMessagePreview } from '../tools/common.js';
22
- import {
23
- createWrappedTools,
24
- listFilesToolInstance,
22
+ import {
23
+ createWrappedTools,
24
+ listFilesToolInstance,
25
25
  searchFilesToolInstance,
26
- clearToolExecutionData
26
+ clearToolExecutionData
27
27
  } from './probeTool.js';
28
+ import { createMockProvider } from './mockProvider.js';
28
29
  import { listFilesByLevel } from '../index.js';
29
- import {
30
+ import {
30
31
  cleanSchemaResponse,
31
32
  isJsonSchema,
32
33
  validateJsonResponse,
@@ -35,6 +36,11 @@ import {
35
36
  createSchemaDefinitionCorrectionPrompt,
36
37
  validateAndFixMermaidResponse
37
38
  } from './schemaUtils.js';
39
+ import {
40
+ MCPXmlBridge,
41
+ parseHybridXmlToolCall,
42
+ loadMCPConfigurationFromPath
43
+ } from './mcp/index.js';
38
44
 
39
45
  // Maximum tool iterations to prevent infinite loops - configurable via MAX_TOOL_ITERATIONS env var
40
46
  const MAX_TOOL_ITERATIONS = parseInt(process.env.MAX_TOOL_ITERATIONS || '30', 10);
@@ -58,6 +64,10 @@ export class ProbeAgent {
58
64
  * @param {boolean} [options.outline] - Enable outline-xml format for search results
59
65
  * @param {number} [options.maxResponseTokens] - Maximum tokens for AI responses
60
66
  * @param {boolean} [options.disableMermaidValidation=false] - Disable automatic mermaid diagram validation and fixing
67
+ * @param {boolean} [options.enableMcp=false] - Enable MCP tool integration
68
+ * @param {string} [options.mcpConfigPath] - Path to MCP configuration file
69
+ * @param {Object} [options.mcpConfig] - MCP configuration object (overrides mcpConfigPath)
70
+ * @param {Array} [options.mcpServers] - Deprecated, use mcpConfig instead
61
71
  */
62
72
  constructor(options = {}) {
63
73
  // Basic configuration
@@ -92,14 +102,29 @@ export class ProbeAgent {
92
102
  // Initialize tools
93
103
  this.initializeTools();
94
104
 
95
- // Initialize the AI model
96
- this.initializeModel();
97
-
98
105
  // Initialize chat history
99
106
  this.history = [];
100
-
107
+
101
108
  // Initialize event emitter for tool execution updates
102
109
  this.events = new EventEmitter();
110
+
111
+ // MCP configuration
112
+ this.enableMcp = !!options.enableMcp || process.env.ENABLE_MCP === '1';
113
+ this.mcpConfigPath = options.mcpConfigPath || null;
114
+ this.mcpConfig = options.mcpConfig || null;
115
+ this.mcpServers = options.mcpServers || null; // Deprecated, keep for backward compatibility
116
+ this.mcpBridge = null;
117
+
118
+ // Initialize the AI model
119
+ this.initializeModel();
120
+
121
+ // Initialize MCP if enabled
122
+ if (this.enableMcp) {
123
+ this.initializeMCP().catch(error => {
124
+ console.error('[MCP] Failed to initialize MCP:', error);
125
+ this.mcpBridge = null;
126
+ });
127
+ }
103
128
  }
104
129
 
105
130
  /**
@@ -138,6 +163,12 @@ export class ProbeAgent {
138
163
  * Initialize the AI model based on available API keys and forced provider setting
139
164
  */
140
165
  initializeModel() {
166
+ // Check if we're in test mode and should use mock provider
167
+ if (process.env.NODE_ENV === 'test' || process.env.USE_MOCK_AI === 'true') {
168
+ this.initializeMockModel();
169
+ return;
170
+ }
171
+
141
172
  // Get API keys from environment variables
142
173
  const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
143
174
  const openaiApiKey = process.env.OPENAI_API_KEY;
@@ -231,12 +262,83 @@ export class ProbeAgent {
231
262
  });
232
263
  this.model = modelName || 'gemini-2.5-pro';
233
264
  this.apiType = 'google';
234
-
265
+
235
266
  if (this.debug) {
236
267
  console.log(`Using Google API with model: ${this.model}${apiUrl ? ` (URL: ${apiUrl})` : ''}`);
237
268
  }
238
269
  }
239
270
 
271
+ /**
272
+ * Initialize mock model for testing
273
+ */
274
+ initializeMockModel(modelName) {
275
+ this.provider = createMockProvider();
276
+ this.model = modelName || 'mock-model';
277
+ this.apiType = 'mock';
278
+
279
+ if (this.debug) {
280
+ console.log(`Using Mock API with model: ${this.model}`);
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Initialize MCP bridge and load tools
286
+ */
287
+ async initializeMCP() {
288
+ if (!this.enableMcp) return;
289
+
290
+ try {
291
+ let mcpConfig = null;
292
+
293
+ // Priority order: mcpConfig > mcpConfigPath > mcpServers (deprecated) > auto-discovery
294
+ if (this.mcpConfig) {
295
+ // Direct config object provided (SDK usage)
296
+ mcpConfig = this.mcpConfig;
297
+ if (this.debug) {
298
+ console.log('[DEBUG] Using provided MCP config object');
299
+ }
300
+ } else if (this.mcpConfigPath) {
301
+ // Explicit config path provided
302
+ try {
303
+ mcpConfig = loadMCPConfigurationFromPath(this.mcpConfigPath);
304
+ if (this.debug) {
305
+ console.log(`[DEBUG] Loaded MCP config from: ${this.mcpConfigPath}`);
306
+ }
307
+ } catch (error) {
308
+ throw new Error(`Failed to load MCP config from ${this.mcpConfigPath}: ${error.message}`);
309
+ }
310
+ } else if (this.mcpServers) {
311
+ // Backward compatibility: convert old mcpServers format
312
+ mcpConfig = { mcpServers: this.mcpServers };
313
+ if (this.debug) {
314
+ console.warn('[DEBUG] Using deprecated mcpServers option. Consider using mcpConfig instead.');
315
+ }
316
+ }
317
+ // Note: auto-discovery fallback is removed - user must explicitly provide config
318
+
319
+ // Initialize the MCP XML bridge
320
+ this.mcpBridge = new MCPXmlBridge({ debug: this.debug });
321
+ await this.mcpBridge.initialize(mcpConfig);
322
+
323
+ const mcpToolCount = this.mcpBridge.getToolNames().length;
324
+ if (mcpToolCount > 0) {
325
+ if (this.debug) {
326
+ console.log(`[DEBUG] Loaded ${mcpToolCount} MCP tools`);
327
+ }
328
+ } else {
329
+ // For backward compatibility: if no tools were loaded, set bridge to null
330
+ // This maintains the behavior expected by existing tests
331
+ if (this.debug) {
332
+ console.log('[DEBUG] No MCP tools loaded, setting bridge to null');
333
+ }
334
+ this.mcpBridge = null;
335
+ }
336
+ } catch (error) {
337
+ console.error('[MCP] Error initializing MCP:', error);
338
+ this.mcpBridge = null;
339
+ }
340
+ }
341
+
240
342
  /**
241
343
  * Get the system message with instructions for the AI (XML Tool Format)
242
344
  */
@@ -428,6 +530,13 @@ When troubleshooting:
428
530
  // Add Tool Definitions
429
531
  systemMessage += `\n# Tools Available\n${toolDefinitions}\n`;
430
532
 
533
+ // Add MCP tools if available
534
+ if (this.mcpBridge && this.mcpBridge.getToolNames().length > 0) {
535
+ systemMessage += `\n## MCP Tools (JSON parameters in <params> tag)\n`;
536
+ systemMessage += this.mcpBridge.getXmlToolDefinitions();
537
+ systemMessage += `\n\nFor MCP tools, use JSON format within the params tag, e.g.:\n<mcp_tool>\n<params>\n{"key": "value"}\n</params>\n</mcp_tool>\n`;
538
+ }
539
+
431
540
  // Add folder information
432
541
  const searchDirectory = this.allowedFolders.length > 0 ? this.allowedFolders[0] : process.cwd();
433
542
  if (this.debug) {
@@ -640,7 +749,11 @@ When troubleshooting:
640
749
  validTools.push('implement');
641
750
  }
642
751
 
643
- const parsedTool = parseXmlToolCallWithThinking(assistantResponseContent, validTools);
752
+ // Try parsing with hybrid parser that supports both native and MCP tools
753
+ const nativeTools = validTools;
754
+ const parsedTool = this.mcpBridge
755
+ ? parseHybridXmlToolCall(assistantResponseContent, nativeTools, this.mcpBridge)
756
+ : parseXmlToolCallWithThinking(assistantResponseContent, validTools);
644
757
  if (parsedTool) {
645
758
  const { toolName, params } = parsedTool;
646
759
  if (this.debug) console.log(`[DEBUG] Parsed tool call: ${toolName} with params:`, params);
@@ -654,7 +767,9 @@ When troubleshooting:
654
767
  const lastAssistantMessage = [...currentMessages].reverse().find(msg =>
655
768
  msg.role === 'assistant' &&
656
769
  msg.content &&
657
- !parseXmlToolCallWithThinking(msg.content, validTools)
770
+ !(this.mcpBridge
771
+ ? parseHybridXmlToolCall(msg.content, validTools, this.mcpBridge)
772
+ : parseXmlToolCallWithThinking(msg.content, validTools))
658
773
  );
659
774
 
660
775
  if (lastAssistantMessage) {
@@ -677,8 +792,32 @@ When troubleshooting:
677
792
  }
678
793
  break;
679
794
  } else {
680
- // Execute the tool
681
- if (this.toolImplementations[toolName]) {
795
+ // Check tool type and execute accordingly
796
+ const { type } = parsedTool;
797
+
798
+ if (type === 'mcp' && this.mcpBridge && this.mcpBridge.isMcpTool(toolName)) {
799
+ // Execute MCP tool
800
+ try {
801
+ if (this.debug) console.log(`[DEBUG] Executing MCP tool '${toolName}' with params:`, params);
802
+
803
+ // Execute MCP tool through the bridge
804
+ const executionResult = await this.mcpBridge.mcpTools[toolName].execute(params);
805
+
806
+ const toolResultContent = typeof executionResult === 'string' ? executionResult : JSON.stringify(executionResult, null, 2);
807
+ const preview = createMessagePreview(toolResultContent);
808
+ if (this.debug) {
809
+ console.log(`[DEBUG] MCP tool '${toolName}' executed successfully. Result preview: ${preview}`);
810
+ }
811
+
812
+ currentMessages.push({ role: 'user', content: `<tool_result>\n${toolResultContent}\n</tool_result>` });
813
+ } catch (error) {
814
+ console.error(`Error executing MCP tool ${toolName}:`, error);
815
+ const toolResultContent = `Error executing MCP tool ${toolName}: ${error.message}`;
816
+ if (this.debug) console.log(`[DEBUG] MCP tool '${toolName}' execution FAILED.`);
817
+ currentMessages.push({ role: 'user', content: `<tool_result>\n${toolResultContent}\n</tool_result>` });
818
+ }
819
+ } else if (this.toolImplementations[toolName]) {
820
+ // Execute native tool
682
821
  try {
683
822
  // Add sessionId to params for tool execution
684
823
  const toolParams = { ...params, sessionId: this.sessionId };
@@ -778,9 +917,15 @@ When troubleshooting:
778
917
  } else {
779
918
  console.error(`[ERROR] Unknown tool: ${toolName}`);
780
919
  currentMessages.push({ role: 'assistant', content: assistantResponseContent });
920
+
921
+ // Build list of available tools including MCP tools
922
+ const nativeTools = Object.keys(this.toolImplementations);
923
+ const mcpTools = this.mcpBridge ? this.mcpBridge.getToolNames() : [];
924
+ const allAvailableTools = [...nativeTools, ...mcpTools];
925
+
781
926
  currentMessages.push({
782
927
  role: 'user',
783
- content: `<tool_result>\nError: Unknown tool '${toolName}'. Available tools: ${Object.keys(this.toolImplementations).join(', ')}\n</tool_result>`
928
+ content: `<tool_result>\nError: Unknown tool '${toolName}'. Available tools: ${allAvailableTools.join(', ')}\n</tool_result>`
784
929
  });
785
930
  }
786
931
  }
@@ -1309,6 +1454,26 @@ Convert your previous response content into actual JSON data that follows this s
1309
1454
  }
1310
1455
  }
1311
1456
 
1457
+ /**
1458
+ * Clean up resources (including MCP connections)
1459
+ */
1460
+ async cleanup() {
1461
+ // Clean up MCP bridge
1462
+ if (this.mcpBridge) {
1463
+ try {
1464
+ await this.mcpBridge.cleanup();
1465
+ if (this.debug) {
1466
+ console.log('[DEBUG] MCP bridge cleaned up');
1467
+ }
1468
+ } catch (error) {
1469
+ console.error('Error cleaning up MCP bridge:', error);
1470
+ }
1471
+ }
1472
+
1473
+ // Clear history and other resources
1474
+ this.clearHistory();
1475
+ }
1476
+
1312
1477
  /**
1313
1478
  * Cancel the current request
1314
1479
  */
@@ -269,7 +269,7 @@ export class ACPToolManager {
269
269
  name: 'search',
270
270
  description: 'Search for code patterns and content using flexible text search with stemming and stopword removal. Supports regex patterns and elastic search query syntax.',
271
271
  kind: ToolCallKind.search,
272
- parameters: {
272
+ inputSchema: {
273
273
  type: 'object',
274
274
  properties: {
275
275
  query: {
@@ -296,7 +296,7 @@ export class ACPToolManager {
296
296
  name: 'query',
297
297
  description: 'Perform structural queries using AST patterns to find specific code structures like functions, classes, or methods.',
298
298
  kind: ToolCallKind.query,
299
- parameters: {
299
+ inputSchema: {
300
300
  type: 'object',
301
301
  properties: {
302
302
  pattern: {
@@ -323,7 +323,7 @@ export class ACPToolManager {
323
323
  name: 'extract',
324
324
  description: 'Extract specific code blocks from files based on file paths and optional line numbers.',
325
325
  kind: ToolCallKind.extract,
326
- parameters: {
326
+ inputSchema: {
327
327
  type: 'object',
328
328
  properties: {
329
329
  files: {
@@ -352,7 +352,7 @@ export class ACPToolManager {
352
352
  name: 'delegate',
353
353
  description: 'Automatically delegate big distinct tasks to specialized probe subagents within the agentic loop. Use when complex requests can be broken into focused, parallel tasks.',
354
354
  kind: ToolCallKind.execute,
355
- parameters: {
355
+ inputSchema: {
356
356
  type: 'object',
357
357
  properties: {
358
358
  task: {
@@ -288,26 +288,26 @@ describe('ACPToolManager', () => {
288
288
  const searchTool = definitions.find(d => d.name === 'search');
289
289
  expect(searchTool).toBeDefined();
290
290
  expect(searchTool.kind).toBe(ToolCallKind.search);
291
- expect(searchTool.parameters.properties.query).toBeDefined();
292
- expect(searchTool.parameters.required).toContain('query');
293
-
291
+ expect(searchTool.inputSchema.properties.query).toBeDefined();
292
+ expect(searchTool.inputSchema.required).toContain('query');
293
+
294
294
  const queryTool = definitions.find(d => d.name === 'query');
295
295
  expect(queryTool).toBeDefined();
296
296
  expect(queryTool.kind).toBe(ToolCallKind.query);
297
- expect(queryTool.parameters.properties.pattern).toBeDefined();
298
- expect(queryTool.parameters.required).toContain('pattern');
299
-
297
+ expect(queryTool.inputSchema.properties.pattern).toBeDefined();
298
+ expect(queryTool.inputSchema.required).toContain('pattern');
299
+
300
300
  const extractTool = definitions.find(d => d.name === 'extract');
301
301
  expect(extractTool).toBeDefined();
302
302
  expect(extractTool.kind).toBe(ToolCallKind.extract);
303
- expect(extractTool.parameters.properties.files).toBeDefined();
304
- expect(extractTool.parameters.required).toContain('files');
305
-
303
+ expect(extractTool.inputSchema.properties.files).toBeDefined();
304
+ expect(extractTool.inputSchema.required).toContain('files');
305
+
306
306
  const delegateTool = definitions.find(d => d.name === 'delegate');
307
307
  expect(delegateTool).toBeDefined();
308
308
  expect(delegateTool.kind).toBe(ToolCallKind.execute);
309
- expect(delegateTool.parameters.properties.task).toBeDefined();
310
- expect(delegateTool.parameters.required).toContain('task');
309
+ expect(delegateTool.inputSchema.properties.task).toBeDefined();
310
+ expect(delegateTool.inputSchema.required).toContain('task');
311
311
  });
312
312
  });
313
313