* fix: OAuth tokens secure storage for Windows & Linux * fix(mcp): MCP Tool Re-exposure & Strict Input Validation Fixes the MCP re-exposure bug by correctly handling tool deduplication, input validation with Ajv, and structured output (including images). Also disables experimental API betas by default to prevent 500 errors on external accounts. * fix(mcp): skip official registry prefetch in non-first-party mode Prevents unnecessary calls to Anthropic's MCP registry when using other API providers. * fix(cli): disable experimental API betas by default This prevents 500 errors from Anthropic's API when tool-calling with non-Anthropic accounts or models that don't support certain beta features. * fix: issues raised in the PR review for #675
128 lines
3.5 KiB
TypeScript
128 lines
3.5 KiB
TypeScript
import { Ajv } from 'ajv'
|
|
import { z } from 'zod/v4'
|
|
import { buildTool, type ToolDef, type ValidationResult } from '../../Tool.js'
|
|
import { lazySchema } from '../../utils/lazySchema.js'
|
|
import type { PermissionResult } from '../../types/permissions.js'
|
|
import { isOutputLineTruncated } from '../../utils/terminal.js'
|
|
import { DESCRIPTION, PROMPT } from './prompt.js'
|
|
import {
|
|
renderToolResultMessage,
|
|
renderToolUseMessage,
|
|
renderToolUseProgressMessage,
|
|
} from './UI.js'
|
|
|
|
// Allow any input object since MCP tools define their own schemas
|
|
export const inputSchema = lazySchema(() => z.object({}).passthrough())
|
|
type InputSchema = ReturnType<typeof inputSchema>
|
|
|
|
// MCP tools can return either a plain string or an array of content blocks
|
|
// (text, images, etc.). The outputSchema must reflect both shapes so the model
|
|
// knows rich content is possible.
|
|
export const outputSchema = lazySchema(() =>
|
|
z.union([
|
|
z.string().describe('MCP tool execution result as text'),
|
|
z
|
|
.array(
|
|
z.object({
|
|
type: z.string(),
|
|
text: z.string().optional(),
|
|
}),
|
|
)
|
|
.describe('MCP tool execution result as content blocks'),
|
|
]),
|
|
)
|
|
type OutputSchema = ReturnType<typeof outputSchema>
|
|
|
|
export type Output = z.infer<OutputSchema>
|
|
|
|
// Re-export MCPProgress from centralized types to break import cycles
|
|
export type { MCPProgress } from '../../types/tools.js'
|
|
|
|
const ajv = new Ajv({ strict: false })
|
|
|
|
export const MCPTool = buildTool({
|
|
isMcp: true,
|
|
// Overridden in mcpClient.ts with the real MCP tool name + args
|
|
isOpenWorld() {
|
|
return false
|
|
},
|
|
// Overridden in mcpClient.ts
|
|
name: 'mcp',
|
|
maxResultSizeChars: 100_000,
|
|
// Overridden in mcpClient.ts
|
|
async description() {
|
|
return DESCRIPTION
|
|
},
|
|
// Overridden in mcpClient.ts
|
|
async prompt() {
|
|
return PROMPT
|
|
},
|
|
get inputSchema(): InputSchema {
|
|
return inputSchema()
|
|
},
|
|
get outputSchema(): OutputSchema {
|
|
return outputSchema()
|
|
},
|
|
// Overridden in mcpClient.ts
|
|
async call() {
|
|
return {
|
|
data: '',
|
|
}
|
|
},
|
|
async checkPermissions(): Promise<PermissionResult> {
|
|
return {
|
|
behavior: 'passthrough',
|
|
message: 'MCPTool requires permission.',
|
|
}
|
|
},
|
|
async validateInput(input, context): Promise<ValidationResult> {
|
|
if (this.inputJSONSchema) {
|
|
try {
|
|
const validate = ajv.compile(this.inputJSONSchema)
|
|
if (!validate(input)) {
|
|
return {
|
|
result: false,
|
|
message: ajv.errorsText(validate.errors),
|
|
errorCode: 400,
|
|
}
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
result: false,
|
|
message: `Failed to compile JSON schema for validation: ${error}`,
|
|
errorCode: 500,
|
|
}
|
|
}
|
|
}
|
|
return { result: true }
|
|
},
|
|
renderToolUseMessage,
|
|
// Overridden in mcpClient.ts
|
|
userFacingName: () => 'mcp',
|
|
renderToolUseProgressMessage,
|
|
renderToolResultMessage,
|
|
isResultTruncated(output: Output): boolean {
|
|
if (typeof output === 'string') {
|
|
return isOutputLineTruncated(output)
|
|
}
|
|
// Array of content blocks — check if any text block exceeds the display limit
|
|
if (Array.isArray(output)) {
|
|
return output.some(
|
|
block =>
|
|
block?.type === 'text' &&
|
|
typeof block.text === 'string' &&
|
|
isOutputLineTruncated(block.text),
|
|
)
|
|
}
|
|
return false
|
|
},
|
|
mapToolResultToToolResultBlockParam(content, toolUseID) {
|
|
return {
|
|
tool_use_id: toolUseID,
|
|
type: 'tool_result',
|
|
content,
|
|
}
|
|
},
|
|
} satisfies ToolDef<InputSchema, Output>)
|
|
|