@juspay/neurolink 7.14.7 → 7.15.0

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.
@@ -7,6 +7,18 @@ import { z } from "zod";
7
7
  import * as fs from "fs";
8
8
  import * as path from "path";
9
9
  import { logger } from "../utils/logger.js";
10
+ import { VertexAI } from "@google-cloud/vertexai";
11
+ // Runtime Google Search tool creation - bypasses TypeScript strict typing
12
+ function createGoogleSearchTools() {
13
+ const searchTool = {};
14
+ // Dynamically assign google_search property at runtime
15
+ Object.defineProperty(searchTool, "google_search", {
16
+ value: {},
17
+ enumerable: true,
18
+ configurable: true,
19
+ });
20
+ return [searchTool];
21
+ }
10
22
  /**
11
23
  * Direct tool definitions that work immediately with Gemini/AI SDK
12
24
  * These bypass MCP complexity and provide reliable agent functionality
@@ -324,6 +336,128 @@ export const directAgentTools = {
324
336
  }
325
337
  },
326
338
  }),
339
+ websearchGrounding: tool({
340
+ description: "Search the web for current information using Google Search grounding. Returns raw search data for AI processing.",
341
+ parameters: z.object({
342
+ query: z.string().describe("Search query to find information about"),
343
+ maxResults: z
344
+ .number()
345
+ .optional()
346
+ .default(3)
347
+ .describe("Maximum number of search results to return (1-5)"),
348
+ maxWords: z
349
+ .number()
350
+ .optional()
351
+ .default(50)
352
+ .describe("Maximum number of words in the response 50"),
353
+ }),
354
+ execute: async ({ query, maxResults = 3, maxWords = 50 }) => {
355
+ try {
356
+ const hasCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;
357
+ const hasProjectId = process.env.GOOGLE_VERTEX_PROJECT;
358
+ const projectLocation = process.env.GOOGLE_VERTEX_LOCATION || "us-central1";
359
+ if (!hasCredentials || !hasProjectId) {
360
+ return {
361
+ success: false,
362
+ error: "Google Vertex AI credentials not configured. Please set GOOGLE_APPLICATION_CREDENTIALS and GOOGLE_VERTEX_PROJECT environment variables.",
363
+ requiredEnvVars: [
364
+ "GOOGLE_APPLICATION_CREDENTIALS",
365
+ "GOOGLE_VERTEX_PROJECT",
366
+ ],
367
+ };
368
+ }
369
+ const limitedResults = Math.min(Math.max(maxResults, 1), 5);
370
+ const vertex_ai = new VertexAI({
371
+ project: hasProjectId,
372
+ location: projectLocation,
373
+ });
374
+ const websearchModel = "gemini-2.5-flash-lite";
375
+ const model = vertex_ai.getGenerativeModel({
376
+ model: websearchModel,
377
+ tools: createGoogleSearchTools(),
378
+ });
379
+ // Search query with word limit constraint
380
+ const searchPrompt = `Search for: "${query}". Provide a concise summary in no more than ${maxWords} words.`;
381
+ const startTime = Date.now();
382
+ const response = await model.generateContent({
383
+ contents: [
384
+ {
385
+ role: "user",
386
+ parts: [{ text: searchPrompt }],
387
+ },
388
+ ],
389
+ });
390
+ const responseTime = Date.now() - startTime;
391
+ // Extract grounding metadata and search results
392
+ const result = response.response;
393
+ const candidates = result.candidates;
394
+ if (!candidates || candidates.length === 0) {
395
+ return {
396
+ success: false,
397
+ error: "No search results returned",
398
+ query,
399
+ };
400
+ }
401
+ const content = candidates[0].content;
402
+ if (!content || !content.parts || content.parts.length === 0) {
403
+ return {
404
+ success: false,
405
+ error: "No search content found",
406
+ query,
407
+ };
408
+ }
409
+ // Extract raw search content
410
+ const searchContent = content.parts[0].text || "";
411
+ // Extract grounding sources if available
412
+ const groundingMetadata = candidates[0]?.groundingMetadata;
413
+ const searchResults = [];
414
+ if (groundingMetadata?.groundingChunks) {
415
+ for (const chunk of groundingMetadata.groundingChunks.slice(0, limitedResults)) {
416
+ if (chunk.web) {
417
+ searchResults.push({
418
+ title: chunk.web.title || "No title",
419
+ url: chunk.web.uri || "",
420
+ snippet: searchContent, // Use full content since maxWords already limits length
421
+ domain: chunk.web.uri
422
+ ? new URL(chunk.web.uri).hostname
423
+ : "unknown",
424
+ });
425
+ }
426
+ }
427
+ }
428
+ // If no grounding metadata, create basic result structure
429
+ if (searchResults.length === 0) {
430
+ searchResults.push({
431
+ title: `Search results for: ${query}`,
432
+ url: "",
433
+ snippet: searchContent,
434
+ domain: "google-search",
435
+ });
436
+ }
437
+ return {
438
+ success: true,
439
+ query,
440
+ searchResults,
441
+ rawContent: searchContent,
442
+ totalResults: searchResults.length,
443
+ provider: "google-search-grounding",
444
+ model: websearchModel,
445
+ responseTime,
446
+ timestamp: startTime,
447
+ grounded: true,
448
+ };
449
+ }
450
+ catch (error) {
451
+ logger.error("Web search grounding error:", error);
452
+ return {
453
+ success: false,
454
+ error: error instanceof Error ? error.message : String(error),
455
+ query,
456
+ provider: "google-search-grounding",
457
+ };
458
+ }
459
+ },
460
+ }),
327
461
  };
328
462
  // eslint-disable-next-line no-redeclare
329
463
  export function getToolsForCategory(category = "all") {
@@ -395,12 +395,41 @@ export class BaseProvider {
395
395
  parameters: z.object({}), // Use empty schema for custom tools
396
396
  execute: async (params) => {
397
397
  logger.debug(`[BaseProvider] Executing custom tool: ${toolName}`, { params });
398
- // Use the tool executor if available (from setupToolExecutor)
399
- if (this.toolExecutor) {
400
- return await this.toolExecutor(toolName, params);
398
+ try {
399
+ // Use the tool executor if available (from setupToolExecutor)
400
+ let result;
401
+ if (this.toolExecutor) {
402
+ result = await this.toolExecutor(toolName, params);
403
+ }
404
+ else {
405
+ result = await typedToolDef.execute(params);
406
+ }
407
+ // Log successful execution
408
+ logger.debug(`[BaseProvider] Tool execution successful: ${toolName}`, {
409
+ resultType: typeof result,
410
+ hasResult: result !== null && result !== undefined,
411
+ toolName,
412
+ });
413
+ return result;
401
414
  }
402
- else {
403
- return await typedToolDef.execute(params);
415
+ catch (error) {
416
+ logger.warn(`[BaseProvider] Tool execution failed: ${toolName}`, {
417
+ error: error instanceof Error ? error.message : String(error),
418
+ params,
419
+ toolName,
420
+ });
421
+ // GENERIC ERROR HANDLING FOR ALL MCP TOOLS:
422
+ // Return a generic error object that works with any MCP server
423
+ // The AI can interpret this and try different approaches
424
+ return {
425
+ _neurolinkToolError: true,
426
+ toolName: toolName,
427
+ error: error instanceof Error ? error.message : String(error),
428
+ timestamp: new Date().toISOString(),
429
+ params: params,
430
+ // Keep it simple - just indicate an error occurred
431
+ message: `Error calling ${toolName}: ${error instanceof Error ? error.message : String(error)}`
432
+ };
404
433
  }
405
434
  },
406
435
  });
@@ -120,6 +120,8 @@ function getToolCategory(toolName) {
120
120
  case "listDirectory":
121
121
  case "searchFiles":
122
122
  return "filesystem";
123
+ case "websearchGrounding":
124
+ return "search";
123
125
  default:
124
126
  return "utility";
125
127
  }
@@ -6,7 +6,7 @@
6
6
  import { EventEmitter } from "events";
7
7
  import { mcpLogger } from "../utils/logger.js";
8
8
  import { globalCircuitBreakerManager } from "./mcpCircuitBreaker.js";
9
- import { isObject, isString, isBoolean, isNullish, } from "../utils/typeUtils.js";
9
+ import { isObject, isNullish, } from "../utils/typeUtils.js";
10
10
  import { validateToolName, validateToolDescription, } from "../utils/parameterValidation.js";
11
11
  /**
12
12
  * ToolDiscoveryService
@@ -342,7 +342,7 @@ export class ToolDiscoveryService extends EventEmitter {
342
342
  // Update tool statistics
343
343
  this.updateToolStats(toolKey, true, duration);
344
344
  // Validate output if requested
345
- if (options.validateOutput !== false && result) {
345
+ if (options.validateOutput !== false) {
346
346
  this.validateToolOutput(result);
347
347
  }
348
348
  mcpLogger.debug(`[ToolDiscoveryService] Tool execution completed: ${toolName}`, {
@@ -445,46 +445,30 @@ export class ToolDiscoveryService extends EventEmitter {
445
445
  * Validate tool output with enhanced type safety
446
446
  */
