@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/dist/bin.js CHANGED
@@ -5,7 +5,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
 
6
6
  // src/server.ts
7
7
  import { readFileSync } from "node:fs";
8
- import { dirname, join } from "node:path";
8
+ import { dirname, join as join2 } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
11
 
@@ -20,10 +20,13 @@ function isHostedArchiveUrl(url) {
20
20
  }
21
21
 
22
22
  // src/resources.ts
23
+ import {
24
+ ResourceTemplate
25
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
23
26
  import { CHAIN_TRIGGER_FIELDS } from "@secondlayer/shared";
24
27
  import { BOOTSTRAP_STEP } from "@secondlayer/shared/archive/instance-diagnosis";
25
28
  import { TRAIT_STANDARDS } from "@secondlayer/stacks/clarity";
26
- import { filterFieldsByType } from "@secondlayer/subgraphs";
29
+ import { camelizeKeys, filterFieldsByType } from "@secondlayer/subgraphs";
27
30
  import { TYPE_MAP } from "@secondlayer/subgraphs/schema";
28
31
 
29
32
  // src/lib/client.ts
@@ -184,7 +187,8 @@ var PRODUCT_BLURBS = {
184
187
  streams: "raw chain event firehose — cursor-paginated reads, tip/canonical/reorg checks, bulk parquet dumps",
185
188
  contracts: "trait-based contract discovery and ABIs",
186
189
  subgraphs: "author/scaffold/deploy/query custom indexes",
187
- subscriptions: "webhook delivery on subgraph rows or raw chain events",
190
+ webhooks: "a signed POST to a URL you run, on subgraph rows or raw chain events",
191
+ subscriptions: "a signed POST to a URL you run, on subgraph rows or raw chain events",
188
192
  account: "identity and self-provisioned API keys",
189
193
  codegen: "ORM schemas for the tables you read",
190
194
  instance: "decoder health and empty-index diagnosis",
@@ -197,6 +201,7 @@ var PRODUCT_ORDER = [
197
201
  "streams",
198
202
  "contracts",
199
203
  "subgraphs",
204
+ "webhooks",
200
205
  "subscriptions",
201
206
  "account",
202
207
  "codegen",
@@ -253,7 +258,8 @@ async function buildContext(deps = { clientProvider: getClient }) {
253
258
  streamsTip: orNull(snap?.streamsTip),
254
259
  indexTip: orNull(snap?.indexTip),
255
260
  subgraphs: snap?.subgraphs.value ? snap.subgraphs.value.map(formatSubgraphSummary) : orNull(snap?.subgraphs),
256
- subscriptions: orNull(snap?.subscriptions),
261
+ webhooks: orNull(snap?.webhooks),
262
+ subscriptions: orNull(snap?.webhooks),
257
263
  activeOperations: orNull(snap?.activeOperations),
258
264
  instance: instance2
259
265
  },
@@ -264,7 +270,7 @@ async function buildContext(deps = { clientProvider: getClient }) {
264
270
  }
265
271
  function registerResources(server) {
266
272
  server.resource("context", "secondlayer://context", {
267
- description: "Live agent context — what exists (your subgraphs, subscriptions, account), what you can do, and read-auth tiers. Read this first."
273
+ description: "Live agent context — what exists (your subgraphs, webhooks, account), what you can do, and read-auth tiers. Read this first."
268
274
  }, async () => ({
269
275
  contents: [
270
276
  {
@@ -309,7 +315,7 @@ function registerResources(server) {
309
315
  ]
310
316
  }));
311
317
  server.resource("chain-triggers", "secondlayer://chain-triggers", {
312
- description: "Chain-subscription trigger types and the filter fields each accepts (for subscriptions_create triggers)."
318
+ description: "Chain-webhook trigger types and the filter fields each accepts (for webhooks_create triggers)."
313
319
  }, async () => ({
314
320
  contents: [
315
321
  {
@@ -319,6 +325,73 @@ function registerResources(server) {
319
325
  }
320
326
  ]
321
327
  }));
328
+ server.resource("samples", new ResourceTemplate("secondlayer://samples/{contractId}", {
329
+ list: undefined
330
+ }), {
331
+ description: "Decoded camelCase print samples for a contract (schema + up to 5 events). Use before writing a print_event handler."
332
+ }, async (uri, variables) => {
333
+ const contractId = String(Array.isArray(variables.contractId) ? variables.contractId[0] : variables.contractId ?? "");
334
+ const body = await buildSamplesResource(contractId);
335
+ return {
336
+ contents: [
337
+ {
338
+ uri: uri.href,
339
+ mimeType: "application/json",
340
+ text: JSON.stringify(body, null, 2)
341
+ }
342
+ ]
343
+ };
344
+ });
345
+ }
346
+ function jsonSafe(value) {
347
+ if (typeof value === "bigint")
348
+ return value.toString();
349
+ if (value === null || value === undefined)
350
+ return value;
351
+ if (Array.isArray(value))
352
+ return value.map(jsonSafe);
353
+ if (typeof value === "object") {
354
+ const out = {};
355
+ for (const [k, v] of Object.entries(value)) {
356
+ out[k] = jsonSafe(v);
357
+ }
358
+ return out;
359
+ }
360
+ return value;
361
+ }
362
+ async function buildSamplesResource(contractId, clientProvider = getClient) {
363
+ const client = clientProvider();
364
+ const printSchema = await client.index.printSchema(contractId);
365
+ const envelope = await client.index.events.list({
366
+ eventType: "print",
367
+ contractId,
368
+ limit: 5
369
+ });
370
+ const schema = printSchema?.topics.map((t) => ({
371
+ topic: t.topic,
372
+ fields: t.fields.map((f) => ({
373
+ camel: f.camel_name,
374
+ snake: f.name.replace(/-/g, "_"),
375
+ column_type: f.column_type,
376
+ always: f.always_present
377
+ }))
378
+ })) ?? [];
379
+ const samples = (envelope.events ?? []).slice(0, 5).map((ev) => {
380
+ const payload = ev.payload;
381
+ const data = jsonSafe(camelizeKeys(payload?.value));
382
+ return {
383
+ txId: ev.tx_id,
384
+ height: ev.block_height,
385
+ topic: payload?.topic ?? "",
386
+ data: data ?? {}
387
+ };
388
+ });
389
+ return {
390
+ contractId,
391
+ handlerShape: "event.data is camelCase; columns are snake_case",
392
+ schema,
393
+ samples
394
+ };
322
395
  }
323
396
 
324
397
  // src/tools/account.ts
@@ -477,7 +550,11 @@ function registerInstanceTools(server, clientProvider = getClient) {
477
550
  }
478
551
 
479
552
  // src/tools/scaffold.ts
480
- import { generateSubgraphCode } from "@secondlayer/scaffold";
553
+ import {
554
+ generatePrintSchemaSubgraph,
555
+ generateSubgraphCode,
556
+ generateTokenSubgraphFromAbi
557
+ } from "@secondlayer/scaffold";
481
558
  import { z as z6 } from "zod";
482
559
  async function fetchAbi(clientProvider, contractId) {
483
560
  const contract = await clientProvider().contracts.get(contractId, {
@@ -489,14 +566,32 @@ async function fetchAbi(clientProvider, contractId) {
489
566
  if (!abi) {
490
567
  throw new Error(`No ABI available for ${contractId} (abi_status: ${contract.abi_status})`);
491
568
  }
492
- return { functions: abi.functions ?? [] };
569
+ return { functions: abi.functions ?? [], abi };
493
570
  }
494
571
  function registerScaffoldTools(server, clientProvider = getClient) {
495
- 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.", {
572
+ 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.", {
496
573
  contractId: z6.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
497
574
  subgraphName: z6.string().optional().describe("Override the subgraph name (defaults to contract name)")
498
575
  }, async ({ contractId, subgraphName }) => {
499
- const { functions } = await fetchAbi(clientProvider, contractId);
576
+ const printSchema = await clientProvider().index.printSchema(contractId);
577
+ if (printSchema && printSchema.topics.length > 0) {
578
+ const code2 = generatePrintSchemaSubgraph({
579
+ contractId,
580
+ name: subgraphName,
581
+ topics: printSchema.topics,
582
+ sample: printSchema.sample
583
+ });
584
+ return { content: [{ type: "text", text: code2 }] };
585
+ }
586
+ const { functions, abi } = await fetchAbi(clientProvider, contractId);
587
+ const token = generateTokenSubgraphFromAbi({
588
+ contractId,
589
+ abi,
590
+ name: subgraphName
591
+ });
592
+ if (token) {
593
+ return { content: [{ type: "text", text: token }] };
594
+ }
500
595
  const code = generateSubgraphCode(contractId, functions, subgraphName);
501
596
  return { content: [{ type: "text", text: code }] };
502
597
  });
@@ -749,14 +844,42 @@ function registerStreamsTools(server, clientProvider = getClient) {
749
844
  }
750
845
 
751
846
  // src/tools/subgraphs.ts
847
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
848
+ import { tmpdir } from "node:os";
849
+ import { join } from "node:path";
850
+ import { pathToFileURL } from "node:url";
752
851
  import { bundleSubgraphCode } from "@secondlayer/bundler";
852
+ import { DECODED_EVENT_TYPES as DECODED_EVENT_TYPES2 } from "@secondlayer/shared";
853
+ import {
854
+ runSubgraphTest
855
+ } from "@secondlayer/subgraphs/testing";
753
856
  import { z as z9 } from "zod";
857
+ function eventTypeFor(filter) {
858
+ if (filter.type === "contract_deploy")
859
+ return null;
860
+ const candidate = filter.type === "print_event" ? "print" : filter.type;
861
+ return DECODED_EVENT_TYPES2.includes(candidate) ? candidate : null;
862
+ }
863
+ function skipReason(name, filter) {
864
+ 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.`;
865
+ }
866
+ async function loadBundledDefinition(handlerCode) {
867
+ const dir = await mkdtemp(join(tmpdir(), "sl-mcp-sg-test-"));
868
+ try {
869
+ const file = join(dir, `handler-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
870
+ await writeFile(file, handlerCode);
871
+ const mod = await import(pathToFileURL(file).href);
872
+ return mod.default ?? {};
873
+ } finally {
874
+ await rm(dir, { recursive: true, force: true });
875
+ }
876
+ }
754
877
  function registerSubgraphTools(server, clientProvider = getClient) {
755
878
  defineTool(server, "subgraphs_list", "List all deployed subgraphs. Returns summary fields only; read rows with subgraphs_query.", {}, async () => {
756
879
  const { data } = await clientProvider().subgraphs.list();
757
880
  return jsonResponse(data.map(formatSubgraphSummary));
758
881
  });
759
- defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health, and table columns.", { name: z9.string().describe("Subgraph name") }, async ({ name }) => {
882
+ 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 }) => {
760
883
  const detail = await clientProvider().subgraphs.status(name);
761
884
  return jsonResponse(detail);
762
885
  });
@@ -852,10 +975,10 @@ function registerSubgraphTools(server, clientProvider = getClient) {
852
975
  const result = await clientProvider().subgraphs.delete(name);
853
976
  return textResponse(result.message);
854
977
  });
855
- 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.", {
978
+ 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.", {
856
979
  code: z9.string().describe("TypeScript source code containing a defineSubgraph() call"),
857
980
  startBlock: z9.number().int().nonnegative().optional().describe("Override the definition startBlock for this deploy"),
858
- dryRun: z9.boolean().optional().describe("Validate and preview the deploy (schema/DDL) without writing changes")
981
+ dryRun: z9.boolean().optional().describe("Validate schema/DDL only — does not run handlers. Use subgraphs_test to prove a mapping writes rows.")
859
982
  }, async ({ code, startBlock, dryRun }) => {
860
983
  const bundled = await bundleSubgraphCode(code);
861
984
  const result = await clientProvider().subgraphs.deploy({
@@ -871,24 +994,201 @@ function registerSubgraphTools(server, clientProvider = getClient) {
871
994
  });
872
995
  return jsonResponse(result);
873
996
  });
997
+ 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.", {
998
+ code: z9.string().describe("TypeScript source containing a defineSubgraph() call"),
999
+ fromHeight: z9.number().int().nonnegative().describe("Start block height (inclusive)"),
1000
+ toHeight: z9.number().int().nonnegative().optional().describe("End block height (inclusive); default fromHeight+100"),
1001
+ limit: z9.number().int().positive().max(200).optional().describe("Max Index rows per source (default 50, max 200)")
1002
+ }, async ({ code, fromHeight, toHeight, limit }) => {
1003
+ const bundled = await bundleSubgraphCode(code);
1004
+ const def = await loadBundledDefinition(bundled.handlerCode);
1005
+ if (!def.handlers || !def.schema) {
1006
+ return jsonResponse({
1007
+ ok: false,
1008
+ code: "NO_SOURCES",
1009
+ matched: 0,
1010
+ written: 0,
1011
+ tables: [],
1012
+ hint: "Bundled module must default-export defineSubgraph() with handlers and schema."
1013
+ }, true);
1014
+ }
1015
+ const sources = bundled.sources ?? {};
1016
+ const to = toHeight ?? fromHeight + 100;
1017
+ const pageLimit = Math.min(limit ?? 50, 200);
1018
+ const index = clientProvider().index;
1019
+ const events = {};
1020
+ const skipped = [];
1021
+ let sourcesTested = 0;
1022
+ for (const [name, filter] of Object.entries(sources)) {
1023
+ if (filter.type === "contract_call") {
1024
+ sourcesTested++;
1025
+ const envelope2 = await index.contractCalls.list({
1026
+ ...filter.contractId ? {
1027
+ contractId: Array.isArray(filter.contractId) ? filter.contractId : filter.contractId
1028
+ } : {},
1029
+ ...filter.functionName ? { functionName: filter.functionName } : {},
1030
+ fromHeight,
1031
+ toHeight: to,
1032
+ limit: pageLimit
1033
+ });
1034
+ events[name] = envelope2.contract_calls;
1035
+ continue;
1036
+ }
1037
+ const eventType = eventTypeFor(filter);
1038
+ if (eventType === null) {
1039
+ skipped.push({ source: name, reason: skipReason(name, filter) });
1040
+ continue;
1041
+ }
1042
+ sourcesTested++;
1043
+ const envelope = await index.events.list({
1044
+ eventType,
1045
+ ...filter.contractId ? {
1046
+ contractId: Array.isArray(filter.contractId) ? filter.contractId : filter.contractId
1047
+ } : {},
1048
+ fromHeight,
1049
+ toHeight: to,
1050
+ limit: pageLimit
1051
+ });
1052
+ events[name] = envelope.events;
1053
+ }
1054
+ if (sourcesTested === 0) {
1055
+ return jsonResponse({
1056
+ ok: false,
1057
+ code: "NO_SOURCES",
1058
+ matched: 0,
1059
+ written: 0,
1060
+ tables: [],
1061
+ hint: "No sources were tested — every source is unreadable from Index.",
1062
+ skipped
1063
+ }, true);
1064
+ }
1065
+ const result = await runSubgraphTest({
1066
+ schema: def.schema,
1067
+ handlers: def.handlers,
1068
+ sources,
1069
+ events
1070
+ });
1071
+ const body = { ...result, skipped };
1072
+ if (skipped.length > 0) {
1073
+ const skipHints = skipped.map((s) => s.reason).join(" ");
1074
+ body.hint = result.hint ? `${result.hint} ${skipHints}` : skipHints;
1075
+ }
1076
+ return jsonResponse(body, !result.ok);
1077
+ });
874
1078
  }
