Skip to content

Commit f50c729

Browse files
github-actions[bot]cursoragentmiguelangaranocurrents
authored
Parity: currents-mcp ↔ Currents API (#124)
* feat(mcp): add GET /context tool and OpenAPI parity fixes - Add currents-get-context for failure context API - Fix list-affected-tests action_type[] query serialization - Require non-empty body for webhook updates; postApi handles 201/empty JSON - Add tests; fix changelog formatting * ci: run TypeScript build before unit tests Co-authored-by: miguelangaranocurrents <miguelangaranocurrents@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: miguelangaranocurrents <miguelangaranocurrents@users.noreply.github.com>
1 parent 7bd0e9b commit f50c729

11 files changed

Lines changed: 345 additions & 15 deletions

File tree

.github/workflows/test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ jobs:
2626
working-directory: ./mcp-server
2727
run: npm ci
2828

29+
- name: Build
30+
working-directory: ./mcp-server
31+
run: npm run build
32+
2933
- name: Run tests
3034
working-directory: ./mcp-server
3135
run: npm run test:run

mcp-server/CHANGELOG.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## Unreleased (2026-04-20)
4+
5+
### Added
6+
7+
* MCP tool `currents-get-context` for `GET /context` (test failure context for AI debugging).
8+
9+
### Fixed
10+
11+
* `currents-list-affected-tests`: serialize `action_type` query params as `action_type[]` to match OpenAPI.
12+
* `currents-update-webhook`: require at least one updatable field so the HTTP request body is never empty (matches OpenAPI `requestBody.required: true`).
13+
* `postApi`: treat HTTP 201 and empty JSON bodies like other successful POST responses.
14+
315
## [2.2.5](https://github.com/currents-dev/currents-mcp/compare/v2.2.4...v2.2.5) (2026-02-05)
416

517
### Bug Fixes
@@ -10,7 +22,9 @@
1022

1123
### Documentation
1224

13-
* add comprehensive parity analysis documentation ([#47](https://github.com/currents-dev/currents-mcp/pull/47))## [2.2.4](https://github.com/currents-dev/currents-mcp/compare/v2.2.3...v2.2.4) (2026-01-27)
25+
* add comprehensive parity analysis documentation ([#47](https://github.com/currents-dev/currents-mcp/pull/47))
26+
27+
## [2.2.4](https://github.com/currents-dev/currents-mcp/compare/v2.2.3...v2.2.4) (2026-01-27)
1428

1529
## [2.2.3](https://github.com/currents-dev/currents-mcp/compare/v2.2.1...v2.2.3) (2026-01-27)
1630

mcp-server/src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { getTestsPerformanceTool } from "./tools/tests/get-tests-performance.js"
3535
import { getTestSignatureTool } from "./tools/tests/get-tests-signature.js";
3636
// Errors tools
3737
import { getErrorsExplorerTool } from "./tools/errors/get-errors-explorer.js";
38+
import { getContextTool } from "./tools/context/get-context.js";
3839
// Webhooks tools
3940
import { listWebhooksTool } from "./tools/webhooks/list-webhooks.js";
4041
import { createWebhookTool } from "./tools/webhooks/create-webhook.js";
@@ -103,7 +104,7 @@ server.tool(
103104

104105
server.tool(
105106
"currents-list-affected-tests",
106-
"List tests affected by actions (quarantine, skip, tag) for a project within a date range. Returns aggregated data grouped by test signature. Supports filtering by action types, action ID, status, and search. Requires projectId, date_start, and date_end. (Preview endpoint: fields and path may change.)",
107+
"List tests affected by actions (quarantine, skip, tag) for a project within a date range. Returns aggregated data grouped by test signature. Supports filtering by action types, action ID, status, and search. Requires projectId, date_start, and date_end.",
107108
listAffectedTestsTool.schema,
108109
listAffectedTestsTool.handler
109110
);
@@ -239,6 +240,13 @@ server.tool(
239240
getErrorsExplorerTool.handler
240241
);
241242

243+
server.tool(
244+
"currents-get-context",
245+
"Retrieve structured test failure context for AI debugging (GET /context). Supports run-level (run_id only), instance-level (run_id + instance_id), or test-level (instance_id + test_id). Optional format json|md, detail default|compact|summary, and pagination limit/page for run/instance levels.",
246+
getContextTool.schema,
247+
getContextTool.handler
248+
);
249+
242250
// Webhooks API tools
243251
server.tool(
244252
"currents-list-webhooks",

mcp-server/src/lib/request.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,14 @@ export async function postApi<T, B>(path: string, body: B): Promise<T | null> {
4949
logger.error(response);
5050
return null;
5151
}
52-
return (await response.json()) as T;
52+
if (response.status === 204 || response.headers.get("content-length") === "0") {
53+
return {} as T;
54+
}
55+
const contentType = response.headers.get("content-type");
56+
if (contentType && contentType.includes("application/json")) {
57+
return (await response.json()) as T;
58+
}
59+
return {} as T;
5360
} catch (error: any) {
5461
logger.error("Error making Currents POST request:", error.toString());
5562
return null;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import * as request from "../../lib/request.js";
3+
import { listAffectedTestsTool } from "./list-affected-tests.js";
4+
5+
vi.mock("../../lib/request.js");
6+
7+
describe("listAffectedTestsTool", () => {
8+
beforeEach(() => {
9+
vi.clearAllMocks();
10+
});
11+
12+
it("serializes action_type as action_type[] per OpenAPI", async () => {
13+
vi.spyOn(request, "fetchApi").mockResolvedValue({ status: "OK", data: [] });
14+
15+
await listAffectedTestsTool.handler({
16+
projectId: "p1",
17+
date_start: "2026-01-01",
18+
date_end: "2026-01-02",
19+
action_type: ["skip", "tag"],
20+
});
21+
22+
expect(request.fetchApi).toHaveBeenCalledWith(
23+
expect.stringContaining("action_type%5B%5D=skip")
24+
);
25+
expect(request.fetchApi).toHaveBeenCalledWith(
26+
expect.stringContaining("action_type%5B%5D=tag")
27+
);
28+
});
29+
});

mcp-server/src/tools/actions/list-affected-tests.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const handler = async ({
7474
}
7575

7676
if (action_type && action_type.length > 0) {
77-
action_type.forEach((t) => queryParams.append("action_type", t));
77+
action_type.forEach((t) => queryParams.append("action_type[]", t));
7878
}
7979

8080
if (action_id) {

mcp-server/src/tools/actions/update-action.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ const handler = async ({
132132
}
133133

134134
const body: UpdateActionRequest = {};
135-
135+
136136
if (name !== undefined) body.name = name;
137137
if (description !== undefined) body.description = description;
138138
if (action !== undefined) body.action = action;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
vi.mock("../../lib/env.js", () => ({
4+
CURRENTS_API_KEY: "k",
5+
CURRENTS_API_URL: "https://api.test.com/v1",
6+
}));
7+
8+
const { getContextTool } = await import("./get-context.js");
9+
10+
describe("getContextTool", () => {
11+
beforeEach(() => {
12+
vi.stubGlobal(
13+
"fetch",
14+
vi.fn().mockResolvedValue({
15+
ok: true,
16+
headers: new Headers({ "content-type": "application/json" }),
17+
json: async () => ({ status: "OK", data: { level: "run" } }),
18+
})
19+
);
20+
});
21+
22+
afterEach(() => {
23+
vi.unstubAllGlobals();
24+
vi.clearAllMocks();
25+
});
26+
27+
it("calls GET /context with query params for run-level", async () => {
28+
await getContextTool.handler({
29+
run_id: "run-1",
30+
format: "json",
31+
detail: "default",
32+
limit: 10,
33+
page: 0,
34+
});
35+
36+
expect(fetch).toHaveBeenCalledWith(
37+
"https://api.test.com/v1/context?run_id=run-1&format=json&detail=default&limit=10&page=0",
38+
expect.objectContaining({
39+
headers: expect.objectContaining({
40+
Authorization: "Bearer k",
41+
Accept: "application/json",
42+
}),
43+
})
44+
);
45+
});
46+
47+
it("rejects test_id without instance_id", async () => {
48+
const result = await getContextTool.handler({
49+
run_id: "run-1",
50+
test_id: "t1",
51+
} as any);
52+
53+
expect(result.content[0].type).toBe("text");
54+
expect(String((result.content[0] as { text: string }).text)).toContain(
55+
"Invalid parameters"
56+
);
57+
expect(fetch).not.toHaveBeenCalled();
58+
});
59+
});

0 commit comments

Comments
 (0)