447
447
  validateToolOutput(result) {
448
- // Check for null/undefined results
448
+ // GENERIC ERROR HANDLING FOR ALL MCP TOOLS
449
+ // Different MCP servers return different error formats, so we should be permissive
450
+ // and let the AI handle any response format instead of throwing errors
451
+ // Only throw for truly invalid responses (null/undefined)
449
452
  if (isNullish(result)) {
450
- throw new Error("Tool returned null or undefined result");
451
- }
452
- // Enhanced error detection for object results
453
- if (isObject(result)) {
454
- // Check for explicit error property
455
- if (result.error !== undefined) {
456
- const errorMessage = isString(result.error)
457
- ? result.error
458
- : "Tool execution failed with error";
459
- throw new Error(`Tool execution error: ${errorMessage}`);
460
- }
461
- // Check for boolean error flag
462
- if (isBoolean(result.isError) && result.isError === true) {
463
- const errorDetail = isString(result.message)
464
- ? `: ${result.message}`
465
- : "";
466
- throw new Error(`Tool execution failed${errorDetail}`);
467
- }
468
- // Check for common error status patterns
469
- if (isString(result.status) &&
470
- (result.status === "error" || result.status === "failed")) {
471
- const errorDetail = isString(result.message) || isString(result.reason)
472
- ? `: ${result.message || result.reason}`
473
- : "";
474
- throw new Error(`Tool execution failed${errorDetail}`);
475
- }
476
- // Check for success: false pattern
477
- if (isBoolean(result.success) && result.success === false) {
478
- const errorDetail = isString(result.message) || isString(result.error)
479
- ? `: ${result.message || result.error}`
480
- : "";
481
- throw new Error(`Tool execution unsuccessful${errorDetail}`);
482
- }
483
- }
484
- // Validate that string results are not empty
485
- if (isString(result) && result.trim() === "") {
486
- throw new Error("Tool returned empty string result");
453
+ mcpLogger.debug("[ToolDiscoveryService] Tool returned null/undefined, treating as empty response");
454
+ // Even null responses can be valid for some tools - don't throw
455
+ return;
487
456
  }
457
+ // Log what we received for debugging, but don't validate specific formats
458
+ mcpLogger.debug("[ToolDiscoveryService] Tool response received", {
459
+ type: typeof result,
460
+ isArray: Array.isArray(result),
461
+ isObject: isObject(result),
462
+ hasKeys: isObject(result) ? Object.keys(result).length : 0,
463
+ fullResponse: result // Log the complete response, not a truncated sample
464
+ });
465
+ // COMPLETELY PERMISSIVE APPROACH:
466
+ // - Any response format is valid (objects, strings, arrays, booleans, numbers)
467
+ // - Even error responses are passed to the AI to handle
468
+ // - The AI can interpret error messages and retry with different approaches
469
+ // - This works with any MCP server regardless of their response format
470
+ // No validation or throwing - let the AI handle everything
471
+ return;
488
472
  }
