149 lines
5.0 KiB
TypeScript
149 lines
5.0 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {
|
|
BaseTool,
|
|
ToolResult,
|
|
ToolCallConfirmationDetails,
|
|
ToolConfirmationOutcome,
|
|
ToolMcpConfirmationDetails,
|
|
} from './tools.js';
|
|
import { CallableTool, Part, FunctionCall, Schema } 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 name: string,
|
|
readonly description: string,
|
|
readonly parameterSchema: Schema,
|
|
readonly serverToolName: string,
|
|
readonly timeout?: number,
|
|
readonly trust?: boolean,
|
|
) {
|
|
super(
|
|
name,
|
|
`${serverToolName} (${serverName} MCP Server)`,
|
|
description,
|
|
parameterSchema,
|
|
true, // isOutputMarkdown
|
|
false, // canUpdateOutput
|
|
);
|
|
}
|
|
|
|
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 allow listed
|
|
}
|
|
|
|
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```';
|
|
}
|