@secondlayer/mcp 6.0.0 → 7.0.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
@@ -16,10 +16,10 @@ import { filterFieldsByType } from "@secondlayer/subgraphs";
16
16
  import { TYPE_MAP } from "@secondlayer/subgraphs/schema";
17
17
 
18
18
  // src/lib/client.ts
19
- import { SecondLayer } from "@secondlayer/sdk";
19
+ import { SecondLayer, resolveApiKey } from "@secondlayer/sdk";
20
20
  var instance = null;
21
21
  function readApiKey() {
22
- return process.env.SL_API_KEY;
22
+ return resolveApiKey();
23
23
  }
24
24
  function getClient() {
25
25
  if (!instance) {
@@ -35,17 +35,35 @@ function getClient() {
35
35
  }
36
36
  return instance;
37
37
  }
38
+ var keyHint = " — set INSTANCE_TOKEN from `sl init` for writes (SL_API_KEY is a legacy alias)";
39
+ async function apiRequest(method, path, body) {
40
+ const apiKey = readApiKey();
41
+ const baseUrl = process.env.SECONDLAYER_API_URL || process.env.SL_API_URL || "http://127.0.0.1:3800";
42
+ const res = await fetch(`${baseUrl}${path}`, {
43
+ method,
44
+ headers: {
45
+ "Content-Type": "application/json",
46
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
47
+ },
48
+ body: body ? JSON.stringify(body) : undefined
49
+ });
50
+ if (!res.ok) {
51
+ const text = await res.text().catch(() => "");
52
+ const needsKey = !apiKey && (res.status === 401 || res.status === 403);
53
+ throw Object.assign(new Error((text || `HTTP ${res.status}`) + (needsKey ? keyHint : "")), { status: res.status });
54
+ }
55
+ if (res.status === 204)
56
+ return;
57
+ return res.json();
58
+ }
38
59
 
39
60
  // src/lib/format.ts
40
61
  function formatSubgraphSummary(s) {
41
- const apiUrl = process.env.SECONDLAYER_API_URL || process.env.SL_API_URL || "http://127.0.0.1:3800";
42
62
  return {
43
63
  name: s.name,
44
64
  status: s.status,
45
65
  tables: Array.isArray(s.tables) ? s.tables : Object.keys(s.tables),
46
- lastProcessedBlock: s.lastProcessedBlock,
47
- ...s.visibility ? { visibility: s.visibility } : {},
48
- ...s.visibility === "public" ? { publicUrl: `${apiUrl}/v1/subgraphs/${s.name}` } : {}
66
+ lastProcessedBlock: s.lastProcessedBlock
49
67
  };
50
68
  }
51
69
  function withCap(items, cap) {
@@ -73,7 +91,7 @@ var registeredToolNames = new Set;
73
91
  function getRegisteredToolNames() {
74
92
  return [...registeredToolNames];
75
93
  }
76
- function defineTool(server, name, description, schema, handler, options = {}) {
94
+ function defineTool(server, name, description, schema, handler) {
77
95
  const wrappedHandler = async (args) => {
78
96
  try {
79
97
  return await handler(args);
@@ -94,9 +112,6 @@ function defineTool(server, name, description, schema, handler, options = {}) {
94
112
  };
95
113
  registeredToolNames.add(name);
96
114
  server.tool(name, description, schema, wrappedHandler);
97
- for (const alias of options.aliases ?? []) {
98
- server.tool(alias, `Deprecated alias for \`${name}\`. ${description}`, schema, wrappedHandler);
99
- }
100
115
  }
101
116
 
102
117
  // src/resources.ts
@@ -128,12 +143,11 @@ var COLUMN_TYPES = [
128
143
  ];
129
144
  var PRODUCT_BLURBS = {
130
145
  index: "decoded events, transfers, contract calls, blocks, transactions",
131
- streams: "bulk parquet dumps of the raw chain event firehose",
146
+ streams: "raw chain event firehose — cursor-paginated reads, tip/canonical/reorg checks, bulk parquet dumps",
132
147
  contracts: "trait-based contract discovery and ABIs",
133
- subgraphs: "author/deploy/query custom indexes",
148
+ subgraphs: "author/scaffold/deploy/query custom indexes",
134
149
  subscriptions: "webhook delivery on subgraph rows or raw chain events",
135
150
  account: "identity and self-provisioned API keys",
136
- scaffold: "generate deploy-ready subgraph code from a deployed contract",
137
151
  codegen: "ORM schemas for the tables you read"
138
152
  };
139
153
  var PRODUCT_ORDER = [
@@ -143,7 +157,6 @@ var PRODUCT_ORDER = [
143
157
  "subgraphs",
144
158
  "subscriptions",
145
159
  "account",
146
- "scaffold",
147
160
  "codegen"
148
161
  ];
149
162
  function buildCapabilities() {
@@ -169,17 +182,17 @@ function buildCapabilities() {
169
182
  };
170
183
  }
171
184
  var READ_AUTH_TIERS = {
172
- index: "anonymous reads allowed; free-tier API keys are rejected (Build+ required)",
173
- streams: "API key required (SL_API_KEY) — keyless calls return 401",
174
- 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"
185
+ index: "keyless while the API is only reachable on loopback; the instance token is required once it is published past loopback",
186
+ streams: "keyless while the API is only reachable on loopback; the instance token is required once it is published past loopback",
187
+ 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"
175
188
  };
176
189
  async function buildContext(deps = { clientProvider: getClient }) {
177
- const unavailable = "unavailable: set SL_API_KEY";
190
+ const unavailable = "unavailable: set INSTANCE_TOKEN";
178
191
  const orNull = (v) => v == null ? unavailable : v;
179
192
  const snap = await deps.clientProvider().context().catch(() => null);
180
193
  return {
181
194
  authState: {
182
- apiKeySet: Boolean(process.env.SL_API_KEY)
195
+ apiKeySet: Boolean(readApiKey())
183
196
  },
184
197
  whatExists: {
185
198
  account: orNull(snap?.account),
@@ -252,18 +265,31 @@ function registerResources(server) {
252
265
  }));
253
266
  }
254
267
 
268
+ // src/tools/account.ts
269
+ import { z } from "zod";
270
+ function registerAccountTools(server) {
271
+ defineTool(server, "account_whoami", "Show the authenticated account's email.", {}, async () => {
272
+ const result = await apiRequest("GET", "/api/accounts/me");
273
+ return jsonResponse(result);
274
+ });
275
+ 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.", {
276
+ product: z.enum(["streams", "index"]).optional().describe("Key scope (default streams)"),
277
+ name: z.string().optional().describe("Optional label for the key")
278
+ }, async ({ product, name }) => jsonResponse(await apiRequest("POST", "/api/keys", { product, name })));
279
+ }
280
+
255
281
  // src/tools/codegen.ts
256
282
  import {
257
283
  generateIndexSchema
258
284
  } from "@secondlayer/subgraphs";
259
- import { z } from "zod";
285
+ import { z as z2 } from "zod";
260
286
  var TARGETS = ["prisma", "kysely", "drizzle", "json-schema"];
261
287
  function registerCodegenTools(server) {
262
288
  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.", {
263
- target: z.enum(TARGETS).optional().describe("Output flavor (default kysely)"),
264
- tables: z.array(z.string()).optional().describe("Restrict to these Index tables (default: all)"),
265
- schemaName: z.string().optional().describe("Postgres schema to qualify table names with"),
266
- datasourceEnv: z.string().optional().describe("Prisma only: datasource url env var (default DATABASE_URL)")
289
+ target: z2.enum(TARGETS).optional().describe("Output flavor (default kysely)"),
290
+ tables: z2.array(z2.string()).optional().describe("Restrict to these Index tables (default: all)"),
291
+ schemaName: z2.string().optional().describe("Postgres schema to qualify table names with"),
292
+ datasourceEnv: z2.string().optional().describe("Prisma only: datasource url env var (default DATABASE_URL)")
267
293
  }, ({ target, tables, schemaName, datasourceEnv }) => textResponse(generateIndexSchema(target ?? "kysely", {
268
294
  tables,
269
295
  schemaName,
@@ -272,17 +298,17 @@ function registerCodegenTools(server) {
272
298
  }
273
299
 
274
300
  // src/tools/contracts.ts
275
- import { z as z2 } from "zod";
301
+ import { z as z3 } from "zod";
276
302
  function registerContractTools(server, clientProvider = getClient) {
277
303
  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.', {
278
- trait: z2.string().describe('Required. Trait to match (e.g. "sip-010").'),
279
- conformance: z2.enum(["declared", "inferred", "any"]).optional().describe("declared = parsed from source, inferred = ABI shape-match, any = either (default)"),
280
- include: z2.literal("abi").optional().describe(`Set to "abi" to include each contract's full ABI`),
281
- limit: z2.number().optional().describe("Page size, 1–500 (default 100)"),
282
- cursor: z2.string().optional().describe("Opaque cursor from a prior response's next_cursor")
304
+ trait: z3.string().describe('Required. Trait to match (e.g. "sip-010").'),
305
+ conformance: z3.enum(["declared", "inferred", "any"]).optional().describe("declared = parsed from source, inferred = ABI shape-match, any = either (default)"),
306
+ include: z3.literal("abi").optional().describe(`Set to "abi" to include each contract's full ABI`),
307
+ limit: z3.number().optional().describe("Page size, 1–500 (default 100)"),
308
+ cursor: z3.string().optional().describe("Opaque cursor from a prior response's next_cursor")
283
309
  }, async (params) => jsonResponse(await clientProvider().contracts.list(params)));
284
- 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.", {
285
- contractId: z2.string().describe("Fully qualified contract id (e.g. SP….amm-pool-v2-01)")
310
+ 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.", {
311
+ contractId: z3.string().describe("Fully qualified contract id (e.g. SP….amm-pool-v2-01)")
286
312
  }, async ({ contractId }) => {
287
313
  const contract = await clientProvider().contracts.get(contractId, {
288
314
  includeAbi: true
@@ -294,19 +320,19 @@ function registerContractTools(server, clientProvider = getClient) {
294
320
  message: `Contract not in registry: ${contractId}`
295
321
  }
296
322
  }, true);
297
- }, { aliases: ["get_contract_abi"] });
323
+ });
298
324
  }
299
325
 
300
326
  // src/tools/index.ts
301
327
  import { DECODED_EVENT_TYPES } from "@secondlayer/shared";
302
- import { z as z3 } from "zod";
328
+ import { z as z4 } from "zod";
303
329
  var INDEX_EVENT_TYPES = DECODED_EVENT_TYPES;
304
330
  var rangeFilters = {
305
- contractId: z3.string().optional().describe("Filter by contract id"),
306
- fromHeight: z3.number().optional().describe("Start block height (inclusive)"),
307
- toHeight: z3.number().optional().describe("End block height (inclusive)"),
308
- cursor: z3.string().optional().describe("Opaque cursor from a prior response's next_cursor"),
309
- limit: z3.number().optional().describe("Max rows for this page")
331
+ contractId: z4.string().optional().describe("Filter by contract id"),
332
+ fromHeight: z4.number().optional().describe("Start block height (inclusive)"),
333
+ toHeight: z4.number().optional().describe("End block height (inclusive)"),
334
+ cursor: z4.string().optional().describe("Opaque cursor from a prior response's next_cursor"),
335
+ limit: z4.number().optional().describe("Max rows for this page")
310
336
  };
311
337
  var heightFilters = {
312
338
  fromHeight: rangeFilters.fromHeight,
@@ -317,52 +343,52 @@ var heightFilters = {
317
343
  function registerIndexTools(server, clientProvider = getClient) {
318
344
  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).", {
319
345
  ...rangeFilters,
320
- sender: z3.string().optional().describe("Filter by sender principal"),
321
- recipient: z3.string().optional().describe("Filter by recipient principal"),
322
- assetIdentifier: z3.string().optional().describe("Filter by asset identifier (contract::asset), e.g. SP…sbtc-token::sbtc-token")
346
+ sender: z4.string().optional().describe("Filter by sender principal"),
347
+ recipient: z4.string().optional().describe("Filter by recipient principal"),
348
+ assetIdentifier: z4.string().optional().describe("Filter by asset identifier (contract::asset), e.g. SP…sbtc-token::sbtc-token")
323
349
  }, async (params) => jsonResponse(await clientProvider().index.ftTransfers.list(params)));
324
350
  defineTool(server, "index_nft_transfers", "List decoded SIP-009 non-fungible-token transfers from the Index. Anonymous reads allowed (free-tier keys rejected).", {
325
351
  ...rangeFilters,
326
- sender: z3.string().optional().describe("Filter by sender principal"),
327
- recipient: z3.string().optional().describe("Filter by recipient principal"),
328
- assetIdentifier: z3.string().optional().describe("Filter by asset identifier (contract::asset)")
352
+ sender: z4.string().optional().describe("Filter by sender principal"),
353
+ recipient: z4.string().optional().describe("Filter by recipient principal"),
354
+ assetIdentifier: z4.string().optional().describe("Filter by asset identifier (contract::asset)")
329
355
  }, async (params) => jsonResponse(await clientProvider().index.nftTransfers.list(params)));
330
356
  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.", {
331
- eventType: z3.enum(INDEX_EVENT_TYPES).describe("Required. Decoded event type to list."),
357
+ eventType: z4.enum(INDEX_EVENT_TYPES).describe("Required. Decoded event type to list."),
332
358
  ...rangeFilters,
333
- sender: z3.string().optional().describe("Filter by sender principal"),
334
- recipient: z3.string().optional().describe("Filter by recipient principal"),
335
- assetIdentifier: z3.string().optional().describe("Filter by asset identifier where applicable"),
336
- 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."),
337
- 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.")
359
+ sender: z4.string().optional().describe("Filter by sender principal"),
360
+ recipient: z4.string().optional().describe("Filter by recipient principal"),
361
+ assetIdentifier: z4.string().optional().describe("Filter by asset identifier where applicable"),
362
+ 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."),
363
+ 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.")
338
364
  }, async (params) => jsonResponse(await clientProvider().index.events.list(params)));
