From 75c2db9cc304855c8c5594212b45550ec36731c7 Mon Sep 17 00:00:00 2001 From: mikemolinet Date: Fri, 1 May 2026 15:54:31 -0700 Subject: [PATCH] feat(tools): add cueapi_fire_cue for firing existing cues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an MCP tool that wraps the existing POST /v1/cues/{id}/fire endpoint, with optional payload_override and merge_strategy. The existing tool surface covers create / list / get / pause / resume / delete / list_executions / report_outcome — but not "fire an existing cue right now," which is the primitive an agent needs for ad-hoc one-shot triggers and for using cues as a lightweight messaging channel between agents (carrying { message, instruction, task, reply_cue_id } in payload_override). Without this tool, MCP-host agents had to fall back to raw HTTP via shell to invoke this endpoint, which is awkward and unguided. - src/tools.ts: +27 LoC (Zod schema + tool definition) - tests/tools.test.ts: +50 LoC (3 HTTP-contract tests mirroring the existing pause/resume pattern: empty-fire, payload_override + merge, url-encoded cue_id) - README.md: add row to "Tools exposed" table + changelog entry - package.json: bump 0.2.0 → 0.3.0 No backend changes — POST /v1/cues/{id}/fire already exists and is used by the CLI today. All 48 tests pass (was 45). Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 ++ package-lock.json | 8 ++----- package.json | 2 +- src/tools.ts | 32 ++++++++++++++++++++++++++ tests/tools.test.ts | 56 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 82bb045..5425008 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Any MCP host that supports stdio servers can run this. Point the host at the `cu | `cueapi_create_cue` | Create a recurring (cron) or one-time (`at`) cue | | `cueapi_list_cues` | List cues, filter by status | | `cueapi_get_cue` | Fetch details for a single cue | +| `cueapi_fire_cue` | Fire an existing cue immediately, optional payload override | | `cueapi_pause_cue` | Pause a cue so it stops firing | | `cueapi_resume_cue` | Resume a paused cue | | `cueapi_delete_cue` | Delete a cue permanently | @@ -81,6 +82,7 @@ npm run dev # run the server locally with tsx ## Changelog +- **0.3.0.** Add `cueapi_fire_cue` tool: fire an existing cue immediately with an optional `payload_override` (and `merge_strategy: 'replace' | 'merge'`). Wraps the existing `POST /v1/cues/{id}/fire` endpoint. Useful for ad-hoc one-shot triggers and for using cues as a messaging channel between agents (carry `{ message, instruction, task, reply_cue_id }` in `payload_override`). - **0.1.4.** Fix `cueapi_pause_cue` / `cueapi_resume_cue` to use `PATCH /v1/cues/{id}` with `{"status": "paused" | "active"}` (previously called non-existent `/pause` and `/resume` endpoints, returning a runtime 404). PR [#1](https://github.com/cueapi/cueapi-mcp/pull/1). This is the release that actually contains the fix; 0.1.3 was published prematurely with this note but without the merged code. - **0.1.3.** Premature publish, superseded by 0.1.4. No functional changes from 0.1.2. - **0.1.2.** Register with the Official MCP Registry. diff --git a/package-lock.json b/package-lock.json index 64a8506..ff4109d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cueapi/mcp", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cueapi/mcp", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", @@ -2116,7 +2116,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -2455,7 +2454,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.13.tgz", "integrity": "sha512-cURhqIxgqO41Ae6/UthPZlsLxMRhRnkF1bY3no8b7YI4nP3klwag/fTfWT+YZAdq8UyusypunAVwl4AkTe++QQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -3619,7 +3617,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -4236,7 +4233,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 2d45208..2de4ba5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cueapi/mcp", - "version": "0.2.0", + "version": "0.3.0", "mcpName": "io.github.govindkavaturi-art/cueapi-mcp", "description": "Official Model Context Protocol (MCP) server for CueAPI — give your AI agent a scheduler and verification gate. Open-source execution accountability primitive for AI agents.", "type": "module", diff --git a/src/tools.ts b/src/tools.ts index 0968384..656d68b 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -80,6 +80,22 @@ const listExecutionsSchema = z.object({ offset: z.number().int().min(0).optional(), }); +const fireCueSchema = z.object({ + cue_id: z.string().describe("CueAPI cue ID to fire (e.g. 'cue_...')"), + payload_override: z + .record(z.unknown()) + .optional() + .describe( + "Override the cue's default payload for this fire only. Common fields when using cues for ad-hoc messaging: { message, instruction, task, reply_cue_id, routing, from }." + ), + merge_strategy: z + .enum(["replace", "merge"]) + .optional() + .describe( + "How payload_override is applied. 'replace' = use override as-is. 'merge' = shallow-merge with cue's default. Default 'replace'." + ), +}); + const reportOutcomeSchema = z.object({ execution_id: z.string(), success: z.boolean(), @@ -130,6 +146,22 @@ export const tools: ToolDefinition[] = [ handler: async (client, args) => client.request("GET", `/v1/cues/${encodeURIComponent(args.cue_id)}`), }, + { + name: "cueapi_fire_cue", + description: + "Fire an existing cue immediately, optionally overriding its payload for this single invocation. The primary primitive for ad-hoc one-shot triggers and for using cues as a messaging channel between agents (with payload_override carrying { message, instruction, task, reply_cue_id }).", + schema: fireCueSchema, + handler: async (client, args) => { + const body: Record = {}; + if (args.payload_override) body.payload_override = args.payload_override; + if (args.merge_strategy) body.merge_strategy = args.merge_strategy; + return client.request( + "POST", + `/v1/cues/${encodeURIComponent(args.cue_id)}/fire`, + body + ); + }, + }, { name: "cueapi_pause_cue", description: "Pause a cue. Paused cues do not fire until resumed.", diff --git a/tests/tools.test.ts b/tests/tools.test.ts index 95adc1a..80e3b5e 100644 --- a/tests/tools.test.ts +++ b/tests/tools.test.ts @@ -36,6 +36,7 @@ describe("cueapi-mcp tool surface", () => { "cueapi_create_cue", "cueapi_list_cues", "cueapi_get_cue", + "cueapi_fire_cue", "cueapi_delete_cue", "cueapi_list_executions", "cueapi_report_outcome", @@ -97,3 +98,58 @@ describe("cueapi_pause_cue / cueapi_resume_cue — HTTP contract", () => { expect(calls[0].path).toBe("/v1/cues/cue%2Fwith%2Fslashes"); }); }); + +describe("cueapi_fire_cue — HTTP contract", () => { + // CueAPI fire endpoint is POST /v1/cues/{id}/fire. Body may include + // payload_override (overrides the cue's default payload for this fire only) + // and merge_strategy ('replace' | 'merge'). These tests pin the handler's + // HTTP behavior so a regression to the wrong path/method is caught at CI. + + function findTool(name: string) { + const t = tools.find((x) => x.name === name); + if (!t) throw new Error(`tool ${name} missing`); + return t; + } + + function stubClient() { + const calls: Array<{ method: string; path: string; body?: unknown; query?: unknown }> = []; + const client = { + request: vi.fn(async (method: string, path: string, body?: unknown, query?: unknown) => { + calls.push({ method, path, body, query }); + return { execution_id: "exec_test", status: "queued" }; + }), + } as unknown as CueAPIClient; + return { client, calls }; + } + + it("fires with no payload_override → POST /v1/cues/{id}/fire with empty body", async () => { + const tool = findTool("cueapi_fire_cue"); + const { client, calls } = stubClient(); + await tool.handler(client, { cue_id: "cue_abc123" }); + + expect(calls).toHaveLength(1); + expect(calls[0].method).toBe("POST"); + expect(calls[0].path).toBe("/v1/cues/cue_abc123/fire"); + expect(calls[0].body).toEqual({}); + }); + + it("includes payload_override + merge_strategy in body when provided", async () => { + const tool = findTool("cueapi_fire_cue"); + const { client, calls } = stubClient(); + const payload = { message: "hello", task: "downstream-handler", reply_cue_id: "cue_xyz" }; + await tool.handler(client, { + cue_id: "cue_abc123", + payload_override: payload, + merge_strategy: "replace", + }); + + expect(calls[0].body).toEqual({ payload_override: payload, merge_strategy: "replace" }); + }); + + it("url-encodes the cue_id in the path", async () => { + const tool = findTool("cueapi_fire_cue"); + const { client, calls } = stubClient(); + await tool.handler(client, { cue_id: "cue/with/slashes" }); + expect(calls[0].path).toBe("/v1/cues/cue%2Fwith%2Fslashes/fire"); + }); +});