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