Files
qwen-code/packages/core/src/tools/mcp-tool.ts
Didier Durand 0f6405e28d fix typos in diverse files (#3550)
Co-authored-by: Pascal Birchler <pascal.birchler@gmail.com>
Co-authored-by: Pascal Birchler <pascalb@google.com>
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
2025-07-20 22:36:34 +00:00

196 lines
6.3 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
BaseTool,
ToolResult,
ToolCallConfirmationDetails,
ToolConfirmationOutcome,
ToolMcpConfirmationDetails,
Icon,
} from './tools.js';
import {
CallableTool,
Part,
FunctionCall,
FunctionDeclaration,
Type,
} from '@google/genai';
type ToolParams = Record<string, unknown>;
export class DiscoveredMCPTool extends BaseTool<ToolParams, ToolResult> {
private static readonly allowlist: Set<string> = new Set();
constructor(
private readonly mcpTool: CallableTool,
readonly serverName: string,
readonly serverToolName: string,
description: string,
readonly parameterSchemaJson: unknown,
readonly timeout?: number,
readonly trust?: boolean,
nameOverride?: string,
) {
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, // canUpdateOutput
);
}
asFullyQualifiedTool(): DiscoveredMCPTool {
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
*/
override get schema(): FunctionDeclaration {
return {
name: this.name,
description: this.description,
parametersJsonSchema: this.parameterSchemaJson,
};
}
async shouldConfirmExecute(
_params: ToolParams,
_abortSignal: AbortSignal,
): Promise<ToolCallConfirmationDetails | false> {
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: ToolMcpConfirmationDetails = {
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: ToolConfirmationOutcome) => {
if (outcome === ToolConfirmationOutcome.ProceedAlwaysServer) {
DiscoveredMCPTool.allowlist.add(serverAllowListKey);
} else if (outcome === ToolConfirmationOutcome.ProceedAlwaysTool) {
DiscoveredMCPTool.allowlist.add(toolAllowListKey);
}
},
};
return confirmationDetails;
}
async execute(params: ToolParams): Promise<ToolResult> {
const functionCalls: FunctionCall[] = [
{
name: this.serverToolName,
args: params,
},
];
const responseParts: Part[] = 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: Part[]) {
if (!result || result.length === 0) {
return '```json\n[]\n```';
}
const processFunctionResponse = (part: 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: Part) => p.text !== undefined,
);
if (allTextParts) {
return responseContent.map((p: Part) => 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: string) {
// 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;
}