339
365
  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.", {
340
366
  ...rangeFilters,
341
- functionName: z3.string().optional().describe("Filter by called function name"),
342
- sender: z3.string().optional().describe("Filter by caller principal"),
343
- trait: z3.string().optional().describe("Match contracts conforming to a trait/standard (e.g. sip-010). Mutually exclusive with contractId.")
367
+ functionName: z4.string().optional().describe("Filter by called function name"),
368
+ sender: z4.string().optional().describe("Filter by caller principal"),
369
+ trait: z4.string().optional().describe("Match contracts conforming to a trait/standard (e.g. sip-010). Mutually exclusive with contractId.")
344
370
  }, async (params) => jsonResponse(await clientProvider().index.contractCalls.list(params)));
345
371
  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)));
346
372
  defineTool(server, "index_transactions", "List decoded transactions from the Index. Filter by type, sender, or contract. Anonymous reads allowed (free-tier keys rejected).", {
347
373
  ...rangeFilters,
348
- type: z3.string().optional().describe("Filter by transaction type"),
349
- sender: z3.string().optional().describe("Filter by sender principal")
374
+ type: z4.string().optional().describe("Filter by transaction type"),
375
+ sender: z4.string().optional().describe("Filter by sender principal")
350
376
  }, async (params) => jsonResponse(await clientProvider().index.transactions.list(params)));
