@webhooks-cc/mcp 1.10.1 → 1.12.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/dist/bin/mcp.js CHANGED
@@ -6,7 +6,7 @@ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
6
6
 
7
7
  // src/index.ts
8
8
  var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
9
- var import_sdk2 = require("@webhooks-cc/sdk");
9
+ var import_sdk3 = require("@webhooks-cc/sdk");
10
10
 
11
11
  // src/prompts.ts
12
12
  var import_zod = require("zod");
@@ -383,6 +383,10 @@ function registerResources(server, client) {
383
383
  );
384
384
  }
385
385
 
386
+ // src/sandbox-tools.ts
387
+ var import_zod3 = require("zod");
388
+ var import_sdk2 = require("@webhooks-cc/sdk");
389
+
386
390
  // src/tools.ts
387
391
  var import_zod2 = require("zod");
388
392
  var import_sdk = require("@webhooks-cc/sdk");
@@ -442,6 +446,44 @@ var mockResponseSchema = import_zod2.z.object({
442
446
  `Response delay in milliseconds (${import_sdk.MOCK_RESPONSE_DELAY_MIN}-${import_sdk.MOCK_RESPONSE_DELAY_MAX}, default: none)`
443
447
  )
444
448
  });
449
+ var createEndpointShape = {
450
+ name: import_zod2.z.string().optional().describe("Display name for the endpoint"),
451
+ ephemeral: import_zod2.z.boolean().optional().describe("Create a temporary endpoint that auto-expires"),
452
+ expiresIn: durationOrTimestampSchema.optional().describe('Auto-expire after this duration, for example "12h"'),
453
+ mockResponse: mockResponseSchema.optional().describe("Optional mock response to return when the endpoint receives a request"),
454
+ responseRules: responseRulesSchema.optional().describe(
455
+ "Conditional response rules. Each rule has conditions and a response. First matching rule wins."
456
+ ),
457
+ notificationUrl: import_zod2.z.string().url().optional().describe(
458
+ "URL to POST a JSON summary to after each captured request (e.g. Slack/Discord webhook)"
459
+ ),
460
+ signingProvider: import_zod2.z.string().optional().describe(
461
+ "Signing provider for automatic signature verification (e.g. stripe, github, shopify)"
462
+ ),
463
+ signingSecret: import_zod2.z.string().optional().describe(
464
+ "Signing secret (encrypted server-side, never returned). Required when signingProvider is set."
465
+ ),
466
+ signingHeader: import_zod2.z.string().optional().describe("Custom signature header name. Only used with generic-hmac provider.")
467
+ };
468
+ var listEndpointsShape = {
469
+ team: import_zod2.z.string().min(1).optional().describe(
470
+ "Team id or name (case-insensitive). Keeps endpoints shared with you from that team and endpoints you own that are shared with it."
471
+ )
472
+ };
473
+ var getEndpointShape = { slug: import_zod2.z.string().describe("The endpoint slug") };
474
+ var deleteEndpointShape = { slug: import_zod2.z.string().describe("The endpoint slug to delete") };
475
+ var listRequestsShape = {
476
+ endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
477
+ limit: import_zod2.z.number().int().min(1).max(100).default(25).describe("Max requests to return"),
478
+ since: import_zod2.z.number().optional().describe("Only return requests after this timestamp in ms"),
479
+ kind: kindSchema
480
+ };
481
+ var getRequestShape = { requestId: import_zod2.z.string().describe("The request ID") };
482
+ var waitForRequestShape = {
483
+ endpointSlug: import_zod2.z.string().describe("The endpoint slug to monitor"),
484
+ timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
485
+ pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"')
486
+ };
445
487
  function textContent(text) {
446
488
  return { content: [{ type: "text", text }] };
447
489
  }
