2025-04-18 17:44:24 -07:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
2025-08-26 00:04:53 +02:00
import type { FunctionDeclaration } from '@google/genai' ;
2026-03-04 05:42:59 +05:30
import {
Kind ,
BaseDeclarativeTool ,
BaseToolInvocation ,
type AnyDeclarativeTool ,
type ToolResult ,
type ToolInvocation ,
2026-04-10 10:11:17 -07:00
type ExecuteOptions ,
2025-08-14 13:28:33 -07:00
} from './tools.js' ;
2025-08-26 00:04:53 +02:00
import type { Config } from '../config/config.js' ;
2026-02-20 13:33:04 -06:00
import { ApprovalMode } from '../policy/types.js' ;
2025-07-06 05:58:51 +08:00
import { spawn } from 'node:child_process' ;
import { StringDecoder } from 'node:string_decoder' ;
2025-05-28 00:43:23 -07:00
import { DiscoveredMCPTool } from './mcp-tool.js' ;
2025-07-06 05:58:51 +08:00
import { parse } from 'shell-quote' ;
2025-08-21 14:40:18 -07:00
import { ToolErrorType } from './tool-error.js' ;
2025-08-20 13:10:02 -07:00
import { safeJsonStringify } from '../utils/safeJsonStringify.js' ;
2025-10-24 13:04:40 -07:00
import type { MessageBus } from '../confirmation-bus/message-bus.js' ;
2025-10-21 16:35:22 -04:00
import { debugLogger } from '../utils/debugLogger.js' ;
2025-10-27 16:46:35 -07:00
import { coreEvents } from '../utils/events.js' ;
2026-02-06 16:49:25 -08:00
import {
DISCOVERED_TOOL_PREFIX ,
TOOL_LEGACY_ALIASES ,
getToolAliases ,
2026-02-20 13:33:04 -06:00
WRITE_FILE_TOOL_NAME ,
EDIT_TOOL_NAME ,
2026-03-27 18:28:25 -07:00
UPDATE_TOPIC_TOOL_NAME ,
2026-04-01 11:56:10 -04:00
ENTER_PLAN_MODE_TOOL_NAME ,
EXIT_PLAN_MODE_TOOL_NAME ,
2026-02-06 16:49:25 -08:00
} from './tool-names.js' ;
2025-11-06 15:51:16 -08:00
2025-05-28 00:43:23 -07:00
type ToolParams = Record < string , unknown > ;
2025-05-17 16:53:22 -07:00
2025-08-14 13:28:33 -07:00
class DiscoveredToolInvocation extends BaseToolInvocation <
ToolParams ,
ToolResult
> {
2025-05-03 19:57:28 -07:00
constructor (
private readonly config : Config ,
2025-11-06 15:51:16 -08:00
private readonly originalToolName : string ,
prefixedToolName : string ,
2025-08-14 13:28:33 -07:00
params : ToolParams ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-05-03 19:57:28 -07:00
) {
2025-11-06 15:51:16 -08:00
super ( params , messageBus , prefixedToolName ) ;
2025-08-14 13:28:33 -07:00
}
2025-05-03 19:57:28 -07:00
2025-08-14 13:28:33 -07:00
getDescription ( ) : string {
2025-08-20 13:10:02 -07:00
return safeJsonStringify ( this . params ) ;
2025-05-03 19:57:28 -07:00
}
2026-04-10 10:11:17 -07:00
async execute ( {
abortSignal : _signal ,
updateOutput : _updateOutput ,
} : ExecuteOptions ) : Promise < ToolResult > {
2025-05-03 19:57:28 -07:00
const callCommand = this . config . getToolCallCommand ( ) ! ;
2026-03-13 14:11:51 -07:00
const args = [ this . originalToolName ] ;
let finalCommand = callCommand ;
let finalArgs = args ;
let finalEnv = process . env ;
2026-04-08 12:03:36 -07:00
let cleanupFunc : ( ( ) = > void ) | undefined ;
2026-03-13 14:11:51 -07:00
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 ;
2026-04-08 12:03:36 -07:00
cleanupFunc = prepared . cleanup ;
2026-03-13 14:11:51 -07:00
}
2025-05-03 19:57:28 -07:00
let stdout = '' ;
let stderr = '' ;
let error : Error | null = null ;
let code : number | null = null ;
let signal : NodeJS.Signals | null = null ;
2025-06-05 06:40:33 -07:00
2026-04-08 12:03:36 -07:00
try {
const child = spawn ( finalCommand , finalArgs , {
env : finalEnv ,
} ) ;
child . stdin . write ( JSON . stringify ( this . params ) ) ;
child . stdin . end ( ) ;
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 ( ) ;
}
} ;
2025-06-05 06:40:33 -07:00
2026-04-08 12:03:36 -07:00
child . stdout . on ( 'data' , onStdout ) ;
child . stderr . on ( 'data' , onStderr ) ;
child . on ( 'error' , onError ) ;
child . on ( 'close' , onClose ) ;
} ) ;
} finally {
cleanupFunc ? . ( ) ;
}
2025-05-03 19:57:28 -07:00
// 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 ,
2025-08-21 14:40:18 -07:00
error : {
message : llmContent ,
type : ToolErrorType . DISCOVERED_TOOL_EXECUTION_ERROR ,
} ,
2025-05-03 19:57:28 -07:00
} ;
}
return {
llmContent : stdout ,
returnDisplay : stdout ,
} ;
}
}
2025-04-15 21:41:08 -07:00
2025-08-14 13:28:33 -07:00
export class DiscoveredTool extends BaseDeclarativeTool <
ToolParams ,
ToolResult
> {
2025-11-06 15:51:16 -08:00
private readonly originalName : string ;
2025-08-14 13:28:33 -07:00
constructor (
private readonly config : Config ,
2025-11-06 15:51:16 -08:00
originalName : string ,
prefixedName : string ,
description : string ,
2025-08-14 13:28:33 -07:00
override readonly parameterSchema : Record < string , unknown > ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-08-14 13:28:33 -07:00
) {
const discoveryCmd = config . getToolDiscoveryCommand ( ) ! ;
const callCommand = config . getToolCallCommand ( ) ! ;
2025-11-06 15:51:16 -08:00
const fullDescription =
description +
`
2025-08-14 13:28:33 -07:00
This tool was discovered from the project by executing the command \` ${ discoveryCmd } \` on project root.
2025-11-06 15:51:16 -08:00
When called, this tool will execute the command \` ${ callCommand } ${ originalName } \` on project root.
2025-08-14 13:28:33 -07:00
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 (
2025-11-06 15:51:16 -08:00
prefixedName ,
prefixedName ,
fullDescription ,
2025-08-14 13:28:33 -07:00
Kind . Other ,
parameterSchema ,
2026-01-04 17:11:43 -05:00
messageBus ,
2025-08-14 13:28:33 -07:00
false , // isOutputMarkdown
false , // canUpdateOutput
) ;
2025-11-06 15:51:16 -08:00
this . originalName = originalName ;
2025-08-14 13:28:33 -07:00
}
protected createInvocation (
params : ToolParams ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-10-24 13:04:40 -07:00
_toolName? : string ,
_displayName? : string ,
2025-08-14 13:28:33 -07:00
) : ToolInvocation < ToolParams , ToolResult > {
2025-11-06 15:51:16 -08:00
return new DiscoveredToolInvocation (
this . config ,
this . originalName ,
2026-01-04 15:51:23 -05:00
_toolName ? ? this . name ,
2025-11-06 15:51:16 -08:00
params ,
2026-01-04 17:11:43 -05:00
messageBus ,
2025-11-06 15:51:16 -08:00
) ;
2025-08-14 13:28:33 -07:00
}
}
2025-04-21 12:59:31 -07:00
export class ToolRegistry {
2025-09-17 11:45:04 -07:00
// The tools keyed by tool name as seen by the LLM.
2025-11-07 12:18:35 -08:00
// 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 ( ) ;
2025-05-20 11:36:21 -07:00
private config : Config ;
2026-03-12 18:56:31 -07:00
readonly messageBus : MessageBus ;
2026-03-17 13:54:07 -07:00
private isMainRegistry : boolean ;
2025-04-15 21:41:08 -07:00
2026-03-17 13:54:07 -07:00
constructor (
config : Config ,
messageBus : MessageBus ,
isMainRegistry : boolean = false ,
) {
2025-05-20 11:36:21 -07:00
this . config = config ;
2025-10-28 09:20:57 -07:00
this . messageBus = messageBus ;
2026-03-17 13:54:07 -07:00
this . isMainRegistry = isMainRegistry ;
2025-10-28 09:20:57 -07:00
}
2026-01-04 17:11:43 -05:00
getMessageBus ( ) : MessageBus {
2025-10-28 09:20:57 -07:00
return this . messageBus ;
}
2026-03-16 16:24:27 +00:00
/**
* 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 ;
}
2025-04-17 18:06:21 -04:00
/**
* Registers a tool definition.
2025-11-07 12:18:35 -08:00
*
* Note that excluded tools are still registered to allow for enabling them
* later in the session.
*
2025-04-17 18:06:21 -04:00
* @param tool - The tool object containing schema and execution logic.
*/
2025-08-06 10:50:02 -07:00
registerTool ( tool : AnyDeclarativeTool ) : void {
2025-11-07 12:18:35 -08:00
if ( this . allKnownTools . has ( tool . name ) ) {
2026-03-12 10:17:36 -04:00
// Decide on behavior: throw error, log warning, or allow overwrite
debugLogger . warn (
` Tool with name " ${ tool . name } " is already registered. Overwriting. ` ,
) ;
2025-04-15 21:41:08 -07:00
}
2025-11-07 12:18:35 -08:00
this . allKnownTools . set ( tool . name , tool ) ;
2025-04-17 18:06:21 -04:00
}
2025-04-15 21:41:08 -07:00
2026-01-05 15:12:51 -08:00
/**
* Unregisters a tool definition by name.
*
* @param name - The name of the tool to unregister.
*/
unregisterTool ( name : string ) : void {
this . allKnownTools . delete ( name ) ;
}
2025-11-05 15:38:44 -08:00
/**
* Sorts tools as:
* 1. Built in tools.
* 2. Discovered tools.
* 3. MCP tools ordered by server name.
*
2025-11-07 12:18:35 -08:00
* This is a stable sort in that tries preserve existing order.
2025-11-05 15:38:44 -08:00
*/
sortTools ( ) : void {
const getPriority = ( tool : AnyDeclarativeTool ) : number = > {
if ( tool instanceof DiscoveredMCPTool ) return 2 ;
if ( tool instanceof DiscoveredTool ) return 1 ;
return 0 ; // Built-in
} ;
2025-11-07 12:18:35 -08:00
this . allKnownTools = new Map (
Array . from ( this . allKnownTools . entries ( ) ) . sort ( ( a , b ) = > {
2025-11-05 15:38:44 -08:00
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 ) {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2025-11-05 15:38:44 -08:00
const serverA = ( toolA as DiscoveredMCPTool ) . serverName ;
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2025-11-05 15:38:44 -08:00
const serverB = ( toolB as DiscoveredMCPTool ) . serverName ;
return serverA . localeCompare ( serverB ) ;
}
return 0 ;
} ) ,
) ;
}
2025-08-05 23:59:31 +02:00
private removeDiscoveredTools ( ) : void {
2025-11-07 12:18:35 -08:00
for ( const tool of this . allKnownTools . values ( ) ) {
2025-08-05 23:59:31 +02:00
if ( tool instanceof DiscoveredTool || tool instanceof DiscoveredMCPTool ) {
2025-11-07 12:18:35 -08:00
this . allKnownTools . delete ( tool . name ) ;
2025-08-05 23:59:31 +02:00
}
}
}
2025-08-13 19:31:24 +00:00
/**
* Removes all tools from a specific MCP server.
* @param serverName The name of the server to remove tools from.
*/
removeMcpToolsByServer ( serverName : string ) : void {
2025-11-07 12:18:35 -08:00
for ( const [ name , tool ] of this . allKnownTools . entries ( ) ) {
2025-08-13 19:31:24 +00:00
if ( tool instanceof DiscoveredMCPTool && tool . serverName === serverName ) {
2025-11-07 12:18:35 -08:00
this . allKnownTools . delete ( name ) ;
2025-08-13 19:31:24 +00:00
}
}
}
2025-05-03 19:57:28 -07:00
/**
2025-06-02 09:56:32 -07:00
* Discovers tools from project (if available and configured).
2025-05-03 19:57:28 -07:00
* Can be called multiple times to update discovered tools.
2025-07-25 03:14:45 +02:00
* This will discover tools from the command line and from MCP servers.
2025-05-03 19:57:28 -07:00
*/
2025-07-25 03:14:45 +02:00
async discoverAllTools ( ) : Promise < void > {
2025-05-03 19:57:28 -07:00
// remove any previously discovered tools
2025-08-05 23:59:31 +02:00
this . removeDiscoveredTools ( ) ;
2025-07-06 05:58:51 +08:00
await this . discoverAndRegisterToolsFromCommand ( ) ;
2025-07-22 09:34:56 -04:00
}
2025-07-06 05:58:51 +08:00
private async discoverAndRegisterToolsFromCommand ( ) : Promise < void > {
2025-05-04 12:11:19 -07:00
const discoveryCmd = this . config . getToolDiscoveryCommand ( ) ;
2025-07-06 05:58:51 +08:00
if ( ! discoveryCmd ) {
return ;
}
try {
const cmdParts = parse ( discoveryCmd ) ;
if ( cmdParts . length === 0 ) {
throw new Error (
'Tool discovery command is empty or contains only whitespace.' ,
) ;
}
2026-03-13 14:11:51 -07:00
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 ;
2026-04-08 12:03:36 -07:00
let cleanupFunc : ( ( ) = > void ) | undefined ;
2026-03-13 14:11:51 -07:00
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 ;
2026-04-08 12:03:36 -07:00
cleanupFunc = prepared . cleanup ;
2026-03-13 14:11:51 -07:00
}
2026-04-08 12:03:36 -07:00
try {
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 ;
2025-07-06 05:58:51 +08:00
}
2026-04-08 12:03:36 -07:00
stdoutByteLength += data . length ;
stdout += stdoutDecoder . write ( data ) ;
} ) ;
2025-07-06 05:58:51 +08:00
2026-04-08 12:03:36 -07:00
proc . stderr . on ( 'data' , ( data ) = > {
if ( sizeLimitExceeded ) return ;
if ( stderrByteLength + data . length > MAX_STDERR_SIZE ) {
sizeLimitExceeded = true ;
proc . kill ( ) ;
return ;
2025-07-06 05:58:51 +08:00
}
2026-04-08 12:03:36 -07:00
stderrByteLength += data . length ;
stderr += stderrDecoder . write ( data ) ;
2025-07-06 05:58:51 +08:00
} ) ;
2026-04-08 12:03:36 -07:00
await new Promise < void > ( ( resolve , reject ) = > {
proc . on ( 'error' , ( err ) = > {
reject ( err ) ;
} ) ;
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 ( ) ;
} ) ;
} ) ;
2025-07-06 05:58:51 +08:00
2026-04-08 12:03:36 -07:00
// 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 ( ) ) ;
2025-07-06 05:58:51 +08:00
2026-04-08 12:03:36 -07:00
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 ) ;
}
2025-07-06 05:58:51 +08:00
}
2025-06-02 13:41:49 -07:00
}
2026-04-08 12:03:36 -07:00
// 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 ,
) ,
) ;
2025-07-06 05:58:51 +08:00
}
2026-04-08 12:03:36 -07:00
} finally {
cleanupFunc ? . ( ) ;
2025-05-04 12:11:19 -07:00
}
2025-07-06 05:58:51 +08:00
} catch ( e ) {
2025-12-29 15:46:10 -05:00
debugLogger . error ( ` Tool discovery command " ${ discoveryCmd } " failed: ` , e ) ;
2025-07-06 05:58:51 +08:00
throw e ;
2025-05-03 19:57:28 -07:00
}
}
2026-02-24 09:20:11 -05:00
private buildToolMetadata ( ) : Map < string , Record < string , unknown > > {
const toolMetadata = new Map < string , Record < string , unknown > > ( ) ;
for ( const [ name , tool ] of this . allKnownTools ) {
2026-03-06 17:17:28 -05:00
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 ) {
2026-02-24 12:17:43 -05:00
toolMetadata . set ( name , metadata ) ;
2026-02-24 09:20:11 -05:00
}
}
return toolMetadata ;
}
2025-11-07 12:18:35 -08:00
/**
* @returns All the tools that are not excluded.
*/
private getActiveTools ( ) : AnyDeclarativeTool [ ] {
2026-02-24 09:20:11 -05:00
const toolMetadata = this . buildToolMetadata ( ) ;
2026-02-24 12:17:43 -05:00
const allKnownNames = new Set ( this . allKnownTools . keys ( ) ) ;
2026-02-06 16:49:25 -08:00
const excludedTools =
2026-02-24 09:20:11 -05:00
this . expandExcludeToolsWithAliases (
2026-02-24 12:17:43 -05:00
this . config . getExcludeTools ( toolMetadata , allKnownNames ) ,
2026-02-24 09:20:11 -05:00
) ? ? new Set ( [ ] ) ;
2025-11-07 12:18:35 -08:00
const activeTools : AnyDeclarativeTool [ ] = [ ] ;
for ( const tool of this . allKnownTools . values ( ) ) {
if ( this . isActiveTool ( tool , excludedTools ) ) {
activeTools . push ( tool ) ;
}
}
return activeTools ;
}
2026-02-06 16:49:25 -08:00
/**
* 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 ;
}
2025-11-07 12:18:35 -08:00
/**
* @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 {
2026-02-06 16:49:25 -08:00
excludeTools ? ? =
2026-02-24 09:20:11 -05:00
this . expandExcludeToolsWithAliases (
2026-02-24 12:17:43 -05:00
this . config . getExcludeTools (
this . buildToolMetadata ( ) ,
new Set ( this . allKnownTools . keys ( ) ) ,
) ,
2026-02-24 09:20:11 -05:00
) ? ? new Set ( [ ] ) ;
2026-02-20 13:33:04 -06:00
2026-03-27 18:28:25 -07:00
if ( tool . name === UPDATE_TOPIC_TOOL_NAME ) {
if ( ! this . config . isTopicUpdateNarrationEnabled ( ) ) {
return false ;
}
}
2026-04-01 11:56:10 -04:00
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 ;
}
2025-11-07 12:18:35 -08:00
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 ) ) ;
}
2025-04-17 18:06:21 -04:00
/**
2025-04-19 19:45:42 +01:00
* Retrieves the list of tool schemas (FunctionDeclaration array).
* Extracts the declarations from the ToolListUnion structure.
2025-05-03 19:57:28 -07:00
* Includes discovered (vs registered) tools if configured.
2026-02-09 15:46:23 -05:00
* @param modelId Optional model identifier to get model-specific schemas.
2025-04-19 19:45:42 +01:00
* @returns An array of FunctionDeclarations.
2025-04-17 18:06:21 -04:00
*/
2026-02-09 15:46:23 -05:00
getFunctionDeclarations ( modelId? : string ) : FunctionDeclaration [ ] {
2026-02-20 13:33:04 -06:00
const isPlanMode = this . config . getApprovalMode ( ) === ApprovalMode . PLAN ;
const plansDir = this . config . storage . getPlansDir ( ) ;
2025-04-17 18:06:21 -04:00
const declarations : FunctionDeclaration [ ] = [ ] ;
2026-03-02 16:12:13 -05:00
const seenNames = new Set < string > ( ) ;
2026-03-17 13:54:07 -07:00
const mainAgentTools = this . isMainRegistry
? this . config . getMainAgentTools ( )
: undefined ;
2025-11-07 12:18:35 -08:00
this . getActiveTools ( ) . forEach ( ( tool ) = > {
2026-03-02 16:12:13 -05:00
const toolName =
tool instanceof DiscoveredMCPTool
? tool . getFullyQualifiedName ( )
: tool . name ;
if ( seenNames . has ( toolName ) ) {
return ;
}
2026-03-17 13:54:07 -07:00
if (
mainAgentTools &&
! mainAgentTools . includes ( toolName ) &&
! mainAgentTools . includes ( tool . constructor . name ) &&
! mainAgentTools . some ( ( t ) = > t . startsWith ( ` ${ tool . constructor . name } ( ` ) )
) {
return ;
}
2026-03-02 16:12:13 -05:00
seenNames . add ( toolName ) ;
2026-02-20 13:33:04 -06:00
let schema = tool . getSchema ( modelId ) ;
2026-03-02 16:12:13 -05:00
// Ensure the schema name matches the qualified name for MCP tools
if ( tool instanceof DiscoveredMCPTool ) {
schema = {
. . . schema ,
name : toolName ,
} ;
}
2026-02-20 13:33:04 -06:00
if (
isPlanMode &&
2026-03-02 16:12:13 -05:00
( toolName === WRITE_FILE_TOOL_NAME || toolName === EDIT_TOOL_NAME )
2026-02-20 13:33:04 -06:00
) {
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 ) ;
2025-04-17 18:06:21 -04:00
} ) ;
2025-04-19 19:45:42 +01:00
return declarations ;
}
2025-04-15 21:41:08 -07:00
2025-08-06 20:34:38 -04:00
/**
* Retrieves a filtered list of tool schemas based on a list of tool names.
* @param toolNames - An array of tool names to include.
2026-02-09 15:46:23 -05:00
* @param modelId Optional model identifier to get model-specific schemas.
2025-08-06 20:34:38 -04:00
* @returns An array of FunctionDeclarations for the specified tools.
*/
2026-02-09 15:46:23 -05:00
getFunctionDeclarationsFiltered (
toolNames : string [ ] ,
modelId? : string ,
) : FunctionDeclaration [ ] {
2025-08-06 20:34:38 -04:00
const declarations : FunctionDeclaration [ ] = [ ] ;
for ( const name of toolNames ) {
2026-01-26 23:53:05 -05:00
const tool = this . getTool ( name ) ;
if ( tool ) {
2026-03-12 10:17:36 -04:00
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 ) ;
2025-08-06 20:34:38 -04:00
}
}
return declarations ;
}
2025-08-26 19:22:05 -04:00
/**
2026-03-02 16:12:13 -05:00
* Returns an array of names for all active tools.
* For MCP tools, this returns their fully qualified names.
* The list is deduplicated.
2025-08-26 19:22:05 -04:00
*/
getAllToolNames ( ) : string [ ] {
2026-03-02 16:12:13 -05:00
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 ) ;
2025-08-26 19:22:05 -04:00
}
2025-04-19 19:45:42 +01:00
/**
2025-05-03 19:57:28 -07:00
* Returns an array of all registered and discovered tool instances.
2025-04-19 19:45:42 +01:00
*/
2025-08-06 10:50:02 -07:00
getAllTools ( ) : AnyDeclarativeTool [ ] {
2026-03-02 16:12:13 -05:00
const seen = new Set < string > ( ) ;
const tools : AnyDeclarativeTool [ ] = [ ] ;
for ( const tool of this . getActiveTools ( ) . sort ( ( a , b ) = >
2025-07-15 14:35:35 -04:00
a . displayName . localeCompare ( b . displayName ) ,
2026-03-02 16:12:13 -05:00
) ) {
const name =
tool instanceof DiscoveredMCPTool
? tool . getFullyQualifiedName ( )
: tool . name ;
if ( ! seen . has ( name ) ) {
seen . add ( name ) ;
tools . push ( tool ) ;
}
}
return tools ;
2025-04-19 19:45:42 +01:00
}
2025-06-02 13:39:25 -07:00
/**
* Returns an array of tools registered from a specific MCP server.
*/
2025-08-06 10:50:02 -07:00
getToolsByServer ( serverName : string ) : AnyDeclarativeTool [ ] {
const serverTools : AnyDeclarativeTool [ ] = [ ] ;
2025-11-07 12:18:35 -08:00
for ( const tool of this . getActiveTools ( ) ) {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2025-06-02 13:39:25 -07:00
if ( ( tool as DiscoveredMCPTool ) ? . serverName === serverName ) {
serverTools . push ( tool ) ;
}
}
2025-07-15 14:35:35 -04:00
return serverTools . sort ( ( a , b ) = > a . name . localeCompare ( b . name ) ) ;
2025-06-02 13:39:25 -07:00
}
2025-04-17 18:06:21 -04:00
/**
* Get the definition of a specific tool.
*/
2025-08-06 10:50:02 -07:00
getTool ( name : string ) : AnyDeclarativeTool | undefined {
2026-01-21 15:18:33 +11:00
let tool = this . allKnownTools . get ( name ) ;
2026-02-02 16:34:14 -08:00
// 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 } " ` ,
) ;
}
}
2025-11-07 12:18:35 -08:00
if ( tool && this . isActiveTool ( tool ) ) {
return tool ;
}
return ;
2025-04-17 18:06:21 -04:00
}
2025-04-15 21:41:08 -07:00
}