351
377
  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.", {
352
- contractId: z3.string().describe("Contract id (SP….name) whose print events to profile")
378
+ contractId: z4.string().describe("Contract id (SP….name) whose print events to profile")
353
379
  }, async (params) => jsonResponse(await clientProvider().index.printSchema(params.contractId)));
354
380
  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()));
355
381
  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.", {
356
- requests: z3.array(z3.object({
357
- path: z3.string().describe("Public /v1 read path"),
358
- params: z3.record(z3.string(), z3.string()).optional()
382
+ requests: z4.array(z4.object({
383
+ path: z4.string().describe("Public /v1 read path"),
384
+ params: z4.record(z4.string(), z4.string()).optional()
359
385
  })).min(1).max(10).describe("Read descriptors, executed concurrently")
360
386
  }, async (params) => jsonResponse(await clientProvider().batch(params.requests)));
361
387
  }
362
388
 
363
389
  // src/tools/scaffold.ts
364
390
  import { generateSubgraphCode } from "@secondlayer/scaffold";
365
- import { z as z4 } from "zod";
391
+ import { z as z5 } from "zod";
366
392
  async function fetchAbi(clientProvider, contractId) {
367
393
  const contract = await clientProvider().contracts.get(contractId, {
368
394
  includeAbi: true
@@ -376,9 +402,9 @@ async function fetchAbi(clientProvider, contractId) {
376
402
  return { functions: abi.functions ?? [] };
377
403
  }
378
404
  function registerScaffoldTools(server, clientProvider = getClient) {
379
- defineTool(server, "scaffold_from_contract", "Generate a subgraph scaffold from a deployed Stacks contract. Fetches the ABI automatically.", {
380
- contractId: z4.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
381
- subgraphName: z4.string().optional().describe("Override the subgraph name (defaults to contract name)")
405
+ 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.", {
406
+ contractId: z5.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
407
+ subgraphName: z5.string().optional().describe("Override the subgraph name (defaults to contract name)")
382
408
  }, async ({ contractId, subgraphName }) => {
383
409
  const { functions } = await fetchAbi(clientProvider, contractId);
384
410
  const code = generateSubgraphCode(contractId, functions, subgraphName);
@@ -387,32 +413,75 @@ function registerScaffoldTools(server, clientProvider = getClient) {
387
413
  }
388
414
 
389
415
  // src/tools/streams.ts
416
+ import { STREAMS_EVENT_TYPES } from "@secondlayer/shared";
417
+ import { z as z6 } from "zod";
418
+ var filterValue = z6.union([z6.string(), z6.array(z6.string())]);
390
419
  function registerStreamsTools(server, clientProvider = getClient) {
391
- 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()));
420
+ 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()));
421
+ 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.", {
422
+ 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"),
423
+ fromHeight: z6.number().int().nonnegative().optional().describe("Start block height (inclusive)"),
424
+ toHeight: z6.number().int().nonnegative().optional().describe("End block height (inclusive)"),
425
+ types: z6.array(z6.enum(STREAMS_EVENT_TYPES)).optional().describe("Event types to include"),
426
+ notTypes: z6.array(z6.enum(STREAMS_EVENT_TYPES)).optional().describe("Event types to exclude (applied after types)"),
427
+ contractId: filterValue.optional().describe("Contract id, or a list of contract ids to OR together"),
428
+ sender: filterValue.optional().describe("Sender principal, or a list to OR together"),
429
+ recipient: filterValue.optional().describe("Recipient principal, or a list to OR together"),
430
+ assetIdentifier: z6.string().optional().describe("Asset identifier (contract::asset), e.g. SP…sbtc-token::sbtc-token"),
431
+ limit: z6.number().int().min(1).max(1000).optional().describe("Page size, 1–1000 (default 100)")
432
+ }, async (params) => jsonResponse(await clientProvider().streams.events.list(params)));
433
+ 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.", {
434
+ txId: z6.string().describe("Transaction id (0x-prefixed)")
435
+ }, async ({ txId }) => jsonResponse(await clientProvider().streams.events.byTxId(txId)));
436
+ 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.", {
437
+ heightOrHash: z6.union([z6.number().int().nonnegative(), z6.string()]).describe("Block height (number) or block hash (0x-prefixed string)")
438
+ }, async ({ heightOrHash }) => jsonResponse(await clientProvider().streams.blocks.events(heightOrHash)));
439
+ 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.", {
440
+ height: z6.number().int().nonnegative().describe("Block height")
441
+ }, async ({ height }) => jsonResponse(await clientProvider().streams.canonical(height)));
442
+ 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.", {
443
+ since: z6.string().describe("Required. ISO 8601 timestamp, or a `next_since` resume token from a prior response"),
444
+ limit: z6.number().int().positive().optional().describe("Max reorgs to return")
445
+ }, async ({ since, limit }) => jsonResponse(await clientProvider().streams.reorgs.list({
446
+ since,
447
+ ...limit !== undefined ? { limit } : {}
448
+ })));
449
+ 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()));
392
450
  }