875
1079
 
876
- // src/tools/subscriptions.ts
1080
+ // src/tools/webhooks.ts
877
1081
  import { CHAIN_TRIGGER_TYPES } from "@secondlayer/shared";
878
1082
  import { z as z10 } from "zod";
879
- function registerSubscriptionTools(server, clientProvider = getClient) {
880
- defineTool(server, "subscriptions_list", "List all subscriptions for the current account. Returns summary fields (no secrets).", {}, async () => {
881
- const { data } = await clientProvider().subscriptions.list();
1083
+ function registerWebhookTools(server, clientProvider = getClient) {
1084
+ defineTool(server, "webhooks_list", "List all webhooks for the current account. Returns summary fields (no secrets).", {}, async () => {
1085
+ const { data } = await clientProvider().webhooks.list();
1086
+ return jsonResponse(data);
1087
+ });
1088
+ defineTool(server, "webhooks_get", "Get full detail for a webhook (filter, auth, retry config, circuit state).", { id: z10.string().describe("Webhook id") }, async ({ id }) => {
1089
+ const detail = await clientProvider().webhooks.get(id);
1090
+ return jsonResponse(detail);
1091
+ });
1092
+ 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.", {
1093
+ name: z10.string().describe("Human-readable name, unique per account"),
1094
+ subgraphName: z10.string().optional().describe("Subgraph to subscribe to (subgraph webhook)"),
1095
+ tableName: z10.string().optional().describe("Table within the subgraph (subgraph webhook)"),
1096
+ triggers: z10.array(z10.object({
1097
+ type: z10.enum(CHAIN_TRIGGER_TYPES),
1098
+ contractId: z10.string().optional(),
1099
+ functionName: z10.string().optional(),
1100
+ caller: z10.string().optional(),
1101
+ sender: z10.string().optional(),
1102
+ recipient: z10.string().optional(),
1103
+ assetIdentifier: z10.string().optional(),
1104
+ deployer: z10.string().optional(),
1105
+ contractName: z10.string().optional(),
1106
+ topic: z10.string().optional(),
1107
+ lockedAddress: z10.string().optional(),
1108
+ trait: z10.string().optional(),
1109
+ minAmount: z10.union([z10.string(), z10.number()]).optional(),
1110
+ maxAmount: z10.union([z10.string(), z10.number()]).optional()
1111
+ })).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."),
1112
+ url: z10.string().describe("Webhook URL"),
1113
+ format: z10.enum([
1114
+ "standard-webhooks",
1115
+ "inngest",
1116
+ "trigger",
1117
+ "cloudflare",
1118
+ "cloudevents",
1119
+ "raw"
1120
+ ]).optional().describe("Wire format (default standard-webhooks)"),
1121
+ runtime: z10.enum(["inngest", "trigger", "cloudflare", "node"]).optional().describe("Receiver runtime label (display only)"),
1122
+ filter: z10.record(z10.string(), z10.unknown()).optional().describe('Scalar filter DSL, e.g. {"amount": {"gte": 100}, "sender": "SP..."}'),
1123
+ authConfig: z10.record(z10.string(), z10.unknown()).optional().describe('Receiver auth sent with each delivery, e.g. {"type": "bearer", "token": "..."}')
1124
+ }, async (input) => {
1125
+ const res = await clientProvider().webhooks.create(input);
1126
+ return jsonResponse(res);
1127
+ });
1128
+ defineTool(server, "webhooks_update", "Patch a webhook (name, url, filter, authConfig, format, runtime, retry, timeout, concurrency).", {
1129
+ id: z10.string(),
1130
+ name: z10.string().optional().describe("Rename the webhook"),
1131
+ url: z10.string().optional(),
1132
+ filter: z10.record(z10.string(), z10.unknown()).optional(),
1133
+ authConfig: z10.record(z10.string(), z10.unknown()).optional().describe("Receiver auth sent with each delivery (bearer/etc.)"),
1134
+ format: z10.enum([
1135
+ "standard-webhooks",
1136
+ "inngest",
1137
+ "trigger",
1138
+ "cloudflare",
1139
+ "cloudevents",
1140
+ "raw"
1141
+ ]).optional(),
1142
+ runtime: z10.enum(["inngest", "trigger", "cloudflare", "node"]).nullable().optional(),
1143
+ maxRetries: z10.number().int().min(0).optional(),
1144
+ timeoutMs: z10.number().int().min(100).optional(),
1145
+ concurrency: z10.number().int().min(1).optional()
1146
+ }, async ({ id, ...patch }) => {
1147
+ const res = await clientProvider().webhooks.update(id, patch);
1148
+ return jsonResponse(res);
1149
+ });
1150
+ defineTool(server, "webhooks_delete", "Delete a webhook. Pending outbox rows are cascade-deleted.", { id: z10.string() }, async ({ id }) => {
1151
+ const res = await clientProvider().webhooks.delete(id);
1152
+ return jsonResponse(res);
1153
+ });
1154
+ 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 }) => {
1155
+ const res = await clientProvider().webhooks.test(id);
1156
+ return jsonResponse(res);
1157
+ });
1158
+ 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)));
1159
+ 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)));
1160
+ 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)));
1161
+ 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)));
1162
+ 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)));
1163
+ 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.", {
1164
+ id: z10.string().describe("Webhook id"),
1165
+ outboxId: z10.string().describe("Outbox id of the dead delivery (from webhooks_dead)")
1166
+ }, async ({ id, outboxId }) => jsonResponse(await clientProvider().webhooks.requeue(id, outboxId)));
1167
+ 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.", {
1168
+ id: z10.string(),
1169
+ fromBlock: z10.number().int().nonnegative(),
1170
+ toBlock: z10.number().int().nonnegative(),
1171
+ force: z10.string().optional().describe("Idempotency suffix to force a duplicate replay of the range")
1172
+ }, async ({ id, fromBlock, toBlock, force }) => {
1173
+ const res = await clientProvider().webhooks.replay(id, {
1174
+ fromBlock,
1175
+ toBlock,
1176
+ ...force !== undefined ? { force } : {}
1177
+ });
1178
+ return jsonResponse(res);
1179
+ });
1180
+ 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 () => {
1181
+ const { data } = await clientProvider().webhooks.list();
882
1182
  return jsonResponse(data);
883
1183
  });
