UNPKG

buroventures-harald-code-core

Version:

Harald Code Core - Core functionality for AI-powered coding assistant

143 lines 6.38 kB
/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { BaseTool, ToolConfirmationOutcome, Icon, } from './tools.js'; import { Type, } from '@google/genai'; export class DiscoveredMCPTool extends BaseTool { mcpTool; serverName; serverToolName; parameterSchemaJson; timeout; trust; static allowlist = new Set(); constructor(mcpTool, serverName, serverToolName, description, parameterSchemaJson, timeout, trust, nameOverride) { super(nameOverride ?? generateValidName(serverToolName), `${serverToolName} (${serverName} MCP Server)`, description, Icon.Hammer, { type: Type.OBJECT }, // this is a dummy Schema for MCP, will be not be used to construct the FunctionDeclaration true, // isOutputMarkdown false); this.mcpTool = mcpTool; this.serverName = serverName; this.serverToolName = serverToolName; this.parameterSchemaJson = parameterSchemaJson; this.timeout = timeout; this.trust = trust; } asFullyQualifiedTool() { return new DiscoveredMCPTool(this.mcpTool, this.serverName, this.serverToolName, this.description, this.parameterSchemaJson, this.timeout, this.trust, `${this.serverName}__${this.serverToolName}`); } /** * Overrides the base schema to use parametersJsonSchema when building * FunctionDeclaration */ get schema() { return { name: this.name, description: this.description, parametersJsonSchema: this.parameterSchemaJson, }; } async shouldConfirmExecute(_params, _abortSignal) { const serverAllowListKey = this.serverName; const toolAllowListKey = `${this.serverName}.${this.serverToolName}`; if (this.trust) { return false; // server is trusted, no confirmation needed } if (DiscoveredMCPTool.allowlist.has(serverAllowListKey) || DiscoveredMCPTool.allowlist.has(toolAllowListKey)) { return false; // server and/or tool already allowlisted } const confirmationDetails = { type: 'mcp', title: 'Confirm MCP Tool Execution', serverName: this.serverName, toolName: this.serverToolName, // Display original tool name in confirmation toolDisplayName: this.name, // Display global registry name exposed to model and user onConfirm: async (outcome) => { if (outcome === ToolConfirmationOutcome.ProceedAlwaysServer) { DiscoveredMCPTool.allowlist.add(serverAllowListKey); } else if (outcome === ToolConfirmationOutcome.ProceedAlwaysTool) { DiscoveredMCPTool.allowlist.add(toolAllowListKey); } }, }; return confirmationDetails; } async execute(params) { const functionCalls = [ { name: this.serverToolName, args: params, }, ]; const responseParts = await this.mcpTool.callTool(functionCalls); return { llmContent: responseParts, returnDisplay: getStringifiedResultForDisplay(responseParts), }; } } /** * Processes an array of `Part` objects, primarily from a tool's execution result, * to generate a user-friendly string representation, typically for display in a CLI. * * The `result` array can contain various types of `Part` objects: * 1. `FunctionResponse` parts: * - If the `response.content` of a `FunctionResponse` is an array consisting solely * of `TextPart` objects, their text content is concatenated into a single string. * This is to present simple textual outputs directly. * - If `response.content` is an array but contains other types of `Part` objects (or a mix), * the `content` array itself is preserved. This handles structured data like JSON objects or arrays * returned by a tool. * - If `response.content` is not an array or is missing, the entire `functionResponse` * object is preserved. * 2. Other `Part` types (e.g., `TextPart` directly in the `result` array): * - These are preserved as is. * * All processed parts are then collected into an array, which is JSON.stringify-ed * with indentation and wrapped in a markdown JSON code block. */ function getStringifiedResultForDisplay(result) { if (!result || result.length === 0) { return '```json\n[]\n```'; } const processFunctionResponse = (part) => { if (part.functionResponse) { const responseContent = part.functionResponse.response?.content; if (responseContent && Array.isArray(responseContent)) { // Check if all parts in responseContent are simple TextParts const allTextParts = responseContent.every((p) => p.text !== undefined); if (allTextParts) { return responseContent.map((p) => p.text).join(''); } // If not all simple text parts, return the array of these content parts for JSON stringification return responseContent; } // If no content, or not an array, or not a functionResponse, stringify the whole functionResponse part for inspection return part.functionResponse; } return part; // Fallback for unexpected structure or non-FunctionResponsePart }; const processedResults = result.length === 1 ? processFunctionResponse(result[0]) : result.map(processFunctionResponse); if (typeof processedResults === 'string') { return processedResults; } return '```json\n' + JSON.stringify(processedResults, null, 2) + '\n```'; } /** Visible for testing */ export function generateValidName(name) { // Replace invalid characters (based on 400 error message from Gemini API) with underscores let validToolname = name.replace(/[^a-zA-Z0-9_.-]/g, '_'); // If longer than 63 characters, replace middle with '___' // (Gemini API says max length 64, but actual limit seems to be 63) if (validToolname.length > 63) { validToolname = validToolname.slice(0, 28) + '___' + validToolname.slice(-32); } return validToolname; } //# sourceMappingURL=mcp-tool.js.map