Skip to content

Commit 96249d1

Browse files
feat: add MCP tools for OpenAPI parity (context, PRs, terms, Jira) (#139)
- Register currents-get-context (GET /context) - Add currents-list-pull-requests, currents-list-project-terms - Add Jira integration tools: list projects, list issue types, create issue - Fix get-context Zod schema export for MCP registration - Add targeted unit tests; update CHANGELOG and README tools table Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent b5582db commit 96249d1

11 files changed

Lines changed: 536 additions & 1 deletion

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ Give your AI coding agents full visibility into your CI test results. The Curren
2828
| `currents-get-projects` | Retrieves projects available in the Currents platform. |
2929
| `currents-get-project` | Get a single project by ID. |
3030
| `currents-get-project-insights` | Get aggregated run and test metrics for a project within a date range. |
31+
| `currents-list-pull-requests` | List pull-request cards for a project (runs grouped by meta.pr.id). |
32+
| `currents-list-project-terms` | List cursor-paginated project terms for one type (tag, branch, authorName, etc.). |
33+
| `currents-create-jira-issue` | Create a Jira issue from a run test using the organization Jira integration. |
34+
| `currents-list-jira-projects` | List Jira projects available for the organization integration. |
35+
| `currents-list-jira-issue-types` | List Jira issue types and custom fields for a Jira project. |
3136
| `currents-get-runs` | Retrieves a list of runs for a specific project with optional filtering. |
3237
| `currents-get-run-details` | Retrieves details of a specific test run. |
3338
| `currents-find-run` | Find a run by query parameters. |
@@ -40,6 +45,7 @@ Give your AI coding agents full visibility into your CI test results. The Curren
4045
| `currents-get-tests-performance` | Retrieves aggregated test metrics for a specific project within a date range. |
4146
| `currents-get-tests-signatures` | Generates a unique test signature based on project, spec file path, and test title. |
4247
| `currents-get-test-results` | Retrieves historical test execution results for a specific test signature. |
48+
| `currents-get-context` | Get test failure context for AI debugging at run, instance, or test level. |
4349
| `currents-get-errors-explorer` | Get aggregated error metrics for a project within a date range. |
4450
| `currents-list-webhooks` | List all webhooks for a project. |
4551
| `currents-create-webhook` | Create a new webhook for a project. |

mcp-server/CHANGELOG.md

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

3+
## Unreleased (2026-05-27)
4+
5+
### Added
6+
7+
- MCP tool `currents-get-context` for `GET /context` (registered; failure context for AI debugging).
8+
- MCP tool `currents-list-pull-requests` for `GET /projects/{projectId}/pull-requests`.
9+
- MCP tool `currents-list-project-terms` for `GET /projects/{projectId}/terms/{termType}`.
10+
- MCP tool `currents-create-jira-issue` for `POST /projects/{projectId}/jira/issues`.
11+
- MCP tool `currents-list-jira-projects` for `GET /integrations/jira/projects`.
12+
- MCP tool `currents-list-jira-issue-types` for `GET /integrations/jira/projects/{jiraProjectId}/issue-types`.
13+
14+
### Fixed
15+
16+
- `currents-get-context`: export full Zod schema (preserves superRefine validation) for MCP registration.
17+
318
## [2.3.1](https://github.com/currents-dev/currents-mcp/compare/v2.3.1-beta.0...v2.3.1) (2026-05-19)
419

520
- build process improvements, no user-facing changes

mcp-server/src/server.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,18 @@ import { getAffectedTestExecutionsTool } from "./tools/actions/get-affected-test
1616
import { listActionsTool } from "./tools/actions/list-actions.js";
1717
import { listAffectedTestsTool } from "./tools/actions/list-affected-tests.js";
1818
import { updateActionTool } from "./tools/actions/update-action.js";
19+
// Context tools
20+
import { getContextTool } from "./tools/context/get-context.js";
21+
// Integrations tools
22+
import { createJiraIssueFromRunTestTool } from "./tools/integrations/create-jira-issue.js";
23+
import { listJiraIssueTypesTool } from "./tools/integrations/list-jira-issue-types.js";
24+
import { listJiraProjectsTool } from "./tools/integrations/list-jira-projects.js";
1925
// Projects tools
2026
import { getProjectInsightsTool } from "./tools/projects/get-project-insights.js";
2127
import { getProjectTool } from "./tools/projects/get-project.js";
2228
import { getProjectsTool } from "./tools/projects/get-projects.js";
29+
import { listProjectPullRequestsTool } from "./tools/projects/list-project-pull-requests.js";
30+
import { listProjectTermsTool } from "./tools/projects/list-project-terms.js";
2331
// Runs tools
2432
import { cancelRunByGithubCITool } from "./tools/runs/cancel-run-github-ci.js";
2533
import { cancelRunTool } from "./tools/runs/cancel-run.js";
@@ -194,6 +202,57 @@ server.registerTool(
194202
getProjectInsightsTool.handler,
195203
);
196204

205+
206+
server.registerTool(
207+
"currents-list-pull-requests",
208+
{
209+
description:
210+
"List pull-request cards for a project (runs grouped by meta.pr.id). Supports cursor pagination, runs_per_pr preview count, and filters by tags, branches, authors, and latest-run status. Requires projectId.",
211+
inputSchema: listProjectPullRequestsTool.schema,
212+
},
213+
listProjectPullRequestsTool.handler,
214+
);
215+
216+
server.registerTool(
217+
"currents-list-project-terms",
218+
{
219+
description:
220+
"List cursor-paginated project terms for one type (tag, branch, authorName, etc.). Supports search, sort direction, and starting_after or ending_before cursors. Requires projectId and termType.",
221+
inputSchema: listProjectTermsTool.schema,
222+
},
223+
listProjectTermsTool.handler,
224+
);
225+
226+
server.registerTool(
227+
"currents-create-jira-issue",
228+
{
229+
description:
230+
"Create a Jira issue from a run test using the organization Jira integration. Requires projectId, runId, testId, jiraInstallationId, jiraProjectId, and jiraIssueType. Optional customFields array.",
231+
inputSchema: createJiraIssueFromRunTestTool.schema,
232+
},
233+
createJiraIssueFromRunTestTool.handler,
234+
);
235+
236+
server.registerTool(
237+
"currents-list-jira-projects",
238+
{
239+
description:
240+
"List Jira projects available for the organization integration. Use returned project IDs as jiraProjectId when creating issues. Requires jira_installation_id.",
241+
inputSchema: listJiraProjectsTool.schema,
242+
},
243+
listJiraProjectsTool.handler,
244+
);
245+
246+
server.registerTool(
247+
"currents-list-jira-issue-types",
248+
{
249+
description:
250+
"List Jira issue types and custom fields for a Jira project. Requires jiraProjectId and jira_installation_id.",
251+
inputSchema: listJiraIssueTypesTool.schema,
252+
},
253+
listJiraIssueTypesTool.handler,
254+
);
255+
197256
// Runs API tools
198257
server.registerTool(
199258
"currents-get-runs",
@@ -317,6 +376,18 @@ server.registerTool(
317376
getTestResultsTool.handler,
318377
);
319378

379+
380+
// Context API tools
381+
server.registerTool(
382+
"currents-get-context",
383+
{
384+
description:
385+
"Get test failure context for AI debugging at run, instance, or test level. Supports json or md format, detail level, and pagination for failed tests. Requires run_id for run-level, or instance_id with optional test_id.",
386+
inputSchema: getContextTool.schema,
387+
},
388+
getContextTool.handler,
389+
);
390+
320391
// Errors API tools
321392
server.registerTool(
322393
"currents-get-errors-explorer",

mcp-server/src/tools/context/get-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,6 @@ const handler = async (args: z.infer<typeof zodSchema>) => {
200200
};
201201

202202
export const getContextTool = {
203-
schema: zodSchema.shape,
203+
schema: zodSchema,
204204
handler,
205205
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { z } from "zod";
2+
import { postApi } from "../../lib/request.js";
3+
import { logger } from "../../lib/logger.js";
4+
5+
const customFieldSchema = z.object({
6+
fieldId: z.string().min(1).describe("Jira field ID from issue type discovery."),
7+
value: z.string().min(1).describe("String value for the Jira custom field."),
8+
});
9+
10+
const zodSchema = z.object({
11+
projectId: z.string().describe("Currents project ID."),
12+
runId: z.string().min(1).describe("Currents run ID containing the test."),
13+
testId: z.string().min(1).describe("Test ID within the run."),
14+
jiraInstallationId: z
15+
.string()
16+
.min(1)
17+
.describe("Jira installation ID for the org integration (dashboard Installation ID)."),
18+
jiraProjectId: z.string().min(1).describe("Jira project ID in which to create the issue."),
19+
jiraIssueType: z.string().min(1).describe("Jira issue type ID."),
20+
customFields: z
21+
.array(customFieldSchema)
22+
.optional()
23+
.describe("Optional Jira custom fields for issue creation."),
24+
});
25+
26+
const handler = async ({
27+
projectId,
28+
runId,
29+
testId,
30+
jiraInstallationId,
31+
jiraProjectId,
32+
jiraIssueType,
33+
customFields,
34+
}: z.infer<typeof zodSchema>) => {
35+
const body: Record<string, unknown> = {
36+
runId,
37+
testId,
38+
jiraInstallationId,
39+
jiraProjectId,
40+
jiraIssueType,
41+
};
42+
if (customFields?.length) {
43+
body.customFields = customFields;
44+
}
45+
46+
const path = `/projects/${encodeURIComponent(projectId)}/jira/issues`;
47+
logger.info(`Creating Jira issue: ${path}`);
48+
49+
const data = await postApi(path, body);
50+
if (!data) {
51+
return {
52+
content: [{ type: "text" as const, text: "Failed to create Jira issue" }],
53+
};
54+
}
55+
56+
return {
57+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
58+
};
59+
};
60+
61+
export const createJiraIssueFromRunTestTool = {
62+
schema: zodSchema,
63+
handler,
64+
};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { z } from "zod";
2+
import { fetchApi } from "../../lib/request.js";
3+
import { logger } from "../../lib/logger.js";
4+
5+
const zodSchema = z.object({
6+
jiraProjectId: z
7+
.string()
8+
.min(1)
9+
.max(128)
10+
.describe("Jira project ID."),
11+
jira_installation_id: z
12+
.string()
13+
.min(1)
14+
.describe("Jira installation ID for the organization integration."),
15+
search: z.string().optional().describe("Search issue types by name."),
16+
page: z
17+
.number()
18+
.int()
19+
.min(0)
20+
.optional()
21+
.describe("Page number for discovery results (default: 0)."),
22+
limit: z
23+
.number()
24+
.int()
25+
.min(1)
26+
.max(100)
27+
.optional()
28+
.describe("Maximum issue types per page (default: 50, max: 100)."),
29+
});
30+
31+
const handler = async ({
32+
jiraProjectId,
33+
jira_installation_id,
34+
search,
35+
page,
36+
limit,
37+
}: z.infer<typeof zodSchema>) => {
38+
const queryParams = new URLSearchParams();
39+
queryParams.append("jira_installation_id", jira_installation_id);
40+
if (search) {
41+
queryParams.append("search", search);
42+
}
43+
if (page !== undefined) {
44+
queryParams.append("page", page.toString());
45+
}
46+
if (limit !== undefined) {
47+
queryParams.append("limit", limit.toString());
48+
}
49+
50+
const path = `/integrations/jira/projects/${encodeURIComponent(jiraProjectId)}/issue-types?${queryParams.toString()}`;
51+
logger.info(`Listing Jira issue types: ${path}`);
52+
53+
const data = await fetchApi(path);
54+
if (!data) {
55+
return {
56+
content: [{ type: "text" as const, text: "Failed to list Jira issue types" }],
57+
};
58+
}
59+
60+
return {
61+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
62+
};
63+
};
64+
65+
export const listJiraIssueTypesTool = {
66+
schema: zodSchema,
67+
handler,
68+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import * as request from "../../lib/request.js";
3+
import { listJiraProjectsTool } from "./list-jira-projects.js";
4+
5+
describe("listJiraProjectsTool", () => {
6+
it("calls GET /integrations/jira/projects with required installation id", async () => {
7+
vi.spyOn(request, "fetchApi").mockResolvedValue({ status: "OK", data: [] });
8+
9+
await listJiraProjectsTool.handler({
10+
jira_installation_id: "inst-1",
11+
search: "core",
12+
page: 1,
13+
limit: 25,
14+
});
15+
16+
const url = vi.mocked(request.fetchApi).mock.calls[0][0] as string;
17+
expect(url).toBe(
18+
"/integrations/jira/projects?jira_installation_id=inst-1&search=core&page=1&limit=25"
19+
);
20+
});
21+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { z } from "zod";
2+
import { fetchApi } from "../../lib/request.js";
3+
import { logger } from "../../lib/logger.js";
4+
5+
const zodSchema = z.object({
6+
jira_installation_id: z
7+
.string()
8+
.min(1)
9+
.describe("Jira installation ID for the organization integration."),
10+
search: z.string().optional().describe("Search Jira projects by name or key."),
11+
page: z
12+
.number()
13+
.int()
14+
.min(0)
15+
.optional()
16+
.describe("Page number for discovery results (default: 0)."),
17+
limit: z
18+
.number()
19+
.int()
20+
.min(1)
21+
.max(100)
22+
.optional()
23+
.describe("Maximum projects per page (default: 50, max: 100)."),
24+
});
25+
26+
const handler = async ({
27+
jira_installation_id,
28+
search,
29+
page,
30+
limit,
31+
}: z.infer<typeof zodSchema>) => {
32+
const queryParams = new URLSearchParams();
33+
queryParams.append("jira_installation_id", jira_installation_id);
34+
if (search) {
35+
queryParams.append("search", search);
36+
}
37+
if (page !== undefined) {
38+
queryParams.append("page", page.toString());
39+
}
40+
if (limit !== undefined) {
41+
queryParams.append("limit", limit.toString());
42+
}
43+
44+
const path = `/integrations/jira/projects?${queryParams.toString()}`;
45+
logger.info(`Listing Jira projects: ${path}`);
46+
47+
const data = await fetchApi(path);
48+
if (!data) {
49+
return {
50+
content: [{ type: "text" as const, text: "Failed to list Jira projects" }],
51+
};
52+
}
53+
54+
return {
55+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
56+
};
57+
};
58+
59+
export const listJiraProjectsTool = {
60+
schema: zodSchema,
61+
handler,
62+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import * as request from "../../lib/request.js";
3+
import { listProjectPullRequestsTool } from "./list-project-pull-requests.js";
4+
5+
describe("listProjectPullRequestsTool", () => {
6+
it("serializes query per OpenAPI (repeated status, bracket arrays)", async () => {
7+
vi.spyOn(request, "fetchApi").mockResolvedValue({ status: "OK", data: [] });
8+
9+
await listProjectPullRequestsTool.handler({
10+
projectId: "p1",
11+
limit: 20,
12+
status: ["PASSED", "FAILED"],
13+
tags: ["smoke"],
14+
branches: ["main"],
15+
authors: ["dev@*"],
16+
});
17+
18+
const url = vi.mocked(request.fetchApi).mock.calls[0][0] as string;
19+
expect(url).toContain("/projects/p1/pull-requests?");
20+
expect(url).toContain("limit=20");
21+
expect(url).toContain("status=PASSED");
22+
expect(url).toContain("status=FAILED");
23+
expect(url).toContain("tags%5B%5D=smoke");
24+
expect(url).toContain("branches%5B%5D=main");
25+
expect(url).toContain("authors%5B%5D=dev%40");
26+
});
27+
});

0 commit comments

Comments
 (0)