Skip to content
Merged
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ Give your AI coding agents full visibility into your CI test results. The Curren
| `currents-get-projects` | Retrieves projects available in the Currents platform. |
| `currents-get-project` | Get a single project by ID. |
| `currents-get-project-insights` | Get aggregated run and test metrics for a project within a date range. |
| `currents-list-pull-requests` | List pull-request cards for a project (runs grouped by meta.pr.id). |
| `currents-list-project-terms` | List cursor-paginated project terms for one type (tag, branch, authorName, etc.). |
| `currents-create-jira-issue` | Create a Jira issue from a run test using the organization Jira integration. |
| `currents-list-jira-projects` | List Jira projects available for the organization integration. |
| `currents-list-jira-issue-types` | List Jira issue types and custom fields for a Jira project. |
| `currents-get-runs` | Retrieves a list of runs for a specific project with optional filtering. |
| `currents-get-run-details` | Retrieves details of a specific test run. |
| `currents-find-run` | Find a run by query parameters. |
Expand All @@ -40,6 +45,7 @@ Give your AI coding agents full visibility into your CI test results. The Curren
| `currents-get-tests-performance` | Retrieves aggregated test metrics for a specific project within a date range. |
| `currents-get-tests-signatures` | Generates a unique test signature based on project, spec file path, and test title. |
| `currents-get-test-results` | Retrieves historical test execution results for a specific test signature. |
| `currents-get-context` | Get test failure context for AI debugging at run, instance, or test level. |
| `currents-get-errors-explorer` | Get aggregated error metrics for a project within a date range. |
| `currents-list-webhooks` | List all webhooks for a project. |
| `currents-create-webhook` | Create a new webhook for a project. |
Expand Down
15 changes: 15 additions & 0 deletions mcp-server/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## Unreleased (2026-05-27)

### Added

- MCP tool `currents-get-context` for `GET /context` (registered; failure context for AI debugging).
- MCP tool `currents-list-pull-requests` for `GET /projects/{projectId}/pull-requests`.
- MCP tool `currents-list-project-terms` for `GET /projects/{projectId}/terms/{termType}`.
- MCP tool `currents-create-jira-issue` for `POST /projects/{projectId}/jira/issues`.
- MCP tool `currents-list-jira-projects` for `GET /integrations/jira/projects`.
- MCP tool `currents-list-jira-issue-types` for `GET /integrations/jira/projects/{jiraProjectId}/issue-types`.

### Fixed

- `currents-get-context`: export full Zod schema (preserves superRefine validation) for MCP registration.

## [2.3.1](https://github.com/currents-dev/currents-mcp/compare/v2.3.1-beta.0...v2.3.1) (2026-05-19)

- build process improvements, no user-facing changes
Expand Down
71 changes: 71 additions & 0 deletions mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@ import { getAffectedTestExecutionsTool } from "./tools/actions/get-affected-test
import { listActionsTool } from "./tools/actions/list-actions.js";
import { listAffectedTestsTool } from "./tools/actions/list-affected-tests.js";
import { updateActionTool } from "./tools/actions/update-action.js";
// Context tools
import { getContextTool } from "./tools/context/get-context.js";
// Integrations tools
import { createJiraIssueFromRunTestTool } from "./tools/integrations/create-jira-issue.js";
import { listJiraIssueTypesTool } from "./tools/integrations/list-jira-issue-types.js";
import { listJiraProjectsTool } from "./tools/integrations/list-jira-projects.js";
// Projects tools
import { getProjectInsightsTool } from "./tools/projects/get-project-insights.js";
import { getProjectTool } from "./tools/projects/get-project.js";
import { getProjectsTool } from "./tools/projects/get-projects.js";
import { listProjectPullRequestsTool } from "./tools/projects/list-project-pull-requests.js";
import { listProjectTermsTool } from "./tools/projects/list-project-terms.js";
// Runs tools
import { cancelRunByGithubCITool } from "./tools/runs/cancel-run-github-ci.js";
import { cancelRunTool } from "./tools/runs/cancel-run.js";
Expand Down Expand Up @@ -194,6 +202,57 @@ server.registerTool(
getProjectInsightsTool.handler,
);


server.registerTool(
"currents-list-pull-requests",
{
description:
"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.",
inputSchema: listProjectPullRequestsTool.schema,
},
listProjectPullRequestsTool.handler,
);