393
451
 
394
452
  // src/tools/subgraphs.ts
395
453
  import { bundleSubgraphCode } from "@secondlayer/bundler";
396
- import { z as z5 } from "zod";
454
+ import { z as z7 } from "zod";
397
455
  function registerSubgraphTools(server, clientProvider = getClient) {
398
- defineTool(server, "subgraphs_list", "List all deployed subgraphs. Returns summary fields only, including visibility — public subgraphs are anon-readable at /v1/subgraphs/<name>.", {}, async () => {
456
+ defineTool(server, "subgraphs_list", "List all deployed subgraphs. Returns summary fields only; read rows with subgraphs_query.", {}, async () => {
399
457
  const { data } = await clientProvider().subgraphs.list();
400
458
  return jsonResponse(data.map(formatSubgraphSummary));
401
459
  });
402
- defineTool(server, "subgraphs_get", "Get full details of a subgraph including schema, health, and table columns.", { name: z5.string().describe("Subgraph name") }, async ({ name }) => {
403
- const detail = await clientProvider().subgraphs.get(name);
460
+ defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health, and table columns.", { name: z7.string().describe("Subgraph name") }, async ({ name }) => {
461
+ const detail = await clientProvider().subgraphs.status(name);
404
462
  return jsonResponse(detail);
405
463
  });
406
- 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.', {
407
- name: z5.string().describe("Subgraph name"),
408
- table: z5.string().describe("Table name"),
409
- filters: z5.record(z5.string(), z5.string()).optional().describe('Column filters — plain values or with operators (e.g. {"amount.gte": "1000", "sender": "SP..."})'),
410
- sort: z5.string().optional().describe("Column to sort by"),
411
- order: z5.enum(["asc", "desc"]).optional().describe("Sort order"),
412
- limit: z5.number().max(200).optional().describe("Max rows (default 50, max 200)"),
413
- offset: z5.number().optional().describe("Offset for pagination"),
414
- fields: z5.string().optional().describe('Comma-separated column list to return (e.g. "sender,amount")'),
415
- count: z5.boolean().optional().describe("If true, return row count instead of rows")
464
+ 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.", {
465
+ name: z7.string().describe("Subgraph name"),
466
+ format: z7.enum(["agent", "openapi", "markdown"]).optional().describe("agent = JSON schema for tool use (default), openapi = OpenAPI JSON, markdown = docs.md")
467
+ }, async ({ name, format }) => {
468
+ const client = clientProvider();
469
+ if (format === "openapi")
470
+ return jsonResponse(await client.subgraphs.openapi(name));
471
+ if (format === "markdown")
472
+ return textResponse(await client.subgraphs.markdown(name));
473
+ return jsonResponse(await client.subgraphs.schema(name));
474
+ });
475
+ 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.', {
476
+ name: z7.string().describe("Subgraph name"),
477
+ table: z7.string().describe("Table name"),
478
+ filters: z7.record(z7.string(), z7.string()).optional().describe('Column filters — plain values or with operators (e.g. {"amount.gte": "1000", "sender": "SP..."})'),
479
+ sort: z7.string().optional().describe("Column to sort by"),
480
+ order: z7.enum(["asc", "desc"]).optional().describe("Sort order"),
481
+ limit: z7.number().max(200).optional().describe("Max rows (default 50, max 200)"),
482
+ offset: z7.number().optional().describe("Offset for pagination"),
483
+ fields: z7.string().optional().describe('Comma-separated column list to return (e.g. "sender,amount")'),
484
+ count: z7.boolean().optional().describe("If true, return row count instead of rows")
416
485
  }, async ({
417
486
  name,
418
487
  table,
@@ -439,16 +508,16 @@ function registerSubgraphTools(server, clientProvider = getClient) {
439
508
  const result = withCap(rows, Math.min(limit ?? 50, 200));
440
509
  return jsonResponse(result);
441
510
  });
442
- 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.", {
443
- name: z5.string().describe("Subgraph name")
511
+ 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.", {
512
+ name: z7.string().describe("Subgraph name")
444
513
  }, async ({ name }) => {
445
514
  const result = await clientProvider().subgraphs.reindex(name);
446
515
  return jsonResponse(result);
447
516
  });