@@ -745,25 +787,7 @@ function registerTools(server, client) {
745
787
  server.tool(
746
788
  "create_endpoint",
747
789
  "Create a webhook endpoint. Returns the endpoint slug, URL, and metadata.",
748
- {
749
- name: import_zod2.z.string().optional().describe("Display name for the endpoint"),
750
- ephemeral: import_zod2.z.boolean().optional().describe("Create a temporary endpoint that auto-expires"),
751
- expiresIn: durationOrTimestampSchema.optional().describe('Auto-expire after this duration, for example "12h"'),
752
- mockResponse: mockResponseSchema.optional().describe("Optional mock response to return when the endpoint receives a request"),
753
- responseRules: responseRulesSchema.optional().describe(
754
- "Conditional response rules. Each rule has conditions and a response. First matching rule wins."
755
- ),
756
- notificationUrl: import_zod2.z.string().url().optional().describe(
757
- "URL to POST a JSON summary to after each captured request (e.g. Slack/Discord webhook)"
758
- ),
759
- signingProvider: import_zod2.z.string().optional().describe(
760
- "Signing provider for automatic signature verification (e.g. stripe, github, shopify)"
761
- ),
762
- signingSecret: import_zod2.z.string().optional().describe(
763
- "Signing secret (encrypted server-side, never returned). Required when signingProvider is set."
764
- ),
765
- signingHeader: import_zod2.z.string().optional().describe("Custom signature header name. Only used with generic-hmac provider.")
766
- },
790
+ createEndpointShape,
767
791
  withErrorHandling(
768
792
  async ({
769
793
  name,
@@ -816,11 +840,7 @@ function registerTools(server, client) {
816
840
  server.tool(
817
841
  "list_endpoints",
818
842
  "List webhook endpoints: those you own (with sharedWith) and those shared with you through teams (with fromTeam). Optionally keep only one team's endpoints.",
819
- {
820
- team: import_zod2.z.string().min(1).optional().describe(
821
- "Team id or name (case-insensitive). Keeps endpoints shared with you from that team and endpoints you own that are shared with it."
822
- )
823
- },
843
+ listEndpointsShape,
824
844
  withErrorHandling(async ({ team }) => {
825
845
  const endpoints = await client.endpoints.list({ team });
826
846
  return jsonContent(endpoints);
@@ -876,7 +896,7 @@ function registerTools(server, client) {
876
896
  server.tool(
877
897
  "get_endpoint",
878
898
  "Get details for a specific webhook endpoint by slug.",
879
- { slug: import_zod2.z.string().describe("The endpoint slug") },
899
+ getEndpointShape,
880
900
  withErrorHandling(async ({ slug }) => {
881
901
  const endpoint = await client.endpoints.get(slug);
882
902
  return jsonContent(endpoint);
@@ -929,7 +949,7 @@ function registerTools(server, client) {
929
949
  server.tool(
930
950
  "delete_endpoint",
931
951
  "Delete a webhook endpoint and all its captured requests.",
932
- { slug: import_zod2.z.string().describe("The endpoint slug to delete") },
952
+ deleteEndpointShape,
933
953
  withErrorHandling(async ({ slug }) => {
934
954
  await client.endpoints.delete(slug);
935
955
  return textContent(`Endpoint "${slug}" deleted.`);
@@ -1033,12 +1053,7 @@ function registerTools(server, client) {
1033
1053
  server.tool(
1034
1054
  "list_requests",
1035
1055
  "List recent captured requests for an endpoint, HTTP requests and emails together (filter with kind). For emails, list_emails is shorter.",
1036
- {
1037
- endpointSlug: import_zod2.z.string().describe("The endpoint slug"),
1038
- limit: import_zod2.z.number().int().min(1).max(100).default(25).describe("Max requests to return"),
1039
- since: import_zod2.z.number().optional().describe("Only return requests after this timestamp in ms"),
1040
- kind: kindSchema
1041
- },
1056
+ listRequestsShape,
1042
1057
  withErrorHandling(async ({ endpointSlug, limit, since, kind }) => {
1043
1058
  const requests = await client.requests.list(endpointSlug, { limit, since, kind });
1044
1059
  return jsonContent(requests.map(compactRequest));
@@ -1092,7 +1107,7 @@ function registerTools(server, client) {
1092
1107
  server.tool(
1093
1108
  "get_request",
1094
1109
  "Get full details for a specific captured request by ID.",
1095
- { requestId: import_zod2.z.string().describe("The request ID") },
1110
+ getRequestShape,
1096
1111
  withErrorHandling(async ({ requestId }) => {
1097
1112
  const request = await client.requests.get(requestId);
1098
1113
  return jsonContent(compactRequest(request));
@@ -1101,11 +1116,7 @@ function registerTools(server, client) {
1101
1116
  server.tool(
1102
1117
  "wait_for_request",
1103
1118
  "Wait for a request to arrive at an endpoint.",
1104
- {
1105
- endpointSlug: import_zod2.z.string().describe("The endpoint slug to monitor"),
1106
- timeout: durationOrTimestampSchema.default("30s").describe('How long to wait, for example "30s"'),
1107
- pollInterval: durationOrTimestampSchema.optional().describe('Interval between polls, for example "500ms" or "1s"')
1108
- },
1119
+ waitForRequestShape,
1109
1120
  withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
1110
1121
  const request = await client.requests.waitFor(endpointSlug, { timeout, pollInterval });
1111
1122
  return jsonContent(compactRequest(request));
@@ -1526,25 +1537,52 @@ function registerTools(server, client) {
1526
1537
  );
1527
1538
  server.tool(
1528
1539
  "configure_forwarding",
1529
- "Set the URL captured emails are forwarded to (as signed JSON) and turn forwarding on or off. Owner only. Saving the first URL creates the signing secret.",
1540
+ "Change an endpoint's forwarding: the URL, on or off, which kinds (HTTP requests, emails), the format, your own headers, and the advanced options. Only the arguments you pass change. Owner only. Saving the first URL creates the signing secret. A Slack or Discord webhook URL gets a chat message automatically.",
1530
1541
  {
1531
1542
  slug: import_zod2.z.string().describe("The endpoint slug"),
1532
1543
  url: httpUrlSchema.nullable().optional().describe("https URL on a public host name, or null to remove it (forwarding must be off)"),
1533
- enabled: import_zod2.z.boolean().optional().describe("Turn forwarding on (needs a URL) or off (fails deliveries still waiting)")
1544
+ enabled: import_zod2.z.boolean().optional().describe("Turn forwarding on (needs a URL) or off (fails deliveries still waiting)"),
1545
+ http: import_zod2.z.boolean().optional().describe("Forward HTTP requests"),
1546
+ email: import_zod2.z.boolean().optional().describe("Forward emails"),
1547
+ format: import_zod2.z.enum(["auto", "as_received", "json", "chat"]).optional().describe(
1548
+ "auto (pick from the URL), as_received (relay HTTP requests unchanged; emails as signed JSON), json (signed JSON envelope for both kinds) or chat (Slack or Discord message)"
1549
+ ),
1550
+ headers: import_zod2.z.array(
1551
+ import_zod2.z.object({
1552
+ name: import_zod2.z.string().describe("Header name, e.g. Authorization"),
1553
+ value: import_zod2.z.string().nullable().describe("Header value, or null to keep the value stored under this name")
1554
+ })
1555
+ ).max(10).optional().describe(
1556
+ "Your own headers for every delivery (not chat). Replaces the whole list; [] removes them"
1557
+ ),
1558
+ appendPath: import_zod2.z.boolean().optional().describe("Add the path after the slug to the URL for relayed requests (default true)"),
1559
+ retrySeconds: import_zod2.z.union([import_zod2.z.literal(0), import_zod2.z.literal(3600), import_zod2.z.literal(86400)]).optional().describe("Retry failed deliveries for 86400 (default), 3600 or 0 seconds"),
1560
+ keepOrder: import_zod2.z.boolean().optional().describe("One delivery at a time, in capture order (default false)"),
1561
+ sentField: import_zod2.z.string().max(128).nullable().optional().describe(
1562
+ "JSON body field with the sender's own timestamp, e.g. publishedAt or data.publishedAt; null clears it"
1563
+ )
1534
1564
  },
1535
- withErrorHandling(async ({ slug, url, enabled }) => {
1536
- const endpoint = await client.forwarding.configure(slug, { url, enabled });
1565
+ withErrorHandling(async ({ slug, ...options }) => {
1566
+ const endpoint = await client.forwarding.configure(slug, options);
1537
1567
  return jsonContent({
1538
1568
  slug: endpoint.slug,
1539
1569
  forwardEnabled: endpoint.forwardEnabled ?? false,
1540
1570
  forwardUrl: endpoint.forwardUrl ?? null,
1541
- hasForwardSecret: endpoint.hasForwardSecret ?? false
1571
+ hasForwardSecret: endpoint.hasForwardSecret ?? false,
1572
+ forwardHttp: endpoint.forwardHttp ?? false,
1573
+ forwardEmail: endpoint.forwardEmail ?? false,
1574
+ forwardFormat: endpoint.forwardFormat ?? "auto",
1575
+ forwardHeaders: endpoint.forwardHeaders ?? [],
1576
+ forwardAppendPath: endpoint.forwardAppendPath ?? true,
1577
+ forwardRetrySeconds: endpoint.forwardRetrySeconds ?? 86400,
1578
+ forwardKeepOrder: endpoint.forwardKeepOrder ?? false,
1579
+ forwardSentField: endpoint.forwardSentField ?? null
1542
1580
  });
1543
1581
  })
1544
1582
  );
1545
1583
  server.tool(
1546
1584
  "get_forwarding_secret",
1547
- "Read (or with rotate: true, replace) the whsec_ secret forwarded emails are signed with. Owner only. Treat the value as a credential: put it in the handler's environment, not in code.",
1585
+ "Read (or with rotate: true, replace) the whsec_ secret forwarded deliveries are signed with. Owner only. Treat the value as a credential: put it in the handler's environment, not in code.",
1548
1586
  {
1549
1587
  slug: import_zod2.z.string().describe("The endpoint slug"),
1550
1588
  rotate: import_zod2.z.boolean().default(false).describe("Replace the secret; every delivery from now on uses the new one")
@@ -1556,35 +1594,57 @@ function registerTools(server, client) {
1556
1594
  );
1557
1595
  server.tool(
1558
1596
  "test_forwarding",
1559
- "Post the endpoint's newest email (or a sample) to its forwarding URL once and report what the server answered. Works with forwarding on or off; nothing is retried or logged.",
1560
- { slug: import_zod2.z.string().describe("The endpoint slug") },
1561
- withErrorHandling(async ({ slug }) => {
1562
- return jsonContent(await client.forwarding.test(slug));
1597
+ "Send the endpoint's newest captured request of a kind (or a sample) to its forwarding URL once, exactly as a delivery would go out, and report the answer. Works with forwarding on or off; nothing is retried or logged.",
1598
+ {
1599
+ slug: import_zod2.z.string().describe("The endpoint slug"),
1600
+ kind: import_zod2.z.enum(["http", "email"]).optional().describe(
1601
+ "Which kind to send (default: http when HTTP requests are forwarded, else email)"
1602
+ )
1603
+ },
1604
+ withErrorHandling(async ({ slug, kind }) => {
1605
+ return jsonContent(await client.forwarding.test(slug, kind ? { kind } : {}));
1563
1606
  })
1564
1607
  );
1565
1608
  server.tool(
1566
1609
  "list_deliveries",
1567
- "List forwarding deliveries: an endpoint's latest (endpointSlug), or every try of one email (requestId).",
1610
+ "List forwarding deliveries: an endpoint's delivery log with its counts (endpointSlug), or every delivery and try of one captured request or email (requestId). Rows carry senderAt when the request had the sender's own timestamp, so receivedAt - senderAt is the time to reach webhooks.cc.",
1568
1611
  {
1569
- endpointSlug: import_zod2.z.string().optional().describe("List the endpoint's latest deliveries"),
1570
- requestId: import_zod2.z.string().optional().describe("List every delivery and try of this email"),
1571
- limit: import_zod2.z.number().int().min(1).max(20).default(5).describe("With endpointSlug: max deliveries")
1612
+ endpointSlug: import_zod2.z.string().optional().describe("List the endpoint's delivery log"),
1613
+ requestId: import_zod2.z.string().optional().describe("List every delivery and try of this request"),
1614
+ limit: import_zod2.z.number().int().min(1).max(100).default(20).describe("With endpointSlug: max rows"),
1615
+ status: import_zod2.z.enum(["all", "pending", "failed"]).default("all").describe("With endpointSlug: all, pending (queued or retrying) or failed"),
1616
+ before: import_zod2.z.string().optional().describe("With endpointSlug: a row's cursor, to list older rows")
1572
1617
  },
1573
- withErrorHandling(async ({ endpointSlug, requestId, limit }) => {
1618
+ withErrorHandling(async ({ endpointSlug, requestId, limit, status, before }) => {
1574
1619
  if (requestId === void 0 === (endpointSlug === void 0)) {
1575
1620
  throw new Error("Pass exactly one of endpointSlug or requestId");
1576
1621
  }
1577
- return jsonContent(
1578
- requestId ? await client.forwarding.emailDeliveries(requestId) : await client.forwarding.deliveries(endpointSlug, { limit })
1579
- );
1622
+ if (requestId) return jsonContent(await client.forwarding.requestDeliveries(requestId));
1623
+ const [summary, deliveries] = await Promise.all([
1624
+ client.forwarding.summary(endpointSlug),
1625
+ client.forwarding.deliveries(endpointSlug, { limit, status, before })
1626
+ ]);
1627
+ return jsonContent({
1628
+ summary,
1629
+ deliveries,
1630
+ nextCursor: deliveries.length === limit ? deliveries[deliveries.length - 1]?.cursor ?? null : null
1631
+ });
1580
1632
  })
1581
1633
  );
1582
1634
  server.tool(
1583
- "redeliver_email",
1584
- "Forward a captured email again with the endpoint's current URL and secret. Owner only; fails while forwarding is off.",
1585
- { requestId: import_zod2.z.string().describe("The email's request ID") },
1586
- withErrorHandling(async ({ requestId }) => {
1587
- return jsonContent(await client.forwarding.redeliver(requestId));
1635
+ "redeliver",
1636
+ "Forward again with the endpoint's current settings: one captured request or email (requestId; it also sends one that was never forwarded), or every request whose latest delivery failed (endpointSlug, oldest first, up to 1,000 at a time). Owner only; fails while forwarding is off.",
1637
+ {
1638
+ requestId: import_zod2.z.string().optional().describe("The request or email to send again"),
1639
+ endpointSlug: import_zod2.z.string().optional().describe("Send every failed delivery of this endpoint again")
1640
+ },
1641
+ withErrorHandling(async ({ requestId, endpointSlug }) => {
1642
+ if (requestId === void 0 === (endpointSlug === void 0)) {
1643
+ throw new Error("Pass exactly one of requestId or endpointSlug");
1644
+ }
1645
+ return jsonContent(
1646
+ requestId ? await client.forwarding.redeliver(requestId) : await client.forwarding.redeliverFailed(endpointSlug)
1647
+ );
1588
1648
  })
1589
1649
  );
1590
1650
  server.tool(
@@ -1596,127 +1656,389 @@ function registerTools(server, client) {
1596
1656
  return jsonContent(description);
1597
1657
  })
1598
1658
  );
1599
- registerAgentRegistrationTools(server);
1600
- }
1601
- function resolveBaseUrl() {
1602
- return process.env.WHK_BASE_URL ?? "https://webhooks.cc";
1603
1659
  }
1604
- function registerAgentRegistrationTools(server) {
1605
- const baseUrl = resolveBaseUrl();
1606
- server.tool(
1607
- "how_to_register",
1608
- "Explain how an agent self-registers for a webhooks.cc API credential (auth.md). Call this FIRST when you have no API key. Returns the three registration flows and the auth.md documentation URL. No authentication required.",
1609
- {},
1610
- withErrorHandling(async () => {
1611
- return jsonContent({
1612
- ...import_sdk.WebhooksCC.describeRegistration(baseUrl),
1613
- next_steps: [
1614
- "Easiest: call register_agent (anonymous) to get a key immediately, then ask a human to open the returned claimUrl while signed in to webhooks.cc and enter the userCode. Poll check_claim until it says claimed.",
1615
- "Or set WHK_API_KEY to an existing whcc_ key and use the authenticated tools."
1616
- ]
1660
+
1661
+ // src/sandbox-tools.ts
1662
+ var SANDBOX = {
1663
+ maxEndpoints: 3,
1664
+ requestsPerEndpoint: 25,
1665
+ requests: 100,
1666
+ lifetimeHours: 24
1667
+ };
1668
+ var DEFAULT_WAIT_SECONDS = 45;
1669
+ var MAX_WAIT_SECONDS = 600;
1670
+ var TOKEN_REFRESH_MARGIN_MS = 6e4;
1671
+ var DOCS_URL = "https://webhooks.cc/docs/agents";
1672
+ var ACCOUNT_ONLY_OPTIONS = [
1673
+ "mockResponse",
1674
+ "responseRules",
1675
+ "notificationUrl",
1676
+ "signingProvider",
1677
+ "signingSecret",
1678
+ "signingHeader"
1679
+ ];
1680
+ var AgentSession = class {
1681
+ constructor(baseUrl, webhookUrl, clientName) {
1682
+ this.baseUrl = baseUrl;
1683
+ this.webhookUrl = webhookUrl;
1684
+ this.clientName = clientName;
1685
+ this.sandbox = null;
1686
+ this.claim = null;
1687
+ this.connected = false;
1688
+ }
1689
+ /**
1690
+ * The sandbox, registered on first use and again after it expired. Tool
1691
+ * calls can run in parallel: this.sandbox is read again after every await,
1692
+ * and a new sandbox is assigned with no await in between, so parallel
1693
+ * calls share one registration. Calls waiting on a registration that fails
1694
+ * get its error (a 429 asks them to wait); the next call starts again.
1695
+ */
1696
+ async sandboxClient() {
1697
+ while (this.sandbox) {
1698
+ const pending = this.sandbox;
1699
+ const sandbox = await pending;
1700
+ if (!this.expired(pending, sandbox)) return sandbox;
1701
+ }
1702
+ const created = import_sdk2.WebhooksCC.sandbox({
1703
+ baseUrl: this.baseUrl,
1704
+ webhookUrl: this.webhookUrl,
1705
+ clientName: this.clientName
1706
+ });
1707
+ this.sandbox = created;
1708
+ created.catch(() => {
1709
+ if (this.sandbox === created) this.sandbox = null;
1710
+ });
1711
+ return created;
1712
+ }
1713
+ /** The live sandbox, if one was registered; never registers one. */
1714
+ async existingSandbox() {
1715
+ const pending = this.sandbox;
1716
+ if (!pending) return null;
1717
+ let sandbox;
1718
+ try {
1719
+ sandbox = await pending;
1720
+ } catch {
1721
+ return null;
1722
+ }
1723
+ return this.expired(pending, sandbox) ? null : sandbox;
1724
+ }
1725
+ /** True when the sandbox is past its 24 hours; then it is dropped. */
1726
+ expired(pending, sandbox) {
1727
+ if (sandbox.expiresAt.getTime() > Date.now()) return false;
1728
+ if (this.sandbox === pending) this.sandbox = null;
1729
+ if (this.claim?.kind === "sandbox" && this.claim.claimToken === sandbox.registration.claimToken) {
1730
+ this.claim = null;
1731
+ }
1732
+ return true;
1733
+ }
1734
+ async requireSandbox() {
1735
+ const sandbox = await this.existingSandbox();
1736
+ if (!sandbox) {
1737
+ throw new Error("No sandbox yet: create_endpoint starts one.");
1738
+ }
1739
+ return sandbox;
1740
+ }
1741
+ forgetSandbox() {
1742
+ this.sandbox = null;
1743
+ }
1744
+ };
1745
+ function claimedTokens(baseUrl, assertion, first) {
1746
+ let current = first;
1747
+ let pending = null;
1748
+ return async (options) => {
1749
+ if (!options?.forceRefresh && current.expiresAt.getTime() - Date.now() > TOKEN_REFRESH_MARGIN_MS) {
1750
+ return current.accessToken;
1751
+ }
1752
+ if (!pending) {
1753
+ pending = import_sdk2.WebhooksCC.agent.exchange({ baseUrl, assertion }).finally(() => {
1754
+ pending = null;
1617
1755
  });
1618
- })
1756
+ }
1757
+ current = await pending;
1758
+ return current.accessToken;
1759
+ };
1760
+ }
1761
+ function minutesLeft(until) {
1762
+ return Math.max(1, Math.round((until.getTime() - Date.now()) / 6e4));
1763
+ }
1764
+ function onlyKind(requests, kind) {
1765
+ if (!kind) return requests;
1766
+ return requests.filter((request) => (request.kind ?? "http") === kind);
1767
+ }
1768
+ function registerSandboxTools(server, options = {}) {
1769
+ const baseUrl = options.baseUrl ?? process.env.WHK_BASE_URL ?? "https://webhooks.cc";
1770
+ const session = new AgentSession(
1771
+ baseUrl,
1772
+ options.webhookUrl ?? process.env.WHK_WEBHOOK_URL,
1773
+ options.clientName ?? "webhooks-cc-mcp"
1619
1774
  );
1620
- server.tool(
1621
- "register_agent",
1622
- "Self-register for a webhooks.cc API credential via the anonymous auth.md flow. Returns a whcc_ API key, plus a short userCode and claimUrl a human uses to bind the key to their account. Until a human claims it, the key only works with the sandbox API (/api/agent/sandbox/endpoints), not with these tools, and it is deleted about 15 minutes after registration. No authentication required.",
1623
- {
1624
- clientName: import_zod2.z.string().optional().describe("A human-readable name for this agent, recorded on the issued key")
1625
- },
1626
- withErrorHandling(async ({ clientName }) => {
1627
- const reg = await import_sdk.WebhooksCC.register.anonymous({ baseUrl, clientName });
1628
- return jsonContent({
1629
- credential: reg.credential,
1630
- scopes: reg.scopes,
1631
- claim: {
1632
- userCode: reg.userCode,
1633
- claimUrl: reg.claimUrl,
1634
- claimToken: reg.claimToken,
1635
- expiresAt: reg.claimTokenExpires,
1636
- instructions: `Ask a human to open ${reg.claimUrl} while logged in to webhooks.cc and either enter the code ${reg.userCode} or open ${reg.claimUrl}?token=${reg.claimToken}. Then call check_claim with the claimToken.`
1637
- },
1638
- usage: "Until a human claims it, the key only works with the sandbox API at /api/agent/sandbox/endpoints (create temporary endpoints, read their requests), and it is deleted about 15 minutes after registration. Once check_claim says claimed, set WHK_API_KEY to `credential` and restart the MCP server to use the authenticated tools."
1639
- });
1640
- })
1775
+ const registered = [];
1776
+ const limits = `up to ${SANDBOX.maxEndpoints} endpoints at a time, ${SANDBOX.requestsPerEndpoint} captured requests each and ${SANDBOX.requests} in all, deleted ${SANDBOX.lifetimeHours} hours after the sandbox was created`;
1777
+ registered.push(
1778
+ server.tool(
1779
+ "create_endpoint",
1780
+ `Create a temporary webhook capture endpoint in the agent sandbox (no account needed): ${limits}. The first call registers the sandbox, which takes a few seconds of proof of work. Mock responses, notifications and signature verification need an account: see connect_account.`,
1781
+ createEndpointShape,
1782
+ withErrorHandling(async (args2) => {
1783
+ const accountOnly = ACCOUNT_ONLY_OPTIONS.filter((key) => args2[key] !== void 0);
1784
+ if (accountOnly.length > 0) {
1785
+ throw new Error(
1786
+ `${accountOnly.join(", ")} need an account; sandbox endpoints only capture and answer 200. Call connect_account to connect one.`
1787
+ );
1788
+ }
1789
+ const sandbox = await session.sandboxClient();
1790
+ const endpoint = await sandbox.endpoints.create();
1791
+ const ignored = args2.name !== void 0 || args2.expiresIn !== void 0;
1792
+ return jsonContent(
1793
+ ignored ? {
1794
+ ...endpoint,
1795
+ note: "Sandbox endpoints have no name and expire with the sandbox."
1796
+ } : endpoint
1797
+ );
1798
+ })
1799
+ )
1641
1800
  );
1642
- server.tool(
1643
- "check_claim",
1644
- "Check whether an anonymous registration's API key has been claimed by a human yet. Poll this after register_agent until status is 'claimed'. No authentication required.",
1645
- {
1646
- claimToken: import_zod2.z.string().describe("The claimToken returned by register_agent")
1647
- },
1648
- withErrorHandling(async ({ claimToken }) => {
1649
- const poll = await import_sdk.WebhooksCC.register.pollClaim(claimToken, { baseUrl });
1650
- return jsonContent(poll);
1651
- })
1801
+ registered.push(
1802
+ server.tool(
1803
+ "list_endpoints",
1804
+ "List this agent's sandbox endpoints. Empty until create_endpoint starts the sandbox.",
1805
+ listEndpointsShape,
1806
+ withErrorHandling(async ({ team }) => {
1807
+ if (team !== void 0) {
1808
+ throw new Error("Teams need an account. Call connect_account to connect one.");
1809
+ }
1810
+ const sandbox = await session.existingSandbox();
1811
+ return jsonContent(sandbox ? await sandbox.endpoints.list() : []);
1812
+ })
1813
+ )
1652
1814
  );
1653
- server.tool(
1654
- "register_agent_with_email",
1655
- "Self-register via the verified_email auth.md flow: webhooks.cc emails a one-time code to the address. The credential is WITHHELD until the code is confirmed with verify_agent_otp. Returns a claimToken to pass to verify_agent_otp. No authentication required.",
1656
- {
1657
- email: import_zod2.z.string().email({ message: "Invalid email address" }).describe("The email address to verify and bind the credential to"),
1658
- clientName: import_zod2.z.string().optional().describe("A human-readable name for this agent, recorded on the issued key")
1659
- },
1660
- withErrorHandling(async ({ email, clientName }) => {
1661
- const challenge = await import_sdk.WebhooksCC.register.withEmail(email, { baseUrl, clientName });
1662
- return jsonContent({
1663
- claimToken: challenge.claimToken,
1664
- expiresAt: challenge.claimTokenExpires,
1665
- postClaimScopes: challenge.postClaimScopes,
1666
- next_step: `A one-time code was emailed to ${email}. Ask the human for it, then call verify_agent_otp with this claimToken and the code.`
1667
- });
1668
- })
1815
+ registered.push(
1816
+ server.tool(
1817
+ "get_endpoint",
1818
+ "Get one of this agent's sandbox endpoints by slug, with its sandbox limits and usage.",
1819
+ getEndpointShape,
1820
+ withErrorHandling(async ({ slug }) => {
1821
+ return jsonContent(await (await session.requireSandbox()).endpoints.get(slug));
1822
+ })
1823
+ )
1669
1824
  );
1670
- server.tool(
1671
- "verify_agent_otp",
1672
- "Complete the verified_email flow: submit the emailed OTP with the claimToken from register_agent_with_email. On success returns the whcc_ API key (bound to the verified email). Set WHK_API_KEY to it. No authentication required.",
1673
- {
1674
- claimToken: import_zod2.z.string().describe("The claimToken returned by register_agent_with_email"),
1675
- otp: import_zod2.z.string().describe("The one-time code the human received by email")
1676
- },
1677
- withErrorHandling(async ({ claimToken, otp }) => {
1678
- const issued = await import_sdk.WebhooksCC.register.confirmEmailOtp({ claimToken, otp }, { baseUrl });
1679
- return jsonContent({
1680
- credential: issued.credential,
1681
- scopes: issued.scopes,
1682
- usage: "Set WHK_API_KEY to `credential` to use the authenticated tools."
1683
- });
1684
- })
1825
+ registered.push(
1826
+ server.tool(
1827
+ "delete_endpoint",
1828
+ "Delete one of this agent's sandbox endpoints and its captured requests. Frees a slot; the sandbox's request budget is not refunded.",
1829
+ deleteEndpointShape,
1830
+ withErrorHandling(async ({ slug }) => {
1831
+ await (await session.requireSandbox()).endpoints.delete(slug);
1832
+ return jsonContent({ deleted: slug });
1833
+ })
1834
+ )
1685
1835
  );
1686
- server.tool(
1687
- "register_agent_with_idjag",
1688
- "Self-register via the identity_assertion (ID-JAG) auth.md flow: present a verified identity-assertion JWT (urn:ietf:params:oauth:token-type:id-jag) from a trusted provider. The credential is returned synchronously \u2014 no human claim step. No webhooks.cc authentication required, but only works if webhooks.cc trusts your provider.",
1689
- {
1690
- assertion: import_zod2.z.string().describe("The ID-JAG assertion JWT (typ oauth-id-jag+jwt) from a trusted provider")
1691
- },
1692
- withErrorHandling(async ({ assertion }) => {
1693
- const issued = await import_sdk.WebhooksCC.register.withIdJag(assertion, { baseUrl });
1694
- return jsonContent({
1695
- credential: issued.credential,
1696
- scopes: issued.scopes,
1697
- usage: "Set WHK_API_KEY to `credential` to use the authenticated tools."
1698
- });
1699
- })
1836
+ registered.push(
1837
+ server.tool(
1838
+ "list_requests",
1839
+ "List recent requests captured by a sandbox endpoint, newest first.",
1840
+ listRequestsShape,
1841
+ withErrorHandling(async ({ endpointSlug, limit, since, kind }) => {
1842
+ const sandbox = await session.requireSandbox();
1843
+ const requests = await sandbox.requests.list(endpointSlug, { limit, since });
1844
+ return jsonContent(onlyKind(requests, kind).map(compactRequest));
1845
+ })
1846
+ )
1847
+ );
1848
+ registered.push(
1849
+ server.tool(
1850
+ "get_request",
1851
+ "Get full details for a request captured by a sandbox endpoint.",
1852
+ getRequestShape,
1853
+ withErrorHandling(async ({ requestId }) => {
1854
+ const sandbox = await session.requireSandbox();
1855
+ return jsonContent(compactRequest(await sandbox.requests.get(requestId)));
1856
+ })
1857
+ )
1858
+ );
1859
+ registered.push(
1860
+ server.tool(
1861
+ "wait_for_request",
1862
+ "Wait for a request to arrive at a sandbox endpoint.",
1863
+ waitForRequestShape,
1864
+ withErrorHandling(async ({ endpointSlug, timeout, pollInterval }) => {
1865
+ const sandbox = await session.requireSandbox();
1866
+ const request = await sandbox.requests.waitFor(endpointSlug, { timeout, pollInterval });
1867
+ return jsonContent(compactRequest(request));
1868
+ })
1869
+ )
1870
+ );
1871
+ registered.push(
1872
+ server.tool(
1873
+ "connect_account",
1874
+ "Connect this agent to a human's webhooks.cc account, for the full tool set (mock responses, signatures, email, teams) and to keep the sandbox endpoints. Returns a link and a 6-digit code: show both to the human, who signs in or signs up with that email and enters the code. Then call wait_for_connection. Calling again gives a new code.",
1875
+ {
1876
+ email: import_zod3.z.string().email({ message: "Invalid email address" }).describe("The human's email address; only someone signed in as it can connect")
1877
+ },
1878
+ withErrorHandling(async ({ email }) => {
1879
+ if (session.connected) {
1880
+ return jsonContent({ connected: true, message: "Already connected to an account." });
1881
+ }
1882
+ const normalized = email.trim().toLowerCase();
1883
+ const sandbox = await session.existingSandbox();
1884
+ let claim;
1885
+ if (sandbox) {
1886
+ claim = {
1887
+ kind: "sandbox",
1888
+ claimToken: sandbox.registration.claimToken,
1889
+ email: normalized,
1890
+ attempt: await sandbox.claim({ email: normalized }),
1891
+ claimExpires: sandbox.expiresAt
1892
+ };
1893
+ } else if (session.claim?.kind === "service_auth" && session.claim.email === normalized && session.claim.claimExpires.getTime() > Date.now()) {
1894
+ const attempt = await import_sdk2.WebhooksCC.agent.startClaim({
1895
+ baseUrl,
1896
+ claimToken: session.claim.claimToken
1897
+ });
1898
+ claim = { ...session.claim, attempt };
1899
+ } else {
1900
+ const registration = await import_sdk2.WebhooksCC.agent.registerServiceAuth({
1901
+ baseUrl,
1902
+ email: normalized,
1903
+ clientName: session.clientName
1904
+ });
1905
+ claim = {
1906
+ kind: "service_auth",
1907
+ claimToken: registration.claimToken,
1908
+ email: normalized,
1909
+ attempt: registration.claim,
1910
+ claimExpires: registration.claimTokenExpires
1911
+ };
1912
+ }
1913
+ session.claim = claim;
1914
+ const { verificationUri, userCode, expiresAt } = claim.attempt;
1915
+ return jsonContent({
1916
+ verificationUri,
1917
+ userCode,
1918
+ expiresAt: expiresAt.toISOString(),
1919
+ tellTheHuman: `Open ${verificationUri}, sign in or sign up as ${normalized}, and enter the code ${userCode}.`,
1920
+ next: `Show the human that line, then call wait_for_connection. The code works for about ${minutesLeft(expiresAt)} minutes; connect_account gives a new one.`,
1921
+ ...claim.kind === "sandbox" ? { endpoints: "The sandbox endpoints move into that account with what they captured." } : {}
1922
+ });
1923
+ })
1924
+ )
1925
+ );
1926
+ registered.push(
1927
+ server.tool(
1928
+ "wait_for_connection",
1929
+ `Wait for the human to enter the code from connect_account. On success the full webhooks.cc tools for their account replace these sandbox tools. Returns after timeoutSeconds (default ${DEFAULT_WAIT_SECONDS}) if they have not finished yet; call it again to keep waiting.`,
1930
+ {
1931
+ timeoutSeconds: import_zod3.z.number().int().min(1).max(MAX_WAIT_SECONDS).optional().describe(`How long to wait, in seconds (default ${DEFAULT_WAIT_SECONDS})`)
1932
+ },
1933
+ withErrorHandling(async ({ timeoutSeconds }) => {
1934
+ if (session.connected) {
1935
+ return jsonContent({ connected: true, message: "Already connected to an account." });
1936
+ }
1937
+ const claim = session.claim;
1938
+ if (!claim) {
1939
+ throw new Error("Nothing to wait for: call connect_account first.");
1940
+ }
1941
+ let claimed;
1942
+ try {
1943
+ claimed = await import_sdk2.WebhooksCC.agent.waitForClaim({
1944
+ baseUrl,
1945
+ claimToken: claim.claimToken,
1946
+ interval: claim.attempt.interval,
1947
+ timeout: (timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3
1948
+ });
1949
+ } catch (error) {
1950
+ if (!(error instanceof import_sdk2.AgentAuthError)) throw error;
1951
+ if (error.code === "timeout") {
1952
+ return jsonContent({
1953
+ connected: false,
1954
+ status: "waiting",
1955
+ message: `Not connected yet. The code works until ${claim.attempt.expiresAt.toISOString()}; call wait_for_connection again.`
1956
+ });
1957
+ }
1958
+ if (error.code === "expired_token") {
1959
+ return jsonContent({
1960
+ connected: false,
1961
+ status: "expired",
1962
+ message: "The code expired. Call connect_account for a new one."
1963
+ });
1964
+ }
1965
+ if (error.code === "access_denied") {
1966
+ return jsonContent({
1967
+ connected: false,
1968
+ status: "declined",
1969
+ message: "The human declined. Call connect_account again only if they ask you to."
1970
+ });
1971
+ }
1972
+ throw error;
1973
+ }
1974
+ const client = new import_sdk2.WebhooksCC({
1975
+ baseUrl,
1976
+ webhookUrl: session.webhookUrl,
1977
+ getAccessToken: claimedTokens(baseUrl, claimed.identityAssertion, claimed.token)
1978
+ });
1979
+ session.connected = true;
1980
+ session.claim = null;
1981
+ session.forgetSandbox();
1982
+ for (const tool of registered) tool.remove();
1983
+ registerTools(server, client);
1984
+ options.onConnected?.(client);
1985
+ return jsonContent({
1986
+ connected: true,
1987
+ email: claim.email,
1988
+ scope: claimed.token.scope,
1989
+ message: `Connected to the webhooks.cc account of ${claim.email}. The full tool set has replaced the sandbox tools (the tool list changed; if your client does not pick that up, ask the user to restart it).${claim.kind === "sandbox" ? " The sandbox endpoints are now that account's endpoints, with what they captured." : ""} This connection lasts while this MCP server runs; for a lasting setup, the human can create an API key under Account and set WHK_API_KEY.`
1990
+ });
1991
+ })
1992
+ )
1993
+ );
1994
+ registered.push(
1995
+ server.tool(
1996
+ "about_sandbox",
1997
+ "Explain what this agent can do without a webhooks.cc account, the sandbox limits, and how to connect an account. No network call.",
1998
+ {},
1999
+ withErrorHandling(async () => {
2000
+ return jsonContent({
2001
+ mode: "sandbox (no WHK_API_KEY)",
2002
+ sandbox: {
2003
+ maxEndpoints: SANDBOX.maxEndpoints,
2004
+ requestsPerEndpoint: SANDBOX.requestsPerEndpoint,
2005
+ requestsInAll: SANDBOX.requests,
2006
+ lifetimeHours: SANDBOX.lifetimeHours,
2007
+ startsWith: "create_endpoint (registers the sandbox; a few seconds of proof of work)",
2008
+ captures: "Endpoints answer every request with 200; only this agent can read what they capture.",
2009
+ notAvailable: "Mock responses, response rules, notifications, signature verification, email capture, forwarding, teams and sending webhooks."
2010
+ },
2011
+ connectAnAccount: [
2012
+ "Call connect_account with the human's email and show them the link and code it returns.",
2013
+ "Call wait_for_connection until it reports connected; the full tools for their account then replace the sandbox tools.",
2014
+ "Sandbox endpoints move into the account with their captured requests."
2015
+ ],
2016
+ withAnApiKey: "Set WHK_API_KEY to a whcc_ key from the dashboard and restart for the full tools from the start.",
2017
+ docs: DOCS_URL,
2018
+ protocol: `${baseUrl}/auth.md`
2019
+ });
2020
+ })
2021
+ )
1700
2022
  );
1701
2023
  }
1702
2024
 
1703
2025
  // src/index.ts
1704
- var VERSION = true ? "1.10.1" : "0.0.0-dev";
2026
+ var VERSION = true ? "1.12.0" : "0.0.0-dev";
1705
2027
  function createServer(options = {}) {
1706
2028
  const apiKey = options.apiKey ?? process.env.WHK_API_KEY;
1707
- const server = new import_mcp2.McpServer({
1708
- name: "webhooks-cc",
1709
- version: VERSION
1710
- });
2029
+ const webhookUrl = options.webhookUrl ?? process.env.WHK_WEBHOOK_URL;
2030
+ const baseUrl = options.baseUrl ?? process.env.WHK_BASE_URL;
2031
+ const server = new import_mcp2.McpServer(
2032
+ { name: "webhooks-cc", version: VERSION },
2033
+ // Connecting an account swaps every tool at once: one list_changed, not dozens.
2034
+ { debouncedNotificationMethods: ["notifications/tools/list_changed"] }
2035
+ );
1711
2036
  if (!apiKey) {
1712
- registerAgentRegistrationTools(server);
2037
+ registerSandboxTools(server, { baseUrl, webhookUrl });
2038
+ registerPrompts(server);
1713
2039
  return server;
1714
2040
  }
1715
- const client = new import_sdk2.WebhooksCC({
1716
- apiKey,
1717
- webhookUrl: options.webhookUrl ?? process.env.WHK_WEBHOOK_URL,
1718
- baseUrl: options.baseUrl ?? process.env.WHK_BASE_URL
1719
- });
2041
+ const client = new import_sdk3.WebhooksCC({ apiKey, webhookUrl, baseUrl });
1720
2042
  registerTools(server, client);
1721
2043
  registerPrompts(server);
1722
2044
  registerResources(server, client);