@secondlayer/mcp 7.4.0 → 7.5.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  Gives your coding agent direct access to the Stacks data on your own instance —
4
4
  Index (decoded rows), Subgraphs (tables you define, served from your instance),
5
5
  and Streams (raw inputs). Exposes the golden-path tools only: Index reads, the
6
- subgraph lifecycle, subscriptions, contract discovery/scaffolding, instance
6
+ subgraph lifecycle, webhooks, contract discovery/scaffolding, instance
7
7
  status, archive verify/bootstrap, and hosted credits/quote. Everything else
8
8
  (single-record lookups, mempool, stacking, proofs, live Streams reads) is
9
9
  available over REST `/v1` + OpenAPI. There is no `consume` tool.
@@ -16,7 +16,7 @@ bun add @secondlayer/mcp
16
16
 
17
17
  ## Auth
18
18
 
19
- Most reads are public: `index_*` and `contracts_find` work with no key. Subgraph tools need an `INSTANCE_TOKEN` past loopback; separately, **public** subgraphs are anon-readable over HTTP at `GET /v1/subgraphs/<name>/<table>` (`{ rows, next_cursor, tip }` cursor envelope), while private ones need the instance token (anon → 404). `streams_dumps` needs no key: the dumps manifest is public; the tool only needs `SL_STREAMS_DUMPS_URL` configured. Every other `streams_*` tool is key-mandatory (keyless → 401). Writes (deploy, reindex, delete, subscriptions) need a key: set `INSTANCE_TOKEN` from `secondlayer init`. Hosted credits/quote use `SECONDLAYER_API_KEY` (`sk-sl_*`). Read `secondlayer://context` first: it reports auth state and read-auth tiers.
19
+ Most reads are public: `index_*` and `contracts_find` work with no key. Subgraph tools need an `INSTANCE_TOKEN` past loopback; separately, **public** subgraphs are anon-readable over HTTP at `GET /v1/subgraphs/<name>/<table>` (`{ rows, next_cursor, tip }` cursor envelope), while private ones need the instance token (anon → 404). `streams_dumps` needs no key: the dumps manifest is public; the tool only needs `SL_STREAMS_DUMPS_URL` configured. Every other `streams_*` tool is key-mandatory (keyless → 401). Writes (deploy, reindex, delete, webhooks) need a key: set `INSTANCE_TOKEN` from `secondlayer init`. Hosted credits/quote use `SECONDLAYER_API_KEY` (`sk-sl_*`). Read `secondlayer://context` first: it reports auth state and read-auth tiers.
20
20
 
21
21
  ## Quick Start — Stdio (IDE)
22
22
 
@@ -65,7 +65,7 @@ bunx -p @secondlayer/mcp secondlayer-mcp-http
65
65
  | --- | --- |
66
66
  | **Index** (9) | `index_events`, `index_ft_transfers`, `index_nft_transfers`, `index_contract_calls`, `index_blocks`, `index_transactions`, `index_print_schema`, `index_discover`, `batch_query` |
67
67
  | **Subgraphs** (12) | `subgraphs_list`, `subgraphs_status`, `subgraphs_spec`, `subgraphs_scaffold`, `subgraphs_deploy`, `subgraphs_delete`, `subgraphs_query`, `subgraphs_backfill`, `subgraphs_reindex`, `subgraphs_stop`, `subgraphs_operations`, `subgraphs_gaps` |
68
- | **Subscriptions** (13) | `subscriptions_create`, `subscriptions_list`, `subscriptions_get`, `subscriptions_update`, `subscriptions_delete`, `subscriptions_test`, `subscriptions_pause`, `subscriptions_resume`, `subscriptions_rotate_secret`, `subscriptions_deliveries`, `subscriptions_dead`, `subscriptions_requeue`, `subscriptions_replay` |
68
+ | **Webhooks** (13) | `webhooks_create`, `webhooks_list`, `webhooks_get`, `webhooks_update`, `webhooks_delete`, `webhooks_test`, `webhooks_pause`, `webhooks_resume`, `webhooks_rotate_secret`, `webhooks_deliveries`, `webhooks_dead`, `webhooks_requeue`, `webhooks_replay` |
69
69
  | **Streams** (7) | `streams_tip`, `streams_events`, `streams_events_by_tx`, `streams_block_events`, `streams_canonical`, `streams_reorgs`, `streams_dumps` |
70
70
  | **Contracts** (2) | `contracts_find`, `contracts_get_abi` |
71
71
  | **Instance** (1) | `instance_status` |
@@ -75,7 +75,7 @@ bunx -p @secondlayer/mcp secondlayer-mcp-http
75
75
  | **Account** (2) | `account_whoami`, `account_create_key` (only when pointed at `https://api.secondlayer.tools`) |
76
76
 
77
77
  Verify after mutating: `subgraphs_operations` for deploy/reindex/backfill/stop,
78
- `subscriptions_deliveries` for create/test/replay. Empty index: `setup` or
78
+ `webhooks_deliveries` for create/test/replay. Empty index: `setup` or
79
79
  `archive_bootstrap`, poll `instance_status` until decoders are ok, then
80
80
  `archive_verify`, then `codegen_index_schema`.
81
81
 