448
- 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.", {
449
- name: z5.string().describe("Subgraph name"),
450
- fromBlock: z5.number().int().nonnegative().describe("Start block (inclusive)"),
451
- toBlock: z5.number().int().nonnegative().describe("End block (inclusive)")
517
+ 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.", {
518
+ name: z7.string().describe("Subgraph name"),
519
+ fromBlock: z7.number().int().nonnegative().describe("Start block (inclusive)"),
520
+ toBlock: z7.number().int().nonnegative().describe("End block (inclusive)")
452
521
  }, async ({ name, fromBlock, toBlock }) => {
453
522
  const result = await clientProvider().subgraphs.backfill(name, {
454
523
  fromBlock,
@@ -456,15 +525,15 @@ function registerSubgraphTools(server, clientProvider = getClient) {
456
525
  });
457
526
  return jsonResponse(result);
458
527
  });
459
- 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 }) => {
528
+ 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 }) => {
460
529
  const result = await clientProvider().subgraphs.stop(name);
461
530
  return jsonResponse(result);
462
531
  });
463
532
  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.", {
464
- name: z5.string().describe("Subgraph name"),
465
- limit: z5.number().int().positive().optional().describe("Max gaps to return"),
466
- offset: z5.number().int().nonnegative().optional().describe("Pagination offset"),
467
- resolved: z5.boolean().optional().describe("Filter by resolved state (omit for unresolved only)")
533
+ name: z7.string().describe("Subgraph name"),
534
+ limit: z7.number().int().positive().optional().describe("Max gaps to return"),
535
+ offset: z7.number().int().nonnegative().optional().describe("Pagination offset"),
536
+ resolved: z7.boolean().optional().describe("Filter by resolved state (omit for unresolved only)")
468
537
  }, async ({ name, limit, offset, resolved }) => {
469
538
  const result = await clientProvider().subgraphs.gaps(name, {
470
539
  limit,
@@ -473,14 +542,21 @@ function registerSubgraphTools(server, clientProvider = getClient) {
473
542
  });
474
543
  return jsonResponse(result);
475
544
  });
