/ src / entrypoints / mcp.ts
mcp.ts
  1  import { Server } from '@modelcontextprotocol/sdk/server/index.js'
  2  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
  3  import {
  4    CallToolRequestSchema,
  5    CallToolResultSchema,
  6    ListToolsRequestSchema,
  7    ListToolsResultSchema,
  8    ToolSchema,
  9  } from '@modelcontextprotocol/sdk/types.js'
 10  import { z } from 'zod'
 11  import { zodToJsonSchema } from 'zod-to-json-schema'
 12  import { AgentTool } from '../tools/AgentTool/AgentTool.js'
 13  import { hasPermissionsToUseTool } from '../permissions.js'
 14  import { setCwd } from '../utils/state.js'
 15  import { getSlowAndCapableModel } from '../utils/model.js'
 16  import { logError } from '../utils/log.js'
 17  import { LSTool } from '../tools/lsTool/lsTool.js'
 18  import { BashTool } from '../tools/BashTool/BashTool.js'
 19  import { FileEditTool } from '../tools/FileEditTool/FileEditTool.js'
 20  import { FileReadTool } from '../tools/FileReadTool/FileReadTool.js'
 21  import { GlobTool } from '../tools/GlobTool/GlobTool.js'
 22  import { GrepTool } from '../tools/GrepTool/GrepTool.js'
 23  import { FileWriteTool } from '../tools/FileWriteTool/FileWriteTool.js'
 24  import { Tool } from '../Tool.js'
 25  import { Command } from '../commands.js'
 26  import review from '../commands/review.js'
 27  import { lastX } from '../utils/generators.js'
 28  
 29  type ToolInput = z.infer<typeof ToolSchema.shape.inputSchema>
 30  
 31  const state: {
 32    readFileTimestamps: Record<string, number>
 33  } = {
 34    readFileTimestamps: {},
 35  }
 36  
 37  const MCP_COMMANDS: Command[] = [review]
 38  
 39  const MCP_TOOLS: Tool[] = [
 40    AgentTool,
 41    BashTool,
 42    FileEditTool,
 43    FileReadTool,
 44    GlobTool,
 45    GrepTool,
 46    FileWriteTool,
 47    LSTool,
 48  ]
 49  
 50  export async function startMCPServer(cwd: string): Promise<void> {
 51    await setCwd(cwd)
 52    const server = new Server(
 53      {
 54        name: 'claude/tengu',
 55        version: MACRO.VERSION,
 56      },
 57      {
 58        capabilities: {
 59          tools: {},
 60        },
 61      },
 62    )
 63  
 64    server.setRequestHandler(
 65      ListToolsRequestSchema,
 66      async (): Promise<Zod.infer<typeof ListToolsResultSchema>> => {
 67        const tools = await Promise.all(
 68          MCP_TOOLS.map(async tool => ({
 69            ...tool,
 70            description: await tool.description(z.object({})),
 71            inputSchema: zodToJsonSchema(tool.inputSchema) as ToolInput,
 72          })),
 73        )
 74  
 75        return {
 76          tools,
 77        }
 78      },
 79    )
 80  
 81    server.setRequestHandler(
 82      CallToolRequestSchema,
 83      async (request): Promise<Zod.infer<typeof CallToolResultSchema>> => {
 84        const { name, arguments: args } = request.params
 85        const tool = MCP_TOOLS.find(_ => _.name === name)
 86        if (!tool) {
 87          throw new Error(`Tool ${name} not found`)
 88        }
 89  
 90        // TODO: validate input types with zod
 91        try {
 92          if (!(await tool.isEnabled())) {
 93            throw new Error(`Tool ${name} is not enabled`)
 94          }
 95          const model = await getSlowAndCapableModel()
 96          const validationResult = await tool.validateInput?.(
 97            (args as never) ?? {},
 98            {
 99              abortController: new AbortController(),
100              options: {
101                commands: MCP_COMMANDS,
102                tools: MCP_TOOLS,
103                slowAndCapableModel: model,
104                forkNumber: 0,
105                messageLogName: 'unused',
106                maxThinkingTokens: 0,
107              },
108              messageId: undefined,
109              readFileTimestamps: state.readFileTimestamps,
110            },
111          )
112          if (validationResult && !validationResult.result) {
113            throw new Error(
114              `Tool ${name} input is invalid: ${validationResult.message}`,
115            )
116          }
117          const result = tool.call(
118            (args ?? {}) as never,
119            {
120              abortController: new AbortController(),
121              messageId: undefined,
122              options: {
123                commands: MCP_COMMANDS,
124                tools: MCP_TOOLS,
125                slowAndCapableModel: await getSlowAndCapableModel(),
126                forkNumber: 0,
127                messageLogName: 'unused',
128                maxThinkingTokens: 0,
129              },
130              readFileTimestamps: state.readFileTimestamps,
131            },
132            hasPermissionsToUseTool,
133          )
134  
135          const finalResult = await lastX(result)
136  
137          if (finalResult.type !== 'result') {
138            throw new Error(`Tool ${name} did not return a result`)
139          }
140  
141          return {
142            content: Array.isArray(finalResult)
143              ? finalResult.map(item => ({
144                  type: 'text' as const,
145                  text: 'text' in item ? item.text : JSON.stringify(item),
146                }))
147              : [
148                  {
149                    type: 'text' as const,
150                    text:
151                      typeof finalResult === 'string'
152                        ? finalResult
153                        : JSON.stringify(finalResult.data),
154                  },
155                ],
156          }
157        } catch (error) {
158          logError(error)
159          return {
160            isError: true,
161            content: [
162              {
163                type: 'text',
164                text: `Error: ${error instanceof Error ? error.message : String(error)}`,
165              },
166            ],
167          }
168        }
169      },
170    )
171  
172    async function runServer() {
173      const transport = new StdioServerTransport()
174      await server.connect(transport)
175    }
176  
177    return await runServer()
178  }