489
473
  /**
490
474
  * Update tool statistics
@@ -32,6 +32,8 @@ import { EventEmitter } from "events";
32
32
  import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
33
33
  import { applyConversationMemoryDefaults, getConversationMessages, storeConversationTurn, } from "./utils/conversationMemoryUtils.js";
34
34
  import { ExternalServerManager } from "./mcp/externalServerManager.js";
35
+ // Import direct tools server for automatic registration
36
+ import { directToolsServer } from "./mcp/servers/agent/directToolsServer.js";
35
37
  import { ContextManager } from "./context/ContextManager.js";
36
38
  import { defaultContextConfig } from "./context/config.js";
37
39
  import { isNonNullObject } from "./utils/typeUtils.js";
@@ -130,6 +132,19 @@ export class NeuroLink {
130
132
  ]);
131
133
  // Register all providers with lazy loading support
132
134
  await ProviderRegistry.registerAllProviders();
135
+ // Register the direct tools server to make websearch and other tools available
136
+ try {
137
+ // Use the server ID string for registration instead of the server object
138
+ await toolRegistry.registerServer("neurolink-direct", directToolsServer);
139
+ mcpLogger.debug("[NeuroLink] Direct tools server registered successfully", {
140
+ serverId: "neurolink-direct",
141
+ });
142
+ }
143
+ catch (error) {
144
+ mcpLogger.warn("[NeuroLink] Failed to register direct tools server", {
145
+ error: error instanceof Error ? error.message : String(error),
146
+ });
147
+ }
133
148
  // Load MCP configuration from .mcp-config.json using ExternalServerManager
134
149
  try {
135
150
  const configResult = await this.externalServerManager.loadMCPConfiguration();
@@ -356,27 +371,55 @@ export class NeuroLink {
356
371
  }
357
372
  // Try MCP-enhanced generation first (if not explicitly disabled)
358
373
  if (!options.disableTools) {
359
- try {
360
- logger.debug(`[${functionTag}] Attempting MCP generation...`);
361
- const mcpResult = await this.tryMCPGeneration(options);
362
- if (mcpResult && mcpResult.content) {
363
- logger.debug(`[${functionTag}] MCP generation successful`);
364
- // Store conversation turn
365
- await storeConversationTurn(this.conversationMemory, options, mcpResult);
366
- return mcpResult;
374
+ let mcpAttempts = 0;
375
+ const maxMcpRetries = 2; // Allow retries for tool-related failures
376
+ while (mcpAttempts <= maxMcpRetries) {
377
+ try {
378
+ logger.debug(`[${functionTag}] Attempting MCP generation (attempt ${mcpAttempts + 1}/${maxMcpRetries + 1})...`);
379
+ const mcpResult = await this.tryMCPGeneration(options);
380
+ if (mcpResult && mcpResult.content) {
381
+ logger.debug(`[${functionTag}] MCP generation successful on attempt ${mcpAttempts + 1}`, {
382
+ contentLength: mcpResult.content.length,
383
+ toolsUsed: mcpResult.toolsUsed?.length || 0,
384
+ toolExecutions: mcpResult.toolExecutions?.length || 0,
385
+ });
386
+ // Store conversation turn
387
+ await storeConversationTurn(this.conversationMemory, options, mcpResult);
388
+ return mcpResult;
389
+ }
390
+ else {
391
+ logger.debug(`[${functionTag}] MCP generation returned empty result on attempt ${mcpAttempts + 1}:`, {
392
+ hasResult: !!mcpResult,
393
+ hasContent: !!(mcpResult && mcpResult.content),
394
+ contentLength: mcpResult?.content?.length || 0,
395
+ toolExecutions: mcpResult?.toolExecutions?.length || 0,
396
+ });
397
+ // If we got a result but no content, and we have tool executions, this might be a tool success case
398
+ if (mcpResult &&
399
+ mcpResult.toolExecutions &&
400
+ mcpResult.toolExecutions.length > 0) {
401
+ logger.debug(`[${functionTag}] Found tool executions but no content, continuing with result`);
402
+ // Store conversation turn even with empty content if tools executed
403
+ await storeConversationTurn(this.conversationMemory, options, mcpResult);
404
+ return mcpResult;
405
+ }
406
+ }
367
407
  }
368
- else {
369
- logger.debug(`[${functionTag}] MCP generation returned empty result:`, {
370
- hasResult: !!mcpResult,
371
- hasContent: !!(mcpResult && mcpResult.content),
372
- contentLength: mcpResult?.content?.length || 0,
408
+ catch (error) {
409
+ mcpAttempts++;
410
+ logger.debug(`[${functionTag}] MCP generation failed on attempt ${mcpAttempts}/${maxMcpRetries + 1}`, {
411
+ error: error instanceof Error ? error.message : String(error),
412
+ willRetry: mcpAttempts <= maxMcpRetries,
373
413
  });
414
+ // If this was the last attempt, break and fall back
415
+ if (mcpAttempts > maxMcpRetries) {
416
+ logger.debug(`[${functionTag}] All MCP attempts exhausted, falling back to direct generation`);
417
+ break;
418
+ }
419
+ // Small delay before retry to allow transient issues to resolve
420
+ await new Promise((resolve) => setTimeout(resolve, 500));
374
421
  }
375
- }
376
- catch (error) {
377
- logger.debug(`[${functionTag}] MCP generation failed, falling back`, {
378
- error: error instanceof Error ? error.message : String(error),
379
- });
422
+ mcpAttempts++;
380
423
  }
381
424
  }
382
425
  // Fall back to direct provider generation
@@ -430,19 +473,40 @@ export class NeuroLink {
430
473
  conversationMessages, // Inject conversation history
431
474
  });
432
475
  const responseTime = Date.now() - startTime;
433
- // Check if result is meaningful
434
- if (!result || !result.content || result.content.trim().length === 0) {
476
+ // Enhanced result validation - consider tool executions as valid results
477
+ const hasContent = result && result.content && result.content.trim().length > 0;
478
+ const hasToolExecutions = result && result.toolExecutions && result.toolExecutions.length > 0;
479
+ // Log detailed result analysis for debugging
480
+ mcpLogger.debug(`[${functionTag}] Result validation:`, {
481
+ hasResult: !!result,
482
+ hasContent,
483
+ hasToolExecutions,
484
+ contentLength: result?.content?.length || 0,
485
+ toolExecutionsCount: result?.toolExecutions?.length || 0,
486
+ toolsUsedCount: result?.toolsUsed?.length || 0,
487
+ });
488
+ // Accept result if it has content OR successful tool executions
489
+ if (!hasContent && !hasToolExecutions) {
490
+ mcpLogger.debug(`[${functionTag}] Result rejected: no content and no tool executions`);
435
491
  return null; // Let caller fall back to direct generation
436
492
  }
437
- // Return enhanced result with external tool information
493
+ // Transform tool executions with enhanced preservation
494
+ const transformedToolExecutions = transformToolExecutionsForMCP(result.toolExecutions);
495
+ // Log transformation results
496
+ mcpLogger.debug(`[${functionTag}] Tool execution transformation:`, {
497
+ originalCount: result?.toolExecutions?.length || 0,
498
+ transformedCount: transformedToolExecutions.length,
499
+ transformedTools: transformedToolExecutions.map((te) => te.toolName),
500
+ });
501
+ // Return enhanced result with preserved tool information
438
502
  return {
439
- content: result.content,
503
+ content: result.content || "", // Ensure content is never undefined
440
504
  provider: providerName,
441
505
  usage: result.usage,
442
506
  responseTime,
443
507
  toolsUsed: result.toolsUsed || [],
444
- toolExecutions: transformToolExecutionsForMCP(result.toolExecutions),
445
- enhancedWithTools: true,
508
+ toolExecutions: transformedToolExecutions,
509
+ enhancedWithTools: Boolean(hasToolExecutions), // Mark as enhanced if tools were actually used
446
510
  availableTools: transformToolsForMCP(availableTools),
447
511
  // Include analytics and evaluation from BaseProvider
448
512
  analytics: result.analytics,
@@ -24,13 +24,56 @@ export function transformToolExecutions(toolExecutions) {
24
24
  if (!toolExecutions || !Array.isArray(toolExecutions)) {
25
25
  return [];
26
26
  }
27
- return toolExecutions.map((te) => {
27
+ return toolExecutions.map((te, index) => {
28
28
  const teRecord = te;
29
+ // Enhanced tool name extraction with multiple fallback strategies
30
+ let toolName = teRecord.name ||
31
+ teRecord.toolName ||
32
+ teRecord.tool ||
33
+ "";
34
+ // If still no name, try to extract from nested objects
35
+ if (!toolName &&
36
+ teRecord.toolCall &&
37
+ typeof teRecord.toolCall === "object") {
38
+ const toolCall = teRecord.toolCall;
39
+ toolName =
40
+ toolCall.name || toolCall.toolName || "";
41
+ }
42
+ // Last resort: use index-based fallback to avoid "Unknown Tool"
43
+ if (!toolName) {
44
+ toolName = `tool_execution_${index}`;
45
+ }
46
+ // Enhanced input extraction
47
+ let input = teRecord.input ||
48
+ teRecord.parameters ||
49
+ teRecord.args ||
50
+ {};
51
+ // Extract input from nested toolCall if available
52
+ if (Object.keys(input).length === 0 &&
53
+ teRecord.toolCall &&
54
+ typeof teRecord.toolCall === "object") {
55
+ const toolCall = teRecord.toolCall;
56
+ input =
57
+ toolCall.input ||
58
+ toolCall.parameters ||
59
+ toolCall.args ||
60
+ {};
61
+ }
62
+ // Enhanced output extraction with success indication
63
+ let output = teRecord.output ||
64
+ teRecord.result ||
65
+ teRecord.response ||
66
+ "success";
67
+ // Enhanced duration extraction
68
+ let duration = teRecord.duration ??
69
+ teRecord.executionTime ??
70
+ teRecord.responseTime ??
71
+ 0;
29
72
  return {
30
- name: teRecord.name || "",
31
- input: teRecord.input || {},
32
- output: teRecord.output || "success",
33
- duration: teRecord.duration || 0,
73
+ name: toolName,
74
+ input: input,
75
+ output: output,
76
+ duration: duration,
34
77
  };
35
78
  });
36
79
  }
@@ -42,13 +85,63 @@ export function transformToolExecutionsForMCP(toolExecutions) {
42
85
  if (!toolExecutions || !Array.isArray(toolExecutions)) {
43
86
  return [];
44
87
  }
45
- return toolExecutions.map((te) => {
88
+ return toolExecutions.map((te, index) => {
46
89
  const teRecord = te;
90
+ // Enhanced tool name extraction matching the main function
91
+ let toolName = teRecord.name ||
92
+ teRecord.toolName ||
93
+ teRecord.tool ||
94
+ "";
95
+ // Try nested toolCall extraction
96
+ if (!toolName &&
97
+ teRecord.toolCall &&
98
+ typeof teRecord.toolCall === "object") {
99
+ const toolCall = teRecord.toolCall;
100
+ toolName =
101
+ toolCall.name || toolCall.toolName || "";
102
+ }
103
+ // Fallback to avoid empty names
104
+ if (!toolName) {
105
+ toolName = `mcp_tool_execution_${index}`;
106
+ }
107
+ // Enhanced execution time extraction
108
+ let executionTime = teRecord.duration ??
109
+ teRecord.executionTime ??
110
+ teRecord.responseTime ??
111
+ 0;
112
+ // Enhanced success detection - check for actual success indicators
113
+ let success = true; // Default to true
114
+ // Check for explicit success/error indicators
115
+ if (teRecord.success !== undefined) {
116
+ success = Boolean(teRecord.success);
117
+ }
118
+ else if (teRecord.error !== undefined) {
119
+ success = false;
120
+ }
121
+ else if (teRecord.status !== undefined) {
122
+ const status = String(teRecord.status).toLowerCase().trim();
123
+ success = !["error", "failed", "failure", "fail"].includes(status);
124
+ }
125
+ // Enhanced server ID extraction
126
+ let serverId = teRecord.serverId ||
127
+ teRecord.server ||
128
+ teRecord.source ||
129
+ undefined;
130
+ // Try to extract from nested structures
131
+ if (!serverId &&
132
+ teRecord.toolCall &&
133
+ typeof teRecord.toolCall === "object") {
134
+ const toolCall = teRecord.toolCall;
135
+ serverId =
136
+ toolCall.serverId ||
137
+ toolCall.server ||
138
+ undefined;
139
+ }
47
140
  return {
48
- toolName: teRecord.name || "",
49
- executionTime: teRecord.duration || 0,
50
- success: true, // Assume success if tool executed (AI providers handle failures differently)
51
- serverId: teRecord.serverId || undefined,
141
+ toolName: toolName,
142
+ executionTime: executionTime,
143
+ success: success,
144
+ serverId: serverId,
52
145
  };
53
146
  });
54
147
  }
@@ -120,6 +120,8 @@ function getToolCategory(toolName) {
120
120
  case "listDirectory":
121
121
  case "searchFiles":
122
122
  return "filesystem";
123
+ case "websearchGrounding":
124
+ return "search";
123
125
  default:
124
126
  return "utility";
125
127
  }