| import { Server } from '@modelcontextprotocol/sdk/server/index.js' |
| import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' |
| 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 { |
| findToolByName, |
| getEmptyToolPermissionContext, |
| 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 async function startMCPServer( |
| cwd: string, |
| debug: boolean, |
| verbose: boolean, |
| ): Promise<void> { |
| |
| |
| 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: {}, |
| }, |
| }, |
| ) |
|
|
| server.setRequestHandler( |
| ListToolsRequestSchema, |
| async (): Promise<ListToolsResult> => { |
| |
| const toolPermissionContext = getEmptyToolPermissionContext() |
| const tools = getTools(toolPermissionContext) |
| return { |
| tools: await Promise.all( |
| tools.map(async tool => { |
| let outputSchema: ToolOutput | undefined |
| if (tool.outputSchema) { |
| const convertedSchema = zodToJsonSchema(tool.outputSchema) |
| |
| |
| |
| 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: zodToJsonSchema(tool.inputSchema) as ToolInput, |
| outputSchema, |
| } |
| }), |
| ), |
| } |
| }, |
| ) |
|
|
| server.setRequestHandler( |
| CallToolRequestSchema, |
| async ({ params: { name, arguments: args } }): Promise<CallToolResult> => { |
| const toolPermissionContext = getEmptyToolPermissionContext() |
| |
| const tools = getTools(toolPermissionContext) |
| const tool = findToolByName(tools, name) |
| if (!tool) { |
| throw new Error(`Tool ${name} not found`) |
| } |
|
|
| |
| |
| 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`) |
| } |
| const validationResult = await tool.validateInput?.( |
| (args as never) ?? {}, |
| toolUseContext, |
| ) |
| if (validationResult && !validationResult.result) { |
| throw new Error( |
| `Tool ${name} input is invalid: ${validationResult.message}`, |
| ) |
| } |
| const finalResult = await tool.call( |
| (args ?? {}) as never, |
| toolUseContext, |
| hasPermissionsToUseTool, |
| createAssistantMessage({ |
| content: [], |
| }), |
| ) |
|
|
| return { |
| content: [ |
| { |
| type: 'text' as const, |
| text: |
| typeof finalResult === 'string' |
| ? finalResult |
| : jsonStringify(finalResult.data), |
| }, |
| ], |
| } |
| } catch (error) { |
| logError(error) |
|
|
| 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() |
| } |
|
|