Skip to content

Commit f646f7c

Browse files
miguelangaranocurrentscursoragenttwk3
authored
feat: api parity for webhooks api, fixes webhooks api (#37)
* feat: add webhook tools and fix parameter requirements for OpenAPI parity - Add 5 missing webhook MCP tools (list, create, get, update, delete) - Implement all webhook endpoints from OpenAPI spec - Fix date_start and date_end to be required parameters (per OpenAPI spec) in: - get-test-results.ts - get-spec-files-performance.ts - get-tests-performance.ts - Register webhook tools in index.ts with comprehensive descriptions - All webhook tools support full CRUD operations per OpenAPI specification Co-authored-by: miguel <miguel@currents.dev> * docs: add comprehensive PR summary and analysis Co-authored-by: miguel <miguel@currents.dev> * docs: add final implementation completion report Co-authored-by: miguel <miguel@currents.dev> * Webhooks tools and tests (#38) * revert: restore custom date defaults for performance and test result tools Co-authored-by: dj <dj@currents.dev> * test: add comprehensive tests for webhook tools - Add tests for listWebhooksTool (success and error cases) - Add tests for getWebhookTool (success and error cases) - Add tests for createWebhookTool (required fields, all fields, error cases) - Add tests for updateWebhookTool (single field, all fields, error cases) - Add tests for deleteWebhookTool (success and error cases) - All tests verify schema structure Co-authored-by: dj <dj@currents.dev> * chore: remove outdated documentation files These files referenced changes that have been reverted. The PR is now focused solely on webhook tools implementation. Co-authored-by: dj <dj@currents.dev> * docs: add webhook tools to README Co-authored-by: dj <dj@currents.dev> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> * fix: remove .default([]) from hookEvents to allow proper undefined check Removes .default([]) from hookEvents parameter in create-webhook.ts. This ensures that when hookEvents is not provided, it remains undefined rather than being set to an empty array, allowing the undefined check in the handler to work correctly and avoid sending empty arrays to the API. Addresses feedback from cubic code review. Co-authored-by: miguel <miguel@currents.dev> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: DJ Mountney <david.mountney@twkie.net>
1 parent 70d319f commit f646f7c

File tree

8 files changed

+750
-0
lines changed

8 files changed

+750
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ This is a MCP server that allows you to provide test results context to your AI
1818
| `currents-get-tests-performance` | Retrieves test historical performance metrics for a specific project. |
1919
| `currents-get-tests-signatures` | Retrieves a test signature by its spec file name and test name. |
2020
| `currents-get-test-results` | Retrieves debugging data from test results of a test by its signature. |
21+
| `currents-list-webhooks` | List all webhooks configured for a project. |
22+
| `currents-create-webhook` | Create a new webhook to receive notifications on run events. |
23+
| `currents-get-webhook` | Get details of a specific webhook by ID. |
24+
| `currents-update-webhook` | Update an existing webhook's URL, headers, events, or label. |
25+
| `currents-delete-webhook` | Delete a webhook. |
2126

2227
## Setup
2328

mcp-server/src/index.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ import { getSpecInstancesTool } from "./tools/specs/get-spec-instances.js";
3030
import { getTestResultsTool } from "./tools/tests/get-test-results.js";
3131
import { getTestsPerformanceTool } from "./tools/tests/get-tests-performance.js";
3232
import { getTestSignatureTool } from "./tools/tests/get-tests-signature.js";
33+
// Webhooks tools
34+
import { listWebhooksTool } from "./tools/webhooks/list-webhooks.js";
35+
import { createWebhookTool } from "./tools/webhooks/create-webhook.js";
36+
import { getWebhookTool } from "./tools/webhooks/get-webhook.js";
37+
import { updateWebhookTool } from "./tools/webhooks/update-webhook.js";
38+
import { deleteWebhookTool } from "./tools/webhooks/delete-webhook.js";
3339

3440
if (CURRENTS_API_KEY === "") {
3541
logger.error("CURRENTS_API_KEY env variable is not set.");
@@ -199,6 +205,42 @@ server.tool(
199205
getTestResultsTool.handler
200206
);
201207

208+
// Webhooks API tools
209+
server.tool(
210+
"currents-list-webhooks",
211+
"List all webhooks for a project. Webhooks allow you to receive HTTP POST notifications when certain events occur in your test runs: RUN_FINISH (run completed), RUN_START (run started), RUN_TIMEOUT (run timed out), RUN_CANCELED (run was cancelled). Requires a projectId.",
212+
listWebhooksTool.schema,
213+
listWebhooksTool.handler
214+
);
215+
216+
server.tool(
217+
"currents-create-webhook",
218+
"Create a new webhook for a project. Specify the URL to receive POST notifications, optional custom headers (as JSON string), events to trigger on (RUN_FINISH, RUN_START, RUN_TIMEOUT, RUN_CANCELED), and an optional label. Requires projectId and url.",
219+
createWebhookTool.schema,
220+
createWebhookTool.handler
221+
);
222+
223+
server.tool(
224+
"currents-get-webhook",
225+
"Get a single webhook by ID. The hookId is a UUID. Returns full webhook details including url, headers, events, label, and timestamps.",
226+
getWebhookTool.schema,
227+
getWebhookTool.handler
228+
);
229+
230+
server.tool(
231+
"currents-update-webhook",
232+
"Update an existing webhook. You can update the url, headers (as JSON string), hookEvents array, or label. All fields are optional. The hookId is a UUID.",
233+
updateWebhookTool.schema,
234+
updateWebhookTool.handler
235+
);
236+
237+
server.tool(
238+
"currents-delete-webhook",
239+
"Delete a webhook. This permanently removes the webhook. The hookId is a UUID.",
240+
deleteWebhookTool.schema,
241+
deleteWebhookTool.handler
242+
);
243+
202244
async function main() {
203245
const transport = new StdioServerTransport();
204246
await server.connect(transport);
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { z } from "zod";
2+
import { postApi } from "../../lib/request.js";
3+
import { logger } from "../../lib/logger.js";
4+
5+
const zodSchema = z.object({
6+
projectId: z
7+
.string()
8+
.describe("The project ID to create the webhook for."),
9+
url: z
10+
.string()
11+
.max(2048)
12+
.describe("URL to send webhook POST requests to."),
13+
headers: z
14+
.string()
15+
.max(4096)
16+
.optional()
17+
.describe("Custom headers as a JSON object string (e.g., {\"Authorization\": \"Bearer token\"})."),
18+
hookEvents: z
19+
.array(z.enum(["RUN_FINISH", "RUN_START", "RUN_TIMEOUT", "RUN_CANCELED"]))
20+
.optional()
21+
.describe("Events that trigger this webhook. Options: RUN_FINISH (run completed), RUN_START (run started), RUN_TIMEOUT (run timed out), RUN_CANCELED (run was cancelled)."),
22+
label: z
23+
.string()
24+
.min(1)
25+
.max(255)
26+
.optional()
27+
.describe("Human-readable label for the webhook."),
28+
});
29+
30+
const handler = async ({
31+
projectId,
32+
url,
33+
headers,
34+
hookEvents,
35+
label,
36+
}: z.infer<typeof zodSchema>) => {
37+
const queryParams = new URLSearchParams();
38+
queryParams.append("projectId", projectId);
39+
40+
const body: Record<string, unknown> = {
41+
url,
42+
};
43+
44+
if (headers !== undefined) {
45+
body.headers = headers;
46+
}
47+
48+
if (hookEvents !== undefined) {
49+
body.hookEvents = hookEvents;
50+
}
51+
52+
if (label !== undefined) {
53+
body.label = label;
54+
}
55+
56+
logger.info(
57+
`Creating webhook for project ${projectId}`
58+
);
59+
60+
const data = await postApi(
61+
`/webhooks?${queryParams.toString()}`,
62+
body
63+
);
64+
65+
if (!data) {
66+
return {
67+
content: [
68+
{
69+
type: "text" as const,
70+
text: "Failed to create webhook",
71+
},
72+
],
73+
};
74+
}
75+
76+
return {
77+
content: [
78+
{
79+
type: "text" as const,
80+
text: JSON.stringify(data, null, 2),
81+
},
82+
],
83+
};
84+
};
85+
86+
export const createWebhookTool = {
87+
schema: zodSchema.shape,
88+
handler,
89+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { z } from "zod";
2+
import { deleteApi } from "../../lib/request.js";
3+
import { logger } from "../../lib/logger.js";
4+
5+
const zodSchema = z.object({
6+
hookId: z
7+
.string()
8+
.describe("The webhook ID (UUID)."),
9+
});
10+
11+
const handler = async ({
12+
hookId,
13+
}: z.infer<typeof zodSchema>) => {
14+
logger.info(`Deleting webhook ${hookId}`);
15+
16+
const data = await deleteApi(`/webhooks/${hookId}`);
17+
18+
if (!data) {
19+
return {
20+
content: [
21+
{
22+
type: "text" as const,
23+
text: "Failed to delete webhook",
24+
},
25+
],
26+
};
27+
}
28+
29+
return {
30+
content: [
31+
{
32+
type: "text" as const,
33+
text: JSON.stringify(data, null, 2),
34+
},
35+
],
36+
};
37+
};
38+
39+
export const deleteWebhookTool = {
40+
schema: zodSchema.shape,
41+
handler,
42+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
hookId: z
7+
.string()
8+
.describe("The webhook ID (UUID)."),
9+
});
10+
11+
const handler = async ({
12+
hookId,
13+
}: z.infer<typeof zodSchema>) => {
14+
logger.info(`Fetching webhook ${hookId}`);
15+
16+
const data = await fetchApi(`/webhooks/${hookId}`);
17+
18+
if (!data) {
19+
return {
20+
content: [
21+
{
22+
type: "text" as const,
23+
text: "Failed to retrieve webhook",
24+
},
25+
],
26+
};
27+
}
28+
29+
return {
30+
content: [
31+
{
32+
type: "text" as const,
33+
text: JSON.stringify(data, null, 2),
34+
},
35+
],
36+
};
37+
};
38+
39+
export const getWebhookTool = {
40+
schema: zodSchema.shape,
41+
handler,
42+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
projectId: z
7+
.string()
8+
.describe("The project ID to fetch webhooks from."),
9+
});
10+
11+
const handler = async ({
12+
projectId,
13+
}: z.infer<typeof zodSchema>) => {
14+
const queryParams = new URLSearchParams();
15+
queryParams.append("projectId", projectId);
16+
17+
logger.info(
18+
`Fetching webhooks for project ${projectId}`
19+
);
20+
21+
const data = await fetchApi(`/webhooks?${queryParams.toString()}`);
22+
23+
if (!data) {
24+
return {
25+
content: [
26+
{
27+
type: "text" as const,
28+
text: "Failed to retrieve webhooks",
29+
},
30+
],
31+
};
32+
}
33+
34+
return {
35+
content: [
36+
{
37+
type: "text" as const,
38+
text: JSON.stringify(data, null, 2),
39+
},
40+
],
41+
};
42+
};
43+
44+
export const listWebhooksTool = {
45+
schema: zodSchema.shape,
46+
handler,
47+
};
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { z } from "zod";
2+
import { putApi } from "../../lib/request.js";
3+
import { logger } from "../../lib/logger.js";
4+
5+
const zodSchema = z.object({
6+
hookId: z
7+
.string()
8+
.describe("The webhook ID (UUID)."),
9+
url: z
10+
.string()
11+
.max(2048)
12+
.optional()
13+
.describe("URL to send webhook POST requests to."),
14+
headers: z
15+
.string()
16+
.max(4096)
17+
.optional()
18+
.describe("Custom headers as a JSON object string (e.g., {\"Authorization\": \"Bearer token\"})."),
19+
hookEvents: z
20+
.array(z.enum(["RUN_FINISH", "RUN_START", "RUN_TIMEOUT", "RUN_CANCELED"]))
21+
.optional()
22+
.describe("Events that trigger this webhook. Options: RUN_FINISH (run completed), RUN_START (run started), RUN_TIMEOUT (run timed out), RUN_CANCELED (run was cancelled)."),
23+
label: z
24+
.string()
25+
.min(1)
26+
.max(255)
27+
.optional()
28+
.describe("Human-readable label for the webhook."),
29+
});
30+
31+
const handler = async ({
32+
hookId,
33+
url,
34+
headers,
35+
hookEvents,
36+
label,
37+
}: z.infer<typeof zodSchema>) => {
38+
const body: Record<string, unknown> = {};
39+
40+
if (url !== undefined) {
41+
body.url = url;
42+
}
43+
44+
if (headers !== undefined) {
45+
body.headers = headers;
46+
}
47+
48+
if (hookEvents !== undefined) {
49+
body.hookEvents = hookEvents;
50+
}
51+
52+
if (label !== undefined) {
53+
body.label = label;
54+
}
55+
56+
logger.info(`Updating webhook ${hookId}`);
57+
58+
const data = await putApi(`/webhooks/${hookId}`, body);
59+
60+
if (!data) {
61+
return {
62+
content: [
63+
{
64+
type: "text" as const,
65+
text: "Failed to update webhook",
66+
},
67+
],
68+
};
69+
}
70+
71+
return {
72+
content: [
73+
{
74+
type: "text" as const,
75+
text: JSON.stringify(data, null, 2),
76+
},
77+
],
78+
};
79+
};
80+
81+
export const updateWebhookTool = {
82+
schema: zodSchema.shape,
83+
handler,
84+
};

0 commit comments

Comments
 (0)