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
4 changes: 2 additions & 2 deletions mcp-server/package-lock.json

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

132 changes: 131 additions & 1 deletion mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,34 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CURRENTS_API_KEY } from "./lib/env.js";
import { logger } from "./lib/logger.js";
// Actions API
import { listActionsTool } from "./tools/actions/list-actions.js";
import { createActionTool } from "./tools/actions/create-action.js";
import { getActionTool } from "./tools/actions/get-action.js";
import { updateActionTool } from "./tools/actions/update-action.js";
import { deleteActionTool } from "./tools/actions/delete-action.js";
import { enableActionTool } from "./tools/actions/enable-action.js";
import { disableActionTool } from "./tools/actions/disable-action.js";
// Projects API
import { getProjectsTool } from "./tools/projects/get-projects.js";
import { getProjectTool } from "./tools/projects/get-project.js";
import { getProjectInsightsTool } from "./tools/projects/get-project-insights.js";
// Runs API
import { getRunsTool } from "./tools/runs/get-runs.js";
import { getRunDetailsTool } from "./tools/runs/get-run.js";
import { deleteRunTool } from "./tools/runs/delete-run.js";
import { updateRunTool } from "./tools/runs/update-run.js";
import { cancelRunTool } from "./tools/runs/cancel-run.js";
import { resetRunTool } from "./tools/runs/reset-run.js";
import { findRunTool } from "./tools/runs/find-run.js";
import { cancelCiGithubTool } from "./tools/runs/cancel-ci-github.js";
// Specs/Instances API
import { getSpecFilesPerformanceTool } from "./tools/specs/get-spec-files-performance.js";
import { getSpecInstancesTool } from "./tools/specs/get-spec-instances.js";
// Tests API
import { getTestResultsTool } from "./tools/tests/get-test-results.js";
import { getTestsPerformanceTool } from "./tools/tests/get-tests-performance.js";
import { getTestSignatureTool } from "./tools/tests/get-tests-signature.js";
import { getRunsTool } from "./tools/runs/get-runs.js";