476
- defineTool(server, "subgraphs_delete", "Delete a subgraph permanently.", { name: z5.string().describe("Subgraph name") }, async ({ name }) => {
545
+ 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.", {
546
+ name: z7.string().describe("Subgraph name"),
547
+ operationId: z7.string().optional().describe("Fetch one operation by id (the operationId returned by reindex/backfill/stop); omit for recent history")
548
+ }, async ({ name, operationId }) => {
549
+ const client = clientProvider();
550
+ return jsonResponse(operationId ? await client.subgraphs.getOperation(name, operationId) : await client.subgraphs.operations(name));
551
+ });
552
+ defineTool(server, "subgraphs_delete", "Delete a subgraph permanently.", { name: z7.string().describe("Subgraph name") }, async ({ name }) => {
477
553
  const result = await clientProvider().subgraphs.delete(name);
478
554
  return textResponse(result.message);
479
555
  });
480
556
  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.", {
481
- code: z5.string().describe("TypeScript source code containing a defineSubgraph() call"),
482
- startBlock: z5.number().int().nonnegative().optional().describe("Override the definition startBlock for this deploy"),
483
- dryRun: z5.boolean().optional().describe("Validate and preview the deploy (schema/DDL) without writing changes")
557
+ code: z7.string().describe("TypeScript source code containing a defineSubgraph() call"),
558
+ startBlock: z7.number().int().nonnegative().optional().describe("Override the definition startBlock for this deploy"),
559
+ dryRun: z7.boolean().optional().describe("Validate and preview the deploy (schema/DDL) without writing changes")
484
560
  }, async ({ code, startBlock, dryRun }) => {
485
561
  const bundled = await bundleSubgraphCode(code);
486
562
  const result = await clientProvider().subgraphs.deploy({
@@ -500,38 +576,38 @@ function registerSubgraphTools(server, clientProvider = getClient) {
500
576
 
501
577
  // src/tools/subscriptions.ts
502
578
  import { CHAIN_TRIGGER_TYPES } from "@secondlayer/shared";
503
- import { z as z6 } from "zod";
579
+ import { z as z8 } from "zod";
504
580
  function registerSubscriptionTools(server, clientProvider = getClient) {
505
581
  defineTool(server, "subscriptions_list", "List all subscriptions for the current account. Returns summary fields (no secrets).", {}, async () => {
506
582
  const { data } = await clientProvider().subscriptions.list();
507
583
  return jsonResponse(data);
508
584
  });
509
- defineTool(server, "subscriptions_get", "Get full detail for a subscription (filter, auth, retry config, circuit state).", { id: z6.string().describe("Subscription id") }, async ({ id }) => {
585
+ defineTool(server, "subscriptions_get", "Get full detail for a subscription (filter, auth, retry config, circuit state).", { id: z8.string().describe("Subscription id") }, async ({ id }) => {
510
586
  const detail = await clientProvider().subscriptions.get(id);
511
587
  return jsonResponse(detail);
512
588
  });
513
589
  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.", {
514
- name: z6.string().describe("Human-readable name, unique per account"),
515
- subgraphName: z6.string().optional().describe("Subgraph to subscribe to (subgraph subscription)"),
516
- tableName: z6.string().optional().describe("Table within the subgraph (subgraph subscription)"),
517
- triggers: z6.array(z6.object({
518
- type: z6.enum(CHAIN_TRIGGER_TYPES),
519
- contractId: z6.string().optional(),
520
- functionName: z6.string().optional(),
521
- caller: z6.string().optional(),
522
- sender: z6.string().optional(),
523
- recipient: z6.string().optional(),
524
- assetIdentifier: z6.string().optional(),
525
- deployer: z6.string().optional(),
526
- contractName: z6.string().optional(),
527
- topic: z6.string().optional(),
528
- lockedAddress: z6.string().optional(),
529
- trait: z6.string().optional(),
530
- minAmount: z6.union([z6.string(), z6.number()]).optional(),
531
- maxAmount: z6.union([z6.string(), z6.number()]).optional()
590
+ name: z8.string().describe("Human-readable name, unique per account"),
591
+ subgraphName: z8.string().optional().describe("Subgraph to subscribe to (subgraph subscription)"),
592
+ tableName: z8.string().optional().describe("Table within the subgraph (subgraph subscription)"),
593
+ triggers: z8.array(z8.object({
594
+ type: z8.enum(CHAIN_TRIGGER_TYPES),
595
+ contractId: z8.string().optional(),
596
+ functionName: z8.string().optional(),
597
+ caller: z8.string().optional(),
598
+ sender: z8.string().optional(),
599
+ recipient: z8.string().optional(),
600
+ assetIdentifier: z8.string().optional(),
601
+ deployer: z8.string().optional(),
602
+ contractName: z8.string().optional(),
603
+ topic: z8.string().optional(),
604
+ lockedAddress: z8.string().optional(),
605
+ trait: z8.string().optional(),
606
+ minAmount: z8.union([z8.string(), z8.number()]).optional(),
607
+ maxAmount: z8.union([z8.string(), z8.number()]).optional()
532
608
  })).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."),
533
- url: z6.string().describe("Webhook URL"),
534
- format: z6.enum([
609
+ url: z8.string().describe("Webhook URL"),
610
+ format: z8.enum([
535
611
  "standard-webhooks",
536
612
  "inngest",
537
613
  "trigger",
@@ -539,20 +615,20 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
539
615
  "cloudevents",
540
616
  "raw"
541
617
  ]).optional().describe("Wire format (default standard-webhooks)"),
542
- runtime: z6.enum(["inngest", "trigger", "cloudflare", "node"]).optional().describe("Receiver runtime label (display only)"),
543
- filter: z6.record(z6.string(), z6.unknown()).optional().describe('Scalar filter DSL, e.g. {"amount": {"gte": 100}, "sender": "SP..."}'),
544
- authConfig: z6.record(z6.string(), z6.unknown()).optional().describe('Receiver auth sent with each delivery, e.g. {"type": "bearer", "token": "..."}')
618
+ runtime: z8.enum(["inngest", "trigger", "cloudflare", "node"]).optional().describe("Receiver runtime label (display only)"),
619
+ filter: z8.record(z8.string(), z8.unknown()).optional().describe('Scalar filter DSL, e.g. {"amount": {"gte": 100}, "sender": "SP..."}'),
620
+ authConfig: z8.record(z8.string(), z8.unknown()).optional().describe('Receiver auth sent with each delivery, e.g. {"type": "bearer", "token": "..."}')
545
621
  }, async (input) => {
546
622
  const res = await clientProvider().subscriptions.create(input);
547
623
  return jsonResponse(res);
548
624
  });
549
625
  defineTool(server, "subscriptions_update", "Patch a subscription (name, url, filter, authConfig, format, runtime, retry, timeout, concurrency).", {
550
- id: z6.string(),
551
- name: z6.string().optional().describe("Rename the subscription"),
552
- url: z6.string().optional(),
553
- filter: z6.record(z6.string(), z6.unknown()).optional(),
554
- authConfig: z6.record(z6.string(), z6.unknown()).optional().describe("Receiver auth sent with each delivery (bearer/etc.)"),
555
- format: z6.enum([
626
+ id: z8.string(),
627
+ name: z8.string().optional().describe("Rename the subscription"),
628
+ url: z8.string().optional(),
629
+ filter: z8.record(z8.string(), z8.unknown()).optional(),
630
+ authConfig: z8.record(z8.string(), z8.unknown()).optional().describe("Receiver auth sent with each delivery (bearer/etc.)"),
631
+ format: z8.enum([
556
632
  "standard-webhooks",
557
633
  "inngest",
558
634
  "trigger",
@@ -560,27 +636,36 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
560
636
  "cloudevents",
561
637
  "raw"
562
638
  ]).optional(),
563
- runtime: z6.enum(["inngest", "trigger", "cloudflare", "node"]).nullable().optional(),
564
- maxRetries: z6.number().int().min(0).optional(),
565
- timeoutMs: z6.number().int().min(100).optional(),
566
- concurrency: z6.number().int().min(1).optional()
639
+ runtime: z8.enum(["inngest", "trigger", "cloudflare", "node"]).nullable().optional(),
640
+ maxRetries: z8.number().int().min(0).optional(),
641
+ timeoutMs: z8.number().int().min(100).optional(),
642
+ concurrency: z8.number().int().min(1).optional()
567
643
  }, async ({ id, ...patch }) => {
568
644
  const res = await clientProvider().subscriptions.update(id, patch);
569
645
  return jsonResponse(res);
570
646
  });
571
- defineTool(server, "subscriptions_delete", "Delete a subscription. Pending outbox rows are cascade-deleted.", { id: z6.string() }, async ({ id }) => {
647
+ defineTool(server, "subscriptions_delete", "Delete a subscription. Pending outbox rows are cascade-deleted.", { id: z8.string() }, async ({ id }) => {
572
648
  const res = await clientProvider().subscriptions.delete(id);
573
649
  return jsonResponse(res);
574
650
  });
575
- 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 }) => {
651
+ 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 }) => {
576
652
  const res = await clientProvider().subscriptions.test(id);
577
653
  return jsonResponse(res);
578
654
  });
655
+ 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)));
656
+ 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)));
657
+ 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)));
658
+ 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)));
659
+ 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)));
660
+ 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.", {
661
+ id: z8.string().describe("Subscription id"),
662
+ outboxId: z8.string().describe("Outbox id of the dead delivery (from subscriptions_dead)")
663
+ }, async ({ id, outboxId }) => jsonResponse(await clientProvider().subscriptions.requeue(id, outboxId)));
579
664
  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.", {
