Skip to content

Commit eb0ae29

Browse files
committed
feat(server): signed webhook management tools — create/list/get/delete (62 → 66)
Refresh taskade-public.v2.json from the live spec (45 → 47 paths; adds the signed-webhook CRUD from upstream v6.213.0) and expose it as four new v2 tools: createWebhook, listWebhooks, getWebhook, deleteWebhook. These REST-shaped ops omit operationId and their path-derived names would all collide as 'webhooks' (deriveToolName drops {param} segments and ignores the method), so the codegen/parser gains an explicit nameOverrides map keyed by '<lowercase-method> <path>'; the v2 gen script supplies the four overrides and its prune matcher now mirrors the codegen's override -> operationId -> derived resolution order. createWebhook's description surfaces the HMAC reality: the signing secret is returned exactly once and must be stored securely.
1 parent 9652b50 commit eb0ae29

12 files changed

Lines changed: 294 additions & 24 deletions

File tree

.changeset/lazy-webhooks-sign.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@taskade/mcp-openapi-codegen': minor
3+
---
4+
5+
New `nameOverrides` codegen/parser option: explicit tool-name overrides keyed by `"<lowercase-method> <path>"` (e.g. `'post /webhooks': 'createWebhook'`). An override beats both `operationId` and the path-derived fallback, disambiguating REST-shaped operations without `operationId` whose derived names would collide (`POST/GET /webhooks`, `GET/DELETE /webhooks/{id}` all derive to `webhooks`). The parser (`parseOpenApi`, `deriveToolName`) is now exported from the package root.

.changeset/many-webhooks-crud.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@taskade/mcp-server': minor
3+
---
4+
5+
Four new signed-webhook management tools from Taskade API v2 (upstream v6.213.0 "Signed Webhooks"): `createWebhook`, `listWebhooks`, `getWebhook`, `deleteWebhook` — 62 → 66 tools. `createWebhook`'s description warns that the HMAC signing secret is returned exactly once and must be stored securely. The legacy unsigned `subscribeWebhook`/`unsubscribeWebhook` remain available but are deprecated upstream.

