export const BRIDGE_TOOL_SEARCH_NAME = "mcp__tool_search__search" export const BRIDGE_TOOL_SEARCH_ALIASES = [ BRIDGE_TOOL_SEARCH_NAME, "tool_search_search", "mcp__plugin_tool-search_tool_search__search", ] as const export const MCP_TOOL_SEARCH_SENTINEL_TYPE = "copilot_api_tool_search" export const ALWAYS_LOADED_TOOL_NAMES = [ "Agent", "AskUserQuestion", "Bash", "Edit", "EnterPlanMode", "ExitPlanMode", "Glob", "Grep", "Read", "Skill", "TodoWrite", "ToolSearch", "WebFetch", "Write", "apply_patch", "bash", "glob", "grep", "plan_exit", "question", "read", "skill", "task", "todowrite", "webfetch", ] as const const alwaysLoadedToolNameSet = new Set(ALWAYS_LOADED_TOOL_NAMES) const bridgeToolSearchNameSet = new Set(BRIDGE_TOOL_SEARCH_ALIASES) export interface ToolSearchToolLike { name: string description?: string input_schema?: Record defer_loading?: boolean } export interface McpToolSearchSentinel { type: typeof MCP_TOOL_SEARCH_SENTINEL_TYPE names: Array } export const isBridgeToolSearchName = (name: string): boolean => bridgeToolSearchNameSet.has(name) export const isAlwaysLoadedToolName = (name: string): boolean => alwaysLoadedToolNameSet.has(name) export const isDeferredToolName = (name: string): boolean => !isBridgeToolSearchName(name) && !isAlwaysLoadedToolName(name) export const supportsResponsesToolSearchModel = (model: string): boolean => { const match = /^gpt-(\d+)(?:\.(\d+))?/iu.exec(model) if (!match) { return false } const major = Number.parseInt(match[1], 10) const minor = match[2] ? Number.parseInt(match[2], 10) : 0 return major > 5 || (major === 5 && minor >= 4) } export const hasBridgeToolSearchTool = ( tools: Array | undefined, ): boolean => Array.isArray(tools) && tools.some((tool) => isBridgeToolSearchName(tool.name)) export const resolveBridgeToolSearchName = ( tools: Array | undefined, ): string => { if (!Array.isArray(tools)) { return BRIDGE_TOOL_SEARCH_NAME } return ( tools.find((tool) => isBridgeToolSearchName(tool.name))?.name ?? BRIDGE_TOOL_SEARCH_NAME ) } export const hasDeferredToolCandidate = ( tools: Array | undefined, ): boolean => Array.isArray(tools) && tools.some((tool) => isDeferredToolName(tool.name)) export const shouldEnableResponsesToolSearch = (params: { model: string tools?: Array }): boolean => supportsResponsesToolSearchModel(params.model) && hasBridgeToolSearchTool(params.tools) && hasDeferredToolCandidate(params.tools) export const hasDeferredNamespaceTool = ( tools: Array | null | undefined, ): boolean => Array.isArray(tools) && tools.some((tool) => { if (!tool || typeof tool !== "object") { return false } const record = tool as Record if (record.type !== "namespace" || typeof record.name !== "string") { return false } if (!isDeferredToolName(record.name)) { return false } const namespaceTools = record.tools return ( Array.isArray(namespaceTools) && namespaceTools.some( (entry) => entry && typeof entry === "object" && (entry as Record).defer_loading === true, ) ) }) export const listDeferredToolNames = ( tools: Array, ): Array => [ ...new Set( tools .filter((tool) => isDeferredToolName(tool.name)) .map((tool) => tool.name), ), ] const extractDeferredToolNamesSource = ( record: Record, ): unknown => record.names ?? record.query ?? record.paths export const parseDeferredToolNames = (names: unknown): Array => { let rawNames: Array = [] if (typeof names === "string") { rawNames = names.split(",") } else if (Array.isArray(names)) { rawNames = names.flatMap((name) => typeof name === "string" ? name.split(",") : [], ) } return [ ...new Set( rawNames.map((name) => name.trim()).filter((name) => name.length > 0), ), ] } export const createMcpToolSearchSentinel = (names: unknown): string => JSON.stringify({ type: MCP_TOOL_SEARCH_SENTINEL_TYPE, names: parseDeferredToolNames(names), } satisfies McpToolSearchSentinel) export const parseMcpToolSearchSentinel = ( text: string, ): McpToolSearchSentinel | null => { try { const parsed: unknown = JSON.parse(text) if (!parsed || typeof parsed !== "object") { return null } const record = parsed as Record if (record.type !== MCP_TOOL_SEARCH_SENTINEL_TYPE) { return null } const names = parseDeferredToolNames(extractDeferredToolNamesSource(record)) if (names.length === 0) { return null } return { type: MCP_TOOL_SEARCH_SENTINEL_TYPE, names, } } catch { return null } } export const normalizeToolSearchBridgeArguments = ( argumentsValue: Record | string, ): Record => { if (typeof argumentsValue !== "string") { const names = parseDeferredToolNames( extractDeferredToolNamesSource(argumentsValue), ) return names.length > 0 ? { names } : {} } try { const parsed: unknown = JSON.parse(argumentsValue) if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { const record = parsed as Record const names = parseDeferredToolNames( extractDeferredToolNamesSource(record), ) return names.length > 0 ? { names } : {} } } catch { // Treat a raw string as the comma-separated protocol payload. } const names = parseDeferredToolNames(argumentsValue) return names.length > 0 ? { names } : {} } export const formatToolSearchBridgeArguments = ( argumentsValue: Record | string, ): Record => { const normalized = normalizeToolSearchBridgeArguments(argumentsValue) const names = normalized.names if (!Array.isArray(names) || names.length === 0) { return {} } return { names: names.join(",") } } export const selectDeferredToolsByNames = ( names: unknown, tools: Array, ): Array => { const requestedNames = parseDeferredToolNames(names) if (requestedNames.length === 0) { return [] } const deferredToolByName = new Map( tools .filter((tool) => isDeferredToolName(tool.name)) .map((tool) => [tool.name, tool]), ) return requestedNames.flatMap((name) => { const tool = deferredToolByName.get(name) return tool ? [tool] : [] }) } export const hasDeferredMcpNamespaceTool = hasDeferredNamespaceTool