884
- defineTool(server, "subscriptions_get", "Get full detail for a subscription (filter, auth, retry config, circuit state).", { id: z10.string().describe("Subscription id") }, async ({ id }) => {
885
- const detail = await clientProvider().subscriptions.get(id);
1184
+ 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 }) => {
1185
+ const detail = await clientProvider().webhooks.get(id);
886
1186
  return jsonResponse(detail);
887
1187
  });
888
- 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.", {
1188
+ 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.", {
889
1189
  name: z10.string().describe("Human-readable name, unique per account"),
890
- subgraphName: z10.string().optional().describe("Subgraph to subscribe to (subgraph subscription)"),
891
- tableName: z10.string().optional().describe("Table within the subgraph (subgraph subscription)"),
1190
+ subgraphName: z10.string().optional().describe("Subgraph to subscribe to (subgraph webhook)"),
1191
+ tableName: z10.string().optional().describe("Table within the subgraph (subgraph webhook)"),
892
1192
  triggers: z10.array(z10.object({
893
1193
  type: z10.enum(CHAIN_TRIGGER_TYPES),
894
1194
  contractId: z10.string().optional(),
@@ -904,7 +1204,7 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
904
1204
  trait: z10.string().optional(),
905
1205
  minAmount: z10.union([z10.string(), z10.number()]).optional(),
906
1206
  maxAmount: z10.union([z10.string(), z10.number()]).optional()
907
- })).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."),
1207
+ })).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."),
908
1208
  url: z10.string().describe("Webhook URL"),