README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
[![Add to Cursor](https://img.shields.io/badge/Add_to-Cursor-0098FF?style=flat-square)](cursor://anysphere.cursor-deeplink/mcp/install?name=taskade&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB0YXNrYWRlL21jcC1zZXJ2ZXIiXSwiZW52Ijp7IlRBU0tBREVfQVBJX0tFWSI6InlvdXItYXBpLWtleS1oZXJlIn19)
1414
[![Install in VS Code](https://img.shields.io/badge/Install_in-VS_Code-007ACC?style=flat-square&logo=visualstudiocode&logoColor=white)](vscode:mcp/install?%7B%22name%22%3A%22taskade%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40taskade%2Fmcp-server%22%5D%2C%22env%22%3A%7B%22TASKADE_API_KEY%22%3A%22%24%7Binput%3Ataskade_api_key%7D%22%7D%7D)
1515

16-
**62 tools** for workspaces, projects, tasks, AI agents, agent chat, webhooks, knowledge bases, templates, media, and sharing — all from your AI client.
16+
**66 tools** for workspaces, projects, tasks, AI agents, agent chat, webhooks, knowledge bases, templates, media, and sharing — all from your AI client.
1717

1818
</div>
1919

@@ -26,7 +26,7 @@
2626

2727
- [Demo](#demo)
2828
- [Quick Start](#quick-start)
29-
- [Tools (62)](#tools-62)
29+
- [Tools (66)](#tools-66)
3030
- [Why Taskade MCP?](#why-taskade-mcp)
3131
- [Agent Recipes](#agent-recipes)
3232
- [Use Cases](#use-cases)
@@ -169,7 +169,7 @@ The server starts at `http://localhost:3000` (configure with `PORT` env var). Co
169169

170170
---
171171

172-
## Tools (62)
172+
## Tools (66)
173173

174174
### Workspaces
175175

@@ -274,8 +274,14 @@ Capabilities the v1 API doesn't have: hold a live conversation with an AI agent,
274274
| `promptAgent` | Send a message to an AI agent and get its reply |
275275
| `listConversations` | List an agent's conversations |
276276
| `getConversation` | Get a conversation's messages |
277-
| `subscribeWebhook` | Subscribe to real-time Taskade event webhooks |
278-
| `unsubscribeWebhook` | Remove a webhook subscription |
277+
| `createWebhook` | Register a signed (HMAC) webhook for one or more events |
278+
| `listWebhooks` | List your registered signed webhooks |
279+
| `getWebhook` | Get a signed webhook by id |
280+
| `deleteWebhook` | Delete a signed webhook |
281+
| `subscribeWebhook` | Subscribe to event webhooks (legacy, unsigned — prefer `createWebhook`) |
282+
| `unsubscribeWebhook` | Remove a legacy webhook subscription |
283+
284+
> **Warning:** `createWebhook` returns the HMAC signing secret exactly **once** — it cannot be retrieved again (not even via `getWebhook`). Store it securely before doing anything else; you need it to verify the signatures on incoming deliveries.
279285
280286
---
281287

@@ -293,7 +299,7 @@ Taskade MCP gives your AI assistant **access to your workspace** — projects, t
293299
294300
┌──────────────────────────────────┐
295301
│ Taskade MCP Server │
296-
│ (62 tools, 8 categories) │
302+
│ (66 tools, 8 categories) │
297303
└──────────────────────────────────┘
298304
↓ ↓ ↓
299305
folderCreateAgent agentKnowledge agentPublicAccess
@@ -320,7 +326,7 @@ Taskade MCP gives your AI assistant **access to your workspace** — projects, t
320326

321327
### Why Taskade MCP Over Other MCP Servers?
322328

323-
Taskade is the only MCP server that includes **AI agent management** (create, train, deploy agents), **knowledge base training** (attach docs, projects, media), and **OpenAPI codegen** (generate MCP tools from any API spec). 62 tools across 8 categories.
329+
Taskade is the only MCP server that includes **AI agent management** (create, train, deploy agents), **knowledge base training** (attach docs, projects, media), and **OpenAPI codegen** (generate MCP tools from any API spec). 66 tools across 8 categories.
324330

325331
---
326332

packages/openapi-codegen/src/codegen.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
44
import path from 'path';
55
import prettier from 'prettier';
66

7-
import { ParsedTool, parseOpenApi } from './parser';
7+
import { ParsedTool, parseOpenApi, ParseOpenApiOpts } from './parser';
88

99
type IsActionsEnabledOpt = Array<string> | ((actionName: string) => boolean);
1010

@@ -64,13 +64,20 @@ type CodegenOpts = {
6464
* the first without an export-name collision.
6565
*/
6666
exportName?: string;
67+
/**
68+
* Explicit tool-name overrides keyed by `"<lowercase-method> <path>"` — see
69+
* `ParseOpenApiOpts.nameOverrides`. Use for operations without `operationId`
70+
* whose path-derived names would collide (REST siblings like `GET /webhooks`
71+
* and `GET /webhooks/{id}`).
72+
*/
73+
nameOverrides?: ParseOpenApiOpts['nameOverrides'];
6774
};
6875

6976
export const codegen = async (opts: CodegenOpts) => {
7077
const { document, path: outputPath } = opts;
7178
const exportName = opts.exportName ?? 'setupTools';
7279

73-
const tools = parseOpenApi(document.paths ?? {});
80+
const tools = parseOpenApi(document.paths ?? {}, { nameOverrides: opts.nameOverrides });
7481

7582
const runtime = fs.readFileSync(path.join(import.meta.dirname, 'runtime.ts'), 'utf8');
7683

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './codegen';
2+
export * from './parser';

packages/openapi-codegen/src/parser.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ describe('deriveToolName', () => {
2323
expect(deriveToolName('get', '/')).toBe('get');
2424
expect(deriveToolName('POST', '/{id}')).toBe('post');
2525
});
26+
27+
it('collides for REST siblings: {param} segments dropped, method ignored (the nameOverrides hazard)', () => {
28+
// Pins the hazard that motivates nameOverrides: without an override, all four
29+
// signed-webhook CRUD ops would emit the same tool name.
30+
expect(deriveToolName('post', '/webhooks')).toBe('webhooks');
31+
expect(deriveToolName('get', '/webhooks')).toBe('webhooks');
32+
expect(deriveToolName('get', '/webhooks/{id}')).toBe('webhooks');
33+
expect(deriveToolName('delete', '/webhooks/{id}')).toBe('webhooks');
34+
});
2635
});
2736

2837
describe('parseOpenApi name resolution', () => {
@@ -87,4 +96,60 @@ describe('parseOpenApi name resolution', () => {
8796
]);
8897
expect(tools[0].inputSchema.required).toEqual(['spaceId', 'agentId', 'prompt']);
8998
});
99+
100+
it('applies nameOverrides keyed by "<lowercase-method> <path>" to disambiguate REST siblings', () => {
101+
const tools = parseOpenApi(
102+
{
103+
'/webhooks': {
104+
post: { summary: 'Register a signed webhook', responses: {} },
105+
get: { summary: 'List signed webhooks', responses: {} },
106+
},
107+
'/webhooks/{id}': {
108+
get: { summary: 'Get a signed webhook', responses: {} },
109+
delete: { summary: 'Delete a signed webhook', responses: {} },
110+
},
111+
} as never,
112+
{
113+
nameOverrides: {
114+
'post /webhooks': 'createWebhook',
115+
'get /webhooks': 'listWebhooks',
116+
'get /webhooks/{id}': 'getWebhook',
117+
'delete /webhooks/{id}': 'deleteWebhook',
118+
},
119+
},
120+
);
121+
expect(tools.map((t) => t.name).sort()).toEqual([
122+
'createWebhook',
123+
'deleteWebhook',
124+
'getWebhook',
125+
'listWebhooks',
126+
]);
127+
});
128+
129+
it('without nameOverrides, REST siblings all fall back to the same derived name (collision pinned)', () => {
130+
const tools = parseOpenApi({
131+
'/webhooks': {
132+
post: { responses: {} },
133+
get: { responses: {} },
134+
},
135+
'/webhooks/{id}': {
136+
get: { responses: {} },
137+
delete: { responses: {} },
138+
},
139+
} as never);
140+
expect(tools.map((t) => t.name)).toEqual(['webhooks', 'webhooks', 'webhooks', 'webhooks']);
141+
});
142+
143+
it('an explicit override beats operationId; unkeyed operations are unaffected', () => {
144+
const tools = parseOpenApi(
145+
{
146+
'/projects': {
147+
post: { operationId: 'projectCreate', responses: {} },
148+
get: { operationId: 'projectsList', responses: {} },
149+
},
150+
} as never,
151+
{ nameOverrides: { 'post /projects': 'createProject' } },
152+
);
153+
expect(tools.map((t) => t.name).sort()).toEqual(['createProject', 'projectsList']);
154+
});
90155
});

packages/openapi-codegen/src/parser.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,28 @@ export type ParsedTool = {
2121
outputSchema: IJsonSchema;
2222
};
2323

24+
export type ParseOpenApiOpts = {
25+
/**
26+
* Explicit tool-name overrides keyed by `"<lowercase-method> <path>"`, e.g.
27+
* `{ 'post /webhooks': 'createWebhook' }`. An override wins over both
28+
* `operationId` and the path-derived fallback. Needed for REST-shaped specs
29+
* without `operationId`, where `deriveToolName` drops `{param}` segments and
30+
* ignores the HTTP method — all four of `POST /webhooks`, `GET /webhooks`,
31+
* `GET /webhooks/{id}` and `DELETE /webhooks/{id}` would otherwise collide
32+
* as `webhooks`.
33+
*/
34+
nameOverrides?: Record<string, string>;
35+
};
36+
2437
/**
2538
* Derive a stable camelCase tool name from an operation's path when the spec omits
2639
* `operationId` — e.g. Taskade API v2's flat RPC routes (`POST /promptAgent`). Path
2740
* params (`{id}`) are dropped and remaining segments are camelCased
2841
* (`/media/{mediaId}/content` → `mediaContent`). Falls back to the HTTP method for a
2942
* root or param-only path (`/`, `/{id}`). Specs that DO provide `operationId` (e.g.
30-
* Taskade v1) are unaffected.
43+
* Taskade v1) are unaffected. NOTE: because `{param}` segments are dropped and the
44+
* method is ignored, sibling REST routes (`GET /webhooks` vs `GET /webhooks/{id}`)
45+
* collide — disambiguate those via `ParseOpenApiOpts.nameOverrides`.
3146
*/
3247
export const deriveToolName = (method: string, path: string): string => {
3348
const words = path
@@ -52,6 +67,7 @@ export const deriveToolName = (method: string, path: string): string => {
5267

5368
export const parseOpenApi = (
5469
paths: OpenAPIV3_1.PathsObject | OpenAPIV3.PathsObject | OpenAPIV2.PathsObject,
70+
opts: ParseOpenApiOpts = {},
5571
): ParsedTool[] => {
5672
const tools: ParsedTool[] = [];
5773

@@ -166,7 +182,10 @@ export const parseOpenApi = (
166182
}
167183

168184
tools.push({
169-
name: operation.operationId ?? deriveToolName(method, path),
185+
name:
186+
opts.nameOverrides?.[`${method.toLowerCase()} ${path}`] ??
187+
operation.operationId ??
188+
deriveToolName(method, path),
170189
method: method,
171190
path: path,
172191
description: operation.description ?? operation.summary ?? '',

packages/server/scripts/gen-taskade-mcp-tools-v2.ts

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
11
import { dereference } from '@readme/openapi-parser';
2-
import { codegen } from '@taskade/mcp-openapi-codegen';
2+
import { codegen, deriveToolName } from '@taskade/mcp-openapi-codegen';
33
import fs from 'fs';
44

5-
import { ENABLED_TASKADE_V2_ACTIONS, HUMANIZED_TASKADE_V2_ACTIONS } from '../src/constants.v2';
5+
import {
6+
ENABLED_TASKADE_V2_ACTIONS,
7+
HUMANIZED_TASKADE_V2_ACTIONS,
8+
TASKADE_V2_ACTION_DESCRIPTIONS,
9+
TaskadeV2Action,
10+
} from '../src/constants.v2';
611

7-
// Taskade API v2 is a flat RPC API whose operations omit `operationId`; the codegen
8-
// derives tool names from the path (e.g. POST /promptAgent -> "promptAgent").
9-
const deriveName = (p: string) => p.replace(/^\//, '').split('/')[0];
12+
// Taskade API v2 is mostly a flat RPC API whose operations omit `operationId`; the
13+
// codegen derives tool names from the path (e.g. POST /promptAgent -> "promptAgent").
14+
// The signed-webhook CRUD (upstream v6.213.0) is the exception: REST-shaped routes
15+
// where path-derived naming would collide (POST/GET /webhooks and GET/DELETE
16+
// /webhooks/{id} all derive to "webhooks"), so those get explicit name overrides,
17+
// keyed by "<lowercase-method> <path>".
18+
const NAME_OVERRIDES: Record<string, string> = {
19+
'post /webhooks': 'createWebhook',
20+
'get /webhooks': 'listWebhooks',
21+
'get /webhooks/{id}': 'getWebhook',
22+
'delete /webhooks/{id}': 'deleteWebhook',
23+
};
24+
25+
const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']);
26+
27+
// Tool names a path item will produce, mirroring the codegen's resolution order
28+
// (override -> operationId -> derived) so the prune allow-list matcher below cannot
29+
// drift from the names the codegen actually emits.
30+
const toolNamesForPath = (p: string, item: object): string[] =>
31+
Object.entries(item)
32+
.filter(([m]) => HTTP_METHODS.has(m.toLowerCase()))
33+
.map(
34+
([m, op]) =>
35+
NAME_OVERRIDES[`${m.toLowerCase()} ${p}`] ??
36+
(op as { operationId?: string }).operationId ??
37+
deriveToolName(m, p),
38+
);
1039

1140
// Why prune before dereferencing: the live v2 spec currently has a broken self-$ref
1241
// inside components.schemas.Field (data.fillerConfig...sourceRef) that makes a full
@@ -19,9 +48,10 @@ const pruneToEnabled = (doc: any) => {
1948
const paths: Record<string, unknown> = {};
2049
const matched = new Set<string>();
2150
for (const [p, ms] of Object.entries(doc.paths ?? {})) {
22-
if (enabled.has(deriveName(p))) {
51+
const enabledNames = toolNamesForPath(p, ms as object).filter((name) => enabled.has(name));
52+
if (enabledNames.length > 0) {
2353
paths[p] = ms;
24-
matched.add(deriveName(p));
54+
enabledNames.forEach((name) => matched.add(name));
2555
}
2656
}
2757

@@ -79,13 +109,17 @@ try {
79109
}
80110

81111
const actions = Object.fromEntries(
82-
Object.entries(HUMANIZED_TASKADE_V2_ACTIONS).map(([name, title]) => [name, { title }]),
112+
Object.entries(HUMANIZED_TASKADE_V2_ACTIONS).map(([name, title]) => [
113+
name,
114+
{ title, description: TASKADE_V2_ACTION_DESCRIPTIONS[name as TaskadeV2Action] },
115+
]),
83116
);
84117

85118
await codegen({
86119
path: 'src/tools.v2.generated.ts',
87120
document: document as never,
88121
isActionsEnabled: [...ENABLED_TASKADE_V2_ACTIONS],
89122
actions,
123+
nameOverrides: NAME_OVERRIDES,
90124
exportName: 'setupToolsV2',
91125
});

packages/server/server.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
33
"name": "io.github.taskade/mcp-server",
4-
"description": "Connect Taskade to any AI assistant — 62 tools: projects, tasks, agents, agent chat, webhooks.",
4+
"description": "Connect Taskade to any AI assistant — 66 tools: projects, tasks, agents, agent chat, signed webhooks.",
55
"repository": {
66
"url": "https://github.com/taskade/mcp.git",
77
"source": "github"

packages/server/src/constants.v2.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ export const ENABLED_TASKADE_V2_ACTIONS = [
1414
// Real-time events
1515
'subscribeWebhook',
1616
'unsubscribeWebhook',
17+
// Signed webhooks (HMAC) — upstream v6.213.0; these ops omit operationId AND
18+
// share paths (/webhooks, /webhooks/{id}), so their names come from the
19+
// nameOverrides map in scripts/gen-taskade-mcp-tools-v2.ts, not the path.
20+
'createWebhook',
21+
'listWebhooks',
22+
'getWebhook',
23+
'deleteWebhook',
1724
] as const;
1825

1926
export type TaskadeV2Action = (typeof ENABLED_TASKADE_V2_ACTIONS)[number];
@@ -26,4 +33,18 @@ export const HUMANIZED_TASKADE_V2_ACTIONS: Record<TaskadeV2Action, string> = {
2633
getConversation: 'Get Agent Conversation',
2734
subscribeWebhook: 'Subscribe to a Webhook',
2835
unsubscribeWebhook: 'Unsubscribe from a Webhook',
36+
createWebhook: 'Create a Signed Webhook',
37+
listWebhooks: 'List Signed Webhooks',
38+
getWebhook: 'Get a Signed Webhook',
39+
deleteWebhook: 'Delete a Signed Webhook',
40+
};
41+
42+
// Per-action description overrides (fed to the codegen's ActionConfig.description).
43+
// Only needed where the spec's summary omits something the model must know.
44+
export const TASKADE_V2_ACTION_DESCRIPTIONS: Partial<Record<TaskadeV2Action, string>> = {
45+
createWebhook:
46+
'Register a signed outbound webhook for one or more Taskade events. ' +
47+
'The response includes the HMAC signing secret exactly ONCE — it cannot be ' +
48+
'retrieved again later, so store it securely (e.g. a secret manager) before ' +
49+
'doing anything else.',
2950
};

0 commit comments

Comments
 (0)