if (CURRENTS_API_KEY === "") {
logger.error("CURRENTS_API_KEY env variable is not set.");
Expand All @@ -21,13 +41,79 @@ const server = new McpServer({
version: "1.0.0",
});

// Actions API
server.tool(
"currents-list-actions",
"Retrieves a list of all actions (test rules) for a project. Actions automatically modify test behavior based on matchers - they can skip tests, quarantine flaky tests, or tag tests. Supports filtering by status (active/disabled/archived/expired) and search by name. Requires a projectId.",
listActionsTool.schema,
listActionsTool.handler
);

server.tool(
"currents-create-action",
"Creates a new action (test rule) for a project. Actions can skip tests, quarantine flaky tests, or tag tests based on matchers (title, file, tag, branch, author, group). Requires a projectId, name, type (skip/quarantine/tag), and matcher configuration.",
createActionTool.schema,
createActionTool.handler
);

server.tool(
"currents-get-action",
"Retrieves details of a specific action by ID. The actionId is globally unique, so projectId is not required.",
getActionTool.schema,
getActionTool.handler
);

server.tool(
"currents-update-action",
"Updates an existing action. Can modify name, type, matcher configuration, tags, reason, or expiration date. The actionId is globally unique, so projectId is not required.",
updateActionTool.schema,
updateActionTool.handler
);

server.tool(
"currents-delete-action",
"Deletes (archives) an action. The action will no longer be applied to tests. The actionId is globally unique, so projectId is not required.",
deleteActionTool.schema,
deleteActionTool.handler
);

server.tool(
"currents-enable-action",
"Enables a previously disabled action. The action will start being applied to matching tests again. The actionId is globally unique, so projectId is not required.",
enableActionTool.schema,
enableActionTool.handler
);

server.tool(
"currents-disable-action",
"Disables an active action. The action will temporarily stop being applied to tests without being deleted. The actionId is globally unique, so projectId is not required.",
disableActionTool.schema,
disableActionTool.handler
);

// Projects API
server.tool(
"currents-get-projects",
"Retrieves a list of all projects available in the Currents platform. This is a prerequisite for using any other tools that require project-specific information.",
getProjectsTool.schema,
getProjectsTool.handler
);

server.tool(
"currents-get-project",
"Retrieves details of a specific project by ID. Returns project configuration, settings, and metadata.",
getProjectTool.schema,
getProjectTool.handler
);

server.tool(
"currents-get-project-insights",
"Retrieves aggregated run and test metrics for a project within a date range. Provides histogram data with configurable time resolution (1h/1d/1w). Supports filtering by tags, branches, groups, and authors. Requires projectId, date_start, and date_end.",
getProjectInsightsTool.schema,
getProjectInsightsTool.handler
);

// Runs API
server.tool(
"currents-get-runs",
"Retrieves a list of runs for a specific project with optional filtering. Supports filtering by branch, tags (with AND/OR operators), status (PASSED/FAILED/RUNNING/FAILING), completion state, date range, commit author, and search by ciBuildId or commit message. Requires a projectId. If the projectId is not known, first call 'currents-get-projects' and ask the user to select the project.",
Expand All @@ -42,6 +128,49 @@ server.tool(
getRunDetailsTool.handler
);

server.tool(
"currents-delete-run",
"Deletes a specific run. This permanently removes the run and all associated data. Requires a runId.",
deleteRunTool.schema,
deleteRunTool.handler
);

server.tool(
"currents-update-run",
"Updates a run's metadata, including tags and CI information (ciBuildId, branch, message, author). Requires a runId.",
updateRunTool.schema,
updateRunTool.handler
);

server.tool(
"currents-cancel-run",
"Cancels a running test run. This stops all in-progress spec executions. Requires a runId.",
cancelRunTool.schema,
cancelRunTool.handler
);

server.tool(
"currents-reset-run",
"Resets failed specs in a run, allowing them to be re-executed. Requires a runId.",
resetRunTool.schema,
resetRunTool.handler
);

server.tool(
"currents-find-run",
"Finds a run by CI build ID within a specific project. Useful for looking up runs from external CI systems. Requires projectId and ciBuildId.",
findRunTool.schema,
findRunTool.handler
);

server.tool(
"currents-cancel-ci-github",
"Cancels a GitHub Actions workflow run. Requires GitHub repository owner, repo name, and GitHub Actions run ID.",
cancelCiGithubTool.schema,
cancelCiGithubTool.handler
);

// Specs/Instances API
server.tool(
"currents-get-spec-instance",
"Retrieves debugging data from a specific execution of a test spec file by instanceId.",
Expand All @@ -56,6 +185,7 @@ server.tool(
getSpecFilesPerformanceTool.handler
);

// Tests API
server.tool(
"currents-get-tests-performance",
"Retrieves aggregated test metrics for a specific project within a date range. Supports ordering by failures, passes, flakiness, duration, executions, title, and various delta metrics. Supports filtering by spec name, test title, tags, branches, groups, authors, minimum executions, and test state. Requires a projectId. If the projectId is not known, first call 'currents-get-projects' and ask the user to select the project.",
Expand Down
53 changes: 53 additions & 0 deletions mcp-server/src/lib/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,59 @@ export async function postApi<T, B>(path: string, body: B): Promise<T | null> {
}
}

export async function putApi<T, B = undefined>(
path: string,
body?: B
): Promise<T | null> {
const headers = {
"User-Agent": USER_AGENT,
Accept: "application/json",
"Content-Type": "application/json",
Authorization: "Bearer " + CURRENTS_API_KEY,
};

try {
const response = await fetch(`${CURRENTS_API_URL}${path}`, {
method: "PUT",
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
logger.error(`HTTP error! status: ${response.status}`);
logger.error(response);
return null;
}
return (await response.json()) as T;
} catch (error: any) {
logger.error("Error making Currents PUT request:", error.toString());
return null;
}
}

export async function deleteApi<T>(path: string): Promise<T | null> {
const headers = {
"User-Agent": USER_AGENT,
Accept: "application/json",
Authorization: "Bearer " + CURRENTS_API_KEY,
};

try {
const response = await fetch(`${CURRENTS_API_URL}${path}`, {
method: "DELETE",
headers,
});
if (!response.ok) {
logger.error(`HTTP error! status: ${response.status}`);
logger.error(response);
return null;
}
return (await response.json()) as T;
} catch (error: any) {
logger.error("Error making Currents DELETE request:", error.toString());
return null;
}
}

export async function fetchCursorBasedPaginatedApi<T>(
path: string
): Promise<T[] | null> {
Expand Down
105 changes: 105 additions & 0 deletions mcp-server/src/tools/actions/create-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { z } from "zod";
import { postApi } from "../../lib/request.js";
import { logger } from "../../lib/logger.js";

const zodSchema = z.object({
projectId: z
.string()
.describe("The project ID to create the action in."),
name: z
.string()
.describe("The name of the action."),
type: z
.enum(["skip", "quarantine", "tag"])
.describe("The type of action: skip, quarantine, or tag."),
matcher: z
.object({
type: z
.enum(["title", "file", "tag", "branch", "author", "group"])
.describe("The type of matcher."),
value: z
.union([z.string(), z.array(z.string())])
.describe("The value(s) to match."),
operator: z
.enum(["equals", "contains", "startsWith", "endsWith", "regex"])
.optional()
.describe("The operator for matching (default: contains)."),
})
.describe("The matcher configuration that defines which tests the action applies to."),
tags: z
.array(z.string())
.optional()
.describe("Tags to apply when action type is 'tag'."),
reason: z
.string()
.optional()
.describe("Optional reason or description for the action."),
expiresAt: z
.string()
.optional()
.describe("Optional expiration date in ISO 8601 format."),
});

interface CreateActionRequest {
name: string;
type: string;
matcher: {
type: string;
value: string | string[];
operator?: string;
};
tags?: string[];
reason?: string;
expiresAt?: string;
}

const handler = async ({
projectId,
name,
type,
matcher,
tags,
reason,
expiresAt,
}: z.infer<typeof zodSchema>) => {
logger.info(`Creating action for project ${projectId}: ${name}`);

const body: CreateActionRequest = {
name,
type,
matcher,
tags,
reason,
expiresAt,
};

const data = await postApi(
`/actions?projectId=${projectId}`,
body
);

if (!data) {
return {
content: [
{
type: "text" as const,
text: "Failed to create action",
},
],
};
}

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

export const createActionTool = {
schema: zodSchema.shape,
handler,
};
40 changes: 40 additions & 0 deletions mcp-server/src/tools/actions/delete-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { z } from "zod";
import { deleteApi } from "../../lib/request.js";
import { logger } from "../../lib/logger.js";

const zodSchema = z.object({
actionId: z
.string()
.describe("The action ID to delete (archive)."),
});

const handler = async ({ actionId }: z.infer<typeof zodSchema>) => {
logger.info(`Deleting action ${actionId}`);

const data = await deleteApi(`/actions/${actionId}`);

if (!data) {
return {
content: [
{
type: "text" as const,
text: "Failed to delete action",
},
],
};
}

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

export const deleteActionTool = {
schema: zodSchema.shape,
handler,
};
Loading