1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
BaseTool,
ToolResult,
ToolCallConfirmationDetails,
ToolConfirmationOutcome,
ToolMcpConfirmationDetails,
} from './tools.js';
import { CallableTool, Part, FunctionCall } 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: Record<string, unknown>,
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```';
}
|