mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-07 11:51:14 -07:00
780 lines
23 KiB
TypeScript
780 lines
23 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type { FunctionDeclaration } from '@google/genai';
|
|
import {
|
|
Kind,
|
|
BaseDeclarativeTool,
|
|
BaseToolInvocation,
|
|
type AnyDeclarativeTool,
|
|
type ToolResult,
|
|
type ToolInvocation,
|
|
} from './tools.js';
|
|
import type { Config } from '../config/config.js';
|
|
import { ApprovalMode } from '../policy/types.js';
|
|
import { spawn } from 'node:child_process';
|
|
import { StringDecoder } from 'node:string_decoder';
|
|
import { DiscoveredMCPTool } from './mcp-tool.js';
|
|
import { parse } from 'shell-quote';
|
|
import { ToolErrorType } from './tool-error.js';
|
|
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
|
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
|
import { debugLogger } from '../utils/debugLogger.js';
|
|
import { coreEvents } from '../utils/events.js';
|
|
import {
|
|
DISCOVERED_TOOL_PREFIX,
|
|
TOOL_LEGACY_ALIASES,
|
|
getToolAliases,
|
|
WRITE_FILE_TOOL_NAME,
|
|
EDIT_TOOL_NAME,
|
|
UPDATE_TOPIC_TOOL_NAME,
|
|
ENTER_PLAN_MODE_TOOL_NAME,
|
|
EXIT_PLAN_MODE_TOOL_NAME,
|
|
} from './tool-names.js';
|
|
|
|
type ToolParams = Record<string, unknown>;
|
|
|
|
class DiscoveredToolInvocation extends BaseToolInvocation<
|
|
ToolParams,
|
|
ToolResult
|
|
> {
|
|
constructor(
|
|
private readonly config: Config,
|
|
private readonly originalToolName: string,
|
|
prefixedToolName: string,
|
|
params: ToolParams,
|
|
messageBus: MessageBus,
|
|
) {
|
|
super(params, messageBus, prefixedToolName);
|
|
}
|
|
|
|
getDescription(): string {
|
|
return safeJsonStringify(this.params);
|
|
}
|
|
|
|
async execute(
|
|
_signal: AbortSignal,
|
|
_updateOutput?: (output: string) => void,
|
|
): Promise<ToolResult> {
|
|
const callCommand = this.config.getToolCallCommand()!;
|
|
const args = [this.originalToolName];
|
|
|
|
let finalCommand = callCommand;
|
|
let finalArgs = args;
|
|
let finalEnv = process.env;
|
|
|
|
const sandboxManager = this.config.sandboxManager;
|
|
if (sandboxManager) {
|
|
const prepared = await sandboxManager.prepareCommand({
|
|
command: callCommand,
|
|
args,
|
|
cwd: process.cwd(),
|
|
env: process.env,
|
|
});
|
|
finalCommand = prepared.program;
|
|
finalArgs = prepared.args;
|
|
finalEnv = prepared.env;
|
|
}
|
|
|
|
const child = spawn(finalCommand, finalArgs, {
|
|
env: finalEnv,
|
|
});
|
|
child.stdin.write(JSON.stringify(this.params));
|
|
child.stdin.end();
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
let error: Error | null = null;
|
|
let code: number | null = null;
|
|
let signal: NodeJS.Signals | null = null;
|
|
|
|
await new Promise<void>((resolve) => {
|
|
const onStdout = (data: Buffer) => {
|
|
stdout += data?.toString();
|
|
};
|
|
|
|
const onStderr = (data: Buffer) => {
|
|
stderr += data?.toString();
|
|
};
|
|
|
|
const onError = (err: Error) => {
|
|
error = err;
|
|
};
|
|
|
|
const onClose = (
|
|
_code: number | null,
|
|
_signal: NodeJS.Signals | null,
|
|
) => {
|
|
code = _code;
|
|
signal = _signal;
|
|
cleanup();
|
|
resolve();
|
|
};
|
|
|
|
const cleanup = () => {
|
|
child.stdout.removeListener('data', onStdout);
|
|
child.stderr.removeListener('data', onStderr);
|
|
child.removeListener('error', onError);
|
|
child.removeListener('close', onClose);
|
|
if (child.connected) {
|
|
child.disconnect();
|
|
}
|
|
};
|
|
|
|
child.stdout.on('data', onStdout);
|
|
child.stderr.on('data', onStderr);
|
|
child.on('error', onError);
|
|
child.on('close', onClose);
|
|
});
|
|
|
|
// if there is any error, non-zero exit code, signal, or stderr, return error details instead of stdout
|
|
if (error || code !== 0 || signal || stderr) {
|
|
const llmContent = [
|
|
`Stdout: ${stdout || '(empty)'}`,
|
|
`Stderr: ${stderr || '(empty)'}`,
|
|
`Error: ${error ?? '(none)'}`,
|
|
`Exit Code: ${code ?? '(none)'}`,
|
|
`Signal: ${signal ?? '(none)'}`,
|
|
].join('\n');
|
|
return {
|
|
llmContent,
|
|
returnDisplay: llmContent,
|
|
error: {
|
|
message: llmContent,
|
|
type: ToolErrorType.DISCOVERED_TOOL_EXECUTION_ERROR,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
llmContent: stdout,
|
|
returnDisplay: stdout,
|
|
};
|
|
}
|
|
}
|
|
|
|
export class DiscoveredTool extends BaseDeclarativeTool<
|
|
ToolParams,
|
|
ToolResult
|
|
> {
|
|
private readonly originalName: string;
|
|
|
|
constructor(
|
|
private readonly config: Config,
|
|
originalName: string,
|
|
prefixedName: string,
|
|
description: string,
|
|
override readonly parameterSchema: Record<string, unknown>,
|
|
messageBus: MessageBus,
|
|
) {
|
|
const discoveryCmd = config.getToolDiscoveryCommand()!;
|
|
const callCommand = config.getToolCallCommand()!;
|
|
const fullDescription =
|
|
description +
|
|
`
|
|
|
|
This tool was discovered from the project by executing the command \`${discoveryCmd}\` on project root.
|
|
When called, this tool will execute the command \`${callCommand} ${originalName}\` on project root.
|
|
Tool discovery and call commands can be configured in project or user settings.
|
|
|
|
When called, the tool call command is executed as a subprocess.
|
|
On success, tool output is returned as a json string.
|
|
Otherwise, the following information is returned:
|
|
|
|
Stdout: Output on stdout stream. Can be \`(empty)\` or partial.
|
|
Stderr: Output on stderr stream. Can be \`(empty)\` or partial.
|
|
Error: Error or \`(none)\` if no error was reported for the subprocess.
|
|
Exit Code: Exit code or \`(none)\` if terminated by signal.
|
|
Signal: Signal number or \`(none)\` if no signal was received.
|
|
`;
|
|
super(
|
|
prefixedName,
|
|
prefixedName,
|
|
fullDescription,
|
|
Kind.Other,
|
|
parameterSchema,
|
|
messageBus,
|
|
false, // isOutputMarkdown
|
|
false, // canUpdateOutput
|
|
);
|
|
this.originalName = originalName;
|
|
}
|
|
|
|
protected createInvocation(
|
|
params: ToolParams,
|
|
messageBus: MessageBus,
|
|
_toolName?: string,
|
|
_displayName?: string,
|
|
): ToolInvocation<ToolParams, ToolResult> {
|
|
return new DiscoveredToolInvocation(
|
|
this.config,
|
|
this.originalName,
|
|
_toolName ?? this.name,
|
|
params,
|
|
messageBus,
|
|
);
|
|
}
|
|
}
|
|
|
|
export class ToolRegistry {
|
|
// The tools keyed by tool name as seen by the LLM.
|
|
// This includes tools which are currently not active, use `getActiveTools`
|
|
// and `isActive` to get only the active tools.
|
|
private allKnownTools: Map<string, AnyDeclarativeTool> = new Map();
|
|
private config: Config;
|
|
readonly messageBus: MessageBus;
|
|
private isMainRegistry: boolean;
|
|
|
|
constructor(
|
|
config: Config,
|
|
messageBus: MessageBus,
|
|
isMainRegistry: boolean = false,
|
|
) {
|
|
this.config = config;
|
|
this.messageBus = messageBus;
|
|
this.isMainRegistry = isMainRegistry;
|
|
}
|
|
|
|
getMessageBus(): MessageBus {
|
|
return this.messageBus;
|
|
}
|
|
|
|
/**
|
|
* Creates a shallow clone of the registry and its current known tools.
|
|
*/
|
|
clone(): ToolRegistry {
|
|
const clone = new ToolRegistry(this.config, this.messageBus);
|
|
clone.allKnownTools = new Map(this.allKnownTools);
|
|
return clone;
|
|
}
|
|
|
|
/**
|
|
* Registers a tool definition.
|
|
*
|
|
* Note that excluded tools are still registered to allow for enabling them
|
|
* later in the session.
|
|
*
|
|
* @param tool - The tool object containing schema and execution logic.
|
|
*/
|
|
registerTool(tool: AnyDeclarativeTool): void {
|
|
if (this.allKnownTools.has(tool.name)) {
|
|
// Decide on behavior: throw error, log warning, or allow overwrite
|
|
debugLogger.warn(
|
|
`Tool with name "${tool.name}" is already registered. Overwriting.`,
|
|
);
|
|
}
|
|
this.allKnownTools.set(tool.name, tool);
|
|
}
|
|
|
|
/**
|
|
* Unregisters a tool definition by name.
|
|
*
|
|
* @param name - The name of the tool to unregister.
|
|
*/
|
|
unregisterTool(name: string): void {
|
|
this.allKnownTools.delete(name);
|
|
}
|
|
|
|
/**
|
|
* Sorts tools as:
|
|
* 1. Built in tools.
|
|
* 2. Discovered tools.
|
|
* 3. MCP tools ordered by server name.
|
|
*
|
|
* This is a stable sort in that tries preserve existing order.
|
|
*/
|
|
sortTools(): void {
|
|
const getPriority = (tool: AnyDeclarativeTool): number => {
|
|
if (tool instanceof DiscoveredMCPTool) return 2;
|
|
if (tool instanceof DiscoveredTool) return 1;
|
|
return 0; // Built-in
|
|
};
|
|
|
|
this.allKnownTools = new Map(
|
|
Array.from(this.allKnownTools.entries()).sort((a, b) => {
|
|
const toolA = a[1];
|
|
const toolB = b[1];
|
|
const priorityA = getPriority(toolA);
|
|
const priorityB = getPriority(toolB);
|
|
|
|
if (priorityA !== priorityB) {
|
|
return priorityA - priorityB;
|
|
}
|
|
|
|
if (priorityA === 2) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
const serverA = (toolA as DiscoveredMCPTool).serverName;
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
const serverB = (toolB as DiscoveredMCPTool).serverName;
|
|
return serverA.localeCompare(serverB);
|
|
}
|
|
|
|
return 0;
|
|
}),
|
|
);
|
|
}
|
|
|
|
private removeDiscoveredTools(): void {
|
|
for (const tool of this.allKnownTools.values()) {
|
|
if (tool instanceof DiscoveredTool || tool instanceof DiscoveredMCPTool) {
|
|
this.allKnownTools.delete(tool.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes all tools from a specific MCP server.
|
|
* @param serverName The name of the server to remove tools from.
|
|
*/
|
|
removeMcpToolsByServer(serverName: string): void {
|
|
for (const [name, tool] of this.allKnownTools.entries()) {
|
|
if (tool instanceof DiscoveredMCPTool && tool.serverName === serverName) {
|
|
this.allKnownTools.delete(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Discovers tools from project (if available and configured).
|
|
* Can be called multiple times to update discovered tools.
|
|
* This will discover tools from the command line and from MCP servers.
|
|
*/
|
|
async discoverAllTools(): Promise<void> {
|
|
// remove any previously discovered tools
|
|
this.removeDiscoveredTools();
|
|
await this.discoverAndRegisterToolsFromCommand();
|
|
}
|
|
|
|
private async discoverAndRegisterToolsFromCommand(): Promise<void> {
|
|
const discoveryCmd = this.config.getToolDiscoveryCommand();
|
|
if (!discoveryCmd) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const cmdParts = parse(discoveryCmd);
|
|
if (cmdParts.length === 0) {
|
|
throw new Error(
|
|
'Tool discovery command is empty or contains only whitespace.',
|
|
);
|
|
}
|
|
|
|
const firstPart = cmdParts[0];
|
|
if (typeof firstPart !== 'string') {
|
|
throw new Error(
|
|
'Tool discovery command must start with a program name.',
|
|
);
|
|
}
|
|
|
|
let finalCommand: string = firstPart;
|
|
let finalArgs: string[] = cmdParts
|
|
.slice(1)
|
|
.filter((p): p is string => typeof p === 'string');
|
|
let finalEnv = process.env;
|
|
|
|
const sandboxManager = this.config.sandboxManager;
|
|
if (sandboxManager) {
|
|
const prepared = await sandboxManager.prepareCommand({
|
|
command: finalCommand,
|
|
args: finalArgs,
|
|
cwd: process.cwd(),
|
|
env: process.env,
|
|
});
|
|
finalCommand = prepared.program;
|
|
finalArgs = prepared.args;
|
|
finalEnv = prepared.env;
|
|
}
|
|
|
|
const proc = spawn(finalCommand, finalArgs, {
|
|
env: finalEnv,
|
|
});
|
|
let stdout = '';
|
|
const stdoutDecoder = new StringDecoder('utf8');
|
|
let stderr = '';
|
|
const stderrDecoder = new StringDecoder('utf8');
|
|
let sizeLimitExceeded = false;
|
|
const MAX_STDOUT_SIZE = 10 * 1024 * 1024; // 10MB limit
|
|
const MAX_STDERR_SIZE = 10 * 1024 * 1024; // 10MB limit
|
|
|
|
let stdoutByteLength = 0;
|
|
let stderrByteLength = 0;
|
|
|
|
proc.stdout.on('data', (data) => {
|
|
if (sizeLimitExceeded) return;
|
|
if (stdoutByteLength + data.length > MAX_STDOUT_SIZE) {
|
|
sizeLimitExceeded = true;
|
|
proc.kill();
|
|
return;
|
|
}
|
|
stdoutByteLength += data.length;
|
|
stdout += stdoutDecoder.write(data);
|
|
});
|
|
|
|
proc.stderr.on('data', (data) => {
|
|
if (sizeLimitExceeded) return;
|
|
if (stderrByteLength + data.length > MAX_STDERR_SIZE) {
|
|
sizeLimitExceeded = true;
|
|
proc.kill();
|
|
return;
|
|
}
|
|
stderrByteLength += data.length;
|
|
stderr += stderrDecoder.write(data);
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
proc.on('error', reject);
|
|
proc.on('close', (code) => {
|
|
stdout += stdoutDecoder.end();
|
|
stderr += stderrDecoder.end();
|
|
|
|
if (sizeLimitExceeded) {
|
|
return reject(
|
|
new Error(
|
|
`Tool discovery command output exceeded size limit of ${MAX_STDOUT_SIZE} bytes.`,
|
|
),
|
|
);
|
|
}
|
|
|
|
if (code !== 0) {
|
|
coreEvents.emitFeedback(
|
|
'error',
|
|
`Tool discovery command failed with code ${code}.`,
|
|
stderr,
|
|
);
|
|
return reject(
|
|
new Error(`Tool discovery command failed with exit code ${code}`),
|
|
);
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// execute discovery command and extract function declarations (w/ or w/o "tool" wrappers)
|
|
const functions: FunctionDeclaration[] = [];
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
const discoveredItems = JSON.parse(stdout.trim());
|
|
|
|
if (!discoveredItems || !Array.isArray(discoveredItems)) {
|
|
throw new Error(
|
|
'Tool discovery command did not return a JSON array of tools.',
|
|
);
|
|
}
|
|
|
|
for (const tool of discoveredItems) {
|
|
if (tool && typeof tool === 'object') {
|
|
if (Array.isArray(tool['function_declarations'])) {
|
|
functions.push(...tool['function_declarations']);
|
|
} else if (Array.isArray(tool['functionDeclarations'])) {
|
|
functions.push(...tool['functionDeclarations']);
|
|
} else if (tool['name']) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
functions.push(tool as FunctionDeclaration);
|
|
}
|
|
}
|
|
}
|
|
// register each function as a tool
|
|
for (const func of functions) {
|
|
if (!func.name) {
|
|
debugLogger.warn('Discovered a tool with no name. Skipping.');
|
|
continue;
|
|
}
|
|
const parameters =
|
|
func.parametersJsonSchema &&
|
|
typeof func.parametersJsonSchema === 'object' &&
|
|
!Array.isArray(func.parametersJsonSchema)
|
|
? func.parametersJsonSchema
|
|
: {};
|
|
this.registerTool(
|
|
new DiscoveredTool(
|
|
this.config,
|
|
func.name,
|
|
DISCOVERED_TOOL_PREFIX + func.name,
|
|
func.description ?? '',
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
parameters as Record<string, unknown>,
|
|
this.messageBus,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
debugLogger.error(`Tool discovery command "${discoveryCmd}" failed:`, e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private buildToolMetadata(): Map<string, Record<string, unknown>> {
|
|
const toolMetadata = new Map<string, Record<string, unknown>>();
|
|
for (const [name, tool] of this.allKnownTools) {
|
|
const metadata: Record<string, unknown> = tool.toolAnnotations
|
|
? { ...tool.toolAnnotations }
|
|
: {};
|
|
// Include server name so the policy engine can resolve composite
|
|
// wildcard patterns (e.g. "*__*") against unqualified tool names.
|
|
if (tool instanceof DiscoveredMCPTool) {
|
|
metadata['_serverName'] = tool.serverName;
|
|
}
|
|
if (Object.keys(metadata).length > 0) {
|
|
toolMetadata.set(name, metadata);
|
|
}
|
|
}
|
|
return toolMetadata;
|
|
}
|
|
|
|
/**
|
|
* @returns All the tools that are not excluded.
|
|
*/
|
|
private getActiveTools(): AnyDeclarativeTool[] {
|
|
const toolMetadata = this.buildToolMetadata();
|
|
const allKnownNames = new Set(this.allKnownTools.keys());
|
|
const excludedTools =
|
|
this.expandExcludeToolsWithAliases(
|
|
this.config.getExcludeTools(toolMetadata, allKnownNames),
|
|
) ?? new Set([]);
|
|
const activeTools: AnyDeclarativeTool[] = [];
|
|
for (const tool of this.allKnownTools.values()) {
|
|
if (this.isActiveTool(tool, excludedTools)) {
|
|
activeTools.push(tool);
|
|
}
|
|
}
|
|
return activeTools;
|
|
}
|
|
|
|
/**
|
|
* Expands an excludeTools set to include all legacy aliases.
|
|
* For example, if 'search_file_content' is excluded and it's an alias for
|
|
* 'grep_search', both names will be in the returned set.
|
|
*/
|
|
private expandExcludeToolsWithAliases(
|
|
excludeTools: Set<string> | undefined,
|
|
): Set<string> | undefined {
|
|
if (!excludeTools || excludeTools.size === 0) {
|
|
return excludeTools;
|
|
}
|
|
const expanded = new Set<string>();
|
|
for (const name of excludeTools) {
|
|
for (const alias of getToolAliases(name)) {
|
|
expanded.add(alias);
|
|
}
|
|
}
|
|
return expanded;
|
|
}
|
|
|
|
/**
|
|
* @param tool
|
|
* @param excludeTools (optional, helps performance for repeated calls)
|
|
* @returns Whether or not the `tool` is not excluded.
|
|
*/
|
|
private isActiveTool(
|
|
tool: AnyDeclarativeTool,
|
|
excludeTools?: Set<string>,
|
|
): boolean {
|
|
excludeTools ??=
|
|
this.expandExcludeToolsWithAliases(
|
|
this.config.getExcludeTools(
|
|
this.buildToolMetadata(),
|
|
new Set(this.allKnownTools.keys()),
|
|
),
|
|
) ?? new Set([]);
|
|
|
|
if (tool.name === UPDATE_TOPIC_TOOL_NAME) {
|
|
if (!this.config.isTopicUpdateNarrationEnabled()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const isPlanMode = this.config.getApprovalMode() === ApprovalMode.PLAN;
|
|
if (
|
|
(tool.name === ENTER_PLAN_MODE_TOOL_NAME && isPlanMode) ||
|
|
(tool.name === EXIT_PLAN_MODE_TOOL_NAME && !isPlanMode)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const normalizedClassName = tool.constructor.name.replace(/^_+/, '');
|
|
const possibleNames = [tool.name, normalizedClassName];
|
|
if (tool instanceof DiscoveredMCPTool) {
|
|
// Check both the unqualified and qualified name for MCP tools.
|
|
if (tool.name.startsWith(tool.getFullyQualifiedPrefix())) {
|
|
possibleNames.push(
|
|
tool.name.substring(tool.getFullyQualifiedPrefix().length),
|
|
);
|
|
} else {
|
|
possibleNames.push(`${tool.getFullyQualifiedPrefix()}${tool.name}`);
|
|
}
|
|
}
|
|
return !possibleNames.some((name) => excludeTools.has(name));
|
|
}
|
|
|
|
/**
|
|
* Retrieves the list of tool schemas (FunctionDeclaration array).
|
|
* Extracts the declarations from the ToolListUnion structure.
|
|
* Includes discovered (vs registered) tools if configured.
|
|
* @param modelId Optional model identifier to get model-specific schemas.
|
|
* @returns An array of FunctionDeclarations.
|
|
*/
|
|
getFunctionDeclarations(modelId?: string): FunctionDeclaration[] {
|
|
const isPlanMode = this.config.getApprovalMode() === ApprovalMode.PLAN;
|
|
const plansDir = this.config.storage.getPlansDir();
|
|
|
|
const declarations: FunctionDeclaration[] = [];
|
|
const seenNames = new Set<string>();
|
|
|
|
const mainAgentTools = this.isMainRegistry
|
|
? this.config.getMainAgentTools()
|
|
: undefined;
|
|
|
|
this.getActiveTools().forEach((tool) => {
|
|
const toolName =
|
|
tool instanceof DiscoveredMCPTool
|
|
? tool.getFullyQualifiedName()
|
|
: tool.name;
|
|
|
|
if (seenNames.has(toolName)) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
mainAgentTools &&
|
|
!mainAgentTools.includes(toolName) &&
|
|
!mainAgentTools.includes(tool.constructor.name) &&
|
|
!mainAgentTools.some((t) => t.startsWith(`${tool.constructor.name}(`))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
seenNames.add(toolName);
|
|
|
|
let schema = tool.getSchema(modelId);
|
|
|
|
// Ensure the schema name matches the qualified name for MCP tools
|
|
if (tool instanceof DiscoveredMCPTool) {
|
|
schema = {
|
|
...schema,
|
|
name: toolName,
|
|
};
|
|
}
|
|
|
|
if (
|
|
isPlanMode &&
|
|
(toolName === WRITE_FILE_TOOL_NAME || toolName === EDIT_TOOL_NAME)
|
|
) {
|
|
schema = {
|
|
...schema,
|
|
description: `ONLY FOR PLANS: ${schema.description}. You are currently in Plan Mode and may ONLY use this tool to write or update plans (.md files) in the plans directory: ${plansDir}/. You cannot use this tool to modify source code directly.`,
|
|
};
|
|
}
|
|
declarations.push(schema);
|
|
});
|
|
return declarations;
|
|
}
|
|
|
|
/**
|
|
* Retrieves a filtered list of tool schemas based on a list of tool names.
|
|
* @param toolNames - An array of tool names to include.
|
|
* @param modelId Optional model identifier to get model-specific schemas.
|
|
* @returns An array of FunctionDeclarations for the specified tools.
|
|
*/
|
|
getFunctionDeclarationsFiltered(
|
|
toolNames: string[],
|
|
modelId?: string,
|
|
): FunctionDeclaration[] {
|
|
const declarations: FunctionDeclaration[] = [];
|
|
for (const name of toolNames) {
|
|
const tool = this.getTool(name);
|
|
if (tool) {
|
|
let schema = tool.getSchema(modelId);
|
|
|
|
// Ensure the schema name matches the qualified name for MCP tools
|
|
if (tool instanceof DiscoveredMCPTool) {
|
|
schema = {
|
|
...schema,
|
|
name: tool.getFullyQualifiedName(),
|
|
};
|
|
}
|
|
|
|
declarations.push(schema);
|
|
}
|
|
}
|
|
return declarations;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of names for all active tools.
|
|
* For MCP tools, this returns their fully qualified names.
|
|
* The list is deduplicated.
|
|
*/
|
|
getAllToolNames(): string[] {
|
|
const names = new Set<string>();
|
|
for (const tool of this.getActiveTools()) {
|
|
if (tool instanceof DiscoveredMCPTool) {
|
|
names.add(tool.getFullyQualifiedName());
|
|
} else {
|
|
names.add(tool.name);
|
|
}
|
|
}
|
|
return Array.from(names);
|
|
}
|
|
|
|
/**
|
|
* Returns an array of all registered and discovered tool instances.
|
|
*/
|
|
getAllTools(): AnyDeclarativeTool[] {
|
|
const seen = new Set<string>();
|
|
const tools: AnyDeclarativeTool[] = [];
|
|
|
|
for (const tool of this.getActiveTools().sort((a, b) =>
|
|
a.displayName.localeCompare(b.displayName),
|
|
)) {
|
|
const name =
|
|
tool instanceof DiscoveredMCPTool
|
|
? tool.getFullyQualifiedName()
|
|
: tool.name;
|
|
if (!seen.has(name)) {
|
|
seen.add(name);
|
|
tools.push(tool);
|
|
}
|
|
}
|
|
return tools;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of tools registered from a specific MCP server.
|
|
*/
|
|
getToolsByServer(serverName: string): AnyDeclarativeTool[] {
|
|
const serverTools: AnyDeclarativeTool[] = [];
|
|
for (const tool of this.getActiveTools()) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
if ((tool as DiscoveredMCPTool)?.serverName === serverName) {
|
|
serverTools.push(tool);
|
|
}
|
|
}
|
|
return serverTools.sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
/**
|
|
* Get the definition of a specific tool.
|
|
*/
|
|
getTool(name: string): AnyDeclarativeTool | undefined {
|
|
let tool = this.allKnownTools.get(name);
|
|
|
|
// If not found, check legacy aliases
|
|
if (!tool && TOOL_LEGACY_ALIASES[name]) {
|
|
const currentName = TOOL_LEGACY_ALIASES[name];
|
|
tool = this.allKnownTools.get(currentName);
|
|
if (tool) {
|
|
debugLogger.debug(
|
|
`Resolved legacy tool name "${name}" to current name "${currentName}"`,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (tool && this.isActiveTool(tool)) {
|
|
return tool;
|
|
}
|
|
return;
|
|
}
|
|
}
|