server.registerTool(
"currents-list-project-terms",
{
description:
"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.",
inputSchema: listProjectTermsTool.schema,
},
listProjectTermsTool.handler,
);

server.registerTool(
"currents-create-jira-issue",
{
description:
"Create a Jira issue from a run test using the organization Jira integration. Requires projectId, runId, testId, jiraInstallationId, jiraProjectId, and jiraIssueType. Optional customFields array.",
inputSchema: createJiraIssueFromRunTestTool.schema,
},
createJiraIssueFromRunTestTool.handler,
);

server.registerTool(
"currents-list-jira-projects",
{
description:
"List Jira projects available for the organization integration. Use returned project IDs as jiraProjectId when creating issues. Requires jira_installation_id.",
inputSchema: listJiraProjectsTool.schema,
},
listJiraProjectsTool.handler,
);

server.registerTool(
"currents-list-jira-issue-types",
{
description:
"List Jira issue types and custom fields for a Jira project. Requires jiraProjectId and jira_installation_id.",
inputSchema: listJiraIssueTypesTool.schema,
},
listJiraIssueTypesTool.handler,
);

// Runs API tools
server.registerTool(
"currents-get-runs",
Expand Down Expand Up @@ -317,6 +376,18 @@ server.registerTool(
getTestResultsTool.handler,
);


// Context API tools
server.registerTool(
"currents-get-context",
{
description:
"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.",
inputSchema: getContextTool.schema,
},
getContextTool.handler,
);

// Errors API tools
server.registerTool(
"currents-get-errors-explorer",
Expand Down
2 changes: 1 addition & 1 deletion mcp-server/src/tools/context/get-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,6 @@ const handler = async (args: z.infer<typeof zodSchema>) => {
};

export const getContextTool = {
schema: zodSchema.shape,
schema: zodSchema,
handler,
};
64 changes: 64 additions & 0 deletions mcp-server/src/tools/integrations/create-jira-issue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { z } from "zod";
import { postApi } from "../../lib/request.js";
import { logger } from "../../lib/logger.js";

const customFieldSchema = z.object({
fieldId: z.string().min(1).describe("Jira field ID from issue type discovery."),
value: z.string().min(1).describe("String value for the Jira custom field."),
});

const zodSchema = z.object({
projectId: z.string().describe("Currents project ID."),
runId: z.string().min(1).describe("Currents run ID containing the test."),
testId: z.string().min(1).describe("Test ID within the run."),
jiraInstallationId: z
.string()
.min(1)
.describe("Jira installation ID for the org integration (dashboard Installation ID)."),
jiraProjectId: z.string().min(1).describe("Jira project ID in which to create the issue."),
jiraIssueType: z.string().min(1).describe("Jira issue type ID."),
customFields: z
.array(customFieldSchema)
.optional()
.describe("Optional Jira custom fields for issue creation."),
});

const handler = async ({
projectId,
runId,
testId,
jiraInstallationId,
jiraProjectId,
jiraIssueType,
customFields,
}: z.infer<typeof zodSchema>) => {
const body: Record<string, unknown> = {
runId,
testId,
jiraInstallationId,
jiraProjectId,
jiraIssueType,
};
if (customFields?.length) {
body.customFields = customFields;
}

const path = `/projects/${encodeURIComponent(projectId)}/jira/issues`;
logger.info(`Creating Jira issue: ${path}`);

const data = await postApi(path, body);
if (!data) {
return {
content: [{ type: "text" as const, text: "Failed to create Jira issue" }],
};
}

return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
};
};

export const createJiraIssueFromRunTestTool = {
schema: zodSchema,
handler,
};
68 changes: 68 additions & 0 deletions mcp-server/src/tools/integrations/list-jira-issue-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { z } from "zod";
import { fetchApi } from "../../lib/request.js";
import { logger } from "../../lib/logger.js";

const zodSchema = z.object({
jiraProjectId: z
.string()
.min(1)
.max(128)
.describe("Jira project ID."),
jira_installation_id: z
.string()
.min(1)
.describe("Jira installation ID for the organization integration."),
search: z.string().optional().describe("Search issue types by name."),
page: z
.number()
.int()
.min(0)
.optional()
.describe("Page number for discovery results (default: 0)."),
limit: z
.number()
.int()
.min(1)
.max(100)
.optional()
.describe("Maximum issue types per page (default: 50, max: 100)."),
});

const handler = async ({
jiraProjectId,
jira_installation_id,
search,
page,
limit,
}: z.infer<typeof zodSchema>) => {
const queryParams = new URLSearchParams();
queryParams.append("jira_installation_id", jira_installation_id);
if (search) {
queryParams.append("search", search);
}
if (page !== undefined) {
queryParams.append("page", page.toString());
}
if (limit !== undefined) {
queryParams.append("limit", limit.toString());
}

const path = `/integrations/jira/projects/${encodeURIComponent(jiraProjectId)}/issue-types?${queryParams.toString()}`;
logger.info(`Listing Jira issue types: ${path}`);

const data = await fetchApi(path);
if (!data) {
return {
content: [{ type: "text" as const, text: "Failed to list Jira issue types" }],
};
}

return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
};
};

