diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..313d03d --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,52 @@ +/** + * Model Context Protocol (MCP) Tools Registry and Barrel Export. + */ + +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { registerTool, type ToolDefinition } from "./common"; +import { projectTools } from "./projects"; +import { workPackageTools } from "./work-packages"; +import { queryTools } from "./queries"; +import { metadataTools } from "./metadata"; + +export * from "./common"; +export * from "./projects"; +export * from "./work-packages"; +export * from "./queries"; +export * from "./metadata"; + +export type AnyToolDefinition = + | (typeof projectTools)[number] + | (typeof workPackageTools)[number] + | (typeof queryTools)[number] + | (typeof metadataTools)[number]; + +export const allTools: AnyToolDefinition[] = [ + ...projectTools, + ...workPackageTools, + ...queryTools, + ...metadataTools, +]; + +export interface RegisterToolsOptions { + readOnly?: boolean; +} + +/** + * Registers all OpenProject tools with the given McpServer instance. + * If options.readOnly is true, tools not marked as readOnly are skipped. + */ +export function registerAllTools( + server: McpServer, + options?: RegisterToolsOptions +): void { + const isReadOnly = options?.readOnly ?? false; + + for (const tool of allTools) { + // If server is in read-only mode, skip any non-read-only tools + if (isReadOnly && !tool.readOnly) { + continue; + } + registerTool(server, tool); + } +} diff --git a/tests/tools.test.ts b/tests/tools.test.ts index db4dcdb..1c6af3f 100644 --- a/tests/tools.test.ts +++ b/tests/tools.test.ts @@ -1,4 +1,10 @@ import { describe, expect, test } from "bun:test"; +import { + allTools, + registerAllTools, + type RegisterToolsOptions, + type AnyToolDefinition, +} from "../src/tools/index"; import { formatToolSuccess, formatToolError, @@ -929,5 +935,59 @@ describe("Metadata Tools", () => { }); }); +describe("Tool Registry", () => { + test("allTools contains exactly 10 Phase 1 tools", () => { + expect(allTools).toHaveLength(10); + const names = allTools.map((t) => t.name); + expect(names).toEqual([ + "openproject_list_projects", + "openproject_get_project", + "openproject_list_work_packages", + "openproject_get_work_package", + "openproject_list_queries", + "openproject_get_query", + "openproject_list_types", + "openproject_list_statuses", + "openproject_list_priorities", + "openproject_list_users", + ]); + }); + + test("all tools in Phase 1 are marked readOnly: true", () => { + for (const tool of allTools) { + expect(tool.readOnly).toBe(true); + } + }); + + test("registerAllTools registers all 10 tools on McpServer", () => { + const server = new McpServer({ name: "test-mcp", version: "1.0.0" }); + registerAllTools(server); + + const registeredTools = ( + server as unknown as { + _registeredTools: Record; + } + )._registeredTools; + + expect(Object.keys(registeredTools)).toHaveLength(10); + for (const tool of allTools) { + expect(registeredTools[tool.name]).toBeDefined(); + } + }); + + test("registerAllTools filters out non-readOnly tools when readOnly is true", () => { + const server = new McpServer({ name: "test-mcp", version: "1.0.0" }); + registerAllTools(server, { readOnly: true }); + + const registeredTools = ( + server as unknown as { + _registeredTools: Record; + } + )._registeredTools; + + expect(Object.keys(registeredTools)).toHaveLength(10); + }); +}); +