909
1209
  format: z10.enum([
910
1210
  "standard-webhooks",
@@ -918,12 +1218,12 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
918
1218
  filter: z10.record(z10.string(), z10.unknown()).optional().describe('Scalar filter DSL, e.g. {"amount": {"gte": 100}, "sender": "SP..."}'),
919
1219
  authConfig: z10.record(z10.string(), z10.unknown()).optional().describe('Receiver auth sent with each delivery, e.g. {"type": "bearer", "token": "..."}')
920
1220
  }, async (input) => {
921
- const res = await clientProvider().subscriptions.create(input);
1221
+ const res = await clientProvider().webhooks.create(input);
922
1222
  return jsonResponse(res);
923
1223
  });
924
- defineTool(server, "subscriptions_update", "Patch a subscription (name, url, filter, authConfig, format, runtime, retry, timeout, concurrency).", {
1224
+ defineTool(server, "subscriptions_update", "Deprecated alias of webhooks_update. Removed next minor. Patch a webhook (name, url, filter, authConfig, format, runtime, retry, timeout, concurrency).", {
925
1225
  id: z10.string(),
926
- name: z10.string().optional().describe("Rename the subscription"),
1226
+ name: z10.string().optional().describe("Rename the webhook"),
927
1227
  url: z10.string().optional(),
928
1228
  filter: z10.record(z10.string(), z10.unknown()).optional(),
929
1229
  authConfig: z10.record(z10.string(), z10.unknown()).optional().describe("Receiver auth sent with each delivery (bearer/etc.)"),
@@ -940,33 +1240,33 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
940
1240
  timeoutMs: z10.number().int().min(100).optional(),
941
1241
  concurrency: z10.number().int().min(1).optional()
942
1242
  }, async ({ id, ...patch }) => {
943
- const res = await clientProvider().subscriptions.update(id, patch);
1243
+ const res = await clientProvider().webhooks.update(id, patch);
944
1244
  return jsonResponse(res);
945
1245
  });
946
- defineTool(server, "subscriptions_delete", "Delete a subscription. Pending outbox rows are cascade-deleted.", { id: z10.string() }, async ({ id }) => {
947
- const res = await clientProvider().subscriptions.delete(id);
1246
+ 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 }) => {
1247
+ const res = await clientProvider().webhooks.delete(id);
948
1248
  return jsonResponse(res);
949
1249
  });
950
- 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 }) => {
951
- const res = await clientProvider().subscriptions.test(id);
1250
+ 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 }) => {
1251
+ const res = await clientProvider().webhooks.test(id);
952
1252
  return jsonResponse(res);
953
1253
  });
