@qovva/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/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # @qovva/mcp
2
+
3
+ Local stdio MCP server for [Qovva](https://qovva.app) — the AI-ready webhook debugger.
4
+
5
+ Connect Cursor, Claude Desktop, Windsurf, VS Code, or any MCP-aware AI client to your live webhook capture history in seconds. Search webhooks with natural language, replay payloads, inspect anomalies, and debug integration issues — all from your AI assistant.
6
+
7
+ ## Quick start
8
+
9
+ ### 1. Get an API key
10
+
11
+ Sign in at [qovva.app/dashboard](https://qovva.app/dashboard), open **API key management**, and create a key named after the client you're connecting (e.g. `Cursor work`).
12
+
13
+ Copy the key immediately — it is shown exactly once.
14
+
15
+ ### 2. Add to your MCP client
16
+
17
+ #### Cursor / Windsurf / Continue
18
+
19
+ Add to `.cursor/mcp.json`, `.windsurf/mcp.json`, or your client's equivalent:
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "qovva": {
25
+ "command": "npx",
26
+ "args": ["-y", "@qovva/mcp"],
27
+ "env": {
28
+ "QOVVA_API_BASE_URL": "https://api.qovva.app",
29
+ "QOVVA_API_KEY": "qw_live_..."
30
+ }
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ #### Claude Desktop
37
+
38
+ Open **Settings → Developer → Edit Config** and add the same block under `mcpServers`.
39
+
40
+ #### VS Code (GitHub Copilot / MCP extension)
41
+
42
+ Add to your `settings.json`:
43
+
44
+ ```json
45
+ {
46
+ "mcp": {
47
+ "servers": {
48
+ "qovva": {
49
+ "command": "npx",
50
+ "args": ["-y", "@qovva/mcp"],
51
+ "env": {
52
+ "QOVVA_API_BASE_URL": "https://api.qovva.app",
53
+ "QOVVA_API_KEY": "qw_live_..."
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ### 3. Ask your assistant
62
+
63
+ Once connected, you can ask questions in plain English and the assistant will translate them into the right tool calls:
64
+
65
+ - *"Show me all failed Stripe payments from last week"*
66
+ - *"What did the last POST to my endpoint send in the body?"*
67
+ - *"Are there any anomalies in my recent Shopify webhooks?"*
68
+ - *"Replay the latest checkout event to localhost:3000"*
69
+ - *"List all my endpoints and their webhook URLs"*
70
+ - *"Compare the last two invoice.paid events — did the schema change?"*
71
+
72
+ ## Available tools
73
+
74
+ | Tool | Description |
75
+ |---|---|
76
+ | `test_qovva_connection` | Verify the API key and return your active plan metadata |
77
+ | `get_recent_webhooks` | Fetch recently captured webhooks with optional pagination and time filters |
78
+ | `get_webhook_by_id` | Fetch full masked detail for a single webhook by its Qovva ID |
79
+ | `search_webhooks` | Search captured webhooks by keyword, HTTP method, endpoint, or time range — supports natural language queries that the assistant decomposes into structured filters |
80
+ | `list_endpoints` | List all your configured catch endpoints with their webhook URLs and custom response settings |
81
+ | `replay_webhook` | Forward a captured webhook's original payload to any URL and get the response status, body, and duration |
82
+ | `get_anomalies` | Retrieve anomalies detected by Qovva's rule-based engine — flags missing fields, type changes, and new fields compared to the baseline for each event type |
83
+
84
+ ## Prompts
85
+
86
+ | Prompt | Description |
87
+ |---|---|
88
+ | `debug_webhooks` | A guided debugging prompt that teaches the assistant how to decompose natural language questions into multi-step webhook investigations using all available tools |
89
+
90
+ ## Natural language search
91
+
92
+ The `search_webhooks` tool is designed to work with natural language. When you ask something like *"Show me all failed Stripe payments from last week"*, the assistant breaks it down:
93
+
94
+ - **query** → `stripe payment_intent.failed` (keywords from the question)
95
+ - **method** → `POST` (webhooks are almost always POST)
96
+ - **since** → ISO 8601 date for 7 days ago
97
+
98
+ This works out of the box — no special syntax required.
99
+
100
+ ## Anomaly detection
101
+
102
+ Qovva automatically monitors the structure of your incoming webhook payloads and flags deviations:
103
+
104
+ - **Missing fields** — a field that was present in the last N events has disappeared
105
+ - **Type changes** — a field changed from `string` to `number` (or any other type shift)
106
+ - **New fields** — a previously unseen field appeared in the payload
107
+
108
+ Anomalies are surfaced through the `get_anomalies` tool and visible in the dashboard inspector. The detection engine uses stored field baselines per event type and starts flagging after 3+ consistent samples.
109
+
110
+ ## Requirements
111
+
112
+ - Node.js 20 or later
113
+ - A valid Qovva API key (generated from the dashboard)
114
+
115
+ ## Environment variables
116
+
117
+ | Variable | Required | Description |
118
+ |---|---|---|
119
+ | `QOVVA_API_KEY` | Yes | Your Qovva MCP API key |
120
+ | `QOVVA_API_BASE_URL` | No | Qovva API origin (default: `https://api.qovva.app`) |
121
+
122
+ ## Learn more
123
+
124
+ - [Qovva documentation](https://qovva.app/docs/mcp)
125
+ - [Dashboard](https://qovva.app/dashboard)
126
+ - [Pricing](https://qovva.app/pricing)
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/schemas.ts
8
+ import { z } from "zod";
9
+ var webhookSummarySchema = z.object({
10
+ id: z.string().uuid(),
11
+ method: z.string(),
12
+ contentType: z.string().nullable(),
13
+ sizeBytes: z.number().int().nonnegative(),
14
+ createdAt: z.string(),
15
+ excerpt: z.string().nullable(),
16
+ signatureStatus: z.string()
17
+ });
18
+ var maskedHeadersSchema = z.record(
19
+ z.string(),
20
+ z.union([z.string(), z.array(z.string())])
21
+ );
22
+ var webhookDetailSchema = webhookSummarySchema.extend({
23
+ queryParams: z.record(z.string(), z.union([z.string(), z.array(z.string())])),
24
+ headers: maskedHeadersSchema,
25
+ bodyText: z.string().nullable(),
26
+ bodyJson: z.unknown().nullable()
27
+ });
28
+ var paginatedWebhookSchema = z.object({
29
+ items: z.array(webhookSummarySchema),
30
+ nextCursor: z.string().nullable()
31
+ });
32
+ var searchWebhooksResponseSchema = z.object({
33
+ items: z.array(webhookSummarySchema),
34
+ query: z.string(),
35
+ nextCursor: z.string().nullable()
36
+ });
37
+ var connectionStatusSchema = z.object({
38
+ ok: z.literal(true),
39
+ userId: z.string(),
40
+ planSlug: z.string(),
41
+ retentionDays: z.number().int().nonnegative(),
42
+ apiKeyLimit: z.number().int().nonnegative()
43
+ });
44
+ var endpointSchema = z.object({
45
+ id: z.string().uuid(),
46
+ slug: z.string(),
47
+ label: z.string(),
48
+ url: z.string(),
49
+ responseStatus: z.number().int(),
50
+ responseBody: z.string(),
51
+ responseContentType: z.string(),
52
+ responseDelayMs: z.number().int(),
53
+ hasSigningSecret: z.boolean(),
54
+ createdAt: z.string(),
55
+ updatedAt: z.string()
56
+ });
57
+ var endpointsResponseSchema = z.object({
58
+ items: z.array(endpointSchema)
59
+ });
60
+ var replayResultSchema = z.object({
61
+ id: z.string().uuid(),
62
+ webhookId: z.string().uuid(),
63
+ targetUrl: z.string(),
64
+ statusCode: z.number().int().nullable(),
65
+ responseBody: z.string().nullable(),
66
+ error: z.string().nullable(),
67
+ durationMs: z.number().int().nullable(),
68
+ createdAt: z.string()
69
+ });
70
+ var anomalySchema = z.object({
71
+ id: z.string().uuid(),
72
+ webhookId: z.string().uuid(),
73
+ ruleType: z.string(),
74
+ severity: z.string(),
75
+ message: z.string(),
76
+ details: z.unknown().nullable(),
77
+ createdAt: z.string()
78
+ });
79
+ var anomaliesResponseSchema = z.object({
80
+ items: z.array(anomalySchema)
81
+ });
82
+
83
+ // src/index.ts
84
+ import { z as z2 } from "zod";
85
+ var QovvaAPIClient = class {
86
+ baseURL = process.env.QOVVA_API_BASE_URL ?? "http://localhost:8080";
87
+ apiKey = process.env.QOVVA_API_KEY ?? "";
88
+ requireAPIKey() {
89
+ if (!this.apiKey) {
90
+ throw new Error("QOVVA_API_KEY is not set");
91
+ }
92
+ }
93
+ async fetchJSON({
94
+ path,
95
+ parse
96
+ }) {
97
+ this.requireAPIKey();
98
+ const response = await fetch(`${this.baseURL}${path}`, {
99
+ headers: {
100
+ Authorization: `Bearer ${this.apiKey}`
101
+ },
102
+ cache: "no-store"
103
+ });
104
+ if (!response.ok) {
105
+ const body = await response.text();
106
+ throw new Error(`Qovva API request failed (${response.status}): ${body}`);
107
+ }
108
+ const json = await response.json();
109
+ const parsed = parse(json);
110
+ if (!parsed.success || !parsed.data) {
111
+ throw new Error(`Qovva API returned an invalid payload for ${path}`);
112
+ }
113
+ return parsed.data;
114
+ }
115
+ async postJSON({
116
+ path,
117
+ body,
118
+ parse
119
+ }) {
120
+ this.requireAPIKey();
121
+ const response = await fetch(`${this.baseURL}${path}`, {
122
+ method: "POST",
123
+ headers: {
124
+ Authorization: `Bearer ${this.apiKey}`,
125
+ "Content-Type": "application/json"
126
+ },
127
+ body: JSON.stringify(body)
128
+ });
129
+ if (!response.ok) {
130
+ const text = await response.text();
131
+ throw new Error(`Qovva API request failed (${response.status}): ${text}`);
132
+ }
133
+ const json = await response.json();
134
+ const parsed = parse(json);
135
+ if (!parsed.success || !parsed.data) {
136
+ throw new Error(`Qovva API returned an invalid payload for ${path}`);
137
+ }
138
+ return parsed.data;
139
+ }
140
+ async connectionStatus() {
141
+ return this.fetchJSON({
142
+ path: "/v1/mcp/health",
143
+ parse: (value) => connectionStatusSchema.safeParse(value)
144
+ });
145
+ }
146
+ async recentWebhooks(args) {
147
+ const params = new URLSearchParams();
148
+ if (args.limit) params.set("limit", String(args.limit));
149
+ if (args.cursor) params.set("cursor", args.cursor);
150
+ if (args.since) params.set("since", args.since);
151
+ return this.fetchJSON({
152
+ path: `/v1/mcp/webhooks${params.size > 0 ? `?${params.toString()}` : ""}`,
153
+ parse: (value) => paginatedWebhookSchema.safeParse(value)
154
+ });
155
+ }
156
+ async webhookByID(id) {
157
+ return this.fetchJSON({
158
+ path: `/v1/mcp/webhooks/${id}`,
159
+ parse: (value) => webhookDetailSchema.safeParse(value)
160
+ });
161
+ }
162
+ async searchWebhooks(args) {
163
+ const params = new URLSearchParams();
164
+ if (args.query) params.set("q", args.query);
165
+ if (args.endpoint) params.set("endpoint", args.endpoint);
166
+ if (args.method) params.set("method", args.method);
167
+ if (args.limit) params.set("limit", String(args.limit));
168
+ if (args.since) params.set("since", args.since);
169
+ if (args.cursor) params.set("cursor", args.cursor);
170
+ return this.fetchJSON({
171
+ path: `/v1/mcp/webhooks/search?${params.toString()}`,
172
+ parse: (value) => searchWebhooksResponseSchema.safeParse(value)
173
+ });
174
+ }
175
+ async listEndpoints() {
176
+ return this.fetchJSON({
177
+ path: "/v1/mcp/endpoints",
178
+ parse: (value) => endpointsResponseSchema.safeParse(value)
179
+ });
180
+ }
181
+ async replayWebhook(args) {
182
+ return this.postJSON({
183
+ path: `/v1/mcp/webhooks/${args.webhookId}/replay`,
184
+ body: { targetUrl: args.targetUrl },
185
+ parse: (value) => replayResultSchema.safeParse(value)
186
+ });
187
+ }
188
+ async listAnomalies(args) {
189
+ const params = new URLSearchParams();
190
+ if (args.webhookId) params.set("webhookId", args.webhookId);
191
+ if (args.limit) params.set("limit", String(args.limit));
192
+ if (args.since) params.set("since", args.since);
193
+ return this.fetchJSON({
194
+ path: `/v1/mcp/anomalies${params.size > 0 ? `?${params.toString()}` : ""}`,
195
+ parse: (value) => anomaliesResponseSchema.safeParse(value)
196
+ });
197
+ }
198
+ };
199
+ function toolResult(payload) {
200
+ return {
201
+ content: [
202
+ {
203
+ type: "text",
204
+ text: JSON.stringify(payload, null, 2)
205
+ }
206
+ ],
207
+ structuredContent: payload
208
+ };
209
+ }
210
+ async function main() {
211
+ const api = new QovvaAPIClient();
212
+ const server = new McpServer({
213
+ name: "qovva",
214
+ version: "0.0.1"
215
+ });
216
+ server.registerTool(
217
+ "test_qovva_connection",
218
+ {
219
+ description: "Validate the Qovva API key and return the active plan metadata.",
220
+ inputSchema: {}
221
+ },
222
+ async () => {
223
+ const status = await api.connectionStatus();
224
+ return toolResult(status);
225
+ }
226
+ );
227
+ server.registerTool(
228
+ "get_recent_webhooks",
229
+ {
230
+ description: "Fetch recently captured webhooks from Qovva.",
231
+ inputSchema: {
232
+ limit: z2.number().int().positive().max(100).optional(),
233
+ cursor: z2.string().optional(),
234
+ since: z2.string().optional()
235
+ }
236
+ },
237
+ async ({ limit, cursor, since }) => {
238
+ const result = await api.recentWebhooks({ limit, cursor, since });
239
+ return toolResult(result);
240
+ }
241
+ );
242
+ server.registerTool(
243
+ "get_webhook_by_id",
244
+ {
245
+ description: "Fetch one webhook by its Qovva ID.",
246
+ inputSchema: {
247
+ id: z2.string().uuid()
248
+ }
249
+ },
250
+ async ({ id }) => {
251
+ const result = await api.webhookByID(id);
252
+ return toolResult(result);
253
+ }
254
+ );
255
+ server.registerTool(
256
+ "search_webhooks",
257
+ {
258
+ description: `Search captured webhooks using structured filters. Supports natural language queries \u2014 decompose user questions into the appropriate parameters:
259
+
260
+ - query: Free-text search across method, headers, query params, and body content. Use for provider names (e.g. "stripe"), event types (e.g. "checkout.session.completed"), or payload content (e.g. "failed").
261
+ - method: HTTP method filter \u2014 "GET", "POST", "PUT", "PATCH", "DELETE".
262
+ - endpoint: Filter by endpoint UUID. Use list_endpoints to resolve names to IDs.
263
+ - since: ISO 8601 timestamp. For relative time like "last week", compute the actual date.
264
+ - limit: Max results (1-100).
265
+ - cursor: Pagination cursor from a previous response.
266
+
267
+ Examples of natural language \u2192 parameters:
268
+ "Show me all failed Stripe payments from last week" \u2192 query:"stripe payment_intent failed", method:"POST", since:"<7 days ago ISO>"
269
+ "GitHub push events today" \u2192 query:"github push", method:"POST", since:"<today midnight ISO>"
270
+ "All POST requests to my Shopify endpoint" \u2192 method:"POST", endpoint:"<shopify endpoint ID>"
271
+
272
+ At least one filter is required.`,
273
+ inputSchema: {
274
+ query: z2.string().optional().describe("Free-text search: provider names, event types, payload content, error messages"),
275
+ endpoint: z2.string().optional().describe("Endpoint UUID \u2014 use list_endpoints to resolve names"),
276
+ method: z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).optional().describe("HTTP method filter"),
277
+ limit: z2.number().int().positive().max(100).optional(),
278
+ since: z2.string().optional().describe("ISO 8601 timestamp \u2014 compute from relative expressions like 'last week'"),
279
+ cursor: z2.string().optional()
280
+ }
281
+ },
282
+ async ({ query, endpoint, method, limit, since, cursor }) => {
283
+ const result = await api.searchWebhooks({ query, endpoint, method, limit, since, cursor });
284
+ return toolResult(result);
285
+ }
286
+ );
287
+ server.registerTool(
288
+ "list_endpoints",
289
+ {
290
+ description: "List all webhook endpoints for the authenticated user, including their URL, custom response config, and creation date.",
291
+ inputSchema: {}
292
+ },
293
+ async () => {
294
+ const result = await api.listEndpoints();
295
+ return toolResult(result);
296
+ }
297
+ );
298
+ server.registerTool(
299
+ "replay_webhook",
300
+ {
301
+ description: "Replay a previously captured webhook to a target URL. Sends the original method, headers, and body to the specified URL and returns the response status, body, and duration.",
302
+ inputSchema: {
303
+ webhookId: z2.string().uuid(),
304
+ targetUrl: z2.string().url()
305
+ }
306
+ },
307
+ async ({ webhookId, targetUrl }) => {
308
+ const result = await api.replayWebhook({ webhookId, targetUrl });
309
+ return toolResult(result);
310
+ }
311
+ );
312
+ server.registerTool(
313
+ "get_anomalies",
314
+ {
315
+ description: `List webhook anomalies detected by Qovva's rule-based pattern matching engine. Anomalies are flagged when an incoming webhook's payload structure deviates from recent events of the same type \u2014 for example, missing fields, unexpected type changes, or new fields.
316
+
317
+ Use this to:
318
+ - Check if a specific webhook triggered any structural warnings
319
+ - Review recent anomalies across all webhooks
320
+ - Investigate payload inconsistencies from a provider
321
+
322
+ Parameters:
323
+ - webhookId: Filter anomalies for a specific webhook UUID.
324
+ - limit: Max results (1-50, default 20).
325
+ - since: ISO 8601 timestamp for time-based filtering.`,
326
+ inputSchema: {
327
+ webhookId: z2.string().uuid().optional().describe("Filter anomalies for a specific webhook"),
328
+ limit: z2.number().int().positive().max(50).optional(),
329
+ since: z2.string().optional().describe("ISO 8601 timestamp")
330
+ }
331
+ },
332
+ async ({ webhookId, limit, since }) => {
333
+ const result = await api.listAnomalies({ webhookId, limit, since });
334
+ return toolResult(result);
335
+ }
336
+ );
337
+ server.registerPrompt(
338
+ "debug_webhooks",
339
+ {
340
+ description: "Interactively debug webhook issues using natural language. Describe what you're looking for and the assistant will search, inspect, and analyze your webhook history.",
341
+ argsSchema: {
342
+ question: z2.string().describe("Your natural language question about webhooks, e.g. 'Show me all failed Stripe payments from last week'")
343
+ }
344
+ },
345
+ async ({ question }) => ({
346
+ messages: [
347
+ {
348
+ role: "user",
349
+ content: {
350
+ type: "text",
351
+ text: `You are a webhook debugging assistant with access to the user's Qovva webhook history via the following tools:
352
+
353
+ - search_webhooks: Search by text query, HTTP method, endpoint, and time range. Decompose natural language into structured parameters.
354
+ - get_webhook_by_id: Fetch full detail (headers, body, query params) for a specific webhook.
355
+ - get_recent_webhooks: List the most recent captured webhooks.
356
+ - list_endpoints: List all the user's webhook endpoints (use to resolve endpoint names to IDs).
357
+ - replay_webhook: Re-send a captured webhook to a target URL.
358
+ - get_anomalies: List structural anomalies detected in webhook payloads (missing fields, type changes, new fields).
359
+
360
+ When translating natural language to search parameters:
361
+ - Extract provider names (Stripe, GitHub, Shopify) as query text
362
+ - Extract event types (checkout.session.completed, push, order/created) as query text
363
+ - Map time expressions to ISO 8601: "last week" = 7 days ago, "today" = midnight today, "last hour" = 1 hour ago
364
+ - Extract HTTP methods if mentioned
365
+ - If the user mentions an endpoint by name, use list_endpoints first to get the UUID
366
+
367
+ For anomaly-related questions:
368
+ - Use get_anomalies to find structural deviations
369
+ - Cross-reference with get_webhook_by_id for full context
370
+ - Compare with search_webhooks to find the "normal" baseline
371
+
372
+ Always show your reasoning, then call the appropriate tools, then summarize findings clearly.
373
+
374
+ The user's question: ${question}`
375
+ }
376
+ }
377
+ ]
378
+ })
379
+ );
380
+ const transport = new StdioServerTransport();
381
+ await server.connect(transport);
382
+ }
383
+ main().catch((error) => {
384
+ console.error(error);
385
+ process.exit(1);
386
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@qovva/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Local stdio MCP server for Qovva webhook debugging.",
5
+ "private": false,
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "bin": {
12
+ "qovva-mcp": "./dist/index.js"
13
+ },
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.27.1",
28
+ "zod": "^3.25.76"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^25.1.0",
32
+ "eslint": "^9.39.2",
33
+ "tsup": "^8.5.0",
34
+ "tsx": "^4.20.6",
35
+ "typescript": "^5.9.3",
36
+ "@workspace/eslint-config": "0.0.0",
37
+ "@workspace/typescript-config": "0.0.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsup src/index.ts --format esm --dts --clean",
41
+ "dev": "tsx src/index.ts",
42
+ "lint": "eslint .",
43
+ "typecheck": "tsc --noEmit"
44
+ }
45
+ }