// OpenClaude: disable experimental API betas by default. // Tool search (defer_loading), global cache scope, and context management // require internal API support not available to external accounts → 500. // Users can opt-in with CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=false. // eslint-disable-next-line custom-rules/no-top-level-side-effects process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS ??= 'true' import { Server } from '@modelcontextprotocol/sdk/server/index.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { ZodError } from 'zod' import { CallToolRequestSchema, type CallToolResult, ListToolsRequestSchema, type ListToolsResult, type Tool, } from '@modelcontextprotocol/sdk/types.js' import { getDefaultAppState } from 'src/state/AppStateStore.js' import review from '../commands/review.js' import type { Command } from '../commands.js' import { getMcpToolsCommandsAndResources } from '../services/mcp/client.js' import type { MCPServerConnection } from '../services/mcp/types.js' import { findToolByName, getEmptyToolPermissionContext, type Tool as InternalTool, type ToolUseContext, } from '../Tool.js' import { getTools } from '../tools.js' import { createAbortController } from '../utils/abortController.js' import { createFileStateCacheWithSizeLimit } from '../utils/fileStateCache.js' import { logError } from '../utils/log.js' import { createAssistantMessage } from '../utils/messages.js' import { getMainLoopModel } from '../utils/model/model.js' import { hasPermissionsToUseTool } from '../utils/permissions/permissions.js' import { setCwd } from '../utils/Shell.js' import { jsonStringify } from '../utils/slowOperations.js' import { getErrorParts } from '../utils/toolErrors.js' import { zodToJsonSchema } from '../utils/zodToJsonSchema.js' type ToolInput = Tool['inputSchema'] type ToolOutput = Tool['outputSchema'] const MCP_COMMANDS: Command[] = [review] export function getCombinedTools( builtins: InternalTool[], mcpTools: InternalTool[], ): InternalTool[] { const mcpToolNames = new Set(mcpTools.map(t => t.name)) const deduplicatedBuiltins = builtins.filter(t => !mcpToolNames.has(t.name)) return [...mcpTools, ...deduplicatedBuiltins] } export async function loadReexposedMcpTools(): Promise<{ mcpClients: MCPServerConnection[] mcpTools: InternalTool[] }> { const mcpClients: MCPServerConnection[] = [] const mcpTools: InternalTool[] = [] // Load configured MCP clients and their tools await getMcpToolsCommandsAndResources(({ client, tools: clientTools }) => { mcpClients.push(client) mcpTools.push(...clientTools) }) return { mcpClients, mcpTools } } export async function startMCPServer( cwd: string, debug: boolean, verbose: boolean, ): Promise { // Use size-limited LRU cache for readFileState to prevent unbounded memory growth // 100 files and 25MB limit should be sufficient for MCP server operations const READ_FILE_STATE_CACHE_SIZE = 100 const readFileStateCache = createFileStateCacheWithSizeLimit( READ_FILE_STATE_CACHE_SIZE, ) setCwd(cwd) const server = new Server( { name: 'claude/tengu', version: MACRO.VERSION, }, { capabilities: { tools: {}, }, }, ) const { mcpClients, mcpTools } = await loadReexposedMcpTools() server.setRequestHandler( ListToolsRequestSchema, async (): Promise => { const toolPermissionContext = getEmptyToolPermissionContext() const tools = getCombinedTools(getTools(toolPermissionContext), mcpTools) return { tools: await Promise.all( tools.map(async tool => { let outputSchema: ToolOutput | undefined if (tool.outputSchema) { const convertedSchema = zodToJsonSchema(tool.outputSchema) // MCP SDK requires outputSchema to have type: "object" at root level // Skip schemas with anyOf/oneOf at root (from z.union, z.discriminatedUnion, etc.) // See: https://github.com/anthropics/claude-code/issues/8014 if ( typeof convertedSchema === 'object' && convertedSchema !== null && 'type' in convertedSchema && convertedSchema.type === 'object' ) { outputSchema = convertedSchema as ToolOutput } } return { ...tool, description: await tool.prompt({ getToolPermissionContext: async () => toolPermissionContext, tools, agents: [], }), inputSchema: (tool.inputJSONSchema ?? zodToJsonSchema(tool.inputSchema)) as ToolInput, outputSchema, } }), ), } }, ) server.setRequestHandler( CallToolRequestSchema, async ({ params: { name, arguments: args } }): Promise => { const toolPermissionContext = getEmptyToolPermissionContext() const tools = getCombinedTools(getTools(toolPermissionContext), mcpTools) const tool = findToolByName(tools, name) if (!tool) { throw new Error(`Tool ${name} not found`) } // Assume MCP servers do not read messages separately from the tool // call arguments. const toolUseContext: ToolUseContext = { abortController: createAbortController(), options: { commands: MCP_COMMANDS, tools, mainLoopModel: getMainLoopModel(), thinkingConfig: { type: 'disabled' }, mcpClients, mcpResources: {}, isNonInteractiveSession: true, debug, verbose, agentDefinitions: { activeAgents: [], allAgents: [] }, }, getAppState: () => getDefaultAppState(), setAppState: () => {}, messages: [], readFileState: readFileStateCache, setInProgressToolUseIDs: () => {}, setResponseLength: () => {}, updateFileHistoryState: () => {}, updateAttributionState: () => {}, } try { if (!tool.isEnabled()) { throw new Error(`Tool ${name} is not enabled`) } // Validate input types with zod const parsedArgs = tool.inputSchema.parse(args ?? {}) const validationResult = await tool.validateInput?.( (parsedArgs as never) ?? {}, toolUseContext, ) if (validationResult && !validationResult.result) { throw new Error( `Tool ${name} input is invalid: ${validationResult.message}`, ) } const finalResult = await tool.call( (parsedArgs ?? {}) as never, toolUseContext, hasPermissionsToUseTool, createAssistantMessage({ content: [], }), ) let content: CallToolResult['content'] const data = finalResult.data as string | { type: string; text?: string; source?: { type: string; media_type: string; data: string } }[] | unknown if (typeof data === 'string') { content = [{ type: 'text', text: data }] } else if (Array.isArray(data)) { content = data.map((block: any) => { if (block.type === 'text') { return { type: 'text', text: block.text || '' } } else if (block.type === 'image' && block.source) { return { type: 'image', data: block.source.data, mimeType: block.source.media_type, } } else { // eslint-disable-next-line custom-rules/no-top-level-side-effects, no-console console.warn(`Unmapped content block type from tool ${name}: ${block.type || 'unknown'}`) return { type: 'text', text: jsonStringify(block) } } }) as CallToolResult['content'] } else { content = [{ type: 'text', text: jsonStringify(data) }] } return { content, isError: !!(finalResult as any).isError, } } catch (error) { logError(error) if (error instanceof ZodError) { return { isError: true, content: [ { type: 'text', text: `Tool ${name} input is invalid:\n${error.errors.map(e => `- ${e.path.join('.')}: ${e.message}`).join('\n')}`, }, ], } } const parts = error instanceof Error ? getErrorParts(error) : [String(error)] const errorText = parts.filter(Boolean).join('\n').trim() || 'Error' return { isError: true, content: [ { type: 'text', text: errorText, }, ], } } }, ) async function runServer() { const transport = new StdioServerTransport() await server.connect(transport) } return await runServer() }