954
- 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)));
955
- 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)));
956
- 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)));
957
- 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)));
958
- 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)));
959
- 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.", {
960
- id: z10.string().describe("Subscription id"),
961
- outboxId: z10.string().describe("Outbox id of the dead delivery (from subscriptions_dead)")
962
- }, async ({ id, outboxId }) => jsonResponse(await clientProvider().subscriptions.requeue(id, outboxId)));
963
- 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.", {
1254
+ 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)));
1255
+ 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)));
1256
+ 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)));
1257
+ 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)));
1258
+ 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)));
1259
+ 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.", {
1260
+ id: z10.string().describe("Webhook id"),
1261
+ outboxId: z10.string().describe("Outbox id of the dead delivery (from webhooks_dead)")
1262
+ }, async ({ id, outboxId }) => jsonResponse(await clientProvider().webhooks.requeue(id, outboxId)));
1263
+ 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.", {
964
1264
  id: z10.string(),
965
1265
  fromBlock: z10.number().int().nonnegative(),
966
1266
  toBlock: z10.number().int().nonnegative(),
967
1267
  force: z10.string().optional().describe("Idempotency suffix to force a duplicate replay of the range")
968
1268
  }, async ({ id, fromBlock, toBlock, force }) => {
969
- const res = await clientProvider().subscriptions.replay(id, {
1269
+ const res = await clientProvider().webhooks.replay(id, {
970
1270
  fromBlock,
971
1271
  toBlock,
972
1272
  ...force !== undefined ? { force } : {}
@@ -977,7 +1277,7 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
977
1277
 
978
1278
  // src/server.ts
979
1279
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
980
- var pkg = JSON.parse(readFileSync(join(__dirname2, "../package.json"), "utf-8"));
1280
+ var pkg = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
981
1281
  function createServer() {
982
1282
  const server = new McpServer({
983
1283
  name: "secondlayer",
@@ -985,7 +1285,7 @@ function createServer() {
985
1285
  });
986
1286
  registerScaffoldTools(server);
987
1287
  registerSubgraphTools(server);
988
- registerSubscriptionTools(server);
1288
+ registerWebhookTools(server);
989
1289
  registerIndexTools(server);
990
1290
  registerStreamsTools(server);
991
1291
  registerContractTools(server);
@@ -1006,5 +1306,5 @@ var server = createServer();
1006
1306
  var transport = new StdioServerTransport;
1007
1307
  await server.connect(transport);
1008
1308
 
1009
- //# debugId=1DDB49B11F03E33264756E2164756E21
1309
+ //# debugId=AA11B5B2A18DEA7664756E2164756E21
1010
1310
  //# sourceMappingURL=bin.js.map