From 191e37252bcc93f88d13ab9493e27a0592bce127 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Sun, 6 Sep 2026 20:00:02 +0200 Subject: [PATCH] test(openapi): add live container tests, update tool count assertions, and update TODO.md --- docs/TODO.md | 8 ++++++++ tests/mcp-server.test.ts | 7 ++++--- tests/openapi.test.ts | 29 +++++++++++++++++++++++++++++ tests/read-only.test.ts | 6 +++--- tests/tools.test.ts | 11 ++++++----- 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/docs/TODO.md b/docs/TODO.md index f7a15be..5909340 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -58,6 +58,7 @@ - [x] Implement `src/services/work-packages.ts` (`listWorkPackages`, `getWorkPackage`, `searchWorkPackages`). - [x] Implement `src/services/queries.ts` (`listQueries`, `getQuery`, `getQueryResults`). - [x] Implement `src/services/metadata.ts` (`listStatuses`, `listTypes`, `listPriorities`, `listUsers`). + - [x] Implement `src/services/openapi.ts` (`getOpenApiSpec` with in-memory caching, summary mode, path endpoint lookup, tag filtering, schema inspection). - [x] Add service tests in `tests/services.test.ts` (unit tests and live container integration suite passing). - [x] **Task 3: MCP Tool Definitions & Schema Registration** @@ -72,6 +73,7 @@ - [x] `openproject_list_statuses` - [x] `openproject_list_priorities` - [x] `openproject_list_users` + - [x] `openproject_get_openapi_spec` - [x] **Task 4: MCP Stdio Server Assembly & Integration** - [x] Wire domain tools into `@modelcontextprotocol/sdk` Server using `StdioServerTransport` in `src/server.ts` and `src/index.ts`. @@ -80,6 +82,12 @@ - [x] Add end-to-end integration tests in `tests/mcp-server.test.ts` verifying tool calling against the running OpenProject 17 instance. - [x] Add read-only mode verification tests in `tests/read-only.test.ts`. +- [x] **Task 5: OpenAPI Introspection Tool & Service** + - [x] Implement `src/services/openapi.ts` with in-memory caching, multi-mode queries (summary, path, tag, schema), and ambient client resolution. + - [x] Implement `src/tools/openapi.ts` registering `openproject_get_openapi_spec` (`readOnly: true`) into `allTools` (bringing tool count to 11). + - [x] Add comprehensive test suite in `tests/openapi.test.ts` covering service methods, MCP client execution, error handling, and live OpenProject 17 container integration. + - [x] Update suite regressions and assertions across all test suites (`tests/tools.test.ts`, `tests/read-only.test.ts`, `tests/mcp-server.test.ts`). + --- ## 3. Backlog & Future Phases diff --git a/tests/mcp-server.test.ts b/tests/mcp-server.test.ts index 838f105..a15efed 100644 --- a/tests/mcp-server.test.ts +++ b/tests/mcp-server.test.ts @@ -5,7 +5,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { createServer, SERVER_NAME, SERVER_VERSION } from "../src/server"; describe("MCP Server Factory", () => { - test("creates server and lists all 10 tools via InMemoryTransport", async () => { + test("creates server and lists all 11 tools via InMemoryTransport", async () => { const config = { baseUrl: "http://localhost:8080", apiKey: "test-key", @@ -21,7 +21,7 @@ describe("MCP Server Factory", () => { await client.connect(clientTransport); const toolsResult = await client.listTools(); - expect(toolsResult.tools).toHaveLength(10); + expect(toolsResult.tools).toHaveLength(11); const names = toolsResult.tools.map((t) => t.name); expect(names).toContain("openproject_list_projects"); expect(names).toContain("openproject_get_project"); @@ -33,6 +33,7 @@ describe("MCP Server Factory", () => { expect(names).toContain("openproject_list_statuses"); expect(names).toContain("openproject_list_priorities"); expect(names).toContain("openproject_list_users"); + expect(names).toContain("openproject_get_openapi_spec"); await client.close(); await mcpServer.stop(); @@ -135,7 +136,7 @@ describe("MCP Server Factory", () => { // In read-only mode, only readOnly: true tools should be registered const toolsResult = await client.listTools(); - expect(toolsResult.tools).toHaveLength(10); + expect(toolsResult.tools).toHaveLength(11); await client.close(); await mcpServer.stop(); diff --git a/tests/openapi.test.ts b/tests/openapi.test.ts index e101f99..cdf05aa 100644 --- a/tests/openapi.test.ts +++ b/tests/openapi.test.ts @@ -238,3 +238,32 @@ describe("OpenApi MCP Tool Registration & Execution", () => { }); }); +describe("Live Container OpenAPI Integration", () => { + const liveBaseUrl = process.env.OPENPROJECT_BASE_URL || "http://localhost:8080"; + const liveApiKey = process.env.OPENPROJECT_API_KEY || ""; + const liveClient = new OpenProjectClient({ baseUrl: liveBaseUrl, apiKey: liveApiKey }); + + test("live query returns OpenProject API summary with paths and tags", async () => { + const summary = (await getOpenApiSpec({}, liveClient)) as OpenApiSummary; + expect(summary.title).toContain("OpenProject API"); + expect(summary.totalPaths).toBeGreaterThan(200); + expect(summary.tags.length).toBeGreaterThan(30); + expect(summary.availablePaths).toContain("/api/v3/work_packages"); + }); + + test("live query fetches operation details for /api/v3/work_packages", async () => { + const res = (await getOpenApiSpec({ path: "/api/v3/work_packages" }, liveClient)) as { + path: string; + operations: { + get?: { + parameters?: Array<{ name: string }>; + }; + }; + }; + expect(res.operations.get).toBeDefined(); + const paramNames = res.operations.get?.parameters?.map((p) => p.name) ?? []; + expect(paramNames).toContain("offset"); + expect(paramNames).toContain("pageSize"); + }); +}); + diff --git a/tests/read-only.test.ts b/tests/read-only.test.ts index 05f9c6b..16f7879 100644 --- a/tests/read-only.test.ts +++ b/tests/read-only.test.ts @@ -74,7 +74,7 @@ describe("Read-Only Mode Enforcement", () => { expect(configFalse.readOnly).toBe(false); }); - test("read-only server lists all 10 Phase 1 tools (all are readOnly: true)", async () => { + test("read-only server lists all 11 tools (all are readOnly: true)", async () => { const mcpServer = createServer({ baseUrl: "http://localhost:8080", apiKey: "test-key", @@ -87,7 +87,7 @@ describe("Read-Only Mode Enforcement", () => { await client.connect(clientTransport); const toolsResult = await client.listTools(); - expect(toolsResult.tools).toHaveLength(10); + expect(toolsResult.tools).toHaveLength(11); await client.close(); await mcpServer.stop(); @@ -118,7 +118,7 @@ describe("Read-Only Mode Enforcement", () => { const standaloneTools = await client2.listTools(); const standaloneNames = standaloneTools.tools.map((t) => t.name); expect(standaloneNames).not.toContain("openproject_create_work_package_synthetic"); - expect(standaloneTools.tools).toHaveLength(10); + expect(standaloneTools.tools).toHaveLength(11); await client2.close(); await standaloneServer.close(); diff --git a/tests/tools.test.ts b/tests/tools.test.ts index 25ad1eb..215b177 100644 --- a/tests/tools.test.ts +++ b/tests/tools.test.ts @@ -955,8 +955,8 @@ describe("Metadata Tools", () => { }); describe("Tool Registry", () => { - test("allTools contains exactly 10 Phase 1 tools", () => { - expect(allTools).toHaveLength(10); + test("allTools contains exactly 11 tools including OpenAPI spec", () => { + expect(allTools).toHaveLength(11); const names = allTools.map((t) => t.name); expect(names).toEqual([ "openproject_list_projects", @@ -969,6 +969,7 @@ describe("Tool Registry", () => { "openproject_list_statuses", "openproject_list_priorities", "openproject_list_users", + "openproject_get_openapi_spec", ]); }); @@ -978,7 +979,7 @@ describe("Tool Registry", () => { } }); - test("registerAllTools registers all 10 tools on McpServer", () => { + test("registerAllTools registers all 11 tools on McpServer", () => { const server = new McpServer({ name: "test-mcp", version: "1.0.0" }); registerAllTools(server); @@ -988,7 +989,7 @@ describe("Tool Registry", () => { } )._registeredTools; - expect(Object.keys(registeredTools)).toHaveLength(10); + expect(Object.keys(registeredTools)).toHaveLength(11); for (const tool of allTools) { expect(registeredTools[tool.name]).toBeDefined(); } @@ -1020,7 +1021,7 @@ describe("Tool Registry", () => { expect( registeredTools["openproject_synthetic_mutating_tool"] ).toBeUndefined(); - expect(Object.keys(registeredTools)).toHaveLength(10); + expect(Object.keys(registeredTools)).toHaveLength(11); for (const tool of allTools) { expect(registeredTools[tool.name]).toBeDefined(); }