export const listJiraIssueTypesTool = {
schema: zodSchema,
handler,
};
21 changes: 21 additions & 0 deletions mcp-server/src/tools/integrations/list-jira-projects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it, vi } from "vitest";
import * as request from "../../lib/request.js";
import { listJiraProjectsTool } from "./list-jira-projects.js";

describe("listJiraProjectsTool", () => {
it("calls GET /integrations/jira/projects with required installation id", async () => {
vi.spyOn(request, "fetchApi").mockResolvedValue({ status: "OK", data: [] });

await listJiraProjectsTool.handler({
jira_installation_id: "inst-1",
search: "core",
page: 1,
limit: 25,
});

const url = vi.mocked(request.fetchApi).mock.calls[0][0] as string;
expect(url).toBe(
"/integrations/jira/projects?jira_installation_id=inst-1&search=core&page=1&limit=25"
);
});
});
62 changes: 62 additions & 0 deletions mcp-server/src/tools/integrations/list-jira-projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { z } from "zod";
import { fetchApi } from "../../lib/request.js";
import { logger } from "../../lib/logger.js";

const zodSchema = z.object({
jira_installation_id: z
.string()
.min(1)
.describe("Jira installation ID for the organization integration."),
search: z.string().optional().describe("Search Jira projects by name or key."),
page: z
.number()
.int()
.min(0)
.optional()
.describe("Page number for discovery results (default: 0)."),
limit: z
.number()
.int()
.min(1)
.max(100)
.optional()
.describe("Maximum projects per page (default: 50, max: 100)."),
});

const handler = async ({
jira_installation_id,
search,
page,
limit,
}: z.infer<typeof zodSchema>) => {
const queryParams = new URLSearchParams();
queryParams.append("jira_installation_id", jira_installation_id);
if (search) {
queryParams.append("search", search);
}
if (page !== undefined) {
queryParams.append("page", page.toString());
}
if (limit !== undefined) {
queryParams.append("limit", limit.toString());
}

const path = `/integrations/jira/projects?${queryParams.toString()}`;
logger.info(`Listing Jira projects: ${path}`);

const data = await fetchApi(path);
if (!data) {
return {
content: [{ type: "text" as const, text: "Failed to list Jira projects" }],
};
}

return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
};
};

export const listJiraProjectsTool = {
schema: zodSchema,
handler,
};
27 changes: 27 additions & 0 deletions mcp-server/src/tools/projects/list-project-pull-requests.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it, vi } from "vitest";
import * as request from "../../lib/request.js";
import { listProjectPullRequestsTool } from "./list-project-pull-requests.js";

describe("listProjectPullRequestsTool", () => {
it("serializes query per OpenAPI (repeated status, bracket arrays)", async () => {
vi.spyOn(request, "fetchApi").mockResolvedValue({ status: "OK", data: [] });

await listProjectPullRequestsTool.handler({
projectId: "p1",
limit: 20,
status: ["PASSED", "FAILED"],
tags: ["smoke"],
branches: ["main"],
authors: ["dev@*"],
});

const url = vi.mocked(request.fetchApi).mock.calls[0][0] as string;
expect(url).toContain("/projects/p1/pull-requests?");
expect(url).toContain("limit=20");
expect(url).toContain("status=PASSED");
expect(url).toContain("status=FAILED");
expect(url).toContain("tags%5B%5D=smoke");
expect(url).toContain("branches%5B%5D=main");
expect(url).toContain("authors%5B%5D=dev%40");
});
});
Loading
Loading