diff --git a/src/server/createServer.ts b/src/server/createServer.ts new file mode 100644 index 000000000..cba3e0bef --- /dev/null +++ b/src/server/createServer.ts @@ -0,0 +1,20 @@ +import { McpServer } from "@modelcontextprotocol/server"; + +import type { HopperToolPort } from "../application/HopperToolPort.js"; +import { registerEnhancedTools } from "./registerEnhancedTools.js"; +import { registerOfficialTools } from "./registerOfficialTools.js"; + +/** Construct one MCP server instance without acquiring subprocess resources. */ +export const createServer = (hopper: HopperToolPort): McpServer => { + const server = new McpServer( + { name: "betterBinaryMCP", version: "0.1.0" }, + { + capabilities: { tools: {} }, + instructions: + "Reverse-engineering tools for Hopper Disassembler. Start with binary_overview.", + }, + ); + registerOfficialTools(server, hopper); + registerEnhancedTools(server, hopper); + return server; +}; diff --git a/src/server/registerEnhancedTools.ts b/src/server/registerEnhancedTools.ts new file mode 100644 index 000000000..fb3773d37 --- /dev/null +++ b/src/server/registerEnhancedTools.ts @@ -0,0 +1,37 @@ +import type { McpServer } from "@modelcontextprotocol/server"; + +import { EnhancedTools } from "../application/EnhancedTools.js"; +import type { HopperToolPort } from "../application/HopperToolPort.js"; +import { enhancedToolNameSchema } from "../contracts/enhancedInputs.js"; +import { + ENHANCED_TOOL_CONTRACTS, + type ToolContract, +} from "../contracts/toolContracts.js"; +import { toCallToolResult } from "./toolResult.js"; + +/** Register all eight enhanced reverse-engineering tools. */ +export const registerEnhancedTools = ( + server: McpServer, + hopper: HopperToolPort, +): void => { + const services = new EnhancedTools(hopper); + for (const contract of ENHANCED_TOOL_CONTRACTS) { + registerEnhancedTool(server, services, contract); + } +}; + +const registerEnhancedTool = ( + server: McpServer, + services: EnhancedTools, + contract: ToolContract, +): void => { + const name = enhancedToolNameSchema.parse(contract.name); + server.registerTool( + name, + { description: contract.description, inputSchema: contract.inputSchema }, + async (input, context) => + toCallToolResult( + await services.execute(name, input, context.mcpReq.signal), + ), + ); +}; diff --git a/src/server/registerOfficialTools.ts b/src/server/registerOfficialTools.ts new file mode 100644 index 000000000..cfca14af2 --- /dev/null +++ b/src/server/registerOfficialTools.ts @@ -0,0 +1,62 @@ +import type { McpServer } from "@modelcontextprotocol/server"; + +import type { HopperToolPort } from "../application/HopperToolPort.js"; +import { + OFFICIAL_TOOL_CONTRACTS, + type ToolContract, +} from "../contracts/toolContracts.js"; +import { jsonValueSchema, type JsonValue } from "../hopper/protocol.js"; +import { toCallToolResult } from "./toolResult.js"; + +/** Register every official Hopper proxy contract on one MCP server. */ +export const registerOfficialTools = ( + server: McpServer, + hopper: HopperToolPort, +): void => { + for (const contract of OFFICIAL_TOOL_CONTRACTS) { + registerOfficialTool(server, hopper, contract); + } +}; + +const registerOfficialTool = ( + server: McpServer, + hopper: HopperToolPort, + contract: ToolContract, +): void => { + server.registerTool( + contract.name, + { + description: contract.description, + inputSchema: contract.inputSchema, + }, + async (input, context) => { + const arguments_ = projectOfficialArguments(contract, input); + const result = await hopper.callTool(contract.name, arguments_, { + signal: context.mcpReq.signal, + }); + return toCallToolResult(result); + }, + ); +}; + +const projectOfficialArguments = ( + contract: ToolContract, + input: unknown, +): Readonly> => { + const parsed = jsonValueSchema.safeParse(input); + if ( + !parsed.success || + typeof parsed.data !== "object" || + parsed.data === null || + Array.isArray(parsed.data) + ) { + // The SDK validates this with the same schema before invoking the callback. + throw new Error("Validated MCP tool input was not a JSON object"); + } + + const projected: Record = {}; + for (const key of Object.keys(contract.inputSchema.shape)) { + projected[key] = parsed.data[key] ?? null; + } + return projected; +}; diff --git a/src/server/toolResult.ts b/src/server/toolResult.ts new file mode 100644 index 000000000..f1d1e342b --- /dev/null +++ b/src/server/toolResult.ts @@ -0,0 +1,28 @@ +import type { CallToolResult } from "@modelcontextprotocol/server"; + +import type { HopperError } from "../domain/errors.js"; +import type { Result } from "../domain/result.js"; +import type { JsonValue } from "../hopper/protocol.js"; + +/** Translate an application result into caller-visible MCP content. */ +export const toCallToolResult = ( + result: Result, +): CallToolResult => + result.ok + ? { content: [{ type: "text", text: formatValue(result.value) }] } + : { + content: [ + { + type: "text", + text: `${result.error._tag}: ${result.error.message}`, + }, + ], + isError: true, + }; + +const formatValue = (value: JsonValue): string => { + if (value === null) return "OK"; + if (typeof value === "string") return value; + if (typeof value === "object") return JSON.stringify(value, null, 2); + return String(value); +};