Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 2 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
32 changes: 32 additions & 0 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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<string, unknown> = {};
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.",
Expand Down
56 changes: 56 additions & 0 deletions tests/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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");
});
});