Add MCP server layer: tool result, server, tool registration

This commit is contained in:
morluto 2026-07-12 03:25:23 +08:00
parent 63b33abcf8
commit 09bec111d5
4 changed files with 147 additions and 0 deletions

View File

@ -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;
};

View File

@ -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),
),
);
};

View File

@ -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<Record<string, JsonValue>> => {
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<string, JsonValue> = {};
for (const key of Object.keys(contract.inputSchema.shape)) {
projected[key] = parsed.data[key] ?? null;
}
return projected;
};

28
src/server/toolResult.ts Normal file
View File

@ -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<JsonValue, HopperError>,
): 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);
};