580
- id: z6.string(),
581
- fromBlock: z6.number().int().nonnegative(),
582
- toBlock: z6.number().int().nonnegative(),
583
- force: z6.string().optional().describe("Idempotency suffix to force a duplicate replay of the range")
665
+ id: z8.string(),
666
+ fromBlock: z8.number().int().nonnegative(),
667
+ toBlock: z8.number().int().nonnegative(),
668
+ force: z8.string().optional().describe("Idempotency suffix to force a duplicate replay of the range")
584
669
  }, async ({ id, fromBlock, toBlock, force }) => {
585
670
  const res = await clientProvider().subscriptions.replay(id, {
586
671
  fromBlock,
@@ -606,6 +691,7 @@ function createServer() {
606
691
  registerStreamsTools(server);
607
692
  registerContractTools(server);
608
693
  registerCodegenTools(server);
694
+ registerAccountTools(server);
609
695
  registerResources(server);
610
696
  return server;
611
697
  }
@@ -615,5 +701,5 @@ var server = createServer();
615
701
  var transport = new StdioServerTransport;
616
702
  await server.connect(transport);
617
703
 
618
- //# debugId=47C86EEF1415CCE464756E2164756E21
704
+ //# debugId=08F9439BE660189C64756E2164756E21
619
705
  //# sourceMappingURL=bin.js.map