feat(tools): add common tool utilities, types, and error formatting
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Common Model Context Protocol (MCP) Tool Utilities and Types.
|
||||
*/
|
||||
|
||||
import type { ZodRawShape } from "zod";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { OpenProjectError } from "../client/errors";
|
||||
|
||||
export interface McpToolResponse {
|
||||
content: Array<{ type: "text"; text: string }>;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats data into a standard MCP tool success text response.
|
||||
*/
|
||||
export function formatToolSuccess(data: unknown): McpToolResponse {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(data, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an error into a standard MCP tool error text response with error code.
|
||||
*/
|
||||
export function formatToolError(error: unknown): McpToolResponse {
|
||||
let message: string;
|
||||
|
||||
if (error instanceof OpenProjectError) {
|
||||
message = `Error [${error.code}]: ${error.message}`;
|
||||
} else if (error instanceof Error) {
|
||||
message = `Error: ${error.message}`;
|
||||
} else {
|
||||
message = `Error: ${String(error)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: message,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Definition interface for OpenProject MCP tools.
|
||||
*/
|
||||
export interface ToolDefinition<TShape extends ZodRawShape = ZodRawShape> {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters?: TShape;
|
||||
readOnly: boolean;
|
||||
execute: (args: any) => Promise<McpToolResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a ToolDefinition instance on an McpServer instance.
|
||||
*/
|
||||
export function registerTool(server: McpServer, tool: ToolDefinition<any>): void {
|
||||
if (tool.parameters) {
|
||||
server.tool(tool.name, tool.description, tool.parameters, async (args: any) => {
|
||||
return tool.execute(args);
|
||||
});
|
||||
} else {
|
||||
server.tool(tool.name, tool.description, async () => {
|
||||
return tool.execute({});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
formatToolSuccess,
|
||||
formatToolError,
|
||||
registerTool,
|
||||
type ToolDefinition,
|
||||
} from "../src/tools/common";
|
||||
import { OpenProjectNotFoundError } from "../src/client/errors";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
describe("Tool Utilities", () => {
|
||||
test("formatToolSuccess formats data into MCP text response", () => {
|
||||
const data = { id: 1, name: "Test" };
|
||||
const response = formatToolSuccess(data);
|
||||
expect(response.isError).toBeUndefined();
|
||||
expect(response.content).toHaveLength(1);
|
||||
expect(response.content[0]?.type).toBe("text");
|
||||
expect(JSON.parse(response.content[0]?.text ?? "{}")).toEqual(data);
|
||||
});
|
||||
|
||||
test("formatToolError formats error into MCP isError response", () => {
|
||||
const error = new OpenProjectNotFoundError("Project 99 not found");
|
||||
const response = formatToolError(error);
|
||||
expect(response.isError).toBe(true);
|
||||
expect(response.content).toHaveLength(1);
|
||||
expect(response.content[0]?.type).toBe("text");
|
||||
expect(response.content[0]?.text).toContain("Project 99 not found");
|
||||
});
|
||||
|
||||
test("formatToolError handles non-Error unknown values", () => {
|
||||
const response = formatToolError("unexpected failure string");
|
||||
expect(response.isError).toBe(true);
|
||||
expect(response.content).toHaveLength(1);
|
||||
expect(response.content[0]?.type).toBe("text");
|
||||
expect(response.content[0]?.text).toContain("unexpected failure string");
|
||||
});
|
||||
|
||||
test("registerTool registers tool with parameters on McpServer", () => {
|
||||
const server = new McpServer({ name: "test-server", version: "1.0.0" });
|
||||
const tool: ToolDefinition<{ id: z.ZodNumber }> = {
|
||||
name: "test_tool_with_params",
|
||||
description: "A test tool with parameters",
|
||||
parameters: { id: z.number() },
|
||||
readOnly: true,
|
||||
execute: async (args: { id: number }) => formatToolSuccess({ doubled: args.id * 2 }),
|
||||
};
|
||||
|
||||
registerTool(server, tool);
|
||||
expect(tool.name).toBe("test_tool_with_params");
|
||||
});
|
||||
|
||||
test("registerTool registers parameterless tool on McpServer", () => {
|
||||
const server = new McpServer({ name: "test-server", version: "1.0.0" });
|
||||
const tool: ToolDefinition = {
|
||||
name: "test_tool_no_params",
|
||||
description: "A test tool without parameters",
|
||||
readOnly: true,
|
||||
execute: async () => formatToolSuccess({ ok: true }),
|
||||
};
|
||||
|
||||
registerTool(server, tool);
|
||||
expect(tool.name).toBe("test_tool_no_params");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user