@webhooks-cc/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/bin/mcp.js +469 -0
- package/dist/index.d.mts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +225 -0
- package/dist/index.mjs +199 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mads Sauer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# @webhooks-cc/mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [webhooks.cc](https://webhooks.cc). Connects AI coding agents to webhook endpoints — create, inspect, test, and replay webhooks through natural language.
|
|
4
|
+
|
|
5
|
+
Works with Claude Code, Cursor, VS Code, Codex, Windsurf, and Claude Desktop.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
### One-click
|
|
10
|
+
|
|
11
|
+
- **Cursor**: [Add to Cursor](https://cursor.com/en/install-mcp?name=webhooks-cc&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB3ZWJob29rcy1jYy9tY3AiXSwiZW52Ijp7IldIS19BUElfS0VZIjoid2hjY18uLi4ifX0=) (paste your API key after install)
|
|
12
|
+
- **VS Code**: [Add to VS Code](https://insiders.vscode.dev/redirect/mcp/install?name=webhooks-cc&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40webhooks-cc%2Fmcp%22%5D%2C%22env%22%3A%7B%22WHK_API_KEY%22%3A%22%24%7Binput%3Awhk_api_key%7D%22%7D%7D&inputs=%5B%7B%22id%22%3A%22whk_api_key%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22webhooks.cc%20API%20key%20%28get%20yours%20at%20webhooks.cc%2Faccount%29%22%2C%22password%22%3Atrue%7D%5D) (prompts for your key)
|
|
13
|
+
|
|
14
|
+
### CLI
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Claude Code
|
|
18
|
+
claude mcp add -s user --transport stdio webhooks-cc -e WHK_API_KEY=whcc_... -- npx -y @webhooks-cc/mcp
|
|
19
|
+
|
|
20
|
+
# Cursor
|
|
21
|
+
npx @webhooks-cc/mcp setup cursor --api-key whcc_...
|
|
22
|
+
|
|
23
|
+
# VS Code
|
|
24
|
+
npx @webhooks-cc/mcp setup vscode --api-key whcc_...
|
|
25
|
+
|
|
26
|
+
# OpenAI Codex
|
|
27
|
+
codex mcp add webhooks-cc -e WHK_API_KEY=whcc_... -- npx -y @webhooks-cc/mcp
|
|
28
|
+
|
|
29
|
+
# Windsurf
|
|
30
|
+
npx @webhooks-cc/mcp setup windsurf --api-key whcc_...
|
|
31
|
+
|
|
32
|
+
# Claude Desktop
|
|
33
|
+
npx @webhooks-cc/mcp setup claude-desktop --api-key whcc_...
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Manual JSON config
|
|
37
|
+
|
|
38
|
+
For any tool that reads an MCP config file:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"webhooks-cc": {
|
|
44
|
+
"command": "npx",
|
|
45
|
+
"args": ["-y", "@webhooks-cc/mcp"],
|
|
46
|
+
"env": {
|
|
47
|
+
"WHK_API_KEY": "whcc_..."
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Get your API key at [webhooks.cc/account](https://webhooks.cc/account).
|
|
55
|
+
|
|
56
|
+
## Tools
|
|
57
|
+
|
|
58
|
+
The MCP server exposes 11 tools:
|
|
59
|
+
|
|
60
|
+
| Tool | Description |
|
|
61
|
+
| ------------------ | ----------------------------------------- |
|
|
62
|
+
| `create_endpoint` | Create a new webhook endpoint |
|
|
63
|
+
| `list_endpoints` | List all your endpoints |
|
|
64
|
+
| `get_endpoint` | Get details for an endpoint by slug |
|
|
65
|
+
| `update_endpoint` | Update an endpoint name or mock response |
|
|
66
|
+
| `delete_endpoint` | Delete an endpoint and its requests |
|
|
67
|
+
| `list_requests` | List captured requests for an endpoint |
|
|
68
|
+
| `get_request` | Get full details of a captured request |
|
|
69
|
+
| `send_webhook` | Send a test webhook to an endpoint |
|
|
70
|
+
| `wait_for_request` | Wait for an incoming request (polling) |
|
|
71
|
+
| `replay_request` | Replay a captured request to a target URL |
|
|
72
|
+
| `describe` | Describe all available SDK operations |
|
|
73
|
+
|
|
74
|
+
## Example conversation
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
You: "Create a webhook endpoint for testing Stripe"
|
|
78
|
+
Agent: Created endpoint "stripe-test" at https://go.webhooks.cc/w/abc123
|
|
79
|
+
|
|
80
|
+
You: "Set it to return 201 with {"received": true}"
|
|
81
|
+
Agent: Updated mock response for stripe-test
|
|
82
|
+
|
|
83
|
+
You: "Send a test POST with a checkout.session.completed event"
|
|
84
|
+
Agent: Sent POST to stripe-test with event payload
|
|
85
|
+
|
|
86
|
+
You: "Show me what was captured"
|
|
87
|
+
Agent: 1 request captured:
|
|
88
|
+
POST /w/abc123 — {"event": "checkout.session.completed", ...}
|
|
89
|
+
|
|
90
|
+
You: "Replay that to my local server"
|
|
91
|
+
Agent: Replayed to http://localhost:3000/webhooks — got 200 OK
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Documentation
|
|
95
|
+
|
|
96
|
+
Full setup guide with interactive API key filling: [webhooks.cc/docs/mcp](https://webhooks.cc/docs/mcp)
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
package/dist/bin/mcp.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// bin/mcp.ts
|
|
5
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
9
|
+
var import_sdk = require("@webhooks-cc/sdk");
|
|
10
|
+
|
|
11
|
+
// src/tools.ts
|
|
12
|
+
var import_zod = require("zod");
|
|
13
|
+
var MAX_BODY_SIZE = 32768;
|
|
14
|
+
function textContent(text) {
|
|
15
|
+
return { content: [{ type: "text", text }] };
|
|
16
|
+
}
|
|
17
|
+
async function readBodyTruncated(response, limit = MAX_BODY_SIZE) {
|
|
18
|
+
const text = await response.text();
|
|
19
|
+
if (text.length <= limit) return text;
|
|
20
|
+
return text.slice(0, limit) + `
|
|
21
|
+
... [truncated, ${text.length} chars total]`;
|
|
22
|
+
}
|
|
23
|
+
function withErrorHandling(handler) {
|
|
24
|
+
return async (args2) => {
|
|
25
|
+
try {
|
|
26
|
+
return await handler(args2);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
return { ...textContent(`Error: ${message}`), isError: true };
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function registerTools(server, client) {
|
|
34
|
+
server.tool(
|
|
35
|
+
"create_endpoint",
|
|
36
|
+
"Create a new webhook endpoint. Returns the endpoint URL and slug.",
|
|
37
|
+
{ name: import_zod.z.string().optional().describe("Display name for the endpoint") },
|
|
38
|
+
withErrorHandling(async ({ name }) => {
|
|
39
|
+
const endpoint = await client.endpoints.create({ name });
|
|
40
|
+
return textContent(JSON.stringify(endpoint, null, 2));
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
server.tool(
|
|
44
|
+
"list_endpoints",
|
|
45
|
+
"List all webhook endpoints for the authenticated user. Returns an array of endpoints with their slugs, names, and URLs.",
|
|
46
|
+
{},
|
|
47
|
+
withErrorHandling(async () => {
|
|
48
|
+
const endpoints = await client.endpoints.list();
|
|
49
|
+
return textContent(JSON.stringify(endpoints, null, 2));
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
server.tool(
|
|
53
|
+
"get_endpoint",
|
|
54
|
+
"Get details for a specific webhook endpoint by its slug.",
|
|
55
|
+
{ slug: import_zod.z.string().describe("The endpoint slug (from the URL)") },
|
|
56
|
+
withErrorHandling(async ({ slug }) => {
|
|
57
|
+
const endpoint = await client.endpoints.get(slug);
|
|
58
|
+
return textContent(JSON.stringify(endpoint, null, 2));
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
server.tool(
|
|
62
|
+
"update_endpoint",
|
|
63
|
+
"Update an endpoint's name or mock response configuration.",
|
|
64
|
+
{
|
|
65
|
+
slug: import_zod.z.string().describe("The endpoint slug to update"),
|
|
66
|
+
name: import_zod.z.string().optional().describe("New display name"),
|
|
67
|
+
mockResponse: import_zod.z.object({
|
|
68
|
+
status: import_zod.z.number().min(100).max(599).describe("HTTP status code (100-599)"),
|
|
69
|
+
body: import_zod.z.string().default("").describe("Response body string (default: empty)"),
|
|
70
|
+
headers: import_zod.z.record(import_zod.z.string()).default({}).describe("Response headers (default: none)")
|
|
71
|
+
}).nullable().optional().describe("Mock response config, or null to clear it")
|
|
72
|
+
},
|
|
73
|
+
withErrorHandling(async ({ slug, name, mockResponse }) => {
|
|
74
|
+
const endpoint = await client.endpoints.update(slug, { name, mockResponse });
|
|
75
|
+
return textContent(JSON.stringify(endpoint, null, 2));
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
server.tool(
|
|
79
|
+
"delete_endpoint",
|
|
80
|
+
"Delete a webhook endpoint and all its captured requests.",
|
|
81
|
+
{ slug: import_zod.z.string().describe("The endpoint slug to delete") },
|
|
82
|
+
withErrorHandling(async ({ slug }) => {
|
|
83
|
+
await client.endpoints.delete(slug);
|
|
84
|
+
return textContent(`Endpoint "${slug}" deleted.`);
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
server.tool(
|
|
88
|
+
"send_webhook",
|
|
89
|
+
"Send a test webhook to an endpoint. Useful for testing webhook handling code.",
|
|
90
|
+
{
|
|
91
|
+
slug: import_zod.z.string().describe("The endpoint slug to send to"),
|
|
92
|
+
method: import_zod.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).default("POST").describe("HTTP method (default: POST)"),
|
|
93
|
+
headers: import_zod.z.record(import_zod.z.string()).optional().describe("HTTP headers to include"),
|
|
94
|
+
body: import_zod.z.unknown().optional().describe("Request body (will be JSON-serialized)")
|
|
95
|
+
},
|
|
96
|
+
withErrorHandling(async ({ slug, method, headers, body }) => {
|
|
97
|
+
const response = await client.endpoints.send(slug, { method, headers, body });
|
|
98
|
+
const responseBody = await readBodyTruncated(response);
|
|
99
|
+
return textContent(
|
|
100
|
+
JSON.stringify(
|
|
101
|
+
{ status: response.status, statusText: response.statusText, body: responseBody },
|
|
102
|
+
null,
|
|
103
|
+
2
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
server.tool(
|
|
109
|
+
"list_requests",
|
|
110
|
+
"List captured webhook requests for an endpoint. Returns the most recent requests (default: 25).",
|
|
111
|
+
{
|
|
112
|
+
endpointSlug: import_zod.z.string().describe("The endpoint slug"),
|
|
113
|
+
limit: import_zod.z.number().default(25).describe("Max number of requests to return (default: 25)"),
|
|
114
|
+
since: import_zod.z.number().optional().describe("Only return requests after this timestamp (ms)")
|
|
115
|
+
},
|
|
116
|
+
withErrorHandling(async ({ endpointSlug, limit, since }) => {
|
|
117
|
+
const requests = await client.requests.list(endpointSlug, { limit, since });
|
|
118
|
+
return textContent(JSON.stringify(requests, null, 2));
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
server.tool(
|
|
122
|
+
"get_request",
|
|
123
|
+
"Get full details of a specific captured webhook request by its ID. Includes method, headers, body, path, and timestamp.",
|
|
124
|
+
{ requestId: import_zod.z.string().describe("The request ID") },
|
|
125
|
+
withErrorHandling(async ({ requestId }) => {
|
|
126
|
+
const request = await client.requests.get(requestId);
|
|
127
|
+
return textContent(JSON.stringify(request, null, 2));
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
server.tool(
|
|
131
|
+
"wait_for_request",
|
|
132
|
+
"Wait for a webhook request to arrive at an endpoint. Polls until a request is captured or timeout expires. Use this after sending a webhook to verify it was received.",
|
|
133
|
+
{
|
|
134
|
+
endpointSlug: import_zod.z.string().describe("The endpoint slug to monitor"),
|
|
135
|
+
timeout: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]).default("30s").describe('How long to wait (e.g. "30s", "5m", or milliseconds as number)'),
|
|
136
|
+
pollInterval: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]).optional().describe('Interval between polls (e.g. "1s", "500", or milliseconds). Default: 500ms')
|
|
137
|
+
},
|
|
138
|
+
withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
|
|
139
|
+
const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
|
|
140
|
+
return textContent(JSON.stringify(request, null, 2));
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
server.tool(
|
|
144
|
+
"replay_request",
|
|
145
|
+
"Replay a previously captured webhook request to a target URL. Sends the original method, headers, and body to the specified URL. Only use with URLs you trust \u2014 the original request data is forwarded.",
|
|
146
|
+
{
|
|
147
|
+
requestId: import_zod.z.string().describe("The ID of the captured request to replay"),
|
|
148
|
+
targetUrl: import_zod.z.string().url().refine(
|
|
149
|
+
(u) => {
|
|
150
|
+
try {
|
|
151
|
+
const p = new URL(u).protocol;
|
|
152
|
+
return p === "http:" || p === "https:";
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{ message: "Only http and https URLs are supported" }
|
|
158
|
+
).describe("The URL to send the replayed request to (http or https only)")
|
|
159
|
+
},
|
|
160
|
+
withErrorHandling(async ({ requestId, targetUrl }) => {
|
|
161
|
+
const response = await client.requests.replay(requestId, targetUrl);
|
|
162
|
+
const responseBody = await readBodyTruncated(response);
|
|
163
|
+
return textContent(
|
|
164
|
+
JSON.stringify(
|
|
165
|
+
{ status: response.status, statusText: response.statusText, body: responseBody },
|
|
166
|
+
null,
|
|
167
|
+
2
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
server.tool(
|
|
173
|
+
"describe",
|
|
174
|
+
"Describe all available SDK operations, their parameters, and types. Useful for discovering what actions are possible.",
|
|
175
|
+
{},
|
|
176
|
+
withErrorHandling(async () => {
|
|
177
|
+
const description = client.describe();
|
|
178
|
+
return textContent(JSON.stringify(description, null, 2));
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/index.ts
|
|
184
|
+
var VERSION = true ? "0.1.0" : "0.0.0-dev";
|
|
185
|
+
function createServer(options = {}) {
|
|
186
|
+
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
187
|
+
if (!apiKey) {
|
|
188
|
+
throw new Error("Missing API key. Set WHK_API_KEY environment variable or pass apiKey option.");
|
|
189
|
+
}
|
|
190
|
+
const client = new import_sdk.WebhooksCC({
|
|
191
|
+
apiKey,
|
|
192
|
+
webhookUrl: options.webhookUrl ?? process.env.WHK_WEBHOOK_URL,
|
|
193
|
+
baseUrl: options.baseUrl ?? process.env.WHK_BASE_URL
|
|
194
|
+
});
|
|
195
|
+
const server = new import_mcp.McpServer({
|
|
196
|
+
name: "webhooks-cc",
|
|
197
|
+
version: VERSION
|
|
198
|
+
});
|
|
199
|
+
registerTools(server, client);
|
|
200
|
+
return server;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/setup.ts
|
|
204
|
+
var import_fs = require("fs");
|
|
205
|
+
var import_path = require("path");
|
|
206
|
+
var import_child_process = require("child_process");
|
|
207
|
+
var import_os = require("os");
|
|
208
|
+
var TOOLS = ["claude-code", "claude-desktop", "cursor", "vscode", "codex", "windsurf"];
|
|
209
|
+
var API_KEY_REGEX = /^whcc_[a-zA-Z0-9_-]+$/;
|
|
210
|
+
function validateApiKeyFormat(key) {
|
|
211
|
+
if (!API_KEY_REGEX.test(key)) {
|
|
212
|
+
console.error("Error: Invalid API key format. Keys must start with 'whcc_' and contain only");
|
|
213
|
+
console.error("alphanumeric characters, hyphens, and underscores.");
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function mcpServerConfig(apiKey) {
|
|
218
|
+
return {
|
|
219
|
+
command: "npx",
|
|
220
|
+
args: ["-y", "@webhooks-cc/mcp"],
|
|
221
|
+
env: { WHK_API_KEY: apiKey }
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function resolveApiKey(flags) {
|
|
225
|
+
return flags["api-key"] ?? process.env.WHK_API_KEY ?? null;
|
|
226
|
+
}
|
|
227
|
+
function mergeJsonConfig(filePath, serverName, serverConfig) {
|
|
228
|
+
let existing = {};
|
|
229
|
+
if ((0, import_fs.existsSync)(filePath)) {
|
|
230
|
+
try {
|
|
231
|
+
existing = JSON.parse((0, import_fs.readFileSync)(filePath, "utf-8"));
|
|
232
|
+
} catch {
|
|
233
|
+
console.warn(`Warning: ${filePath} contains invalid JSON and will be overwritten.`);
|
|
234
|
+
console.warn(`A backup has been saved to ${filePath}.bak`);
|
|
235
|
+
(0, import_fs.copyFileSync)(filePath, `${filePath}.bak`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const mcpServers = existing.mcpServers ?? {};
|
|
239
|
+
mcpServers[serverName] = serverConfig;
|
|
240
|
+
existing.mcpServers = mcpServers;
|
|
241
|
+
const dir = (0, import_path.dirname)(filePath);
|
|
242
|
+
if (!(0, import_fs.existsSync)(dir)) (0, import_fs.mkdirSync)(dir, { recursive: true });
|
|
243
|
+
(0, import_fs.writeFileSync)(filePath, JSON.stringify(existing, null, 2) + "\n");
|
|
244
|
+
}
|
|
245
|
+
function getClaudeDesktopConfigPath() {
|
|
246
|
+
const os = (0, import_os.platform)();
|
|
247
|
+
if (os === "darwin") {
|
|
248
|
+
return (0, import_path.join)(
|
|
249
|
+
(0, import_os.homedir)(),
|
|
250
|
+
"Library",
|
|
251
|
+
"Application Support",
|
|
252
|
+
"Claude",
|
|
253
|
+
"claude_desktop_config.json"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (os === "win32") {
|
|
257
|
+
return (0, import_path.join)(
|
|
258
|
+
process.env.APPDATA ?? (0, import_path.join)((0, import_os.homedir)(), "AppData", "Roaming"),
|
|
259
|
+
"Claude",
|
|
260
|
+
"claude_desktop_config.json"
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return (0, import_path.join)((0, import_os.homedir)(), ".config", "Claude", "claude_desktop_config.json");
|
|
264
|
+
}
|
|
265
|
+
function setupClaudeCode(apiKey) {
|
|
266
|
+
try {
|
|
267
|
+
(0, import_child_process.execFileSync)(
|
|
268
|
+
"claude",
|
|
269
|
+
[
|
|
270
|
+
"mcp",
|
|
271
|
+
"add",
|
|
272
|
+
"-s",
|
|
273
|
+
"user",
|
|
274
|
+
"--transport",
|
|
275
|
+
"stdio",
|
|
276
|
+
"webhooks-cc",
|
|
277
|
+
"-e",
|
|
278
|
+
`WHK_API_KEY=${apiKey}`,
|
|
279
|
+
"--",
|
|
280
|
+
"npx",
|
|
281
|
+
"-y",
|
|
282
|
+
"@webhooks-cc/mcp"
|
|
283
|
+
],
|
|
284
|
+
{ stdio: "inherit" }
|
|
285
|
+
);
|
|
286
|
+
console.log("\nDone! webhooks-cc MCP server added to Claude Code (user scope).");
|
|
287
|
+
console.log("It will be available in all your Claude Code sessions.");
|
|
288
|
+
} catch {
|
|
289
|
+
console.error("\nFailed to run 'claude mcp add'. Is Claude Code CLI installed?");
|
|
290
|
+
console.error("Install it: npm install -g @anthropic-ai/claude-code");
|
|
291
|
+
console.error("\nManual alternative \u2014 run this command:");
|
|
292
|
+
console.error(
|
|
293
|
+
` claude mcp add -s user --transport stdio webhooks-cc -e WHK_API_KEY=<your-key> -- npx -y @webhooks-cc/mcp`
|
|
294
|
+
);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function setupClaudeDesktop(apiKey) {
|
|
299
|
+
const configPath = getClaudeDesktopConfigPath();
|
|
300
|
+
mergeJsonConfig(configPath, "webhooks-cc", mcpServerConfig(apiKey));
|
|
301
|
+
console.log(`Done! Config written to ${configPath}`);
|
|
302
|
+
console.log("Restart Claude Desktop for changes to take effect.");
|
|
303
|
+
}
|
|
304
|
+
function setupCursor(apiKey) {
|
|
305
|
+
const configPath = (0, import_path.join)(process.cwd(), ".cursor", "mcp.json");
|
|
306
|
+
mergeJsonConfig(configPath, "webhooks-cc", mcpServerConfig(apiKey));
|
|
307
|
+
console.log(`Done! Config written to ${configPath}`);
|
|
308
|
+
console.log("Restart Cursor or reload the window for changes to take effect.");
|
|
309
|
+
}
|
|
310
|
+
function setupVSCode(apiKey) {
|
|
311
|
+
const configPath = (0, import_path.join)(process.cwd(), ".vscode", "mcp.json");
|
|
312
|
+
mergeJsonConfig(configPath, "webhooks-cc", mcpServerConfig(apiKey));
|
|
313
|
+
console.log(`Done! Config written to ${configPath}`);
|
|
314
|
+
console.log("Reload VS Code window for changes to take effect.");
|
|
315
|
+
}
|
|
316
|
+
function setupCodex(apiKey) {
|
|
317
|
+
try {
|
|
318
|
+
(0, import_child_process.execFileSync)(
|
|
319
|
+
"codex",
|
|
320
|
+
[
|
|
321
|
+
"mcp",
|
|
322
|
+
"add",
|
|
323
|
+
"webhooks-cc",
|
|
324
|
+
"-e",
|
|
325
|
+
`WHK_API_KEY=${apiKey}`,
|
|
326
|
+
"--",
|
|
327
|
+
"npx",
|
|
328
|
+
"-y",
|
|
329
|
+
"@webhooks-cc/mcp"
|
|
330
|
+
],
|
|
331
|
+
{ stdio: "inherit" }
|
|
332
|
+
);
|
|
333
|
+
console.log("\nDone! webhooks-cc MCP server added to Codex.");
|
|
334
|
+
} catch {
|
|
335
|
+
console.error("\nFailed to run 'codex mcp add'. Is OpenAI Codex CLI installed?");
|
|
336
|
+
console.error("\nManual alternative \u2014 add to ~/.codex/config.toml:");
|
|
337
|
+
console.error(`
|
|
338
|
+
[mcp.webhooks-cc]
|
|
339
|
+
command = "npx"
|
|
340
|
+
args = ["-y", "@webhooks-cc/mcp"]
|
|
341
|
+
|
|
342
|
+
[mcp.webhooks-cc.env]
|
|
343
|
+
WHK_API_KEY = "<your-key>"
|
|
344
|
+
`);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function setupWindsurf(apiKey) {
|
|
349
|
+
const configPath = (0, import_path.join)((0, import_os.homedir)(), ".codeium", "windsurf", "mcp_config.json");
|
|
350
|
+
mergeJsonConfig(configPath, "webhooks-cc", mcpServerConfig(apiKey));
|
|
351
|
+
console.log(`Done! Config written to ${configPath}`);
|
|
352
|
+
console.log("Restart Windsurf for changes to take effect.");
|
|
353
|
+
}
|
|
354
|
+
function printUsage() {
|
|
355
|
+
console.log(`
|
|
356
|
+
@webhooks-cc/mcp setup \u2014 Configure the MCP server for your AI tool
|
|
357
|
+
|
|
358
|
+
Usage:
|
|
359
|
+
npx @webhooks-cc/mcp setup <tool> [--api-key <key>]
|
|
360
|
+
|
|
361
|
+
Tools:
|
|
362
|
+
claude-code Claude Code CLI (runs 'claude mcp add')
|
|
363
|
+
claude-desktop Claude Desktop app
|
|
364
|
+
cursor Cursor editor (writes .cursor/mcp.json)
|
|
365
|
+
vscode VS Code Copilot (writes .vscode/mcp.json)
|
|
366
|
+
codex OpenAI Codex CLI (runs 'codex mcp add')
|
|
367
|
+
windsurf Windsurf editor
|
|
368
|
+
|
|
369
|
+
Options:
|
|
370
|
+
--api-key <key> Your webhooks.cc API key (or set WHK_API_KEY env var)
|
|
371
|
+
|
|
372
|
+
Examples:
|
|
373
|
+
npx @webhooks-cc/mcp setup claude-code --api-key whcc_abc123
|
|
374
|
+
WHK_API_KEY=whcc_abc123 npx @webhooks-cc/mcp setup cursor
|
|
375
|
+
npx @webhooks-cc/mcp setup vscode --api-key whcc_abc123
|
|
376
|
+
`);
|
|
377
|
+
}
|
|
378
|
+
function runSetup(args2) {
|
|
379
|
+
const flags = {};
|
|
380
|
+
const positional = [];
|
|
381
|
+
for (let i = 0; i < args2.length; i++) {
|
|
382
|
+
if (args2[i] === "--api-key" && i + 1 < args2.length) {
|
|
383
|
+
flags["api-key"] = args2[++i];
|
|
384
|
+
} else if (args2[i].startsWith("--api-key=")) {
|
|
385
|
+
flags["api-key"] = args2[i].slice("--api-key=".length);
|
|
386
|
+
} else if (!args2[i].startsWith("-")) {
|
|
387
|
+
positional.push(args2[i]);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const tool = positional[0];
|
|
391
|
+
if (!tool || !TOOLS.includes(tool)) {
|
|
392
|
+
printUsage();
|
|
393
|
+
if (tool) {
|
|
394
|
+
console.error(`Unknown tool: "${tool}"
|
|
395
|
+
`);
|
|
396
|
+
}
|
|
397
|
+
process.exit(tool ? 1 : 0);
|
|
398
|
+
}
|
|
399
|
+
const apiKey = resolveApiKey(flags);
|
|
400
|
+
if (!apiKey) {
|
|
401
|
+
console.error("Error: API key required. Pass --api-key or set WHK_API_KEY env var.");
|
|
402
|
+
console.error("Get your API key at https://webhooks.cc/account\n");
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
validateApiKeyFormat(apiKey);
|
|
406
|
+
console.log(`Setting up webhooks-cc MCP server for ${tool}...
|
|
407
|
+
`);
|
|
408
|
+
switch (tool) {
|
|
409
|
+
case "claude-code":
|
|
410
|
+
setupClaudeCode(apiKey);
|
|
411
|
+
break;
|
|
412
|
+
case "claude-desktop":
|
|
413
|
+
setupClaudeDesktop(apiKey);
|
|
414
|
+
break;
|
|
415
|
+
case "cursor":
|
|
416
|
+
setupCursor(apiKey);
|
|
417
|
+
break;
|
|
418
|
+
case "vscode":
|
|
419
|
+
setupVSCode(apiKey);
|
|
420
|
+
break;
|
|
421
|
+
case "codex":
|
|
422
|
+
setupCodex(apiKey);
|
|
423
|
+
break;
|
|
424
|
+
case "windsurf":
|
|
425
|
+
setupWindsurf(apiKey);
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// bin/mcp.ts
|
|
431
|
+
var args = process.argv.slice(2);
|
|
432
|
+
var command = args[0];
|
|
433
|
+
if (command === "setup") {
|
|
434
|
+
runSetup(args.slice(1));
|
|
435
|
+
} else if (command === "--help" || command === "-h") {
|
|
436
|
+
console.log(`
|
|
437
|
+
@webhooks-cc/mcp \u2014 MCP server for webhooks.cc
|
|
438
|
+
|
|
439
|
+
Usage:
|
|
440
|
+
npx @webhooks-cc/mcp Start stdio MCP server
|
|
441
|
+
npx @webhooks-cc/mcp setup <tool> Configure for an AI tool
|
|
442
|
+
npx @webhooks-cc/mcp --help Show this help
|
|
443
|
+
|
|
444
|
+
Environment:
|
|
445
|
+
WHK_API_KEY Your webhooks.cc API key (required)
|
|
446
|
+
WHK_WEBHOOK_URL Custom webhook receiver URL (optional)
|
|
447
|
+
WHK_BASE_URL Custom API base URL (optional)
|
|
448
|
+
|
|
449
|
+
Setup:
|
|
450
|
+
npx @webhooks-cc/mcp setup claude-code --api-key whcc_...
|
|
451
|
+
npx @webhooks-cc/mcp setup cursor --api-key whcc_...
|
|
452
|
+
npx @webhooks-cc/mcp setup vscode --api-key whcc_...
|
|
453
|
+
npx @webhooks-cc/mcp setup codex --api-key whcc_...
|
|
454
|
+
npx @webhooks-cc/mcp setup windsurf --api-key whcc_...
|
|
455
|
+
npx @webhooks-cc/mcp setup claude-desktop --api-key whcc_...
|
|
456
|
+
|
|
457
|
+
Get your API key at https://webhooks.cc/account
|
|
458
|
+
`);
|
|
459
|
+
} else if (command && command !== "--") {
|
|
460
|
+
console.error(`Unknown command: "${command}". Run with --help for usage.`);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
} else {
|
|
463
|
+
const server = createServer();
|
|
464
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
465
|
+
server.connect(transport).catch((error) => {
|
|
466
|
+
console.error("Fatal:", error instanceof Error ? error.message : error);
|
|
467
|
+
process.exit(1);
|
|
468
|
+
});
|
|
469
|
+
}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { WebhooksCC } from '@webhooks-cc/sdk';
|
|
3
|
+
|
|
4
|
+
/** Register all 11 webhook tools on an MCP server instance. */
|
|
5
|
+
declare function registerTools(server: McpServer, client: WebhooksCC): void;
|
|
6
|
+
|
|
7
|
+
interface CreateServerOptions {
|
|
8
|
+
/** API key for webhooks.cc (default: reads WHK_API_KEY env var) */
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
/** Custom webhook receiver URL (default: reads WHK_WEBHOOK_URL or https://go.webhooks.cc) */
|
|
11
|
+
webhookUrl?: string;
|
|
12
|
+
/** Custom API base URL (default: https://webhooks.cc) */
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create an MCP server with all webhooks.cc tools registered.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { createServer } from "@webhooks-cc/mcp";
|
|
21
|
+
* import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
+
*
|
|
23
|
+
* const server = createServer({ apiKey: "whcc_..." });
|
|
24
|
+
* await server.connect(new StdioServerTransport());
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare function createServer(options?: CreateServerOptions): McpServer;
|
|
28
|
+
|
|
29
|
+
export { type CreateServerOptions, createServer, registerTools };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { WebhooksCC } from '@webhooks-cc/sdk';
|
|
3
|
+
|
|
4
|
+
/** Register all 11 webhook tools on an MCP server instance. */
|
|
5
|
+
declare function registerTools(server: McpServer, client: WebhooksCC): void;
|
|
6
|
+
|
|
7
|
+
interface CreateServerOptions {
|
|
8
|
+
/** API key for webhooks.cc (default: reads WHK_API_KEY env var) */
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
/** Custom webhook receiver URL (default: reads WHK_WEBHOOK_URL or https://go.webhooks.cc) */
|
|
11
|
+
webhookUrl?: string;
|
|
12
|
+
/** Custom API base URL (default: https://webhooks.cc) */
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create an MCP server with all webhooks.cc tools registered.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { createServer } from "@webhooks-cc/mcp";
|
|
21
|
+
* import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
+
*
|
|
23
|
+
* const server = createServer({ apiKey: "whcc_..." });
|
|
24
|
+
* await server.connect(new StdioServerTransport());
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare function createServer(options?: CreateServerOptions): McpServer;
|
|
28
|
+
|
|
29
|
+
export { type CreateServerOptions, createServer, registerTools };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createServer: () => createServer,
|
|
24
|
+
registerTools: () => registerTools
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
28
|
+
var import_sdk = require("@webhooks-cc/sdk");
|
|
29
|
+
|
|
30
|
+
// src/tools.ts
|
|
31
|
+
var import_zod = require("zod");
|
|
32
|
+
var MAX_BODY_SIZE = 32768;
|
|
33
|
+
function textContent(text) {
|
|
34
|
+
return { content: [{ type: "text", text }] };
|
|
35
|
+
}
|
|
36
|
+
async function readBodyTruncated(response, limit = MAX_BODY_SIZE) {
|
|
37
|
+
const text = await response.text();
|
|
38
|
+
if (text.length <= limit) return text;
|
|
39
|
+
return text.slice(0, limit) + `
|
|
40
|
+
... [truncated, ${text.length} chars total]`;
|
|
41
|
+
}
|
|
42
|
+
function withErrorHandling(handler) {
|
|
43
|
+
return async (args) => {
|
|
44
|
+
try {
|
|
45
|
+
return await handler(args);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
48
|
+
return { ...textContent(`Error: ${message}`), isError: true };
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function registerTools(server, client) {
|
|
53
|
+
server.tool(
|
|
54
|
+
"create_endpoint",
|
|
55
|
+
"Create a new webhook endpoint. Returns the endpoint URL and slug.",
|
|
56
|
+
{ name: import_zod.z.string().optional().describe("Display name for the endpoint") },
|
|
57
|
+
withErrorHandling(async ({ name }) => {
|
|
58
|
+
const endpoint = await client.endpoints.create({ name });
|
|
59
|
+
return textContent(JSON.stringify(endpoint, null, 2));
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
server.tool(
|
|
63
|
+
"list_endpoints",
|
|
64
|
+
"List all webhook endpoints for the authenticated user. Returns an array of endpoints with their slugs, names, and URLs.",
|
|
65
|
+
{},
|
|
66
|
+
withErrorHandling(async () => {
|
|
67
|
+
const endpoints = await client.endpoints.list();
|
|
68
|
+
return textContent(JSON.stringify(endpoints, null, 2));
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
server.tool(
|
|
72
|
+
"get_endpoint",
|
|
73
|
+
"Get details for a specific webhook endpoint by its slug.",
|
|
74
|
+
{ slug: import_zod.z.string().describe("The endpoint slug (from the URL)") },
|
|
75
|
+
withErrorHandling(async ({ slug }) => {
|
|
76
|
+
const endpoint = await client.endpoints.get(slug);
|
|
77
|
+
return textContent(JSON.stringify(endpoint, null, 2));
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
server.tool(
|
|
81
|
+
"update_endpoint",
|
|
82
|
+
"Update an endpoint's name or mock response configuration.",
|
|
83
|
+
{
|
|
84
|
+
slug: import_zod.z.string().describe("The endpoint slug to update"),
|
|
85
|
+
name: import_zod.z.string().optional().describe("New display name"),
|
|
86
|
+
mockResponse: import_zod.z.object({
|
|
87
|
+
status: import_zod.z.number().min(100).max(599).describe("HTTP status code (100-599)"),
|
|
88
|
+
body: import_zod.z.string().default("").describe("Response body string (default: empty)"),
|
|
89
|
+
headers: import_zod.z.record(import_zod.z.string()).default({}).describe("Response headers (default: none)")
|
|
90
|
+
}).nullable().optional().describe("Mock response config, or null to clear it")
|
|
91
|
+
},
|
|
92
|
+
withErrorHandling(async ({ slug, name, mockResponse }) => {
|
|
93
|
+
const endpoint = await client.endpoints.update(slug, { name, mockResponse });
|
|
94
|
+
return textContent(JSON.stringify(endpoint, null, 2));
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
server.tool(
|
|
98
|
+
"delete_endpoint",
|
|
99
|
+
"Delete a webhook endpoint and all its captured requests.",
|
|
100
|
+
{ slug: import_zod.z.string().describe("The endpoint slug to delete") },
|
|
101
|
+
withErrorHandling(async ({ slug }) => {
|
|
102
|
+
await client.endpoints.delete(slug);
|
|
103
|
+
return textContent(`Endpoint "${slug}" deleted.`);
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
server.tool(
|
|
107
|
+
"send_webhook",
|
|
108
|
+
"Send a test webhook to an endpoint. Useful for testing webhook handling code.",
|
|
109
|
+
{
|
|
110
|
+
slug: import_zod.z.string().describe("The endpoint slug to send to"),
|
|
111
|
+
method: import_zod.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).default("POST").describe("HTTP method (default: POST)"),
|
|
112
|
+
headers: import_zod.z.record(import_zod.z.string()).optional().describe("HTTP headers to include"),
|
|
113
|
+
body: import_zod.z.unknown().optional().describe("Request body (will be JSON-serialized)")
|
|
114
|
+
},
|
|
115
|
+
withErrorHandling(async ({ slug, method, headers, body }) => {
|
|
116
|
+
const response = await client.endpoints.send(slug, { method, headers, body });
|
|
117
|
+
const responseBody = await readBodyTruncated(response);
|
|
118
|
+
return textContent(
|
|
119
|
+
JSON.stringify(
|
|
120
|
+
{ status: response.status, statusText: response.statusText, body: responseBody },
|
|
121
|
+
null,
|
|
122
|
+
2
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
server.tool(
|
|
128
|
+
"list_requests",
|
|
129
|
+
"List captured webhook requests for an endpoint. Returns the most recent requests (default: 25).",
|
|
130
|
+
{
|
|
131
|
+
endpointSlug: import_zod.z.string().describe("The endpoint slug"),
|
|
132
|
+
limit: import_zod.z.number().default(25).describe("Max number of requests to return (default: 25)"),
|
|
133
|
+
since: import_zod.z.number().optional().describe("Only return requests after this timestamp (ms)")
|
|
134
|
+
},
|
|
135
|
+
withErrorHandling(async ({ endpointSlug, limit, since }) => {
|
|
136
|
+
const requests = await client.requests.list(endpointSlug, { limit, since });
|
|
137
|
+
return textContent(JSON.stringify(requests, null, 2));
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
server.tool(
|
|
141
|
+
"get_request",
|
|
142
|
+
"Get full details of a specific captured webhook request by its ID. Includes method, headers, body, path, and timestamp.",
|
|
143
|
+
{ requestId: import_zod.z.string().describe("The request ID") },
|
|
144
|
+
withErrorHandling(async ({ requestId }) => {
|
|
145
|
+
const request = await client.requests.get(requestId);
|
|
146
|
+
return textContent(JSON.stringify(request, null, 2));
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
server.tool(
|
|
150
|
+
"wait_for_request",
|
|
151
|
+
"Wait for a webhook request to arrive at an endpoint. Polls until a request is captured or timeout expires. Use this after sending a webhook to verify it was received.",
|
|
152
|
+
{
|
|
153
|
+
endpointSlug: import_zod.z.string().describe("The endpoint slug to monitor"),
|
|
154
|
+
timeout: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]).default("30s").describe('How long to wait (e.g. "30s", "5m", or milliseconds as number)'),
|
|
155
|
+
pollInterval: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]).optional().describe('Interval between polls (e.g. "1s", "500", or milliseconds). Default: 500ms')
|
|
156
|
+
},
|
|
157
|
+
withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
|
|
158
|
+
const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
|
|
159
|
+
return textContent(JSON.stringify(request, null, 2));
|
|
160
|
+
})
|
|
161
|
+
);
|
|
162
|
+
server.tool(
|
|
163
|
+
"replay_request",
|
|
164
|
+
"Replay a previously captured webhook request to a target URL. Sends the original method, headers, and body to the specified URL. Only use with URLs you trust \u2014 the original request data is forwarded.",
|
|
165
|
+
{
|
|
166
|
+
requestId: import_zod.z.string().describe("The ID of the captured request to replay"),
|
|
167
|
+
targetUrl: import_zod.z.string().url().refine(
|
|
168
|
+
(u) => {
|
|
169
|
+
try {
|
|
170
|
+
const p = new URL(u).protocol;
|
|
171
|
+
return p === "http:" || p === "https:";
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{ message: "Only http and https URLs are supported" }
|
|
177
|
+
).describe("The URL to send the replayed request to (http or https only)")
|
|
178
|
+
},
|
|
179
|
+
withErrorHandling(async ({ requestId, targetUrl }) => {
|
|
180
|
+
const response = await client.requests.replay(requestId, targetUrl);
|
|
181
|
+
const responseBody = await readBodyTruncated(response);
|
|
182
|
+
return textContent(
|
|
183
|
+
JSON.stringify(
|
|
184
|
+
{ status: response.status, statusText: response.statusText, body: responseBody },
|
|
185
|
+
null,
|
|
186
|
+
2
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
server.tool(
|
|
192
|
+
"describe",
|
|
193
|
+
"Describe all available SDK operations, their parameters, and types. Useful for discovering what actions are possible.",
|
|
194
|
+
{},
|
|
195
|
+
withErrorHandling(async () => {
|
|
196
|
+
const description = client.describe();
|
|
197
|
+
return textContent(JSON.stringify(description, null, 2));
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/index.ts
|
|
203
|
+
var VERSION = true ? "0.1.0" : "0.0.0-dev";
|
|
204
|
+
function createServer(options = {}) {
|
|
205
|
+
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
206
|
+
if (!apiKey) {
|
|
207
|
+
throw new Error("Missing API key. Set WHK_API_KEY environment variable or pass apiKey option.");
|
|
208
|
+
}
|
|
209
|
+
const client = new import_sdk.WebhooksCC({
|
|
210
|
+
apiKey,
|
|
211
|
+
webhookUrl: options.webhookUrl ?? process.env.WHK_WEBHOOK_URL,
|
|
212
|
+
baseUrl: options.baseUrl ?? process.env.WHK_BASE_URL
|
|
213
|
+
});
|
|
214
|
+
const server = new import_mcp.McpServer({
|
|
215
|
+
name: "webhooks-cc",
|
|
216
|
+
version: VERSION
|
|
217
|
+
});
|
|
218
|
+
registerTools(server, client);
|
|
219
|
+
return server;
|
|
220
|
+
}
|
|
221
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
222
|
+
0 && (module.exports = {
|
|
223
|
+
createServer,
|
|
224
|
+
registerTools
|
|
225
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { WebhooksCC } from "@webhooks-cc/sdk";
|
|
4
|
+
|
|
5
|
+
// src/tools.ts
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
var MAX_BODY_SIZE = 32768;
|
|
8
|
+
function textContent(text) {
|
|
9
|
+
return { content: [{ type: "text", text }] };
|
|
10
|
+
}
|
|
11
|
+
async function readBodyTruncated(response, limit = MAX_BODY_SIZE) {
|
|
12
|
+
const text = await response.text();
|
|
13
|
+
if (text.length <= limit) return text;
|
|
14
|
+
return text.slice(0, limit) + `
|
|
15
|
+
... [truncated, ${text.length} chars total]`;
|
|
16
|
+
}
|
|
17
|
+
function withErrorHandling(handler) {
|
|
18
|
+
return async (args) => {
|
|
19
|
+
try {
|
|
20
|
+
return await handler(args);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
return { ...textContent(`Error: ${message}`), isError: true };
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function registerTools(server, client) {
|
|
28
|
+
server.tool(
|
|
29
|
+
"create_endpoint",
|
|
30
|
+
"Create a new webhook endpoint. Returns the endpoint URL and slug.",
|
|
31
|
+
{ name: z.string().optional().describe("Display name for the endpoint") },
|
|
32
|
+
withErrorHandling(async ({ name }) => {
|
|
33
|
+
const endpoint = await client.endpoints.create({ name });
|
|
34
|
+
return textContent(JSON.stringify(endpoint, null, 2));
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
server.tool(
|
|
38
|
+
"list_endpoints",
|
|
39
|
+
"List all webhook endpoints for the authenticated user. Returns an array of endpoints with their slugs, names, and URLs.",
|
|
40
|
+
{},
|
|
41
|
+
withErrorHandling(async () => {
|
|
42
|
+
const endpoints = await client.endpoints.list();
|
|
43
|
+
return textContent(JSON.stringify(endpoints, null, 2));
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
server.tool(
|
|
47
|
+
"get_endpoint",
|
|
48
|
+
"Get details for a specific webhook endpoint by its slug.",
|
|
49
|
+
{ slug: z.string().describe("The endpoint slug (from the URL)") },
|
|
50
|
+
withErrorHandling(async ({ slug }) => {
|
|
51
|
+
const endpoint = await client.endpoints.get(slug);
|
|
52
|
+
return textContent(JSON.stringify(endpoint, null, 2));
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
server.tool(
|
|
56
|
+
"update_endpoint",
|
|
57
|
+
"Update an endpoint's name or mock response configuration.",
|
|
58
|
+
{
|
|
59
|
+
slug: z.string().describe("The endpoint slug to update"),
|
|
60
|
+
name: z.string().optional().describe("New display name"),
|
|
61
|
+
mockResponse: z.object({
|
|
62
|
+
status: z.number().min(100).max(599).describe("HTTP status code (100-599)"),
|
|
63
|
+
body: z.string().default("").describe("Response body string (default: empty)"),
|
|
64
|
+
headers: z.record(z.string()).default({}).describe("Response headers (default: none)")
|
|
65
|
+
}).nullable().optional().describe("Mock response config, or null to clear it")
|
|
66
|
+
},
|
|
67
|
+
withErrorHandling(async ({ slug, name, mockResponse }) => {
|
|
68
|
+
const endpoint = await client.endpoints.update(slug, { name, mockResponse });
|
|
69
|
+
return textContent(JSON.stringify(endpoint, null, 2));
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
server.tool(
|
|
73
|
+
"delete_endpoint",
|
|
74
|
+
"Delete a webhook endpoint and all its captured requests.",
|
|
75
|
+
{ slug: z.string().describe("The endpoint slug to delete") },
|
|
76
|
+
withErrorHandling(async ({ slug }) => {
|
|
77
|
+
await client.endpoints.delete(slug);
|
|
78
|
+
return textContent(`Endpoint "${slug}" deleted.`);
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
server.tool(
|
|
82
|
+
"send_webhook",
|
|
83
|
+
"Send a test webhook to an endpoint. Useful for testing webhook handling code.",
|
|
84
|
+
{
|
|
85
|
+
slug: z.string().describe("The endpoint slug to send to"),
|
|
86
|
+
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).default("POST").describe("HTTP method (default: POST)"),
|
|
87
|
+
headers: z.record(z.string()).optional().describe("HTTP headers to include"),
|
|
88
|
+
body: z.unknown().optional().describe("Request body (will be JSON-serialized)")
|
|
89
|
+
},
|
|
90
|
+
withErrorHandling(async ({ slug, method, headers, body }) => {
|
|
91
|
+
const response = await client.endpoints.send(slug, { method, headers, body });
|
|
92
|
+
const responseBody = await readBodyTruncated(response);
|
|
93
|
+
return textContent(
|
|
94
|
+
JSON.stringify(
|
|
95
|
+
{ status: response.status, statusText: response.statusText, body: responseBody },
|
|
96
|
+
null,
|
|
97
|
+
2
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
server.tool(
|
|
103
|
+
"list_requests",
|
|
104
|
+
"List captured webhook requests for an endpoint. Returns the most recent requests (default: 25).",
|
|
105
|
+
{
|
|
106
|
+
endpointSlug: z.string().describe("The endpoint slug"),
|
|
107
|
+
limit: z.number().default(25).describe("Max number of requests to return (default: 25)"),
|
|
108
|
+
since: z.number().optional().describe("Only return requests after this timestamp (ms)")
|
|
109
|
+
},
|
|
110
|
+
withErrorHandling(async ({ endpointSlug, limit, since }) => {
|
|
111
|
+
const requests = await client.requests.list(endpointSlug, { limit, since });
|
|
112
|
+
return textContent(JSON.stringify(requests, null, 2));
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
server.tool(
|
|
116
|
+
"get_request",
|
|
117
|
+
"Get full details of a specific captured webhook request by its ID. Includes method, headers, body, path, and timestamp.",
|
|
118
|
+
{ requestId: z.string().describe("The request ID") },
|
|
119
|
+
withErrorHandling(async ({ requestId }) => {
|
|
120
|
+
const request = await client.requests.get(requestId);
|
|
121
|
+
return textContent(JSON.stringify(request, null, 2));
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
server.tool(
|
|
125
|
+
"wait_for_request",
|
|
126
|
+
"Wait for a webhook request to arrive at an endpoint. Polls until a request is captured or timeout expires. Use this after sending a webhook to verify it was received.",
|
|
127
|
+
{
|
|
128
|
+
endpointSlug: z.string().describe("The endpoint slug to monitor"),
|
|
129
|
+
timeout: z.union([z.string(), z.number()]).default("30s").describe('How long to wait (e.g. "30s", "5m", or milliseconds as number)'),
|
|
130
|
+
pollInterval: z.union([z.string(), z.number()]).optional().describe('Interval between polls (e.g. "1s", "500", or milliseconds). Default: 500ms')
|
|
131
|
+
},
|
|
132
|
+
withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
|
|
133
|
+
const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
|
|
134
|
+
return textContent(JSON.stringify(request, null, 2));
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
server.tool(
|
|
138
|
+
"replay_request",
|
|
139
|
+
"Replay a previously captured webhook request to a target URL. Sends the original method, headers, and body to the specified URL. Only use with URLs you trust \u2014 the original request data is forwarded.",
|
|
140
|
+
{
|
|
141
|
+
requestId: z.string().describe("The ID of the captured request to replay"),
|
|
142
|
+
targetUrl: z.string().url().refine(
|
|
143
|
+
(u) => {
|
|
144
|
+
try {
|
|
145
|
+
const p = new URL(u).protocol;
|
|
146
|
+
return p === "http:" || p === "https:";
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{ message: "Only http and https URLs are supported" }
|
|
152
|
+
).describe("The URL to send the replayed request to (http or https only)")
|
|
153
|
+
},
|
|
154
|
+
withErrorHandling(async ({ requestId, targetUrl }) => {
|
|
155
|
+
const response = await client.requests.replay(requestId, targetUrl);
|
|
156
|
+
const responseBody = await readBodyTruncated(response);
|
|
157
|
+
return textContent(
|
|
158
|
+
JSON.stringify(
|
|
159
|
+
{ status: response.status, statusText: response.statusText, body: responseBody },
|
|
160
|
+
null,
|
|
161
|
+
2
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
server.tool(
|
|
167
|
+
"describe",
|
|
168
|
+
"Describe all available SDK operations, their parameters, and types. Useful for discovering what actions are possible.",
|
|
169
|
+
{},
|
|
170
|
+
withErrorHandling(async () => {
|
|
171
|
+
const description = client.describe();
|
|
172
|
+
return textContent(JSON.stringify(description, null, 2));
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/index.ts
|
|
178
|
+
var VERSION = true ? "0.1.0" : "0.0.0-dev";
|
|
179
|
+
function createServer(options = {}) {
|
|
180
|
+
const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
|
|
181
|
+
if (!apiKey) {
|
|
182
|
+
throw new Error("Missing API key. Set WHK_API_KEY environment variable or pass apiKey option.");
|
|
183
|
+
}
|
|
184
|
+
const client = new WebhooksCC({
|
|
185
|
+
apiKey,
|
|
186
|
+
webhookUrl: options.webhookUrl ?? process.env.WHK_WEBHOOK_URL,
|
|
187
|
+
baseUrl: options.baseUrl ?? process.env.WHK_BASE_URL
|
|
188
|
+
});
|
|
189
|
+
const server = new McpServer({
|
|
190
|
+
name: "webhooks-cc",
|
|
191
|
+
version: VERSION
|
|
192
|
+
});
|
|
193
|
+
registerTools(server, client);
|
|
194
|
+
return server;
|
|
195
|
+
}
|
|
196
|
+
export {
|
|
197
|
+
createServer,
|
|
198
|
+
registerTools
|
|
199
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webhooks-cc/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for webhooks.cc — AI agent integration for webhook testing",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"webhooks-cc-mcp": "dist/bin/mcp.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"require": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"webhook",
|
|
24
|
+
"mcp",
|
|
25
|
+
"model-context-protocol",
|
|
26
|
+
"ai",
|
|
27
|
+
"agent",
|
|
28
|
+
"webhooks-cc"
|
|
29
|
+
],
|
|
30
|
+
"author": "",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/webhooks-cc/webhooks-cc",
|
|
35
|
+
"directory": "packages/mcp"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://webhooks.cc",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
40
|
+
"zod": "^3.25.0",
|
|
41
|
+
"@webhooks-cc/sdk": "0.3.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"tsup": "^8.5.1",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vitest": "^3.0.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"dev": "tsup --watch",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run"
|
|
53
|
+
}
|
|
54
|
+
}
|