@secondlayer/mcp 7.2.1 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -9
- package/dist/bin-http.js +428 -141
- package/dist/bin-http.js.map +10 -6
- package/dist/bin.js +428 -141
- package/dist/bin.js.map +10 -6
- package/dist/index.js +428 -141
- package/dist/index.js.map +10 -6
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -16,13 +16,20 @@ function isHostedArchiveUrl(url) {
|
|
|
16
16
|
|
|
17
17
|
// src/resources.ts
|
|
18
18
|
import { CHAIN_TRIGGER_FIELDS } from "@secondlayer/shared";
|
|
19
|
+
import { BOOTSTRAP_STEP } from "@secondlayer/shared/archive/instance-diagnosis";
|
|
19
20
|
import { TRAIT_STANDARDS } from "@secondlayer/stacks/clarity";
|
|
20
21
|
import { filterFieldsByType } from "@secondlayer/subgraphs";
|
|
21
22
|
import { TYPE_MAP } from "@secondlayer/subgraphs/schema";
|
|
22
23
|
|
|
23
24
|
// src/lib/client.ts
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
SecondLayer,
|
|
27
|
+
resolveAccountKey,
|
|
28
|
+
resolveApiKey
|
|
29
|
+
} from "@secondlayer/sdk";
|
|
25
30
|
var instance = null;
|
|
31
|
+
var HOSTED_KEY_HINT = "set SECONDLAYER_API_KEY (sk-sl_*) for credits; INSTANCE_TOKEN is the instance";
|
|
32
|
+
var DEFAULT_ARCHIVE_OPS_URL = "https://api.secondlayer.tools";
|
|
26
33
|
function readApiKey() {
|
|
27
34
|
return resolveApiKey();
|
|
28
35
|
}
|
|
@@ -40,10 +47,31 @@ function getClient() {
|
|
|
40
47
|
}
|
|
41
48
|
return instance;
|
|
42
49
|
}
|
|
43
|
-
|
|
50
|
+
function readArchiveApiKey() {
|
|
51
|
+
return resolveAccountKey();
|
|
52
|
+
}
|
|
53
|
+
function getArchiveOpsClient() {
|
|
54
|
+
const accountKey = readArchiveApiKey();
|
|
55
|
+
if (!accountKey) {
|
|
56
|
+
throw new Error(HOSTED_KEY_HINT);
|
|
57
|
+
}
|
|
58
|
+
const archiveOpsUrl = process.env.SL_CREDITS_API_URL || process.env.ARCHIVE_OPS_API_URL || DEFAULT_ARCHIVE_OPS_URL;
|
|
59
|
+
return new SecondLayer({
|
|
60
|
+
accountKey,
|
|
61
|
+
origin: "mcp",
|
|
62
|
+
archiveOpsUrl
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
var keyHint = " — set INSTANCE_TOKEN from `sl init` for writes";
|
|
44
66
|
async function apiRequest(method, path, body) {
|
|
45
|
-
const apiKey = readApiKey();
|
|
46
67
|
const baseUrl = process.env.SECONDLAYER_API_URL || process.env.SL_API_URL || "http://127.0.0.1:3800";
|
|
68
|
+
let host = "";
|
|
69
|
+
try {
|
|
70
|
+
host = new URL(baseUrl).hostname;
|
|
71
|
+
} catch {
|
|
72
|
+
host = "";
|
|
73
|
+
}
|
|
74
|
+
const apiKey = host === "api.secondlayer.tools" ? resolveAccountKey() : readApiKey();
|
|
47
75
|
const res = await fetch(`${baseUrl}${path}`, {
|
|
48
76
|
method,
|
|
49
77
|
headers: {
|
|
@@ -153,7 +181,11 @@ var PRODUCT_BLURBS = {
|
|
|
153
181
|
subgraphs: "author/scaffold/deploy/query custom indexes",
|
|
154
182
|
subscriptions: "webhook delivery on subgraph rows or raw chain events",
|
|
155
183
|
account: "identity and self-provisioned API keys",
|
|
156
|
-
codegen: "ORM schemas for the tables you read"
|
|
184
|
+
codegen: "ORM schemas for the tables you read",
|
|
185
|
+
instance: "decoder health and empty-index diagnosis",
|
|
186
|
+
archive: "signed archive verify, bootstrap/repair, quote, latest pointer",
|
|
187
|
+
setup: "one-command self-host onboarding",
|
|
188
|
+
credits: "archive credits balance at api.secondlayer.tools"
|
|
157
189
|
};
|
|
158
190
|
var PRODUCT_ORDER = [
|
|
159
191
|
"index",
|
|
@@ -162,12 +194,17 @@ var PRODUCT_ORDER = [
|
|
|
162
194
|
"subgraphs",
|
|
163
195
|
"subscriptions",
|
|
164
196
|
"account",
|
|
165
|
-
"codegen"
|
|
197
|
+
"codegen",
|
|
198
|
+
"instance",
|
|
199
|
+
"archive",
|
|
200
|
+
"setup",
|
|
201
|
+
"credits"
|
|
166
202
|
];
|
|
167
203
|
function buildCapabilities() {
|
|
168
204
|
const byPrefix = new Map;
|
|
169
205
|
for (const name of getRegisteredToolNames()) {
|
|
170
|
-
const
|
|
206
|
+
const sep = name.indexOf("_");
|
|
207
|
+
const prefix = sep === -1 ? name : name.slice(0, sep);
|
|
171
208
|
const tools = byPrefix.get(prefix) ?? [];
|
|
172
209
|
tools.push(name);
|
|
173
210
|
byPrefix.set(prefix, tools);
|
|
@@ -201,6 +238,7 @@ async function buildContext(deps = { clientProvider: getClient }) {
|
|
|
201
238
|
return field.error ? `unavailable: ${field.error.message}` : unavailable;
|
|
202
239
|
};
|
|
203
240
|
const snap = await deps.clientProvider().context().catch(() => null);
|
|
241
|
+
const instance2 = snap?.instance?.value ? snap.instance.value : orNull(snap?.instance);
|
|
204
242
|
return {
|
|
205
243
|
authState: {
|
|
206
244
|
apiKeySet: Boolean(readApiKey())
|
|
@@ -211,10 +249,12 @@ async function buildContext(deps = { clientProvider: getClient }) {
|
|
|
211
249
|
indexTip: orNull(snap?.indexTip),
|
|
212
250
|
subgraphs: snap?.subgraphs.value ? snap.subgraphs.value.map(formatSubgraphSummary) : orNull(snap?.subgraphs),
|
|
213
251
|
subscriptions: orNull(snap?.subscriptions),
|
|
214
|
-
activeOperations: orNull(snap?.activeOperations)
|
|
252
|
+
activeOperations: orNull(snap?.activeOperations),
|
|
253
|
+
instance: instance2
|
|
215
254
|
},
|
|
216
255
|
whatYouCanDo: buildCapabilities(),
|
|
217
|
-
readAuthTiers: READ_AUTH_TIERS
|
|
256
|
+
readAuthTiers: READ_AUTH_TIERS,
|
|
257
|
+
...snap?.instance?.value?.state === "empty-index" ? { nextStep: BOOTSTRAP_STEP } : {}
|
|
218
258
|
};
|
|
219
259
|
}
|
|
220
260
|
function registerResources(server) {
|
|
@@ -288,18 +328,43 @@ function registerAccountTools(server) {
|
|
|
288
328
|
}, async ({ name }) => jsonResponse(await apiRequest("POST", "/api/keys", { name })));
|
|
289
329
|
}
|
|
290
330
|
|
|
331
|
+
// src/tools/archive.ts
|
|
332
|
+
import { z as z2 } from "zod";
|
|
333
|
+
var FLOW = ["bootstrap", "repair"];
|
|
334
|
+
function registerArchiveTools(server, clientProvider = getClient, archiveOpsClientProvider = getArchiveOpsClient) {
|
|
335
|
+
defineTool(server, "archive_verify", "Read-only, free compare of this instance against a signed archive (POST /v1/archive/verify). unanchored means pin a key or check the URL — not that the instance is fine. After bootstrap, poll instance_status until decoders ok, then this, then codegen_index_schema. Do not download parquet.", {
|
|
336
|
+
against: z2.string().describe("Archive manifest URL"),
|
|
337
|
+
fromBlock: z2.number().int().nonnegative().optional().describe("First height (inclusive)"),
|
|
338
|
+
toBlock: z2.number().int().nonnegative().optional().describe("Last height (inclusive)"),
|
|
339
|
+
target: z2.string().optional().describe("Verify target (default raw)")
|
|
340
|
+
}, async ({ against, fromBlock, toBlock, target }) => jsonResponse(await clientProvider().archive.verify({
|
|
341
|
+
against,
|
|
342
|
+
fromBlock,
|
|
343
|
+
toBlock,
|
|
344
|
+
target
|
|
345
|
+
})));
|
|
346
|
+
defineTool(server, "archive_latest", "Signed pointer to the official archive tree (hosted). Needs SECONDLAYER_API_KEY (sk-sl_*), not INSTANCE_TOKEN. Use the returned origin as --against for archive_bootstrap. Do not download parquet.", {
|
|
347
|
+
against: z2.string().optional().describe("Override latest.json URL (default official tree)")
|
|
348
|
+
}, async ({ against }) => jsonResponse(await archiveOpsClientProvider().archive.latest(against)));
|
|
349
|
+
defineTool(server, "archive_quote", "Price a hosted archive fetch (metered). Needs SECONDLAYER_API_KEY (sk-sl_*), not INSTANCE_TOKEN. Quote happens here; bootstrap/repair also quote inside the CLI. Do not download parquet.", {
|
|
350
|
+
paths: z2.array(z2.string()).describe("Partition paths to price"),
|
|
351
|
+
flow: z2.enum(FLOW).describe("bootstrap or repair")
|
|
352
|
+
}, async ({ paths, flow }) => jsonResponse(await archiveOpsClientProvider().archive.quote({ paths, flow })));
|
|
353
|
+
defineTool(server, "credits_balance", "Archive credits balance at api.secondlayer.tools. Needs SECONDLAYER_API_KEY (sk-sl_*); INSTANCE_TOKEN is the instance.", {}, async () => jsonResponse(await archiveOpsClientProvider().archive.credits.balance()));
|
|
354
|
+
}
|
|
355
|
+
|
|
291
356
|
// src/tools/codegen.ts
|
|
292
357
|
import {
|
|
293
358
|
generateIndexSchema
|
|
294
359
|
} from "@secondlayer/subgraphs";
|
|
295
|
-
import { z as
|
|
360
|
+
import { z as z3 } from "zod";
|
|
296
361
|
var TARGETS = ["prisma", "kysely", "drizzle", "json-schema"];
|
|
297
362
|
function registerCodegenTools(server) {
|
|
298
363
|
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.", {
|
|
299
|
-
target:
|
|
300
|
-
tables:
|
|
301
|
-
schemaName:
|
|
302
|
-
datasourceEnv:
|
|
364
|
+
target: z3.enum(TARGETS).optional().describe("Output flavor (default kysely)"),
|
|
365
|
+
tables: z3.array(z3.string()).optional().describe("Restrict to these Index tables (default: all)"),
|
|
366
|
+
schemaName: z3.string().optional().describe("Postgres schema to qualify table names with"),
|
|
367
|
+
datasourceEnv: z3.string().optional().describe("Prisma only: datasource url env var (default DATABASE_URL)")
|
|
303
368
|
}, ({ target, tables, schemaName, datasourceEnv }) => textResponse(generateIndexSchema(target ?? "kysely", {
|
|
304
369
|
tables,
|
|
305
370
|
schemaName,
|
|
@@ -308,17 +373,17 @@ function registerCodegenTools(server) {
|
|
|
308
373
|
}
|
|
309
374
|
|
|
310
375
|
// src/tools/contracts.ts
|
|
311
|
-
import { z as
|
|
376
|
+
import { z as z4 } from "zod";
|
|
312
377
|
function registerContractTools(server, clientProvider = getClient) {
|
|
313
378
|
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.', {
|
|
314
|
-
trait:
|
|
315
|
-
conformance:
|
|
316
|
-
include:
|
|
317
|
-
limit:
|
|
318
|
-
cursor:
|
|
379
|
+
trait: z4.string().describe('Required. Trait to match (e.g. "sip-010").'),
|
|
380
|
+
conformance: z4.enum(["declared", "inferred", "any"]).optional().describe("declared = parsed from source, inferred = ABI shape-match, any = either (default)"),
|
|
381
|
+
include: z4.literal("abi").optional().describe(`Set to "abi" to include each contract's full ABI`),
|
|
382
|
+
limit: z4.number().optional().describe("Page size, 1–500 (default 100)"),
|
|
383
|
+
cursor: z4.string().optional().describe("Opaque cursor from a prior response's next_cursor")
|
|
319
384
|
}, async (params) => jsonResponse(await clientProvider().contracts.list(params)));
|
|
320
385
|
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.", {
|
|
321
|
-
contractId:
|
|
386
|
+
contractId: z4.string().describe("Fully qualified contract id (e.g. SP….amm-pool-v2-01)")
|
|
322
387
|
}, async ({ contractId }) => {
|
|
323
388
|
const contract = await clientProvider().contracts.get(contractId, {
|
|
324
389
|
includeAbi: true
|
|
@@ -335,14 +400,14 @@ function registerContractTools(server, clientProvider = getClient) {
|
|
|
335
400
|
|
|
336
401
|
// src/tools/index.ts
|
|
337
402
|
import { DECODED_EVENT_TYPES } from "@secondlayer/shared";
|
|
338
|
-
import { z as
|
|
403
|
+
import { z as z5 } from "zod";
|
|
339
404
|
var INDEX_EVENT_TYPES = DECODED_EVENT_TYPES;
|
|
340
405
|
var rangeFilters = {
|
|
341
|
-
contractId:
|
|
342
|
-
fromHeight:
|
|
343
|
-
toHeight:
|
|
344
|
-
cursor:
|
|
345
|
-
limit:
|
|
406
|
+
contractId: z5.string().optional().describe("Filter by contract id"),
|
|
407
|
+
fromHeight: z5.number().optional().describe("Start block height (inclusive)"),
|
|
408
|
+
toHeight: z5.number().optional().describe("End block height (inclusive)"),
|
|
409
|
+
cursor: z5.string().optional().describe("Opaque cursor from a prior response's next_cursor"),
|
|
410
|
+
limit: z5.number().optional().describe("Max rows for this page")
|
|
346
411
|
};
|
|
347
412
|
var heightFilters = {
|
|
348
413
|
fromHeight: rangeFilters.fromHeight,
|
|
@@ -353,52 +418,62 @@ var heightFilters = {
|
|
|
353
418
|
function registerIndexTools(server, clientProvider = getClient) {
|
|
354
419
|
defineTool(server, "index_ft_transfers", "List decoded SIP-010 fungible-token transfers from Index. Reads are keyless.", {
|
|
355
420
|
...rangeFilters,
|
|
356
|
-
sender:
|
|
357
|
-
recipient:
|
|
358
|
-
assetIdentifier:
|
|
421
|
+
sender: z5.string().optional().describe("Filter by sender principal"),
|
|
422
|
+
recipient: z5.string().optional().describe("Filter by recipient principal"),
|
|
423
|
+
assetIdentifier: z5.string().optional().describe("Filter by asset identifier (contract::asset), e.g. SP…sbtc-token::sbtc-token")
|
|
359
424
|
}, async (params) => jsonResponse(await clientProvider().index.ftTransfers.list(params)));
|
|
360
425
|
defineTool(server, "index_nft_transfers", "List decoded SIP-009 non-fungible-token transfers from Index. Reads are keyless.", {
|
|
361
426
|
...rangeFilters,
|
|
362
|
-
sender:
|
|
363
|
-
recipient:
|
|
364
|
-
assetIdentifier:
|
|
427
|
+
sender: z5.string().optional().describe("Filter by sender principal"),
|
|
428
|
+
recipient: z5.string().optional().describe("Filter by recipient principal"),
|
|
429
|
+
assetIdentifier: z5.string().optional().describe("Filter by asset identifier (contract::asset)")
|
|
365
430
|
}, async (params) => jsonResponse(await clientProvider().index.nftTransfers.list(params)));
|
|
366
431
|
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.", {
|
|
367
|
-
eventType:
|
|
432
|
+
eventType: z5.enum(INDEX_EVENT_TYPES).describe("Required. Decoded event type to list."),
|
|
368
433
|
...rangeFilters,
|
|
369
|
-
sender:
|
|
370
|
-
recipient:
|
|
371
|
-
assetIdentifier:
|
|
372
|
-
trait:
|
|
373
|
-
txContext:
|
|
434
|
+
sender: z5.string().optional().describe("Filter by sender principal"),
|
|
435
|
+
recipient: z5.string().optional().describe("Filter by recipient principal"),
|
|
436
|
+
assetIdentifier: z5.string().optional().describe("Filter by asset identifier where applicable"),
|
|
437
|
+
trait: z5.string().optional().describe("Match contracts conforming to a trait/standard (e.g. sip-010). Mutually exclusive with contractId; contract-keyed event types only."),
|
|
438
|
+
txContext: z5.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.")
|
|
374
439
|
}, async (params) => jsonResponse(await clientProvider().index.events.list(params)));
|
|
375
440
|
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.", {
|
|
376
441
|
...rangeFilters,
|
|
377
|
-
functionName:
|
|
378
|
-
sender:
|
|
379
|
-
trait:
|
|
442
|
+
functionName: z5.string().optional().describe("Filter by called function name"),
|
|
443
|
+
sender: z5.string().optional().describe("Filter by caller principal"),
|
|
444
|
+
trait: z5.string().optional().describe("Match contracts conforming to a trait/standard (e.g. sip-010). Mutually exclusive with contractId.")
|
|
380
445
|
}, async (params) => jsonResponse(await clientProvider().index.contractCalls.list(params)));
|
|
381
446
|
defineTool(server, "index_blocks", "List decoded blocks from Index. Reads are keyless.", { ...heightFilters }, async (params) => jsonResponse(await clientProvider().index.blocks.list(params)));
|
|
382
447
|
defineTool(server, "index_transactions", "List decoded transactions from Index. Filter by type, sender, or contract. Reads are keyless.", {
|
|
383
448
|
...rangeFilters,
|
|
384
|
-
type:
|
|
385
|
-
sender:
|
|
449
|
+
type: z5.string().optional().describe("Filter by transaction type"),
|
|
450
|
+
sender: z5.string().optional().describe("Filter by sender principal")
|
|
386
451
|
}, async (params) => jsonResponse(await clientProvider().index.transactions.list(params)));
|
|
387
452
|
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.", {
|
|
388
|
-
contractId:
|
|
453
|
+
contractId: z5.string().describe("Contract id (SP….name) whose print events to profile")
|
|
389
454
|
}, async (params) => jsonResponse(await clientProvider().index.printSchema(params.contractId)));
|
|
390
455
|
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()));
|
|
391
456
|
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.", {
|
|
392
|
-
requests:
|
|
393
|
-
path:
|
|
394
|
-
params:
|
|
457
|
+
requests: z5.array(z5.object({
|
|
458
|
+
path: z5.string().describe("Public /v1 read path"),
|
|
459
|
+
params: z5.record(z5.string(), z5.string()).optional()
|
|
395
460
|
})).min(1).max(10).describe("Read descriptors, executed concurrently")
|
|
396
461
|
}, async (params) => jsonResponse(await clientProvider().batch(params.requests)));
|
|
397
462
|
}
|
|
398
463
|
|
|
464
|
+
// src/tools/instance.ts
|
|
465
|
+
function registerInstanceTools(server, clientProvider = getClient) {
|
|
466
|
+
defineTool(server, "instance_status", "Decoder health and empty-index diagnosis from GET /public/status. If state is empty-index, next tool is archive_bootstrap or setup. Poll until index.decoders are ok before treating Index as ready — do not start decoders. Then archive_verify, then codegen_index_schema. Do not download parquet.", {}, async () => {
|
|
467
|
+
const client = clientProvider();
|
|
468
|
+
const status = await client.instance.status();
|
|
469
|
+
const diagnosis = await client.instance.diagnose();
|
|
470
|
+
return jsonResponse({ status, diagnosis });
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
399
474
|
// src/tools/scaffold.ts
|
|
400
475
|
import { generateSubgraphCode } from "@secondlayer/scaffold";
|
|
401
|
-
import { z as
|
|
476
|
+
import { z as z6 } from "zod";
|
|
402
477
|
async function fetchAbi(clientProvider, contractId) {
|
|
403
478
|
const contract = await clientProvider().contracts.get(contractId, {
|
|
404
479
|
includeAbi: true
|
|
@@ -413,8 +488,8 @@ async function fetchAbi(clientProvider, contractId) {
|
|
|
413
488
|
}
|
|
414
489
|
function registerScaffoldTools(server, clientProvider = getClient) {
|
|
415
490
|
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.", {
|
|
416
|
-
contractId:
|
|
417
|
-
subgraphName:
|
|
491
|
+
contractId: z6.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
|
|
492
|
+
subgraphName: z6.string().optional().describe("Override the subgraph name (defaults to contract name)")
|
|
418
493
|
}, async ({ contractId, subgraphName }) => {
|
|
419
494
|
const { functions } = await fetchAbi(clientProvider, contractId);
|
|
420
495
|
const code = generateSubgraphCode(contractId, functions, subgraphName);
|
|
@@ -422,36 +497,245 @@ function registerScaffoldTools(server, clientProvider = getClient) {
|
|
|
422
497
|
});
|
|
423
498
|
}
|
|
424
499
|
|
|
500
|
+
// src/tools/setup.ts
|
|
501
|
+
import { z as z7 } from "zod";
|
|
502
|
+
|
|
503
|
+
// src/lib/exec-cli.ts
|
|
504
|
+
import {
|
|
505
|
+
spawn
|
|
506
|
+
} from "node:child_process";
|
|
507
|
+
var ALLOWED = ["setup", "bootstrap", "repair", "verify"];
|
|
508
|
+
var TIMEOUT_MS = {
|
|
509
|
+
setup: 15 * 60 * 1000,
|
|
510
|
+
bootstrap: 10 * 60 * 1000,
|
|
511
|
+
repair: 10 * 60 * 1000,
|
|
512
|
+
verify: 2 * 60 * 1000
|
|
513
|
+
};
|
|
514
|
+
var VALUE_FLAGS = new Set([
|
|
515
|
+
"--against",
|
|
516
|
+
"--from-block",
|
|
517
|
+
"--to-block",
|
|
518
|
+
"--network",
|
|
519
|
+
"--node-mode",
|
|
520
|
+
"--dir"
|
|
521
|
+
]);
|
|
522
|
+
var BOOL_FLAGS = new Set(["--yes", "--json", "--apply", "--skip-bootstrap"]);
|
|
523
|
+
var FLAGS_BY_CMD = {
|
|
524
|
+
setup: new Set([
|
|
525
|
+
"--yes",
|
|
526
|
+
"--network",
|
|
527
|
+
"--node-mode",
|
|
528
|
+
"--against",
|
|
529
|
+
"--dir",
|
|
530
|
+
"--skip-bootstrap"
|
|
531
|
+
]),
|
|
532
|
+
bootstrap: new Set([
|
|
533
|
+
"--against",
|
|
534
|
+
"--yes",
|
|
535
|
+
"--json",
|
|
536
|
+
"--from-block",
|
|
537
|
+
"--to-block"
|
|
538
|
+
]),
|
|
539
|
+
repair: new Set([
|
|
540
|
+
"--against",
|
|
541
|
+
"--json",
|
|
542
|
+
"--apply",
|
|
543
|
+
"--from-block",
|
|
544
|
+
"--to-block",
|
|
545
|
+
"--yes"
|
|
546
|
+
]),
|
|
547
|
+
verify: new Set(["--against", "--json", "--from-block", "--to-block"])
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
class ExecCliError extends Error {
|
|
551
|
+
constructor(message) {
|
|
552
|
+
super(message);
|
|
553
|
+
this.name = "ExecCliError";
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function isUnsafeArg(arg) {
|
|
557
|
+
if (arg.includes("\x00"))
|
|
558
|
+
return true;
|
|
559
|
+
if (/[\n\r]/.test(arg))
|
|
560
|
+
return true;
|
|
561
|
+
return /^[;|&]/.test(arg.trim());
|
|
562
|
+
}
|
|
563
|
+
function assertSafeArgs(subcommand, args) {
|
|
564
|
+
const allowed = FLAGS_BY_CMD[subcommand];
|
|
565
|
+
let expectingValue = null;
|
|
566
|
+
for (const arg of args) {
|
|
567
|
+
if (isUnsafeArg(arg)) {
|
|
568
|
+
throw new ExecCliError(`rejected argument: ${JSON.stringify(arg)}`);
|
|
569
|
+
}
|
|
570
|
+
if (expectingValue) {
|
|
571
|
+
if (arg.startsWith("-")) {
|
|
572
|
+
throw new ExecCliError(`${expectingValue} requires a value`);
|
|
573
|
+
}
|
|
574
|
+
expectingValue = null;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (!arg.startsWith("--") || !allowed.has(arg)) {
|
|
578
|
+
throw new ExecCliError(`flag not allowed for ${subcommand}: ${JSON.stringify(arg)}`);
|
|
579
|
+
}
|
|
580
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
581
|
+
expectingValue = arg;
|
|
582
|
+
} else if (!BOOL_FLAGS.has(arg)) {
|
|
583
|
+
throw new ExecCliError(`flag not allowed for ${subcommand}: ${JSON.stringify(arg)}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (expectingValue) {
|
|
587
|
+
throw new ExecCliError(`${expectingValue} requires a value`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function killProcessGroup(child) {
|
|
591
|
+
const pid = child.pid;
|
|
592
|
+
if (pid && process.platform !== "win32") {
|
|
593
|
+
try {
|
|
594
|
+
process.kill(-pid, "SIGKILL");
|
|
595
|
+
return;
|
|
596
|
+
} catch {}
|
|
597
|
+
}
|
|
598
|
+
child.kill("SIGKILL");
|
|
599
|
+
}
|
|
600
|
+
async function execSecondlayer(subcommand, args, deps = {}) {
|
|
601
|
+
if (!ALLOWED.includes(subcommand)) {
|
|
602
|
+
throw new ExecCliError(`subcommand not allowed: ${subcommand}`);
|
|
603
|
+
}
|
|
604
|
+
assertSafeArgs(subcommand, args);
|
|
605
|
+
const argv = [subcommand, ...args];
|
|
606
|
+
if (subcommand === "setup") {
|
|
607
|
+
if (!argv.includes("--yes"))
|
|
608
|
+
argv.push("--yes");
|
|
609
|
+
} else if (!argv.includes("--json")) {
|
|
610
|
+
argv.push("--json");
|
|
611
|
+
}
|
|
612
|
+
const env = deps.env ?? process.env;
|
|
613
|
+
const bin = env.SECONDLAYER_BIN || "secondlayer";
|
|
614
|
+
const cwd = deps.cwd ?? env.SECONDLAYER_CWD ?? process.cwd();
|
|
615
|
+
const spawnFn = deps.spawn ?? spawn;
|
|
616
|
+
const timeout = deps.timeoutMs ?? TIMEOUT_MS[subcommand];
|
|
617
|
+
return new Promise((resolve) => {
|
|
618
|
+
let stdout = "";
|
|
619
|
+
let stderr = "";
|
|
620
|
+
let settled = false;
|
|
621
|
+
const child = spawnFn(bin, argv, {
|
|
622
|
+
cwd,
|
|
623
|
+
env,
|
|
624
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
625
|
+
detached: process.platform !== "win32",
|
|
626
|
+
shell: false
|
|
627
|
+
});
|
|
628
|
+
const timer = setTimeout(() => {
|
|
629
|
+
killProcessGroup(child);
|
|
630
|
+
finish({
|
|
631
|
+
code: 124,
|
|
632
|
+
stdout,
|
|
633
|
+
stderr: `${stderr}
|
|
634
|
+
timeout after ${timeout}ms`.trim()
|
|
635
|
+
});
|
|
636
|
+
}, timeout);
|
|
637
|
+
function finish(result) {
|
|
638
|
+
if (settled)
|
|
639
|
+
return;
|
|
640
|
+
settled = true;
|
|
641
|
+
clearTimeout(timer);
|
|
642
|
+
resolve(result);
|
|
643
|
+
}
|
|
644
|
+
child.stdout?.on("data", (chunk) => {
|
|
645
|
+
stdout += String(chunk);
|
|
646
|
+
});
|
|
647
|
+
child.stderr?.on("data", (chunk) => {
|
|
648
|
+
stderr += String(chunk);
|
|
649
|
+
});
|
|
650
|
+
child.on("error", (err) => {
|
|
651
|
+
finish({ code: 127, stdout, stderr: err.message });
|
|
652
|
+
});
|
|
653
|
+
child.on("close", (code) => {
|
|
654
|
+
finish({ code: code ?? 1, stdout, stderr });
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/tools/setup.ts
|
|
660
|
+
function cliResponse(result) {
|
|
661
|
+
return jsonResponse(result, result.code !== 0);
|
|
662
|
+
}
|
|
663
|
+
function registerSetupTools(server, exec = execSecondlayer) {
|
|
664
|
+
defineTool(server, "setup", "Non-interactive self-host onboarding (secondlayer setup --yes). If the index is empty, this or archive_bootstrap fills it. Bootstrap is metered against the official archive. After it returns, poll instance_status until index.decoders are ok, then archive_verify, then codegen_index_schema. Do not download parquet.", {
|
|
665
|
+
network: z7.enum(["mainnet", "testnet", "devnet"]).describe("Stacks network"),
|
|
666
|
+
nodeMode: z7.enum(["external", "stacks", "full"]).describe("Node mode: external, stacks, or full"),
|
|
667
|
+
against: z7.string().optional().describe("Archive manifest URL"),
|
|
668
|
+
dir: z7.string().optional().describe("Compose project directory"),
|
|
669
|
+
skipBootstrap: z7.boolean().optional().describe("Sync from genesis instead of restoring an archive")
|
|
670
|
+
}, async ({ network, nodeMode, against, dir, skipBootstrap }) => {
|
|
671
|
+
const args = ["--yes", "--network", network, "--node-mode", nodeMode];
|
|
672
|
+
if (against !== undefined)
|
|
673
|
+
args.push("--against", against);
|
|
674
|
+
if (dir !== undefined)
|
|
675
|
+
args.push("--dir", dir);
|
|
676
|
+
if (skipBootstrap)
|
|
677
|
+
args.push("--skip-bootstrap");
|
|
678
|
+
return cliResponse(await exec("setup", args));
|
|
679
|
+
});
|
|
680
|
+
defineTool(server, "archive_bootstrap", "Restore verified history into this instance (secondlayer bootstrap --against … --yes --json). Metered against the official archive; quote happens inside the CLI. After bootstrap, poll instance_status until decoders ok, then archive_verify, then codegen_index_schema. Do not download parquet.", {
|
|
681
|
+
against: z7.string().describe("Archive manifest URL"),
|
|
682
|
+
fromBlock: z7.number().int().nonnegative().optional().describe("Restore from this height instead of genesis"),
|
|
683
|
+
toBlock: z7.number().int().nonnegative().optional().describe("Stop at this height instead of the archive tip")
|
|
684
|
+
}, async ({ against, fromBlock, toBlock }) => {
|
|
685
|
+
const args = ["--against", against, "--yes", "--json"];
|
|
686
|
+
if (fromBlock !== undefined)
|
|
687
|
+
args.push("--from-block", String(fromBlock));
|
|
688
|
+
if (toBlock !== undefined)
|
|
689
|
+
args.push("--to-block", String(toBlock));
|
|
690
|
+
return cliResponse(await exec("bootstrap", args));
|
|
691
|
+
});
|
|
692
|
+
defineTool(server, "archive_repair", "Plan (default) or apply a repair of local chain data against a signed archive. Metered when apply fetches from the official archive. After apply, poll instance_status until decoders ok, then archive_verify. Do not download parquet.", {
|
|
693
|
+
against: z7.string().describe("Archive manifest URL"),
|
|
694
|
+
apply: z7.boolean().optional().describe("Write the repair (default plan-only)"),
|
|
695
|
+
fromBlock: z7.number().int().nonnegative().optional().describe("First height to consider"),
|
|
696
|
+
toBlock: z7.number().int().nonnegative().optional().describe("Last height to consider")
|
|
697
|
+
}, async ({ against, apply, fromBlock, toBlock }) => {
|
|
698
|
+
const args = ["--against", against, "--json"];
|
|
699
|
+
if (apply)
|
|
700
|
+
args.push("--apply", "--yes");
|
|
701
|
+
if (fromBlock !== undefined)
|
|
702
|
+
args.push("--from-block", String(fromBlock));
|
|
703
|
+
if (toBlock !== undefined)
|
|
704
|
+
args.push("--to-block", String(toBlock));
|
|
705
|
+
return cliResponse(await exec("repair", args));
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
425
709
|
// src/tools/streams.ts
|
|
426
710
|
import { STREAMS_EVENT_TYPES } from "@secondlayer/shared";
|
|
427
|
-
import { z as
|
|
428
|
-
var filterValue =
|
|
711
|
+
import { z as z8 } from "zod";
|
|
712
|
+
var filterValue = z8.union([z8.string(), z8.array(z8.string())]);
|
|
429
713
|
function registerStreamsTools(server, clientProvider = getClient) {
|
|
430
714
|
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()));
|
|
431
715
|
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.", {
|
|
432
|
-
cursor:
|
|
433
|
-
fromHeight:
|
|
434
|
-
toHeight:
|
|
435
|
-
types:
|
|
436
|
-
notTypes:
|
|
716
|
+
cursor: z8.string().optional().describe("Resume strictly after this cursor (`<block_height>:<event_index>`, e.g. 951475:3) — pass a prior response's next_cursor"),
|
|
717
|
+
fromHeight: z8.number().int().nonnegative().optional().describe("Start block height (inclusive)"),
|
|
718
|
+
toHeight: z8.number().int().nonnegative().optional().describe("End block height (inclusive)"),
|
|
719
|
+
types: z8.array(z8.enum(STREAMS_EVENT_TYPES)).optional().describe("Event types to include"),
|
|
720
|
+
notTypes: z8.array(z8.enum(STREAMS_EVENT_TYPES)).optional().describe("Event types to exclude (applied after types)"),
|
|
437
721
|
contractId: filterValue.optional().describe("Contract id, or a list of contract ids to OR together"),
|
|
438
722
|
sender: filterValue.optional().describe("Sender principal, or a list to OR together"),
|
|
439
723
|
recipient: filterValue.optional().describe("Recipient principal, or a list to OR together"),
|
|
440
|
-
assetIdentifier:
|
|
441
|
-
limit:
|
|
724
|
+
assetIdentifier: z8.string().optional().describe("Asset identifier (contract::asset), e.g. SP…sbtc-token::sbtc-token"),
|
|
725
|
+
limit: z8.number().int().min(1).max(1000).optional().describe("Page size, 1–1000 (default 100)")
|
|
442
726
|
}, async (params) => jsonResponse(await clientProvider().streams.events.list(params)));
|
|
443
727
|
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.", {
|
|
444
|
-
txId:
|
|
728
|
+
txId: z8.string().describe("Transaction id (0x-prefixed)")
|
|
445
729
|
}, async ({ txId }) => jsonResponse(await clientProvider().streams.events.byTxId(txId)));
|
|
446
730
|
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.", {
|
|
447
|
-
heightOrHash:
|
|
731
|
+
heightOrHash: z8.union([z8.number().int().nonnegative(), z8.string()]).describe("Block height (number) or block hash (0x-prefixed string)")
|
|
448
732
|
}, async ({ heightOrHash }) => jsonResponse(await clientProvider().streams.blocks.events(heightOrHash)));
|
|
449
733
|
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.", {
|
|
450
|
-
height:
|
|
734
|
+
height: z8.number().int().nonnegative().describe("Block height")
|
|
451
735
|
}, async ({ height }) => jsonResponse(await clientProvider().streams.canonical(height)));
|
|
452
736
|
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.", {
|
|
453
|
-
since:
|
|
454
|
-
limit:
|
|
737
|
+
since: z8.string().describe("Required. ISO 8601 timestamp, or a `next_since` resume token from a prior response"),
|
|
738
|
+
limit: z8.number().int().positive().optional().describe("Max reorgs to return")
|
|
455
739
|
}, async ({ since, limit }) => jsonResponse(await clientProvider().streams.reorgs.list({
|
|
456
740
|
since,
|
|
457
741
|
...limit !== undefined ? { limit } : {}
|
|
@@ -461,19 +745,19 @@ function registerStreamsTools(server, clientProvider = getClient) {
|
|
|
461
745
|
|
|
462
746
|
// src/tools/subgraphs.ts
|
|
463
747
|
import { bundleSubgraphCode } from "@secondlayer/bundler";
|
|
464
|
-
import { z as
|
|
748
|
+
import { z as z9 } from "zod";
|
|
465
749
|
function registerSubgraphTools(server, clientProvider = getClient) {
|
|
466
750
|
defineTool(server, "subgraphs_list", "List all deployed subgraphs. Returns summary fields only; read rows with subgraphs_query.", {}, async () => {
|
|
467
751
|
const { data } = await clientProvider().subgraphs.list();
|
|
468
752
|
return jsonResponse(data.map(formatSubgraphSummary));
|
|
469
753
|
});
|
|
470
|
-
defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health, and table columns.", { name:
|
|
754
|
+
defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health, and table columns.", { name: z9.string().describe("Subgraph name") }, async ({ name }) => {
|
|
471
755
|
const detail = await clientProvider().subgraphs.status(name);
|
|
472
756
|
return jsonResponse(detail);
|
|
473
757
|
});
|
|
474
758
|
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.", {
|
|
475
|
-
name:
|
|
476
|
-
format:
|
|
759
|
+
name: z9.string().describe("Subgraph name"),
|
|
760
|
+
format: z9.enum(["agent", "openapi", "markdown"]).optional().describe("agent = JSON schema for tool use (default), openapi = OpenAPI JSON, markdown = docs.md")
|
|
477
761
|
}, async ({ name, format }) => {
|
|
478
762
|
const client = clientProvider();
|
|
479
763
|
if (format === "openapi")
|
|
@@ -483,15 +767,15 @@ function registerSubgraphTools(server, clientProvider = getClient) {
|
|
|
483
767
|
return jsonResponse(await client.subgraphs.schema(name));
|
|
484
768
|
});
|
|
485
769
|
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.', {
|
|
486
|
-
name:
|
|
487
|
-
table:
|
|
488
|
-
filters:
|
|
489
|
-
sort:
|
|
490
|
-
order:
|
|
491
|
-
limit:
|
|
492
|
-
offset:
|
|
493
|
-
fields:
|
|
494
|
-
count:
|
|
770
|
+
name: z9.string().describe("Subgraph name"),
|
|
771
|
+
table: z9.string().describe("Table name"),
|
|
772
|
+
filters: z9.record(z9.string(), z9.string()).optional().describe('Column filters — plain values or with operators (e.g. {"amount.gte": "1000", "sender": "SP..."})'),
|
|
773
|
+
sort: z9.string().optional().describe("Column to sort by"),
|
|
774
|
+
order: z9.enum(["asc", "desc"]).optional().describe("Sort order"),
|
|
775
|
+
limit: z9.number().max(200).optional().describe("Max rows (default 50, max 200)"),
|
|
776
|
+
offset: z9.number().optional().describe("Offset for pagination"),
|
|
777
|
+
fields: z9.string().optional().describe('Comma-separated column list to return (e.g. "sender,amount")'),
|
|
778
|
+
count: z9.boolean().optional().describe("If true, return row count instead of rows")
|
|
495
779
|
}, async ({
|
|
496
780
|
name,
|
|
497
781
|
table,
|
|
@@ -519,15 +803,15 @@ function registerSubgraphTools(server, clientProvider = getClient) {
|
|
|
519
803
|
return jsonResponse(result);
|
|
520
804
|
});
|
|
521
805
|
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.", {
|
|
522
|
-
name:
|
|
806
|
+
name: z9.string().describe("Subgraph name")
|
|
523
807
|
}, async ({ name }) => {
|
|
524
808
|
const result = await clientProvider().subgraphs.reindex(name);
|
|
525
809
|
return jsonResponse(result);
|
|
526
810
|
});
|
|
527
811
|
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.", {
|
|
528
|
-
name:
|
|
529
|
-
fromBlock:
|
|
530
|
-
toBlock:
|
|
812
|
+
name: z9.string().describe("Subgraph name"),
|
|
813
|
+
fromBlock: z9.number().int().nonnegative().describe("Start block (inclusive)"),
|
|
814
|
+
toBlock: z9.number().int().nonnegative().describe("End block (inclusive)")
|
|
531
815
|
}, async ({ name, fromBlock, toBlock }) => {
|
|
532
816
|
const result = await clientProvider().subgraphs.backfill(name, {
|
|
533
817
|
fromBlock,
|
|
@@ -535,15 +819,15 @@ function registerSubgraphTools(server, clientProvider = getClient) {
|
|
|
535
819
|
});
|
|
536
820
|
return jsonResponse(result);
|
|
537
821
|
});
|
|
538
|
-
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:
|
|
822
|
+
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: z9.string().describe("Subgraph name") }, async ({ name }) => {
|
|
539
823
|
const result = await clientProvider().subgraphs.stop(name);
|
|
540
824
|
return jsonResponse(result);
|
|
541
825
|
});
|
|
542
826
|
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.", {
|
|
543
|
-
name:
|
|
544
|
-
limit:
|
|
545
|
-
offset:
|
|
546
|
-
resolved:
|
|
827
|
+
name: z9.string().describe("Subgraph name"),
|
|
828
|
+
limit: z9.number().int().positive().optional().describe("Max gaps to return"),
|
|
829
|
+
offset: z9.number().int().nonnegative().optional().describe("Pagination offset"),
|
|
830
|
+
resolved: z9.boolean().optional().describe("Filter by resolved state (omit for unresolved only)")
|
|
547
831
|
}, async ({ name, limit, offset, resolved }) => {
|
|
548
832
|
const result = await clientProvider().subgraphs.gaps(name, {
|
|
549
833
|
limit,
|
|
@@ -553,20 +837,20 @@ function registerSubgraphTools(server, clientProvider = getClient) {
|
|
|
553
837
|
return jsonResponse(result);
|
|
554
838
|
});
|
|
555
839
|
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.", {
|
|
556
|
-
name:
|
|
557
|
-
operationId:
|
|
840
|
+
name: z9.string().describe("Subgraph name"),
|
|
841
|
+
operationId: z9.string().optional().describe("Fetch one operation by id (the operationId returned by reindex/backfill/stop); omit for recent history")
|
|
558
842
|
}, async ({ name, operationId }) => {
|
|
559
843
|
const client = clientProvider();
|
|
560
844
|
return jsonResponse(operationId ? await client.subgraphs.getOperation(name, operationId) : await client.subgraphs.operations(name));
|
|
561
845
|
});
|
|
562
|
-
defineTool(server, "subgraphs_delete", "Delete a subgraph permanently.", { name:
|
|
846
|
+
defineTool(server, "subgraphs_delete", "Delete a subgraph permanently.", { name: z9.string().describe("Subgraph name") }, async ({ name }) => {
|
|
563
847
|
const result = await clientProvider().subgraphs.delete(name);
|
|
564
848
|
return textResponse(result.message);
|
|
565
849
|
});
|
|
566
850
|
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.", {
|
|
567
|
-
code:
|
|
568
|
-
startBlock:
|
|
569
|
-
dryRun:
|
|
851
|
+
code: z9.string().describe("TypeScript source code containing a defineSubgraph() call"),
|
|
852
|
+
startBlock: z9.number().int().nonnegative().optional().describe("Override the definition startBlock for this deploy"),
|
|
853
|
+
dryRun: z9.boolean().optional().describe("Validate and preview the deploy (schema/DDL) without writing changes")
|
|
570
854
|
}, async ({ code, startBlock, dryRun }) => {
|
|
571
855
|
const bundled = await bundleSubgraphCode(code);
|
|
572
856
|
const result = await clientProvider().subgraphs.deploy({
|
|
@@ -586,38 +870,38 @@ function registerSubgraphTools(server, clientProvider = getClient) {
|
|
|
586
870
|
|
|
587
871
|
// src/tools/subscriptions.ts
|
|
588
872
|
import { CHAIN_TRIGGER_TYPES } from "@secondlayer/shared";
|
|
589
|
-
import { z as
|
|
873
|
+
import { z as z10 } from "zod";
|
|
590
874
|
function registerSubscriptionTools(server, clientProvider = getClient) {
|
|
591
875
|
defineTool(server, "subscriptions_list", "List all subscriptions for the current account. Returns summary fields (no secrets).", {}, async () => {
|
|
592
876
|
const { data } = await clientProvider().subscriptions.list();
|
|
593
877
|
return jsonResponse(data);
|
|
594
878
|
});
|
|
595
|
-
defineTool(server, "subscriptions_get", "Get full detail for a subscription (filter, auth, retry config, circuit state).", { id:
|
|
879
|
+
defineTool(server, "subscriptions_get", "Get full detail for a subscription (filter, auth, retry config, circuit state).", { id: z10.string().describe("Subscription id") }, async ({ id }) => {
|
|
596
880
|
const detail = await clientProvider().subscriptions.get(id);
|
|
597
881
|
return jsonResponse(detail);
|
|
598
882
|
});
|
|
599
883
|
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.", {
|
|
600
|
-
name:
|
|
601
|
-
subgraphName:
|
|
602
|
-
tableName:
|
|
603
|
-
triggers:
|
|
604
|
-
type:
|
|
605
|
-
contractId:
|
|
606
|
-
functionName:
|
|
607
|
-
caller:
|
|
608
|
-
sender:
|
|
609
|
-
recipient:
|
|
610
|
-
assetIdentifier:
|
|
611
|
-
deployer:
|
|
612
|
-
contractName:
|
|
613
|
-
topic:
|
|
614
|
-
lockedAddress:
|
|
615
|
-
trait:
|
|
616
|
-
minAmount:
|
|
617
|
-
maxAmount:
|
|
884
|
+
name: z10.string().describe("Human-readable name, unique per account"),
|
|
885
|
+
subgraphName: z10.string().optional().describe("Subgraph to subscribe to (subgraph subscription)"),
|
|
886
|
+
tableName: z10.string().optional().describe("Table within the subgraph (subgraph subscription)"),
|
|
887
|
+
triggers: z10.array(z10.object({
|
|
888
|
+
type: z10.enum(CHAIN_TRIGGER_TYPES),
|
|
889
|
+
contractId: z10.string().optional(),
|
|
890
|
+
functionName: z10.string().optional(),
|
|
891
|
+
caller: z10.string().optional(),
|
|
892
|
+
sender: z10.string().optional(),
|
|
893
|
+
recipient: z10.string().optional(),
|
|
894
|
+
assetIdentifier: z10.string().optional(),
|
|
895
|
+
deployer: z10.string().optional(),
|
|
896
|
+
contractName: z10.string().optional(),
|
|
897
|
+
topic: z10.string().optional(),
|
|
898
|
+
lockedAddress: z10.string().optional(),
|
|
899
|
+
trait: z10.string().optional(),
|
|
900
|
+
minAmount: z10.union([z10.string(), z10.number()]).optional(),
|
|
901
|
+
maxAmount: z10.union([z10.string(), z10.number()]).optional()
|
|
618
902
|
})).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."),
|
|
619
|
-
url:
|
|
620
|
-
format:
|
|
903
|
+
url: z10.string().describe("Webhook URL"),
|
|
904
|
+
format: z10.enum([
|
|
621
905
|
"standard-webhooks",
|
|
622
906
|
"inngest",
|
|
623
907
|
"trigger",
|
|
@@ -625,20 +909,20 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
|
|
|
625
909
|
"cloudevents",
|
|
626
910
|
"raw"
|
|
627
911
|
]).optional().describe("Wire format (default standard-webhooks)"),
|
|
628
|
-
runtime:
|
|
629
|
-
filter:
|
|
630
|
-
authConfig:
|
|
912
|
+
runtime: z10.enum(["inngest", "trigger", "cloudflare", "node"]).optional().describe("Receiver runtime label (display only)"),
|
|
913
|
+
filter: z10.record(z10.string(), z10.unknown()).optional().describe('Scalar filter DSL, e.g. {"amount": {"gte": 100}, "sender": "SP..."}'),
|
|
914
|
+
authConfig: z10.record(z10.string(), z10.unknown()).optional().describe('Receiver auth sent with each delivery, e.g. {"type": "bearer", "token": "..."}')
|
|
631
915
|
}, async (input) => {
|
|
632
916
|
const res = await clientProvider().subscriptions.create(input);
|
|
633
917
|
return jsonResponse(res);
|
|
634
918
|
});
|
|
635
919
|
defineTool(server, "subscriptions_update", "Patch a subscription (name, url, filter, authConfig, format, runtime, retry, timeout, concurrency).", {
|
|
636
|
-
id:
|
|
637
|
-
name:
|
|
638
|
-
url:
|
|
639
|
-
filter:
|
|
640
|
-
authConfig:
|
|
641
|
-
format:
|
|
920
|
+
id: z10.string(),
|
|
921
|
+
name: z10.string().optional().describe("Rename the subscription"),
|
|
922
|
+
url: z10.string().optional(),
|
|
923
|
+
filter: z10.record(z10.string(), z10.unknown()).optional(),
|
|
924
|
+
authConfig: z10.record(z10.string(), z10.unknown()).optional().describe("Receiver auth sent with each delivery (bearer/etc.)"),
|
|
925
|
+
format: z10.enum([
|
|
642
926
|
"standard-webhooks",
|
|
643
927
|
"inngest",
|
|
644
928
|
"trigger",
|
|
@@ -646,36 +930,36 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
|
|
|
646
930
|
"cloudevents",
|
|
647
931
|
"raw"
|
|
648
932
|
]).optional(),
|
|
649
|
-
runtime:
|
|
650
|
-
maxRetries:
|
|
651
|
-
timeoutMs:
|
|
652
|
-
concurrency:
|
|
933
|
+
runtime: z10.enum(["inngest", "trigger", "cloudflare", "node"]).nullable().optional(),
|
|
934
|
+
maxRetries: z10.number().int().min(0).optional(),
|
|
935
|
+
timeoutMs: z10.number().int().min(100).optional(),
|
|
936
|
+
concurrency: z10.number().int().min(1).optional()
|
|
653
937
|
}, async ({ id, ...patch }) => {
|
|
654
938
|
const res = await clientProvider().subscriptions.update(id, patch);
|
|
655
939
|
return jsonResponse(res);
|
|
656
940
|
});
|
|
657
|
-
defineTool(server, "subscriptions_delete", "Delete a subscription. Pending outbox rows are cascade-deleted.", { id:
|
|
941
|
+
defineTool(server, "subscriptions_delete", "Delete a subscription. Pending outbox rows are cascade-deleted.", { id: z10.string() }, async ({ id }) => {
|
|
658
942
|
const res = await clientProvider().subscriptions.delete(id);
|
|
659
943
|
return jsonResponse(res);
|
|
660
944
|
});
|
|
661
|
-
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:
|
|
945
|
+
defineTool(server, "subscriptions_test", "Send a one-off test webhook to a subscription's URL (built for its format, SSRF-guarded). Logged as a delivery row. Returns {ok, statusCode, error, durationMs, deliveryId}.", { id: z10.string().describe("Subscription id") }, async ({ id }) => {
|
|
662
946
|
const res = await clientProvider().subscriptions.test(id);
|
|
663
947
|
return jsonResponse(res);
|
|
664
948
|
});
|
|
665
|
-
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:
|
|
666
|
-
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:
|
|
667
|
-
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:
|
|
668
|
-
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:
|
|
669
|
-
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:
|
|
949
|
+
defineTool(server, "subscriptions_pause", "Pause a subscription: stops delivery attempts and stops queueing new ones. Nothing is deleted — resume picks the subscription back up from the tip. Verify with subscriptions_get (status).", { id: z10.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.pause(id)));
|
|
950
|
+
defineTool(server, "subscriptions_resume", "Resume a paused subscription (also clears a tripped circuit breaker). Deliveries restart from the current tip — use subscriptions_replay for the blocks missed while paused. Verify with subscriptions_get (status).", { id: z10.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.resume(id)));
|
|
951
|
+
defineTool(server, "subscriptions_rotate_secret", "Rotate a subscription's webhook signing secret. Returns the NEW `signingSecret` ONCE — forward it to the user; deliveries signed with the old secret stop verifying as soon as this returns, so the receiver must be updated.", { id: z10.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.rotateSecret(id)));
|
|
952
|
+
defineTool(server, "subscriptions_deliveries", "List recent delivery attempts for a subscription — status code, attempt count, error, duration, timestamps. THIS IS THE VERIFY CALL: after subscriptions_create, subscriptions_test, or subscriptions_replay, read it to confirm the webhook actually landed (and to see the receiver's response when it didn't) instead of trusting the enqueue result.", { id: z10.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.deliveries(id)));
|
|
953
|
+
defineTool(server, "subscriptions_dead", "List a subscription's dead-letter queue — deliveries that exhausted their retries, with the outbox id, payload, and last error. Diagnose the receiver first (subscriptions_get for the URL/auth, subscriptions_test for a live probe), then requeue rows with subscriptions_requeue.", { id: z10.string().describe("Subscription id") }, async ({ id }) => jsonResponse(await clientProvider().subscriptions.dead(id)));
|
|
670
954
|
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.", {
|
|
671
|
-
id:
|
|
672
|
-
outboxId:
|
|
955
|
+
id: z10.string().describe("Subscription id"),
|
|
956
|
+
outboxId: z10.string().describe("Outbox id of the dead delivery (from subscriptions_dead)")
|
|
673
957
|
}, async ({ id, outboxId }) => jsonResponse(await clientProvider().subscriptions.requeue(id, outboxId)));
|
|
674
958
|
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.", {
|
|
675
|
-
id:
|
|
676
|
-
fromBlock:
|
|
677
|
-
toBlock:
|
|
678
|
-
force:
|
|
959
|
+
id: z10.string(),
|
|
960
|
+
fromBlock: z10.number().int().nonnegative(),
|
|
961
|
+
toBlock: z10.number().int().nonnegative(),
|
|
962
|
+
force: z10.string().optional().describe("Idempotency suffix to force a duplicate replay of the range")
|
|
679
963
|
}, async ({ id, fromBlock, toBlock, force }) => {
|
|
680
964
|
const res = await clientProvider().subscriptions.replay(id, {
|
|
681
965
|
fromBlock,
|
|
@@ -701,6 +985,9 @@ function createServer() {
|
|
|
701
985
|
registerStreamsTools(server);
|
|
702
986
|
registerContractTools(server);
|
|
703
987
|
registerCodegenTools(server);
|
|
988
|
+
registerInstanceTools(server);
|
|
989
|
+
registerArchiveTools(server);
|
|
990
|
+
registerSetupTools(server);
|
|
704
991
|
const baseUrl = process.env.SECONDLAYER_API_URL || process.env.SL_API_URL || "http://127.0.0.1:3800";
|
|
705
992
|
if (isHostedArchiveUrl(baseUrl)) {
|
|
706
993
|
registerAccountTools(server);
|
|
@@ -712,5 +999,5 @@ export {
|
|
|
712
999
|
createServer
|
|
713
1000
|
};
|
|
714
1001
|
|
|
715
|
-
//# debugId=
|
|
1002
|
+
//# debugId=8023DE628F04195164756E2164756E21
|
|
716
1003
|
//# sourceMappingURL=index.js.map
|