@secondlayer/mcp 6.0.1 → 7.0.1

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/index.js CHANGED
@@ -11,10 +11,10 @@ import { filterFieldsByType } from "@secondlayer/subgraphs";
11
11
  import { TYPE_MAP } from "@secondlayer/subgraphs/schema";
12
12
 
13
13
  // src/lib/client.ts
14
- import { SecondLayer } from "@secondlayer/sdk";
14
+ import { SecondLayer, resolveApiKey } from "@secondlayer/sdk";
15
15
  var instance = null;
16
16
  function readApiKey() {
17
- return process.env.SL_API_KEY;
17
+ return resolveApiKey();
18
18
  }
19
19
  function getClient() {
20
20
  if (!instance) {
@@ -30,17 +30,35 @@ function getClient() {
30
30
  }
31
31
  return instance;
32
32
  }
33
+ var keyHint = " — set INSTANCE_TOKEN from `sl init` for writes (SL_API_KEY is a legacy alias)";
34
+ async function apiRequest(method, path, body) {
35
+ const apiKey = readApiKey();
36
+ const baseUrl = process.env.SECONDLAYER_API_URL || process.env.SL_API_URL || "http://127.0.0.1:3800";
37
+ const res = await fetch(`${baseUrl}${path}`, {
38
+ method,
39
+ headers: {
40
+ "Content-Type": "application/json",
41
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
42
+ },
43
+ body: body ? JSON.stringify(body) : undefined
44
+ });
45
+ if (!res.ok) {
46
+ const text = await res.text().catch(() => "");
47
+ const needsKey = !apiKey && (res.status === 401 || res.status === 403);
48
+ throw Object.assign(new Error((text || `HTTP ${res.status}`) + (needsKey ? keyHint : "")), { status: res.status });
49
+ }
50
+ if (res.status === 204)
51
+ return;
52
+ return res.json();
53
+ }
33
54
 
34
55
  // src/lib/format.ts
35
56
  function formatSubgraphSummary(s) {
36
- const apiUrl = process.env.SECONDLAYER_API_URL || process.env.SL_API_URL || "http://127.0.0.1:3800";
37
57
  return {
38
58
  name: s.name,
39
59
  status: s.status,
40
60
  tables: Array.isArray(s.tables) ? s.tables : Object.keys(s.tables),
41
- lastProcessedBlock: s.lastProcessedBlock,
42
- ...s.visibility ? { visibility: s.visibility } : {},
43
- ...s.visibility === "public" ? { publicUrl: `${apiUrl}/v1/subgraphs/${s.name}` } : {}
61
+ lastProcessedBlock: s.lastProcessedBlock
44
62
  };
45
63
  }
46
64
  function withCap(items, cap) {
@@ -68,7 +86,7 @@ var registeredToolNames = new Set;
68
86
  function getRegisteredToolNames() {
69
87
  return [...registeredToolNames];
70
88
  }
71
- function defineTool(server, name, description, schema, handler, options = {}) {
89
+ function defineTool(server, name, description, schema, handler) {
72
90
  const wrappedHandler = async (args) => {
73
91
  try {
74
92
  return await handler(args);
@@ -89,9 +107,6 @@ function defineTool(server, name, description, schema, handler, options = {}) {
89
107
  };
90
108
  registeredToolNames.add(name);
91
109
  server.tool(name, description, schema, wrappedHandler);
92
- for (const alias of options.aliases ?? []) {
93
- server.tool(alias, `Deprecated alias for \`${name}\`. ${description}`, schema, wrappedHandler);
94
- }
95
110
  }
96
111
 
97
112
  // src/resources.ts
@@ -123,12 +138,11 @@ var COLUMN_TYPES = [
123
138
  ];
124
139
  var PRODUCT_BLURBS = {
125
140
  index: "decoded events, transfers, contract calls, blocks, transactions",
126
- streams: "bulk parquet dumps of the raw chain event firehose",
141
+ streams: "raw chain event firehose — cursor-paginated reads, tip/canonical/reorg checks, bulk parquet dumps",
127
142
  contracts: "trait-based contract discovery and ABIs",
128
- subgraphs: "author/deploy/query custom indexes",
143
+ subgraphs: "author/scaffold/deploy/query custom indexes",
129
144
  subscriptions: "webhook delivery on subgraph rows or raw chain events",
130
145
  account: "identity and self-provisioned API keys",
131
- scaffold: "generate deploy-ready subgraph code from a deployed contract",
132
146
  codegen: "ORM schemas for the tables you read"
133
147
  };
134
148
  var PRODUCT_ORDER = [
@@ -138,7 +152,6 @@ var PRODUCT_ORDER = [
138
152
  "subgraphs",
139
153
  "subscriptions",
140
154
  "account",
141
- "scaffold",
142
155
  "codegen"
143
156
  ];
144
157
  function buildCapabilities() {
@@ -164,17 +177,17 @@ function buildCapabilities() {
164
177
  };
165
178
  }
166
179
  var READ_AUTH_TIERS = {
167
- index: "anonymous reads allowed; free-tier API keys are rejected (Build+ required)",
168
- streams: "API key required (SL_API_KEY) — keyless calls return 401",
169
- subgraphs: "public subgraphs anon-readable at /v1/subgraphs/<name>/<table> ({ rows, next_cursor, tip } cursor envelope); private subgraphs need the owning account's API key (anon → 404); writes require an API key"
180
+ index: "keyless while the API is only reachable on loopback; the instance token is required once it is published past loopback",
181
+ streams: "keyless while the API is only reachable on loopback; the instance token is required once it is published past loopback",
182
+ subgraphs: "reads at /v1/subgraphs/<name>/<table> ({ rows, next_cursor, tip } cursor envelope) follow the same loopback rule as the other read planes; writes always require the instance token"
170
183
  };
171
184
  async function buildContext(deps = { clientProvider: getClient }) {
172
- const unavailable = "unavailable: set SL_API_KEY";
185
+ const unavailable = "unavailable: set INSTANCE_TOKEN";
173
186
  const orNull = (v) => v == null ? unavailable : v;
174
187
  const snap = await deps.clientProvider().context().catch(() => null);
175
188
  return {
176
189
  authState: {
177
- apiKeySet: Boolean(process.env.SL_API_KEY)
190
+ apiKeySet: Boolean(readApiKey())
178
191
  },
179
192
  whatExists: {
180
193
  account: orNull(snap?.account),
@@ -247,18 +260,31 @@ function registerResources(server) {
247
260
  }));
248
261
  }
249
262
 
263
+ // src/tools/account.ts
264
+ import { z } from "zod";
265
+ function registerAccountTools(server) {
266
+ defineTool(server, "account_whoami", "Show the authenticated account's email.", {}, async () => {
267
+ const result = await apiRequest("GET", "/api/accounts/me");
268
+ return jsonResponse(result);
269
+ });
270
+ defineTool(server, "account_create_key", "Mint a scoped streams/index read key. Only the metered archive deployment can do this — a self-hosted instance has no account system and returns 404, so on your own instance use the INSTANCE_TOKEN from `secondlayer init` instead. The returned `key` is shown ONCE.", {
271
+ product: z.enum(["streams", "index"]).optional().describe("Key scope (default streams)"),
272
+ name: z.string().optional().describe("Optional label for the key")
273
+ }, async ({ product, name }) => jsonResponse(await apiRequest("POST", "/api/keys", { product, name })));
274
+ }
275
+
250
276
  // src/tools/codegen.ts
251
277
  import {
252
278
  generateIndexSchema
253
279
  } from "@secondlayer/subgraphs";
254
- import { z } from "zod";
280
+ import { z as z2 } from "zod";
255
281
  var TARGETS = ["prisma", "kysely", "drizzle", "json-schema"];
256
282
  function registerCodegenTools(server) {
257
283
  defineTool(server, "codegen_index_schema", "Generate an ORM schema for the Index tables (blocks, transactions, decoded_events, …) — the same output as `sl codegen index`. Returns the file contents as text; write it wherever your project keeps its schema. Use `tables` to emit only what you read.", {
258
- target: z.enum(TARGETS).optional().describe("Output flavor (default kysely)"),
259
- tables: z.array(z.string()).optional().describe("Restrict to these Index tables (default: all)"),
260
- schemaName: z.string().optional().describe("Postgres schema to qualify table names with"),
261
- datasourceEnv: z.string().optional().describe("Prisma only: datasource url env var (default DATABASE_URL)")
284
+ target: z2.enum(TARGETS).optional().describe("Output flavor (default kysely)"),
285
+ tables: z2.array(z2.string()).optional().describe("Restrict to these Index tables (default: all)"),
286
+ schemaName: z2.string().optional().describe("Postgres schema to qualify table names with"),
287
+ datasourceEnv: z2.string().optional().describe("Prisma only: datasource url env var (default DATABASE_URL)")
262
288
  }, ({ target, tables, schemaName, datasourceEnv }) => textResponse(generateIndexSchema(target ?? "kysely", {
263
289
  tables,
264
290
  schemaName,
@@ -267,17 +293,17 @@ function registerCodegenTools(server) {
267
293
  }
268
294
 
269
295
  // src/tools/contracts.ts
270
- import { z as z2 } from "zod";
296
+ import { z as z3 } from "zod";
271
297
  function registerContractTools(server, clientProvider = getClient) {
272
298
  defineTool(server, "contracts_find", 'Discover deployed Stacks contracts conforming to a trait (e.g. "sip-010", "sip-009", "sip-013"). The discovery endpoint for "which contracts implement X". Reads are public.', {
273
- trait: z2.string().describe('Required. Trait to match (e.g. "sip-010").'),
274
- conformance: z2.enum(["declared", "inferred", "any"]).optional().describe("declared = parsed from source, inferred = ABI shape-match, any = either (default)"),
275
- include: z2.literal("abi").optional().describe(`Set to "abi" to include each contract's full ABI`),
276
- limit: z2.number().optional().describe("Page size, 1–500 (default 100)"),
277
- cursor: z2.string().optional().describe("Opaque cursor from a prior response's next_cursor")
299
+ trait: z3.string().describe('Required. Trait to match (e.g. "sip-010").'),
300
+ conformance: z3.enum(["declared", "inferred", "any"]).optional().describe("declared = parsed from source, inferred = ABI shape-match, any = either (default)"),
301
+ include: z3.literal("abi").optional().describe(`Set to "abi" to include each contract's full ABI`),
302
+ limit: z3.number().optional().describe("Page size, 1–500 (default 100)"),
303
+ cursor: z3.string().optional().describe("Opaque cursor from a prior response's next_cursor")
278
304
  }, async (params) => jsonResponse(await clientProvider().contracts.list(params)));
279
- defineTool(server, "contracts_get_abi", "Fetch a single deployed contract's ABI from the registry (prod-safe). Returns the contract's metadata + full ABI (functions, maps, variables, fungible/non-fungible tokens). Returns not_found if the contract isn't in the registry. Use it to inspect a contract before scaffold_from_contract.", {
280
- contractId: z2.string().describe("Fully qualified contract id (e.g. SP….amm-pool-v2-01)")
305
+ defineTool(server, "contracts_get_abi", "Fetch a single deployed contract's ABI from the registry (prod-safe). Returns the contract's metadata + full ABI (functions, maps, variables, fungible/non-fungible tokens). Returns not_found if the contract isn't in the registry. Use it to inspect a contract before subgraphs_scaffold.", {
306
+ contractId: z3.string().describe("Fully qualified contract id (e.g. SP….amm-pool-v2-01)")
281
307
  }, async ({ contractId }) => {
282
308
  const contract = await clientProvider().contracts.get(contractId, {
283
309
  includeAbi: true
@@ -289,19 +315,19 @@ function registerContractTools(server, clientProvider = getClient) {
289
315
  message: `Contract not in registry: ${contractId}`
290
316
  }
291
317
  }, true);
292
- }, { aliases: ["get_contract_abi"] });
318
+ });
293
319
  }
294
320
 
295
321
  // src/tools/index.ts
296
322
  import { DECODED_EVENT_TYPES } from "@secondlayer/shared";
297
- import { z as z3 } from "zod";
323
+ import { z as z4 } from "zod";
298
324
  var INDEX_EVENT_TYPES = DECODED_EVENT_TYPES;
299
325
  var rangeFilters = {
300
- contractId: z3.string().optional().describe("Filter by contract id"),
301
- fromHeight: z3.number().optional().describe("Start block height (inclusive)"),
302
- toHeight: z3.number().optional().describe("End block height (inclusive)"),
303
- cursor: z3.string().optional().describe("Opaque cursor from a prior response's next_cursor"),
304
- limit: z3.number().optional().describe("Max rows for this page")
326
+ contractId: z4.string().optional().describe("Filter by contract id"),
327
+ fromHeight: z4.number().optional().describe("Start block height (inclusive)"),
328
+ toHeight: z4.number().optional().describe("End block height (inclusive)"),
329
+ cursor: z4.string().optional().describe("Opaque cursor from a prior response's next_cursor"),
330
+ limit: z4.number().optional().describe("Max rows for this page")
305
331
  };
306
332
  var heightFilters = {
307
333
  fromHeight: rangeFilters.fromHeight,
@@ -310,54 +336,54 @@ var heightFilters = {
310
336
  limit: rangeFilters.limit
311
337
  };
312
338
  function registerIndexTools(server, clientProvider = getClient) {
313
- defineTool(server, "index_ft_transfers", "List decoded SIP-010 fungible-token transfers from the Index (decoded layer). Anonymous reads allowed (free-tier API keys are rejected — Build+ required).", {
339
+ defineTool(server, "index_ft_transfers", "List decoded SIP-010 fungible-token transfers from Index. Reads are keyless.", {
314
340
  ...rangeFilters,
315
- sender: z3.string().optional().describe("Filter by sender principal"),
316
- recipient: z3.string().optional().describe("Filter by recipient principal"),
317
- assetIdentifier: z3.string().optional().describe("Filter by asset identifier (contract::asset), e.g. SP…sbtc-token::sbtc-token")
341
+ sender: z4.string().optional().describe("Filter by sender principal"),
342
+ recipient: z4.string().optional().describe("Filter by recipient principal"),
343
+ assetIdentifier: z4.string().optional().describe("Filter by asset identifier (contract::asset), e.g. SP…sbtc-token::sbtc-token")
318
344
  }, async (params) => jsonResponse(await clientProvider().index.ftTransfers.list(params)));
319
- defineTool(server, "index_nft_transfers", "List decoded SIP-009 non-fungible-token transfers from the Index. Anonymous reads allowed (free-tier keys rejected).", {
345
+ defineTool(server, "index_nft_transfers", "List decoded SIP-009 non-fungible-token transfers from Index. Reads are keyless.", {
320
346
  ...rangeFilters,
321
- sender: z3.string().optional().describe("Filter by sender principal"),
322
- recipient: z3.string().optional().describe("Filter by recipient principal"),
323
- assetIdentifier: z3.string().optional().describe("Filter by asset identifier (contract::asset)")
347
+ sender: z4.string().optional().describe("Filter by sender principal"),
348
+ recipient: z4.string().optional().describe("Filter by recipient principal"),
349
+ assetIdentifier: z4.string().optional().describe("Filter by asset identifier (contract::asset)")
324
350
  }, async (params) => jsonResponse(await clientProvider().index.nftTransfers.list(params)));
325
351
  defineTool(server, "index_events", "List decoded chain events from the Index by event type. Use this for event types without a dedicated tool (stx_*, ft_mint/burn, nft_mint/burn, print), and for trait-scoped queries: set `trait` (e.g. sip-010) to match all contracts conforming to a standard — pair with contracts_find to discover traits. For ft/nft transfers without a trait prefer index_ft_transfers / index_nft_transfers.", {
326
- eventType: z3.enum(INDEX_EVENT_TYPES).describe("Required. Decoded event type to list."),
352
+ eventType: z4.enum(INDEX_EVENT_TYPES).describe("Required. Decoded event type to list."),
327
353
  ...rangeFilters,
328
- sender: z3.string().optional().describe("Filter by sender principal"),
329
- recipient: z3.string().optional().describe("Filter by recipient principal"),
330
- assetIdentifier: z3.string().optional().describe("Filter by asset identifier where applicable"),
331
- trait: z3.string().optional().describe("Match contracts conforming to a trait/standard (e.g. sip-010). Mutually exclusive with contractId; contract-keyed event types only."),
332
- txContext: z3.boolean().optional().describe("Join the submitting transaction into each event (tx_sender, tx_type, tx_status, tx_contract_id, tx_function_name). For print events it's the only source of the submitting sender.")
354
+ sender: z4.string().optional().describe("Filter by sender principal"),
355
+ recipient: z4.string().optional().describe("Filter by recipient principal"),
356
+ assetIdentifier: z4.string().optional().describe("Filter by asset identifier where applicable"),
357
+ trait: z4.string().optional().describe("Match contracts conforming to a trait/standard (e.g. sip-010). Mutually exclusive with contractId; contract-keyed event types only."),
358
+ txContext: z4.boolean().optional().describe("Join the submitting transaction into each event (tx_sender, tx_type, tx_status, tx_contract_id, tx_function_name). For print events it's the only source of the submitting sender.")
333
359
  }, async (params) => jsonResponse(await clientProvider().index.events.list(params)));
334
360
  defineTool(server, "index_contract_calls", "List decoded contract calls from the Index (function name, args, result). Set `trait` (e.g. sip-010) to match calls to all contracts conforming to a standard. Note: contract-call cursors are a SEPARATE keyspace from event cursors — they are not interchangeable.", {
335
361
  ...rangeFilters,
336
- functionName: z3.string().optional().describe("Filter by called function name"),
337
- sender: z3.string().optional().describe("Filter by caller principal"),
338
- trait: z3.string().optional().describe("Match contracts conforming to a trait/standard (e.g. sip-010). Mutually exclusive with contractId.")
362
+ functionName: z4.string().optional().describe("Filter by called function name"),
363
+ sender: z4.string().optional().describe("Filter by caller principal"),
364
+ trait: z4.string().optional().describe("Match contracts conforming to a trait/standard (e.g. sip-010). Mutually exclusive with contractId.")
339
365
  }, async (params) => jsonResponse(await clientProvider().index.contractCalls.list(params)));
340
- defineTool(server, "index_blocks", "List decoded blocks from the Index. Anonymous reads allowed (free-tier keys rejected).", { ...heightFilters }, async (params) => jsonResponse(await clientProvider().index.blocks.list(params)));
341
- defineTool(server, "index_transactions", "List decoded transactions from the Index. Filter by type, sender, or contract. Anonymous reads allowed (free-tier keys rejected).", {
366
+ defineTool(server, "index_blocks", "List decoded blocks from Index. Reads are keyless.", { ...heightFilters }, async (params) => jsonResponse(await clientProvider().index.blocks.list(params)));
367
+ defineTool(server, "index_transactions", "List decoded transactions from Index. Filter by type, sender, or contract. Reads are keyless.", {
342
368
  ...rangeFilters,
343
- type: z3.string().optional().describe("Filter by transaction type"),
344
- sender: z3.string().optional().describe("Filter by sender principal")
369
+ type: z4.string().optional().describe("Filter by transaction type"),
370
+ sender: z4.string().optional().describe("Filter by sender principal")
345
371
  }, async (params) => jsonResponse(await clientProvider().index.transactions.list(params)));
346
372
  defineTool(server, "index_print_schema", "Empirical per-topic print payload schemas for a contract, inferred from sampled on-chain events: each topic's fields with observed Clarity types, decoded TS types, subgraph column types, and presence rates. Use before writing print_event subgraph handlers to learn what's on e.data. Anonymous read.", {
347
- contractId: z3.string().describe("Contract id (SP….name) whose print events to profile")
373
+ contractId: z4.string().describe("Contract id (SP….name) whose print events to profile")
348
374
  }, async (params) => jsonResponse(await clientProvider().index.printSchema(params.contractId)));
349
375
  defineTool(server, "index_discover", "Discover the Index vocabulary: every event type and its columns, allowed/equality filters, and required-non-null fields (and which types accept `trait`). Read this before building Index queries instead of guessing filters. Anonymous read.", {}, async () => jsonResponse(await clientProvider().index.discover()));
350
376
  defineTool(server, "batch_query", "Run up to 10 public /v1 reads in one round trip (POST /v1/batch). Each item keeps its own auth/quota/pay-per-call semantics; results return in order with per-item status. Paths must start with /v1/index, /v1/subgraphs, /v1/streams, or /v1/contracts.", {
351
- requests: z3.array(z3.object({
352
- path: z3.string().describe("Public /v1 read path"),
353
- params: z3.record(z3.string(), z3.string()).optional()
377
+ requests: z4.array(z4.object({
378
+ path: z4.string().describe("Public /v1 read path"),
379
+ params: z4.record(z4.string(), z4.string()).optional()
354
380
  })).min(1).max(10).describe("Read descriptors, executed concurrently")
355
381
  }, async (params) => jsonResponse(await clientProvider().batch(params.requests)));
356
382
  }
357
383
 
358
384
  // src/tools/scaffold.ts
359
385
  import { generateSubgraphCode } from "@secondlayer/scaffold";
360
- import { z as z4 } from "zod";
386
+ import { z as z5 } from "zod";
361
387
  async function fetchAbi(clientProvider, contractId) {
362
388
  const contract = await clientProvider().contracts.get(contractId, {
363
389
  includeAbi: true
@@ -371,9 +397,9 @@ async function fetchAbi(clientProvider, contractId) {
371
397
  return { functions: abi.functions ?? [] };
372
398
  }
373
399
  function registerScaffoldTools(server, clientProvider = getClient) {
374
- defineTool(server, "scaffold_from_contract", "Generate a subgraph scaffold from a deployed Stacks contract. Fetches the ABI automatically.", {
375
- contractId: z4.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
376
- subgraphName: z4.string().optional().describe("Override the subgraph name (defaults to contract name)")
400
+ 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.", {
401
+ contractId: z5.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
402
+ subgraphName: z5.string().optional().describe("Override the subgraph name (defaults to contract name)")
377
403
  }, async ({ contractId, subgraphName }) => {
378
404
  const { functions } = await fetchAbi(clientProvider, contractId);
379
405
  const code = generateSubgraphCode(contractId, functions, subgraphName);
@@ -382,32 +408,75 @@ function registerScaffoldTools(server, clientProvider = getClient) {
382
408
  }
383
409
 
384
410
  // src/tools/streams.ts
411
+ import { STREAMS_EVENT_TYPES } from "@secondlayer/shared";
412
+ import { z as z6 } from "zod";
413
+ var filterValue = z6.union([z6.string(), z6.array(z6.string())]);
385
414
  function registerStreamsTools(server, clientProvider = getClient) {
386
- defineTool(server, "streams_dumps", "List the Streams bulk parquet dumps manifest — coverage range, latest_finalized_cursor, and per-file metadata (block range, row count, size, sha256, signed URL). This is the cold backfill path for downloading all raw data; fetch the file URLs directly (e.g. with DuckDB). Requires the dumps base URL to be configured (SL_STREAMS_DUMPS_URL). Live Streams reads (tip, events, consume) are REST-only: see /v1/streams/* in the OpenAPI spec.", {}, async () => jsonResponse(await clientProvider().streams.dumps.list()));
415
+ defineTool(server, "streams_tip", "Current canonical chain tip as Streams sees it: block height/hash, burn height, finalized_height, lag_seconds, and the oldest seekable height/cursor for this key's retention window. Call it before a cursor walk to know where the stream ends, and after an ingest to check lag.", {}, async () => jsonResponse(await clientProvider().streams.tip()));
416
+ defineTool(server, "streams_events", "List raw chain events from Streams — ONE page per call, newest filters applied server-side. Returns { events, next_cursor, tip, reorgs }. To follow the chain live, poll this tool with `cursor` set to the previous response's `next_cursor` (input is exclusive, so no duplicates); there is no open-stream tool because a tool call cannot hold a connection. ALWAYS narrow with types/contractId/sender/recipient — an unfiltered page is the full firehose.", {
417
+ cursor: z6.string().optional().describe("Resume strictly after this cursor (`<block_height>:<event_index>`, e.g. 951475:3) — pass a prior response's next_cursor"),
418
+ fromHeight: z6.number().int().nonnegative().optional().describe("Start block height (inclusive)"),
419
+ toHeight: z6.number().int().nonnegative().optional().describe("End block height (inclusive)"),
420
+ types: z6.array(z6.enum(STREAMS_EVENT_TYPES)).optional().describe("Event types to include"),
421
+ notTypes: z6.array(z6.enum(STREAMS_EVENT_TYPES)).optional().describe("Event types to exclude (applied after types)"),
422
+ contractId: filterValue.optional().describe("Contract id, or a list of contract ids to OR together"),
423
+ sender: filterValue.optional().describe("Sender principal, or a list to OR together"),
424
+ recipient: filterValue.optional().describe("Recipient principal, or a list to OR together"),
425
+ assetIdentifier: z6.string().optional().describe("Asset identifier (contract::asset), e.g. SP…sbtc-token::sbtc-token"),
426
+ limit: z6.number().int().min(1).max(1000).optional().describe("Page size, 1–1000 (default 100)")
427
+ }, async (params) => jsonResponse(await clientProvider().streams.events.list(params)));
428
+ defineTool(server, "streams_events_by_tx", "List every event emitted by a single transaction, in emission order. Use it to explain one transaction end to end after finding it in streams_events or the Index.", {
429
+ txId: z6.string().describe("Transaction id (0x-prefixed)")
430
+ }, async ({ txId }) => jsonResponse(await clientProvider().streams.events.byTxId(txId)));
431
+ defineTool(server, "streams_block_events", "List every event in one block, addressed by block height or block hash. Use it to audit a block a consumer flagged, or to diff what a reorg replaced.", {
432
+ heightOrHash: z6.union([z6.number().int().nonnegative(), z6.string()]).describe("Block height (number) or block hash (0x-prefixed string)")
433
+ }, async ({ heightOrHash }) => jsonResponse(await clientProvider().streams.blocks.events(heightOrHash)));
434
+ defineTool(server, "streams_canonical", "Get the canonical block at a height: hash, burn block hash, parent, and whether the height is still canonical. Use it to confirm a height you already consumed was not reorged away before you trust derived state.", {
435
+ height: z6.number().int().nonnegative().describe("Block height")
436
+ }, async ({ height }) => jsonResponse(await clientProvider().streams.canonical(height)));
437
+ defineTool(server, "streams_reorgs", "List reorgs detected since a point in time, newest last, with the new canonical tip for each. Returns { reorgs, next_since } — resume with next_since. Call it when a consumer's derived state disagrees with streams_canonical, or on a schedule to know which heights to reprocess.", {
438
+ since: z6.string().describe("Required. ISO 8601 timestamp, or a `next_since` resume token from a prior response"),
439
+ limit: z6.number().int().positive().optional().describe("Max reorgs to return")
440
+ }, async ({ since, limit }) => jsonResponse(await clientProvider().streams.reorgs.list({
441
+ since,
442
+ ...limit !== undefined ? { limit } : {}
443
+ })));
444
+ defineTool(server, "streams_dumps", "List the Streams bulk parquet dumps manifest — coverage range, latest_finalized_cursor, and per-file metadata (block range, row count, size, sha256, signed URL). This is the cold backfill path for downloading all raw data; fetch the file URLs directly (e.g. with DuckDB). Requires the dumps base URL to be configured (SL_STREAMS_DUMPS_URL). For live reads use streams_events (cursor-paginated) and streams_tip.", {}, async () => jsonResponse(await clientProvider().streams.dumps.list()));
387
445
  }
388
446
 
389
447
  // src/tools/subgraphs.ts
390
448
  import { bundleSubgraphCode } from "@secondlayer/bundler";
391
- import { z as z5 } from "zod";
449
+ import { z as z7 } from "zod";
392
450
  function registerSubgraphTools(server, clientProvider = getClient) {
393
- defineTool(server, "subgraphs_list", "List all deployed subgraphs. Returns summary fields only, including visibility — public subgraphs are anon-readable at /v1/subgraphs/<name>.", {}, async () => {
451
+ defineTool(server, "subgraphs_list", "List all deployed subgraphs. Returns summary fields only; read rows with subgraphs_query.", {}, async () => {
394
452
  const { data } = await clientProvider().subgraphs.list();
395
453
  return jsonResponse(data.map(formatSubgraphSummary));
396
454
  });
397
- defineTool(server, "subgraphs_get", "Get full details of a subgraph including schema, health, and table columns.", { name: z5.string().describe("Subgraph name") }, async ({ name }) => {
398
- const detail = await clientProvider().subgraphs.get(name);
455
+ defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health, and table columns.", { name: z7.string().describe("Subgraph name") }, async ({ name }) => {
456
+ const detail = await clientProvider().subgraphs.status(name);
399
457
  return jsonResponse(detail);
400
458
  });
401
- defineTool(server, "subgraphs_query", 'Query rows from a subgraph table (max 200 rows). Filters support operators: "amount.gte": "1000", "sender.neq": "SP...", "name.like": "%token%". Available operators: eq, neq, gt, gte, lt, lte, like. To TAIL new rows (no streaming over MCP): sort=_id, order=desc for the latest, then poll forward with the filter {"_id.gt": "<last _id seen>"}, order=asc — when tailing with a `fields` list, include "_id" in it or the next poll filter cannot be formed. Fetch one row by id with {"_id": "<id>"}. Public subgraphs are also keyless over HTTP at GET /v1/subgraphs/<name>/<table> — { rows, next_cursor, tip } envelope, resume with ?cursor=<next_cursor> + _order=asc|desc (no _offset/_sort on /v1); hand that URL to third parties.', {
402
- name: z5.string().describe("Subgraph name"),
403
- table: z5.string().describe("Table name"),
404
- filters: z5.record(z5.string(), z5.string()).optional().describe('Column filters — plain values or with operators (e.g. {"amount.gte": "1000", "sender": "SP..."})'),
405
- sort: z5.string().optional().describe("Column to sort by"),
406
- order: z5.enum(["asc", "desc"]).optional().describe("Sort order"),
407
- limit: z5.number().max(200).optional().describe("Max rows (default 50, max 200)"),
408
- offset: z5.number().optional().describe("Offset for pagination"),
409
- fields: z5.string().optional().describe('Comma-separated column list to return (e.g. "sender,amount")'),
410
- count: z5.boolean().optional().describe("If true, return row count instead of rows")
459
+ defineTool(server, "subgraphs_spec", "Fetch a subgraph's self-describing spec — the tables, columns, filters, and read URLs a consumer needs, without guessing. `agent` (default) returns the compact JSON schema built for tool use, `openapi` the OpenAPI document for the subgraph's read routes, `markdown` human-readable docs. Read it before writing queries against an unfamiliar subgraph.", {
460
+ name: z7.string().describe("Subgraph name"),
461
+ format: z7.enum(["agent", "openapi", "markdown"]).optional().describe("agent = JSON schema for tool use (default), openapi = OpenAPI JSON, markdown = docs.md")
462
+ }, async ({ name, format }) => {
463
+ const client = clientProvider();
464
+ if (format === "openapi")
465
+ return jsonResponse(await client.subgraphs.openapi(name));
466
+ if (format === "markdown")
467
+ return textResponse(await client.subgraphs.markdown(name));
468
+ return jsonResponse(await client.subgraphs.schema(name));
469
+ });
470
+ defineTool(server, "subgraphs_query", 'Query rows from a subgraph table (max 200 rows). Filters support operators: "amount.gte": "1000", "sender.neq": "SP...", "name.like": "%token%". Available operators: eq, neq, gt, gte, lt, lte, like. To TAIL new rows (no streaming over MCP): sort=_id, order=desc for the latest, then poll forward with the filter {"_id.gt": "<last _id seen>"}, order=asc — when tailing with a `fields` list, include "_id" in it or the next poll filter cannot be formed. Fetch one row by id with {"_id": "<id>"}. The same rows are readable over HTTP at GET /v1/subgraphs/<name>/<table> — { rows, next_cursor, tip } envelope, resume with ?cursor=<next_cursor> + _order=asc|desc (no _offset/_sort on /v1); hand that URL to third parties.', {
471
+ name: z7.string().describe("Subgraph name"),
472
+ table: z7.string().describe("Table name"),
473
+ filters: z7.record(z7.string(), z7.string()).optional().describe('Column filters — plain values or with operators (e.g. {"amount.gte": "1000", "sender": "SP..."})'),
474
+ sort: z7.string().optional().describe("Column to sort by"),
475
+ order: z7.enum(["asc", "desc"]).optional().describe("Sort order"),
476
+ limit: z7.number().max(200).optional().describe("Max rows (default 50, max 200)"),
477
+ offset: z7.number().optional().describe("Offset for pagination"),
478
+ fields: z7.string().optional().describe('Comma-separated column list to return (e.g. "sender,amount")'),
479
+ count: z7.boolean().optional().describe("If true, return row count instead of rows")
411
480
  }, async ({
412
481
  name,
413
482
  table,
@@ -434,16 +503,16 @@ function registerSubgraphTools(server, clientProvider = getClient) {
434
503
  const result = withCap(rows, Math.min(limit ?? 50, 200));
435
504
  return jsonResponse(result);
436
505
  });
437
- defineTool(server, "subgraphs_reindex", "Reindex a subgraph: drops and rebuilds the whole subgraph from its start block to chain tip. Takes no block range — use subgraphs_backfill to process a specific range. Returns an operationId — check subgraphs_get (health) or the REST operations endpoint to track progress to completion.", {
438
- name: z5.string().describe("Subgraph name")
506
+ defineTool(server, "subgraphs_reindex", "Reindex a subgraph: drops and rebuilds the whole subgraph from its start block to chain tip. Takes no block range — use subgraphs_backfill to process a specific range. Returns an operationId — check subgraphs_status (health) or the REST operations endpoint to track progress to completion.", {
507
+ name: z7.string().describe("Subgraph name")
439
508
  }, async ({ name }) => {
440
509
  const result = await clientProvider().subgraphs.reindex(name);
441
510
  return jsonResponse(result);
442
511
  });
443
- defineTool(server, "subgraphs_backfill", "Backfill a subgraph over a block range. Non-destructive forward fill (does not drop existing data) — unlike subgraphs_reindex, and the only data-fill path for BYO subgraphs (reindex is blocked there). Both blocks required. Returns an operationId — check subgraphs_get (health) or the REST operations endpoint to track progress.", {
444
- name: z5.string().describe("Subgraph name"),
445
- fromBlock: z5.number().int().nonnegative().describe("Start block (inclusive)"),
446
- toBlock: z5.number().int().nonnegative().describe("End block (inclusive)")
512
+ defineTool(server, "subgraphs_backfill", "Backfill a subgraph over a block range. Non-destructive forward fill (does not drop existing data) — unlike subgraphs_reindex, and the only data-fill path for BYO subgraphs (reindex is blocked there). Both blocks required. Returns an operationId — check subgraphs_status (health) or the REST operations endpoint to track progress.", {
513
+ name: z7.string().describe("Subgraph name"),
514
+ fromBlock: z7.number().int().nonnegative().describe("Start block (inclusive)"),
515
+ toBlock: z7.number().int().nonnegative().describe("End block (inclusive)")
447
516
  }, async ({ name, fromBlock, toBlock }) => {
448
517
  const result = await clientProvider().subgraphs.backfill(name, {
449
518
  fromBlock,
@@ -451,15 +520,15 @@ function registerSubgraphTools(server, clientProvider = getClient) {
451
520
  });
452
521
  return jsonResponse(result);
453
522
  });
454
- defineTool(server, "subgraphs_stop", "Cancel an in-flight reindex or backfill operation for a subgraph. Returns the stop request status; check subgraphs_get (health) or the REST operations endpoint to confirm it reaches a terminal state.", { name: z5.string().describe("Subgraph name") }, async ({ name }) => {
523
+ defineTool(server, "subgraphs_stop", "Cancel an in-flight reindex or backfill operation for a subgraph. Returns the stop request status; check subgraphs_status (health) or the REST operations endpoint to confirm it reaches a terminal state.", { name: z7.string().describe("Subgraph name") }, async ({ name }) => {
455
524
  const result = await clientProvider().subgraphs.stop(name);
456
525
  return jsonResponse(result);
457
526
  });
458
527
  defineTool(server, "subgraphs_gaps", "List indexing gaps (missing block ranges) for a subgraph. Each gap reports start/end/size, reason, and detected/resolved timestamps. Feed an unresolved gap's range into subgraphs_backfill to fill it. Defaults to unresolved gaps.", {
459
- name: z5.string().describe("Subgraph name"),
460
- limit: z5.number().int().positive().optional().describe("Max gaps to return"),
461
- offset: z5.number().int().nonnegative().optional().describe("Pagination offset"),
462
- resolved: z5.boolean().optional().describe("Filter by resolved state (omit for unresolved only)")
528
+ name: z7.string().describe("Subgraph name"),
529
+ limit: z7.number().int().positive().optional().describe("Max gaps to return"),
530
+ offset: z7.number().int().nonnegative().optional().describe("Pagination offset"),
531
+ resolved: z7.boolean().optional().describe("Filter by resolved state (omit for unresolved only)")
463
532
  }, async ({ name, limit, offset, resolved }) => {
464
533
  const result = await clientProvider().subgraphs.gaps(name, {
465
534
  limit,
@@ -468,14 +537,21 @@ function registerSubgraphTools(server, clientProvider = getClient) {
468
537
  });
469
538
  return jsonResponse(result);
470
539
  });
471
- defineTool(server, "subgraphs_delete", "Delete a subgraph permanently.", { name: z5.string().describe("Subgraph name") }, async ({ name }) => {
540
+ defineTool(server, "subgraphs_operations", "Operation history for a subgraph (deploy/reindex/backfill/stop), newest first — status, block range, progress, and error. THIS IS THE VERIFY CALL: after subgraphs_deploy, subgraphs_reindex, subgraphs_backfill, or subgraphs_stop, poll it with the returned operationId until `status` is terminal (completed/failed/cancelled) before reporting success. Omit operationId for the recent history.", {
541
+ name: z7.string().describe("Subgraph name"),
542
+ operationId: z7.string().optional().describe("Fetch one operation by id (the operationId returned by reindex/backfill/stop); omit for recent history")
543
+ }, async ({ name, operationId }) => {
544
+ const client = clientProvider();
545
+ return jsonResponse(operationId ? await client.subgraphs.getOperation(name, operationId) : await client.subgraphs.operations(name));
546
+ });
547
+ defineTool(server, "subgraphs_delete", "Delete a subgraph permanently.", { name: z7.string().describe("Subgraph name") }, async ({ name }) => {
472
548
  const result = await clientProvider().subgraphs.delete(name);
473
549
  return textResponse(result.message);
474
550
  });
475
551
  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.", {
476
- code: z5.string().describe("TypeScript source code containing a defineSubgraph() call"),
477
- startBlock: z5.number().int().nonnegative().optional().describe("Override the definition startBlock for this deploy"),
478
- dryRun: z5.boolean().optional().describe("Validate and preview the deploy (schema/DDL) without writing changes")
552
+ code: z7.string().describe("TypeScript source code containing a defineSubgraph() call"),
553
+ startBlock: z7.number().int().nonnegative().optional().describe("Override the definition startBlock for this deploy"),
554
+ dryRun: z7.boolean().optional().describe("Validate and preview the deploy (schema/DDL) without writing changes")
479
555
  }, async ({ code, startBlock, dryRun }) => {
480
556
  const bundled = await bundleSubgraphCode(code);
481
557
  const result = await clientProvider().subgraphs.deploy({
@@ -495,38 +571,38 @@ function registerSubgraphTools(server, clientProvider = getClient) {
495
571
 
496
572
  // src/tools/subscriptions.ts
497
573
  import { CHAIN_TRIGGER_TYPES } from "@secondlayer/shared";
498
- import { z as z6 } from "zod";
574
+ import { z as z8 } from "zod";
499
575
  function registerSubscriptionTools(server, clientProvider = getClient) {
500
576
  defineTool(server, "subscriptions_list", "List all subscriptions for the current account. Returns summary fields (no secrets).", {}, async () => {
501
577
  const { data } = await clientProvider().subscriptions.list();
502
578
  return jsonResponse(data);
503
579
  });
504
- defineTool(server, "subscriptions_get", "Get full detail for a subscription (filter, auth, retry config, circuit state).", { id: z6.string().describe("Subscription id") }, async ({ id }) => {
580
+ defineTool(server, "subscriptions_get", "Get full detail for a subscription (filter, auth, retry config, circuit state).", { id: z8.string().describe("Subscription id") }, async ({ id }) => {
505
581
  const detail = await clientProvider().subscriptions.get(id);
506
582
  return jsonResponse(detail);
507
583
  });
508
584
  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.", {
509
- name: z6.string().describe("Human-readable name, unique per account"),
510
- subgraphName: z6.string().optional().describe("Subgraph to subscribe to (subgraph subscription)"),
511
- tableName: z6.string().optional().describe("Table within the subgraph (subgraph subscription)"),
512
- triggers: z6.array(z6.object({
513
- type: z6.enum(CHAIN_TRIGGER_TYPES),
514
- contractId: z6.string().optional(),
515
- functionName: z6.string().optional(),
516
- caller: z6.string().optional(),
517
- sender: z6.string().optional(),
518
- recipient: z6.string().optional(),
519
- assetIdentifier: z6.string().optional(),
520
- deployer: z6.string().optional(),
521
- contractName: z6.string().optional(),
522
- topic: z6.string().optional(),
523
- lockedAddress: z6.string().optional(),
524
- trait: z6.string().optional(),
525
- minAmount: z6.union([z6.string(), z6.number()]).optional(),
526
- maxAmount: z6.union([z6.string(), z6.number()]).optional()
585
+ name: z8.string().describe("Human-readable name, unique per account"),
586
+ subgraphName: z8.string().optional().describe("Subgraph to subscribe to (subgraph subscription)"),
587
+ tableName: z8.string().optional().describe("Table within the subgraph (subgraph subscription)"),
588
+ triggers: z8.array(z8.object({
589
+ type: z8.enum(CHAIN_TRIGGER_TYPES),
590
+ contractId: z8.string().optional(),
591
+ functionName: z8.string().optional(),
592
+ caller: z8.string().optional(),
593
+ sender: z8.string().optional(),
594
+ recipient: z8.string().optional(),
595
+ assetIdentifier: z8.string().optional(),
596
+ deployer: z8.string().optional(),
597
+ contractName: z8.string().optional(),
598
+ topic: z8.string().optional(),
599
+ lockedAddress: z8.string().optional(),
600
+ trait: z8.string().optional(),
601
+ minAmount: z8.union([z8.string(), z8.number()]).optional(),
602
+ maxAmount: z8.union([z8.string(), z8.number()]).optional()
527
603
  })).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."),
528
- url: z6.string().describe("Webhook URL"),
529
- format: z6.enum([
604
+ url: z8.string().describe("Webhook URL"),
605
+ format: z8.enum([
530
606
  "standard-webhooks",
531
607
  "inngest",
532
608
  "trigger",
@@ -534,20 +610,20 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
534
610
  "cloudevents",
535
611
  "raw"
536
612
  ]).optional().describe("Wire format (default standard-webhooks)"),
537
- runtime: z6.enum(["inngest", "trigger", "cloudflare", "node"]).optional().describe("Receiver runtime label (display only)"),
538
- filter: z6.record(z6.string(), z6.unknown()).optional().describe('Scalar filter DSL, e.g. {"amount": {"gte": 100}, "sender": "SP..."}'),
539
- authConfig: z6.record(z6.string(), z6.unknown()).optional().describe('Receiver auth sent with each delivery, e.g. {"type": "bearer", "token": "..."}')
613
+ runtime: z8.enum(["inngest", "trigger", "cloudflare", "node"]).optional().describe("Receiver runtime label (display only)"),
614
+ filter: z8.record(z8.string(), z8.unknown()).optional().describe('Scalar filter DSL, e.g. {"amount": {"gte": 100}, "sender": "SP..."}'),
615
+ authConfig: z8.record(z8.string(), z8.unknown()).optional().describe('Receiver auth sent with each delivery, e.g. {"type": "bearer", "token": "..."}')
540
616
  }, async (input) => {
541
617
  const res = await clientProvider().subscriptions.create(input);
542
618
  return jsonResponse(res);
543
619
  });
544
620
  defineTool(server, "subscriptions_update", "Patch a subscription (name, url, filter, authConfig, format, runtime, retry, timeout, concurrency).", {
545
- id: z6.string(),
546
- name: z6.string().optional().describe("Rename the subscription"),
547
- url: z6.string().optional(),
548
- filter: z6.record(z6.string(), z6.unknown()).optional(),
549
- authConfig: z6.record(z6.string(), z6.unknown()).optional().describe("Receiver auth sent with each delivery (bearer/etc.)"),
550
- format: z6.enum([
621
+ id: z8.string(),
622
+ name: z8.string().optional().describe("Rename the subscription"),
623
+ url: z8.string().optional(),
624
+ filter: z8.record(z8.string(), z8.unknown()).optional(),
625
+ authConfig: z8.record(z8.string(), z8.unknown()).optional().describe("Receiver auth sent with each delivery (bearer/etc.)"),
626
+ format: z8.enum([
551
627
  "standard-webhooks",
552
628
  "inngest",
553
629
  "trigger",
@@ -555,27 +631,36 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
555
631
  "cloudevents",
556
632
  "raw"
557
633
  ]).optional(),
558
- runtime: z6.enum(["inngest", "trigger", "cloudflare", "node"]).nullable().optional(),
559
- maxRetries: z6.number().int().min(0).optional(),
560
- timeoutMs: z6.number().int().min(100).optional(),
561
- concurrency: z6.number().int().min(1).optional()
634
+ runtime: z8.enum(["inngest", "trigger", "cloudflare", "node"]).nullable().optional(),
635
+ maxRetries: z8.number().int().min(0).optional(),
636
+ timeoutMs: z8.number().int().min(100).optional(),
637
+ concurrency: z8.number().int().min(1).optional()
562
638
  }, async ({ id, ...patch }) => {
563
639
  const res = await clientProvider().subscriptions.update(id, patch);
564
640
  return jsonResponse(res);
565
641
  });
566
- defineTool(server, "subscriptions_delete", "Delete a subscription. Pending outbox rows are cascade-deleted.", { id: z6.string() }, async ({ id }) => {
642
+ defineTool(server, "subscriptions_delete", "Delete a subscription. Pending outbox rows are cascade-deleted.", { id: z8.string() }, async ({ id }) => {
567
643
  const res = await clientProvider().subscriptions.delete(id);
568
644
  return jsonResponse(res);
569
645
  });
570
- 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: z6.string().describe("Subscription id") }, async ({ id }) => {
646
+ 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: z8.string().describe("Subscription id") }, async ({ id }) => {
571
647
  const res = await clientProvider().subscriptions.test(id);
572
648
  return jsonResponse(res);
573
649
  });
650
+ 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: z8.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.pause(id)));
651
+ 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: z8.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.resume(id)));
652
+ 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: z8.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.rotateSecret(id)));
653
+ 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: z8.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.deliveries(id)));
654
+ 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: z8.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.dead(id)));
655
+ 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.", {
656
+ id: z8.string().describe("Subscription id"),
657
+ outboxId: z8.string().describe("Outbox id of the dead delivery (from subscriptions_dead)")
658
+ }, async ({ id, outboxId }) => jsonResponse(await clientProvider().subscriptions.requeue(id, outboxId)));
574
659
  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.", {
575
- id: z6.string(),
576
- fromBlock: z6.number().int().nonnegative(),
577
- toBlock: z6.number().int().nonnegative(),
578
- force: z6.string().optional().describe("Idempotency suffix to force a duplicate replay of the range")
660
+ id: z8.string(),
661
+ fromBlock: z8.number().int().nonnegative(),
662
+ toBlock: z8.number().int().nonnegative(),
663
+ force: z8.string().optional().describe("Idempotency suffix to force a duplicate replay of the range")
579
664
  }, async ({ id, fromBlock, toBlock, force }) => {
580
665
  const res = await clientProvider().subscriptions.replay(id, {
581
666
  fromBlock,
@@ -601,6 +686,7 @@ function createServer() {
601
686
  registerStreamsTools(server);
602
687
  registerContractTools(server);
603
688
  registerCodegenTools(server);
689
+ registerAccountTools(server);
604
690
  registerResources(server);
605
691
  return server;
606
692
  }
@@ -608,5 +694,5 @@ export {
608
694
  createServer
609
695
  };
610
696
 
611
- //# debugId=2983B9D0681592E564756E2164756E21
697
+ //# debugId=3CC8D4B48D678B3964756E2164756E21
612
698
  //# sourceMappingURL=index.js.map