Add MCP server layer: tool result, server, tool registration
This commit is contained in:
parent
63b33abcf8
commit
09bec111d5
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
Loading…
Reference in New Issue