@@ -88,10 +88,10 @@ Point the server at your instance with `SECONDLAYER_API_URL` (default
88
88
  `secondlayer init`. `account_*` tools appear only when the server is pointed at
89
89
  `https://api.secondlayer.tools`.
90
90
 
91
- ### `subscriptions_create` kinds
91
+ ### `webhooks_create` kinds
92
92
 
93
- Subscriptions are polymorphic. Pass `subgraphName` + `tableName` for a
94
- **subgraph** subscription, or a `triggers` array for a **chain** subscription —
93
+ Webhooks are polymorphic. Pass `subgraphName` + `tableName` for a
94
+ **subgraph** webhook, or a `triggers` array for a **chain** webhook —
95
95
  a webhook on raw chain events (contract / event / function / trait) with no
96
96
  subgraph (e.g. `[{ "type": "contract_call", "contractId": "SP....amm",
97
97
  "functionName": "swap-*" }]`).
@@ -107,11 +107,11 @@ subgraph (e.g. `[{ "type": "contract_call", "contractId": "SP....amm",
107
107
 
108
108
  | URI | Description |
109
109
  | --- | --- |
110
- | `secondlayer://context` | Live state — what exists (your subgraphs, subscriptions, account), what you can do, and read-auth tiers. Read first. |
110
+ | `secondlayer://context` | Live state — what exists (your subgraphs, webhooks, account), what you can do, and read-auth tiers. Read first. |
111
111
  | `secondlayer://filters` | Subgraph source filter types and their fields |
112
112
  | `secondlayer://column-types` | Column type mappings and options |
113
113
  | `secondlayer://traits` | SIP trait standards (valid `trait` values) |
114
- | `secondlayer://chain-triggers` | Chain-subscription trigger types and fields |
114
+ | `secondlayer://chain-triggers` | Chain-webhook trigger types and fields |
115
115
 
116
116
  ## Error Handling
117
117
 
package/dist/bin-http.js CHANGED
@@ -8,7 +8,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
8
8
 
9
9
  // src/server.ts
10
10
  import { readFileSync } from "node:fs";
11
- import { dirname, join } from "node:path";
11
+ import { dirname, join as join2 } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
 
@@ -23,10 +23,13 @@ function isHostedArchiveUrl(url) {
23
23
  }
24
24
 
25
25
  // src/resources.ts
26
+ import {
27
+ ResourceTemplate
28
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
26
29
  import { CHAIN_TRIGGER_FIELDS } from "@secondlayer/shared";
27
30
  import { BOOTSTRAP_STEP } from "@secondlayer/shared/archive/instance-diagnosis";
28
31
  import { TRAIT_STANDARDS } from "@secondlayer/stacks/clarity";
29
- import { filterFieldsByType } from "@secondlayer/subgraphs";
32
+ import { camelizeKeys, filterFieldsByType } from "@secondlayer/subgraphs";
30
33
  import { TYPE_MAP } from "@secondlayer/subgraphs/schema";
31
34
 
32
35
  // src/lib/client.ts
@@ -187,7 +190,8 @@ var PRODUCT_BLURBS = {
187
190
  streams: "raw chain event firehose — cursor-paginated reads, tip/canonical/reorg checks, bulk parquet dumps",
188
191
  contracts: "trait-based contract discovery and ABIs",
189
192
  subgraphs: "author/scaffold/deploy/query custom indexes",
190
- subscriptions: "webhook delivery on subgraph rows or raw chain events",
193
+ webhooks: "a signed POST to a URL you run, on subgraph rows or raw chain events",
194
+ subscriptions: "a signed POST to a URL you run, on subgraph rows or raw chain events",
191
195
  account: "identity and self-provisioned API keys",
192
196
  codegen: "ORM schemas for the tables you read",
193
197
  instance: "decoder health and empty-index diagnosis",
@@ -200,6 +204,7 @@ var PRODUCT_ORDER = [
200
204
  "streams",
201
205
  "contracts",
202
206
  "subgraphs",
207
+ "webhooks",
203
208
  "subscriptions",
204
209
  "account",
205
210
  "codegen",
@@ -256,7 +261,8 @@ async function buildContext(deps = { clientProvider: getClient }) {
256
261
  streamsTip: orNull(snap?.streamsTip),
257
262
  indexTip: orNull(snap?.indexTip),
258
263
  subgraphs: snap?.subgraphs.value ? snap.subgraphs.value.map(formatSubgraphSummary) : orNull(snap?.subgraphs),
259
- subscriptions: orNull(snap?.subscriptions),
264
+ webhooks: orNull(snap?.webhooks),
265
+ subscriptions: orNull(snap?.webhooks),
260
266
  activeOperations: orNull(snap?.activeOperations),
261
267
  instance: instance2
262
268
  },
@@ -267,7 +273,7 @@ async function buildContext(deps = { clientProvider: getClient }) {
267
273
  }
268
274
  function registerResources(server) {
269
275
  server.resource("context", "secondlayer://context", {
270
- description: "Live agent context — what exists (your subgraphs, subscriptions, account), what you can do, and read-auth tiers. Read this first."
276
+ description: "Live agent context — what exists (your subgraphs, webhooks, account), what you can do, and read-auth tiers. Read this first."
271
277
  }, async () => ({
272
278
  contents: [
273
279
  {
@@ -312,7 +318,7 @@ function registerResources(server) {
312
318
  ]
313
319
  }));
314
320
  server.resource("chain-triggers", "secondlayer://chain-triggers", {
315
- description: "Chain-subscription trigger types and the filter fields each accepts (for subscriptions_create triggers)."
321
+ description: "Chain-webhook trigger types and the filter fields each accepts (for webhooks_create triggers)."
316
322
  }, async () => ({
317
323
  contents: [
318
324
  {
@@ -322,6 +328,73 @@ function registerResources(server) {
322
328
  }
323
329
  ]
324
330
  }));
331
+ server.resource("samples", new ResourceTemplate("secondlayer://samples/{contractId}", {
332
+ list: undefined
333
+ }), {
334
+ description: "Decoded camelCase print samples for a contract (schema + up to 5 events). Use before writing a print_event handler."
335
+ }, async (uri, variables) => {
336
+ const contractId = String(Array.isArray(variables.contractId) ? variables.contractId[0] : variables.contractId ?? "");
337
+ const body = await buildSamplesResource(contractId);
338
+ return {
339
+ contents: [
340
+ {
341
+ uri: uri.href,
342
+ mimeType: "application/json",
343
+ text: JSON.stringify(body, null, 2)
344
+ }
345
+ ]
346
+ };
347
+ });
348
+ }
349
+ function jsonSafe(value) {
350
+ if (typeof value === "bigint")
351
+ return value.toString();
352
+ if (value === null || value === undefined)
353
+ return value;
354
+ if (Array.isArray(value))
355
+ return value.map(jsonSafe);
356
+ if (typeof value === "object") {
357
+ const out = {};
358
+ for (const [k, v] of Object.entries(value)) {
359
+ out[k] = jsonSafe(v);
360
+ }
361
+ return out;
362
+ }
363
+ return value;
364
+ }
365
+ async function buildSamplesResource(contractId, clientProvider = getClient) {
366
+ const client = clientProvider();
367
+ const printSchema = await client.index.printSchema(contractId);
368
+ const envelope = await client.index.events.list({
369
+ eventType: "print",
370
+ contractId,
371
+ limit: 5
372
+ });
373
+ const schema = printSchema?.topics.map((t) => ({
374
+ topic: t.topic,
375
+ fields: t.fields.map((f) => ({
376
+ camel: f.camel_name,
377
+ snake: f.name.replace(/-/g, "_"),
378
+ column_type: f.column_type,
379
+ always: f.always_present
380
+ }))
381
+ })) ?? [];
382
+ const samples = (envelope.events ?? []).slice(0, 5).map((ev) => {
383
+ const payload = ev.payload;
384
+ const data = jsonSafe(camelizeKeys(payload?.value));
385
+ return {
386
+ txId: ev.tx_id,
387
+ height: ev.block_height,
388
+ topic: payload?.topic ?? "",
389
+ data: data ?? {}
390
+ };
391
+ });
392
+ return {
393
+ contractId,
394
+ handlerShape: "event.data is camelCase; columns are snake_case",
395
+ schema,
396
+ samples
397
+ };
325
398
  }
326
399
 
327
400
  // src/tools/account.ts
@@ -480,7 +553,11 @@ function registerInstanceTools(server, clientProvider = getClient) {
480
553
  }
481
554
 
482
555
  // src/tools/scaffold.ts
483
- import { generateSubgraphCode } from "@secondlayer/scaffold";
556
+ import {
557
+ generatePrintSchemaSubgraph,
558
+ generateSubgraphCode,
559
+ generateTokenSubgraphFromAbi
560
+ } from "@secondlayer/scaffold";
484
561
  import { z as z6 } from "zod";
485
562
  async function fetchAbi(clientProvider, contractId) {
486
563
  const contract = await clientProvider().contracts.get(contractId, {
@@ -492,14 +569,32 @@ async function fetchAbi(clientProvider, contractId) {
492
569
  if (!abi) {
493
570
  throw new Error(`No ABI available for ${contractId} (abi_status: ${contract.abi_status})`);
494
571
  }
495
- return { functions: abi.functions ?? [] };
572
+ return { functions: abi.functions ?? [], abi };
496
573
  }
497
574
  function registerScaffoldTools(server, clientProvider = getClient) {
498
- defineTool(server, "subgraphs_scaffold", "Generate a subgraph scaffold from a deployed Stacks contract. Fetches the ABI automatically. Returns TypeScript source — review it, then pass it to subgraphs_deploy.", {
575
+ defineTool(server, "subgraphs_scaffold", "Generate a subgraph from observed print events when available; ABI token transfers or public functions only when the contract has never printed. Review the TypeScript, run subgraphs_test until ok: true with written >= 1, read skipped, then subgraphs_deploy. dryRun is DDL only.", {
499
576
  contractId: z6.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
500
577
  subgraphName: z6.string().optional().describe("Override the subgraph name (defaults to contract name)")
501
578
  }, async ({ contractId, subgraphName }) => {
502
- const { functions } = await fetchAbi(clientProvider, contractId);
579
+ const printSchema = await clientProvider().index.printSchema(contractId);
580
+ if (printSchema && printSchema.topics.length > 0) {
581
+ const code2 = generatePrintSchemaSubgraph({
582
+ contractId,
583
+ name: subgraphName,
584
+ topics: printSchema.topics,
585
+ sample: printSchema.sample
586
+ });
587
+ return { content: [{ type: "text", text: code2 }] };
588
+ }
589
+ const { functions, abi } = await fetchAbi(clientProvider, contractId);
590
+ const token = generateTokenSubgraphFromAbi({
591
+ contractId,
592
+ abi,
593
+ name: subgraphName
594
+ });
595
+ if (token) {
596
+ return { content: [{ type: "text", text: token }] };
597
+ }
503
598
  const code = generateSubgraphCode(contractId, functions, subgraphName);
504
599
  return { content: [{ type: "text", text: code }] };
505
600
  });
@@ -752,14 +847,42 @@ function registerStreamsTools(server, clientProvider = getClient) {
752
847
  }
753
848
 
754
849
  // src/tools/subgraphs.ts
850
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
851
+ import { tmpdir } from "node:os";
852
+ import { join } from "node:path";
853
+ import { pathToFileURL } from "node:url";
755
854
  import { bundleSubgraphCode } from "@secondlayer/bundler";
855
+ import { DECODED_EVENT_TYPES as DECODED_EVENT_TYPES2 } from "@secondlayer/shared";
856
+ import {
857
+ runSubgraphTest
858
+ } from "@secondlayer/subgraphs/testing";
756
859
  import { z as z9 } from "zod";
860
+ function eventTypeFor(filter) {
861
+ if (filter.type === "contract_deploy")
862
+ return null;
863
+ const candidate = filter.type === "print_event" ? "print" : filter.type;
864
+ return DECODED_EVENT_TYPES2.includes(candidate) ? candidate : null;
865
+ }
866
+ function skipReason(name, filter) {
867
+ return filter.type === "contract_deploy" ? `source "${name}" (contract_deploy) has no Index list endpoint — skipped.` : `source "${name}" (${filter.type}) is not readable from Index — skipped.`;
868
+ }
869
+ async function loadBundledDefinition(handlerCode) {
870
+ const dir = await mkdtemp(join(tmpdir(), "sl-mcp-sg-test-"));
871
+ try {
872
+ const file = join(dir, `handler-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
873
+ await writeFile(file, handlerCode);
874
+ const mod = await import(pathToFileURL(file).href);
875
+ return mod.default ?? {};
876
+ } finally {
877
+ await rm(dir, { recursive: true, force: true });
878
+ }
879
+ }
757
880
  function registerSubgraphTools(server, clientProvider = getClient) {
758
881
  defineTool(server, "subgraphs_list", "List all deployed subgraphs. Returns summary fields only; read rows with subgraphs_query.", {}, async () => {
759
882
  const { data } = await clientProvider().subgraphs.list();
760
883
  return jsonResponse(data.map(formatSubgraphSummary));
761
884
  });
762
- defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health, and table columns.", { name: z9.string().describe("Subgraph name") }, async ({ name }) => {
885
+ defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health (incl. health.emptyMapping when processed events wrote 0 rows), and table columns.", { name: z9.string().describe("Subgraph name") }, async ({ name }) => {
763
886
  const detail = await clientProvider().subgraphs.status(name);
764
887
  return jsonResponse(detail);
765
888
  });
@@ -855,10 +978,10 @@ function registerSubgraphTools(server, clientProvider = getClient) {
855
978
  const result = await clientProvider().subgraphs.delete(name);
856
979
  return textResponse(result.message);
857
980
  });
858
- defineTool(server, "subgraphs_deploy", "Deploy a subgraph from TypeScript code. Pass the full defineSubgraph() source — it will be bundled, validated, and deployed. Optional startBlock overrides the source definition for this deploy. Set dryRun to validate and preview the schema/DDL without writing anything. Call `subgraphs_reindex` separately if you need a forced reindex.", {
981
+ defineTool(server, "subgraphs_deploy", "Deploy a subgraph from TypeScript code. Pass the full defineSubgraph() source — it will be bundled, validated, and deployed. Optional startBlock overrides the source definition for this deploy. dryRun validates schema/DDL only and does not run handlers — call subgraphs_test first and deploy only when it returns ok: true with written >= 1. Call subgraphs_reindex separately if you need a forced reindex.", {
859
982
  code: z9.string().describe("TypeScript source code containing a defineSubgraph() call"),
860
983
  startBlock: z9.number().int().nonnegative().optional().describe("Override the definition startBlock for this deploy"),
861
- dryRun: z9.boolean().optional().describe("Validate and preview the deploy (schema/DDL) without writing changes")
984
+ dryRun: z9.boolean().optional().describe("Validate schema/DDL only — does not run handlers. Use subgraphs_test to prove a mapping writes rows.")
862
985
  }, async ({ code, startBlock, dryRun }) => {
863
986
  const bundled = await bundleSubgraphCode(code);
864
987
  const result = await clientProvider().subgraphs.deploy({
@@ -874,24 +997,201 @@ function registerSubgraphTools(server, clientProvider = getClient) {
874
997
  });
875
998
  return jsonResponse(result);
876
999
  });
1000
+ defineTool(server, "subgraphs_test", "Prove a subgraph mapping writes rows against Index data before deploy. Pass full defineSubgraph() source plus a height range. Returns ok/written/tables; EMPTY_MAPPING means events matched but 0 rows were written — read hint for observed event.data keys and do not invent fields. subgraphs_deploy dryRun is DDL only; do not deploy until ok: true with written >= 1.", {
1001
+ code: z9.string().describe("TypeScript source containing a defineSubgraph() call"),
1002
+ fromHeight: z9.number().int().nonnegative().describe("Start block height (inclusive)"),
1003
+ toHeight: z9.number().int().nonnegative().optional().describe("End block height (inclusive); default fromHeight+100"),
1004
+ limit: z9.number().int().positive().max(200).optional().describe("Max Index rows per source (default 50, max 200)")
1005
+ }, async ({ code, fromHeight, toHeight, limit }) => {
1006
+ const bundled = await bundleSubgraphCode(code);
1007
+ const def = await loadBundledDefinition(bundled.handlerCode);
1008
+ if (!def.handlers || !def.schema) {
1009
+ return jsonResponse({
1010
+ ok: false,
1011
+ code: "NO_SOURCES",
1012
+ matched: 0,
1013
+ written: 0,
1014
+ tables: [],
1015
+ hint: "Bundled module must default-export defineSubgraph() with handlers and schema."
1016
+ }, true);
1017
+ }
1018
+ const sources = bundled.sources ?? {};
1019
+ const to = toHeight ?? fromHeight + 100;
1020
+ const pageLimit = Math.min(limit ?? 50, 200);
1021
+ const index = clientProvider().index;
1022
+ const events = {};
1023
+ const skipped = [];
1024
+ let sourcesTested = 0;
1025
+ for (const [name, filter] of Object.entries(sources)) {
1026
+ if (filter.type === "contract_call") {
1027
+ sourcesTested++;
1028
+ const envelope2 = await index.contractCalls.list({
1029
+ ...filter.contractId ? {
1030
+ contractId: Array.isArray(filter.contractId) ? filter.contractId : filter.contractId
1031
+ } : {},
1032
+ ...filter.functionName ? { functionName: filter.functionName } : {},
1033
+ fromHeight,
1034
+ toHeight: to,
1035
+ limit: pageLimit
1036
+ });
1037
+ events[name] = envelope2.contract_calls;
1038
+ continue;
1039
+ }
1040
+ const eventType = eventTypeFor(filter);
1041
+ if (eventType === null) {
1042
+ skipped.push({ source: name, reason: skipReason(name, filter) });
1043
+ continue;
1044
+ }
1045
+ sourcesTested++;
1046
+ const envelope = await index.events.list({
1047
+ eventType,
1048
+ ...filter.contractId ? {
1049
+ contractId: Array.isArray(filter.contractId) ? filter.contractId : filter.contractId
1050
+ } : {},
1051
+ fromHeight,
1052
+ toHeight: to,
1053
+ limit: pageLimit
1054
+ });
1055
+ events[name] = envelope.events;
1056
+ }
1057
+ if (sourcesTested === 0) {
1058
+ return jsonResponse({
1059
+ ok: false,
1060
+ code: "NO_SOURCES",
1061
+ matched: 0,
1062
+ written: 0,
1063
+ tables: [],
1064
+ hint: "No sources were tested — every source is unreadable from Index.",
1065
+ skipped
1066
+ }, true);
1067
+ }
1068
+ const result = await runSubgraphTest({
1069
+ schema: def.schema,
1070
+ handlers: def.handlers,
1071
+ sources,
1072
+ events
1073
+ });
1074
+ const body = { ...result, skipped };
1075
+ if (skipped.length > 0) {
1076
+ const skipHints = skipped.map((s) => s.reason).join(" ");
1077
+ body.hint = result.hint ? `${result.hint} ${skipHints}` : skipHints;
1078
+ }
1079
+ return jsonResponse(body, !result.ok);
1080
+ });
877
1081
  }
878
1082
 
879
- // src/tools/subscriptions.ts
1083
+ // src/tools/webhooks.ts
880
1084
  import { CHAIN_TRIGGER_TYPES } from "@secondlayer/shared";
881
1085
  import { z as z10 } from "zod";
882
- function registerSubscriptionTools(server, clientProvider = getClient) {
883
- defineTool(server, "subscriptions_list", "List all subscriptions for the current account. Returns summary fields (no secrets).", {}, async () => {
884
- const { data } = await clientProvider().subscriptions.list();
1086
+ function registerWebhookTools(server, clientProvider = getClient) {
1087
+ defineTool(server, "webhooks_list", "List all webhooks for the current account. Returns summary fields (no secrets).", {}, async () => {
1088
+ const { data } = await clientProvider().webhooks.list();
1089
+ return jsonResponse(data);
1090
+ });
1091
+ defineTool(server, "webhooks_get", "Get full detail for a webhook (filter, auth, retry config, circuit state).", { id: z10.string().describe("Webhook id") }, async ({ id }) => {
1092
+ const detail = await clientProvider().webhooks.get(id);
1093
+ return jsonResponse(detail);
1094
+ });
1095
+ defineTool(server, "webhooks_create", "Create a webhook. Two kinds (mutually exclusive): a SUBGRAPH webhook fires on a subgraph table's rows (set subgraphName + tableName + optional filter); a CHAIN webhook fires on raw chain events with no subgraph (set triggers). Returns `signingSecret` ONCE — forward it to the user so they can wire it into their receiver.", {
1096
+ name: z10.string().describe("Human-readable name, unique per account"),
1097
+ subgraphName: z10.string().optional().describe("Subgraph to subscribe to (subgraph webhook)"),
1098
+ tableName: z10.string().optional().describe("Table within the subgraph (subgraph webhook)"),
1099
+ triggers: z10.array(z10.object({
1100
+ type: z10.enum(CHAIN_TRIGGER_TYPES),
1101
+ contractId: z10.string().optional(),
1102
+ functionName: z10.string().optional(),
1103
+ caller: z10.string().optional(),
1104
+ sender: z10.string().optional(),
1105
+ recipient: z10.string().optional(),
1106
+ assetIdentifier: z10.string().optional(),
1107
+ deployer: z10.string().optional(),
1108
+ contractName: z10.string().optional(),
1109
+ topic: z10.string().optional(),
1110
+ lockedAddress: z10.string().optional(),
1111
+ trait: z10.string().optional(),
1112
+ minAmount: z10.union([z10.string(), z10.number()]).optional(),
1113
+ maxAmount: z10.union([z10.string(), z10.number()]).optional()
1114
+ })).optional().describe("Chain triggers (chain webhook) — provide INSTEAD of subgraphName/tableName. Each targets a raw chain event/tx; string fields accept `*` wildcards, `trait` scopes to a SIP/trait. Per-type accepted fields: see the secondlayer://chain-triggers resource. Forward-looking: starts at chain tip, no backfill."),
1115
+ url: z10.string().describe("Webhook URL"),
1116
+ format: z10.enum([
1117
+ "standard-webhooks",
1118
+ "inngest",
1119
+ "trigger",
1120
+ "cloudflare",
1121
+ "cloudevents",
1122
+ "raw"
1123
+ ]).optional().describe("Wire format (default standard-webhooks)"),
1124
+ runtime: z10.enum(["inngest", "trigger", "cloudflare", "node"]).optional().describe("Receiver runtime label (display only)"),
1125
+ filter: z10.record(z10.string(), z10.unknown()).optional().describe('Scalar filter DSL, e.g. {"amount": {"gte": 100}, "sender": "SP..."}'),
1126
+ authConfig: z10.record(z10.string(), z10.unknown()).optional().describe('Receiver auth sent with each delivery, e.g. {"type": "bearer", "token": "..."}')
1127
+ }, async (input) => {
1128
+ const res = await clientProvider().webhooks.create(input);
1129
+ return jsonResponse(res);
1130
+ });
1131
+ defineTool(server, "webhooks_update", "Patch a webhook (name, url, filter, authConfig, format, runtime, retry, timeout, concurrency).", {
1132
+ id: z10.string(),
1133
+ name: z10.string().optional().describe("Rename the webhook"),
1134
+ url: z10.string().optional(),
1135
+ filter: z10.record(z10.string(), z10.unknown()).optional(),
1136
+ authConfig: z10.record(z10.string(), z10.unknown()).optional().describe("Receiver auth sent with each delivery (bearer/etc.)"),
1137
+ format: z10.enum([
1138
+ "standard-webhooks",
1139
+ "inngest",
1140
+ "trigger",
1141
+ "cloudflare",
1142
+ "cloudevents",
1143
+ "raw"
1144
+ ]).optional(),
1145
+ runtime: z10.enum(["inngest", "trigger", "cloudflare", "node"]).nullable().optional(),
1146
+ maxRetries: z10.number().int().min(0).optional(),
1147
+ timeoutMs: z10.number().int().min(100).optional(),
1148
+ concurrency: z10.number().int().min(1).optional()
1149
+ }, async ({ id, ...patch }) => {
1150
+ const res = await clientProvider().webhooks.update(id, patch);
1151
+ return jsonResponse(res);
1152
+ });
1153
+ defineTool(server, "webhooks_delete", "Delete a webhook. Pending outbox rows are cascade-deleted.", { id: z10.string() }, async ({ id }) => {
1154
+ const res = await clientProvider().webhooks.delete(id);
1155
+ return jsonResponse(res);
1156
+ });
1157
+ defineTool(server, "webhooks_test", "Send a one-off test webhook to a webhook's URL (built for its format, SSRF-guarded). Logged as a delivery row. Returns {ok, statusCode, error, durationMs, deliveryId}.", { id: z10.string().describe("Webhook id") }, async ({ id }) => {
1158
+ const res = await clientProvider().webhooks.test(id);
1159
+ return jsonResponse(res);
1160
+ });
1161
+ defineTool(server, "webhooks_pause", "Pause a webhook: stops delivery attempts and stops queueing new ones. Nothing is deleted — resume picks the webhook back up from the tip. Verify with webhooks_get (status).", { id: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.pause(id)));
1162
+ defineTool(server, "webhooks_resume", "Resume a paused webhook (also clears a tripped circuit breaker). Deliveries restart from the current tip — use webhooks_replay for the blocks missed while paused. Verify with webhooks_get (status).", { id: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.resume(id)));
1163
+ defineTool(server, "webhooks_rotate_secret", "Rotate a webhook's signing secret. Returns the NEW `signingSecret` ONCE — forward it to the user; deliveries signed with the old secret stop verifying as soon as this returns, so the receiver must be updated.", { id: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.rotateSecret(id)));
1164
+ defineTool(server, "webhooks_deliveries", "List recent delivery attempts for a webhook — status code, attempt count, error, duration, timestamps. THIS IS THE VERIFY CALL: after webhooks_create, webhooks_test, or webhooks_replay, read it to confirm the webhook actually landed (and to see the receiver's response when it didn't) instead of trusting the enqueue result.", { id: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.deliveries(id)));
1165
+ defineTool(server, "webhooks_dead", "List a webhook's dead-letter queue — deliveries that exhausted their retries, with the outbox id, payload, and last error. Diagnose the receiver first (webhooks_get for the URL/auth, webhooks_test for a live probe), then requeue rows with webhooks_requeue.", { id: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.dead(id)));
1166
+ defineTool(server, "webhooks_requeue", "Requeue ONE dead-lettered delivery for another attempt, by its outbox id from webhooks_dead. Fix the receiver first — a requeue against a still-broken endpoint just dies again. Verify with webhooks_deliveries.", {
1167
+ id: z10.string().describe("Webhook id"),
1168
+ outboxId: z10.string().describe("Outbox id of the dead delivery (from webhooks_dead)")
1169
+ }, async ({ id, outboxId }) => jsonResponse(await clientProvider().webhooks.requeue(id, outboxId)));
1170
+ defineTool(server, "webhooks_replay", "Replay a block range for a webhook. Replays run at 10% of batch capacity — use sparingly. Pass `force` (a short idempotency suffix) to re-run a range that was already replayed.", {
1171
+ id: z10.string(),
1172
+ fromBlock: z10.number().int().nonnegative(),
1173
+ toBlock: z10.number().int().nonnegative(),
1174
+ force: z10.string().optional().describe("Idempotency suffix to force a duplicate replay of the range")
1175
+ }, async ({ id, fromBlock, toBlock, force }) => {
1176
+ const res = await clientProvider().webhooks.replay(id, {
1177
+ fromBlock,
1178
+ toBlock,
1179
+ ...force !== undefined ? { force } : {}
1180
+ });
1181
+ return jsonResponse(res);
1182
+ });
1183
+ defineTool(server, "subscriptions_list", "Deprecated alias of webhooks_list. Removed next minor. List all webhooks for the current account. Returns summary fields (no secrets).", {}, async () => {
1184
+ const { data } = await clientProvider().webhooks.list();
885
1185
  return jsonResponse(data);
886
1186
  });
887
- defineTool(server, "subscriptions_get", "Get full detail for a subscription (filter, auth, retry config, circuit state).", { id: z10.string().describe("Subscription id") }, async ({ id }) => {
888
- const detail = await clientProvider().subscriptions.get(id);
1187
+ defineTool(server, "subscriptions_get", "Deprecated alias of webhooks_get. Removed next minor. Get full detail for a webhook (filter, auth, retry config, circuit state).", { id: z10.string().describe("Webhook id") }, async ({ id }) => {
1188
+ const detail = await clientProvider().webhooks.get(id);
889
1189
  return jsonResponse(detail);
890
1190
  });
891
- defineTool(server, "subscriptions_create", "Create a subscription. Two kinds (mutually exclusive): a SUBGRAPH subscription fires on a subgraph table's rows (set subgraphName + tableName + optional filter); a CHAIN subscription fires on raw chain events with no subgraph (set triggers). Returns `signingSecret` ONCE — forward it to the user so they can wire it into their receiver.", {
1191
+ defineTool(server, "subscriptions_create", "Deprecated alias of webhooks_create. Removed next minor. Create a webhook. Two kinds (mutually exclusive): a SUBGRAPH webhook fires on a subgraph table's rows (set subgraphName + tableName + optional filter); a CHAIN webhook fires on raw chain events with no subgraph (set triggers). Returns `signingSecret` ONCE — forward it to the user so they can wire it into their receiver.", {
892
1192
  name: z10.string().describe("Human-readable name, unique per account"),
893
- subgraphName: z10.string().optional().describe("Subgraph to subscribe to (subgraph subscription)"),
894
- tableName: z10.string().optional().describe("Table within the subgraph (subgraph subscription)"),
1193
+ subgraphName: z10.string().optional().describe("Subgraph to subscribe to (subgraph webhook)"),
1194
+ tableName: z10.string().optional().describe("Table within the subgraph (subgraph webhook)"),
895
1195
  triggers: z10.array(z10.object({
896
1196
  type: z10.enum(CHAIN_TRIGGER_TYPES),
897
1197
  contractId: z10.string().optional(),
@@ -907,7 +1207,7 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
907
1207
  trait: z10.string().optional(),
908
1208
  minAmount: z10.union([z10.string(), z10.number()]).optional(),
909
1209
  maxAmount: z10.union([z10.string(), z10.number()]).optional()
910
- })).optional().describe("Chain triggers (chain subscription) — provide INSTEAD of subgraphName/tableName. Each targets a raw chain event/tx; string fields accept `*` wildcards, `trait` scopes to a SIP/trait. Per-type accepted fields: see the secondlayer://chain-triggers resource. Forward-looking: starts at chain tip, no backfill."),
1210
+ })).optional().describe("Chain triggers (chain webhook) — provide INSTEAD of subgraphName/tableName. Each targets a raw chain event/tx; string fields accept `*` wildcards, `trait` scopes to a SIP/trait. Per-type accepted fields: see the secondlayer://chain-triggers resource. Forward-looking: starts at chain tip, no backfill."),
911
1211
  url: z10.string().describe("Webhook URL"),
912
1212
  format: z10.enum([
913
1213
  "standard-webhooks",
@@ -921,12 +1221,12 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
921
1221
  filter: z10.record(z10.string(), z10.unknown()).optional().describe('Scalar filter DSL, e.g. {"amount": {"gte": 100}, "sender": "SP..."}'),
922
1222
  authConfig: z10.record(z10.string(), z10.unknown()).optional().describe('Receiver auth sent with each delivery, e.g. {"type": "bearer", "token": "..."}')
923
1223
  }, async (input) => {
924
- const res = await clientProvider().subscriptions.create(input);
1224
+ const res = await clientProvider().webhooks.create(input);
925
1225
  return jsonResponse(res);
926
1226
  });
927
- defineTool(server, "subscriptions_update", "Patch a subscription (name, url, filter, authConfig, format, runtime, retry, timeout, concurrency).", {
1227
+ defineTool(server, "subscriptions_update", "Deprecated alias of webhooks_update. Removed next minor. Patch a webhook (name, url, filter, authConfig, format, runtime, retry, timeout, concurrency).", {
928
1228
  id: z10.string(),
929
- name: z10.string().optional().describe("Rename the subscription"),
1229
+ name: z10.string().optional().describe("Rename the webhook"),
930
1230
  url: z10.string().optional(),
931
1231
  filter: z10.record(z10.string(), z10.unknown()).optional(),
932
1232
  authConfig: z10.record(z10.string(), z10.unknown()).optional().describe("Receiver auth sent with each delivery (bearer/etc.)"),
@@ -943,33 +1243,33 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
943
1243
  timeoutMs: z10.number().int().min(100).optional(),
944
1244
  concurrency: z10.number().int().min(1).optional()
945
1245
  }, async ({ id, ...patch }) => {
946
- const res = await clientProvider().subscriptions.update(id, patch);
1246
+ const res = await clientProvider().webhooks.update(id, patch);
947
1247
  return jsonResponse(res);
948
1248
  });
949
- defineTool(server, "subscriptions_delete", "Delete a subscription. Pending outbox rows are cascade-deleted.", { id: z10.string() }, async ({ id }) => {
950
- const res = await clientProvider().subscriptions.delete(id);
1249
+ defineTool(server, "subscriptions_delete", "Deprecated alias of webhooks_delete. Removed next minor. Delete a webhook. Pending outbox rows are cascade-deleted.", { id: z10.string() }, async ({ id }) => {
1250
+ const res = await clientProvider().webhooks.delete(id);
951
1251
  return jsonResponse(res);
952
1252
  });
953
- defineTool(server, "subscriptions_test", "Send a one-off test webhook to a subscription's URL (built for its format, SSRF-guarded). Logged as a delivery row. Returns {ok, statusCode, error, durationMs, deliveryId}.", { id: z10.string().describe("Subscription id") }, async ({ id }) => {
954
- const res = await clientProvider().subscriptions.test(id);
1253
+ defineTool(server, "subscriptions_test", "Deprecated alias of webhooks_test. Removed next minor. Send a one-off test webhook to a webhook's URL (built for its format, SSRF-guarded). Logged as a delivery row. Returns {ok, statusCode, error, durationMs, deliveryId}.", { id: z10.string().describe("Webhook id") }, async ({ id }) => {
1254
+ const res = await clientProvider().webhooks.test(id);
955
1255
  return jsonResponse(res);
956
1256
  });
957
- defineTool(server, "subscriptions_pause", "Pause a subscription: stops delivery attempts and stops queueing new ones. Nothing is deleted — resume picks the subscription back up from the tip. Verify with subscriptions_get (status).", { id: z10.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.pause(id)));
958
- defineTool(server, "subscriptions_resume", "Resume a paused subscription (also clears a tripped circuit breaker). Deliveries restart from the current tip — use subscriptions_replay for the blocks missed while paused. Verify with subscriptions_get (status).", { id: z10.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.resume(id)));
959
- defineTool(server, "subscriptions_rotate_secret", "Rotate a subscription's webhook signing secret. Returns the NEW `signingSecret` ONCE — forward it to the user; deliveries signed with the old secret stop verifying as soon as this returns, so the receiver must be updated.", { id: z10.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.rotateSecret(id)));
960
- defineTool(server, "subscriptions_deliveries", "List recent delivery attempts for a subscription — status code, attempt count, error, duration, timestamps. THIS IS THE VERIFY CALL: after subscriptions_create, subscriptions_test, or subscriptions_replay, read it to confirm the webhook actually landed (and to see the receiver's response when it didn't) instead of trusting the enqueue result.", { id: z10.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.deliveries(id)));
961
- defineTool(server, "subscriptions_dead", "List a subscription's dead-letter queue — deliveries that exhausted their retries, with the outbox id, payload, and last error. Diagnose the receiver first (subscriptions_get for the URL/auth, subscriptions_test for a live probe), then requeue rows with subscriptions_requeue.", { id: z10.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.dead(id)));
962
- defineTool(server, "subscriptions_requeue", "Requeue ONE dead-lettered delivery for another attempt, by its outbox id from subscriptions_dead. Fix the receiver first — a requeue against a still-broken endpoint just dies again. Verify with subscriptions_deliveries.", {
963
- id: z10.string().describe("Subscription id"),
964
- outboxId: z10.string().describe("Outbox id of the dead delivery (from subscriptions_dead)")
965
- }, async ({ id, outboxId }) => jsonResponse(await clientProvider().subscriptions.requeue(id, outboxId)));
966
- defineTool(server, "subscriptions_replay", "Replay a block range for a subscription. Replays run at 10% of batch capacity — use sparingly. Pass `force` (a short idempotency suffix) to re-run a range that was already replayed.", {
1257
+ defineTool(server, "subscriptions_pause", "Deprecated alias of webhooks_pause. Removed next minor. Pause a webhook: stops delivery attempts and stops queueing new ones. Nothing is deleted — resume picks the webhook back up from the tip. Verify with webhooks_get (status).", { id: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.pause(id)));
1258
+ defineTool(server, "subscriptions_resume", "Deprecated alias of webhooks_resume. Removed next minor. Resume a paused webhook (also clears a tripped circuit breaker). Deliveries restart from the current tip — use webhooks_replay for the blocks missed while paused. Verify with webhooks_get (status).", { id: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.resume(id)));
1259
+ defineTool(server, "subscriptions_rotate_secret", "Deprecated alias of webhooks_rotate_secret. Removed next minor. Rotate a webhook's signing secret. Returns the NEW `signingSecret` ONCE — forward it to the user; deliveries signed with the old secret stop verifying as soon as this returns, so the receiver must be updated.", { id: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.rotateSecret(id)));
1260
+ defineTool(server, "subscriptions_deliveries", "Deprecated alias of webhooks_deliveries. Removed next minor. List recent delivery attempts for a webhook — status code, attempt count, error, duration, timestamps. THIS IS THE VERIFY CALL: after webhooks_create, webhooks_test, or webhooks_replay, read it to confirm the webhook actually landed (and to see the receiver's response when it didn't) instead of trusting the enqueue result.", { id: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.deliveries(id)));
1261
+ defineTool(server, "subscriptions_dead", "Deprecated alias of webhooks_dead. Removed next minor. List a webhook's dead-letter queue — deliveries that exhausted their retries, with the outbox id, payload, and last error. Diagnose the receiver first (webhooks_get for the URL/auth, webhooks_test for a live probe), then requeue rows with webhooks_requeue.", { id: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.dead(id)));
1262
+ defineTool(server, "subscriptions_requeue", "Deprecated alias of webhooks_requeue. Removed next minor. Requeue ONE dead-lettered delivery for another attempt, by its outbox id from webhooks_dead. Fix the receiver first — a requeue against a still-broken endpoint just dies again. Verify with webhooks_deliveries.", {
1263
+ id: z10.string().describe("Webhook id"),
1264
+ outboxId: z10.string().describe("Outbox id of the dead delivery (from webhooks_dead)")
1265
+ }, async ({ id, outboxId }) => jsonResponse(await clientProvider().webhooks.requeue(id, outboxId)));
1266
+ defineTool(server, "subscriptions_replay", "Deprecated alias of webhooks_replay. Removed next minor. Replay a block range for a webhook. Replays run at 10% of batch capacity — use sparingly. Pass `force` (a short idempotency suffix) to re-run a range that was already replayed.", {
967
1267
  id: z10.string(),
968
1268
  fromBlock: z10.number().int().nonnegative(),
969
1269
  toBlock: z10.number().int().nonnegative(),
970
1270
  force: z10.string().optional().describe("Idempotency suffix to force a duplicate replay of the range")
971
1271
  }, async ({ id, fromBlock, toBlock, force }) => {
972
- const res = await clientProvider().subscriptions.replay(id, {
1272
+ const res = await clientProvider().webhooks.replay(id, {
973
1273
  fromBlock,
974
1274
  toBlock,
975
1275
  ...force !== undefined ? { force } : {}
@@ -980,7 +1280,7 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
980
1280
 
981
1281
  // src/server.ts
982
1282
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
983
- var pkg = JSON.parse(readFileSync(join(__dirname2, "../package.json"), "utf-8"));
1283
+ var pkg = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
984
1284
  function createServer() {
985
1285
  const server = new McpServer({
986
1286
  name: "secondlayer",
@@ -988,7 +1288,7 @@ function createServer() {
988
1288
  });
989
1289
  registerScaffoldTools(server);
990
1290
  registerSubgraphTools(server);
991
- registerSubscriptionTools(server);
1291
+ registerWebhookTools(server);
992
1292
  registerIndexTools(server);
993
1293
  registerStreamsTools(server);
994
1294
  registerContractTools(server);
@@ -1090,5 +1390,5 @@ httpServer.listen(port, () => {
1090
1390
  console.error("Warning: SECONDLAYER_MCP_SECRET not set, authentication disabled");
1091
1391
  });
1092
1392
 
1093
- //# debugId=978D99EF4EC8A61364756E2164756E21
1393
+ //# debugId=E0F49AE0DC13C56764756E2164756E21
1094
1394
  //# sourceMappingURL=bin-http.js.map