@secondlayer/mcp 10.0.1 → 11.1.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/index.js CHANGED
@@ -1,34 +1,3 @@
1
- // src/server.ts
2
- import { readFileSync } from "node:fs";
3
- import { dirname, join as join2 } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
- import { resolveBaseUrl as resolveBaseUrl3 } from "@secondlayer/sdk";
7
-
8
- // src/lib/hosted.ts
9
- var HOSTED_ARCHIVE_HOST = "api.secondlayer.tools";
10
- function isHostedArchiveUrl(url) {
11
- try {
12
- return new URL(url).hostname === HOSTED_ARCHIVE_HOST;
13
- } catch {
14
- return false;
15
- }
16
- }
17
-
18
- // src/resources.ts
19
- import {
20
- ResourceTemplate
21
- } from "@modelcontextprotocol/sdk/server/mcp.js";
22
- import {
23
- isHostedApiUrl as isHostedApiUrl2,
24
- resolveBaseUrl as resolveBaseUrl2
25
- } from "@secondlayer/sdk";
26
- import { CHAIN_TRIGGER_FIELDS } from "@secondlayer/shared";
27
- import { BOOTSTRAP_STEP } from "@secondlayer/shared/archive/instance-diagnosis";
28
- import { TRAIT_STANDARDS } from "@secondlayer/stacks/clarity";
29
- import { camelizeKeys, filterFieldsByType } from "@secondlayer/subgraphs";
30
- import { TYPE_MAP } from "@secondlayer/subgraphs/schema";
31
-
32
1
  // src/lib/client.ts
33
2
  import {
34
3
  SecondLayer,
@@ -75,71 +44,66 @@ function getArchiveOpsClient() {
75
44
  function keyHint(baseUrl = resolveBaseUrl()) {
76
45
  return isHostedApiUrl(baseUrl) ? " — set SECONDLAYER_API_KEY (an sk-sl_ key from secondlayer.tools/account/keys)" : " — set INSTANCE_TOKEN from `sl init` for writes";
77
46
  }
78
- async function apiRequest(method, path, body) {
79
- const baseUrl = resolveBaseUrl();
80
- const apiKey = readApiKey();
81
- const res = await fetch(`${baseUrl}${path}`, {
82
- method,
83
- headers: {
84
- "Content-Type": "application/json",
85
- ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
86
- },
87
- body: body ? JSON.stringify(body) : undefined
88
- });
89
- if (!res.ok) {
90
- const text = await res.text().catch(() => "");
91
- const needsKey = !apiKey && (res.status === 401 || res.status === 403);
92
- let parsed;
93
- try {
94
- parsed = text ? JSON.parse(text) : undefined;
95
- } catch {
96
- parsed = undefined;
97
- }
98
- const code = parsed && typeof parsed === "object" && typeof parsed.code === "string" ? parsed.code : undefined;
99
- throw Object.assign(new Error((text || `HTTP ${res.status}`) + (needsKey ? keyHint(baseUrl) : "")), {
100
- status: res.status,
101
- ...parsed !== undefined ? { body: parsed } : {},
102
- ...code ? { code } : {}
47
+ function createApiRequest(config) {
48
+ const doFetch = config.fetchImpl ?? ((...args) => fetch(...args));
49
+ return async (method, path, body) => {
50
+ const { baseUrl, apiKey } = config;
51
+ const res = await doFetch(`${baseUrl}${path}`, {
52
+ method,
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
56
+ },
57
+ body: body ? JSON.stringify(body) : undefined
103
58
  });
104
- }
105
- if (res.status === 204)
106
- return;
107
- return res.json();
108
- }
109
-
110
- // src/lib/format.ts
111
- function formatSubgraphSummary(s) {
112
- return {
113
- name: s.name,
114
- status: s.status,
115
- tables: Array.isArray(s.tables) ? s.tables : Object.keys(s.tables),
116
- lastProcessedBlock: s.lastProcessedBlock
117
- };
118
- }
119
- function withCap(items, cap) {
120
- return {
121
- items: items.slice(0, cap),
122
- truncated: items.length > cap,
123
- total: items.length
124
- };
125
- }
126
- function jsonResponse(data, isError) {
127
- return {
128
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
129
- ...isError && { isError: true }
59
+ if (!res.ok) {
60
+ const text = await res.text().catch(() => "");
61
+ const needsKey = !apiKey && (res.status === 401 || res.status === 403);
62
+ let parsed;
63
+ try {
64
+ parsed = text ? JSON.parse(text) : undefined;
65
+ } catch {
66
+ parsed = undefined;
67
+ }
68
+ const code = parsed && typeof parsed === "object" && typeof parsed.code === "string" ? parsed.code : undefined;
69
+ throw Object.assign(new Error((text || `HTTP ${res.status}`) + (needsKey ? keyHint(baseUrl) : "")), {
70
+ status: res.status,
71
+ ...parsed !== undefined ? { body: parsed } : {},
72
+ ...code ? { code } : {}
73
+ });
74
+ }
75
+ if (res.status === 204)
76
+ return;
77
+ return res.json();
130
78
  };
131
79
  }
132
- function textResponse(text, isError) {
133
- return {
134
- content: [{ type: "text", text }],
135
- ...isError && { isError: true }
136
- };
80
+ var apiRequest = (method, path, body) => createApiRequest({ baseUrl: resolveBaseUrl(), apiKey: readApiKey() })(method, path, body);
81
+ // src/server.ts
82
+ import { readFileSync } from "node:fs";
83
+ import { dirname, join as join2 } from "node:path";
84
+ import { fileURLToPath } from "node:url";
85
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
86
+ import { resolveBaseUrl as resolveBaseUrl3 } from "@secondlayer/sdk";
87
+
88
+ // src/lib/hosted.ts
89
+ var HOSTED_ARCHIVE_HOST = "api.secondlayer.tools";
90
+ function isHostedArchiveUrl(url) {
91
+ try {
92
+ return new URL(url).hostname === HOSTED_ARCHIVE_HOST;
93
+ } catch {
94
+ return false;
95
+ }
137
96
  }
138
97
 
139
98
  // src/lib/tool.ts
140
99
  var registeredToolNames = new Set;
141
- function getRegisteredToolNames() {
142
- return [...registeredToolNames];
100
+ var serverToolNames = new WeakMap;
101
+ var serverToolFilters = new WeakMap;
102
+ function getRegisteredToolNames(server) {
103
+ return server ? [...serverToolNames.get(server) ?? []] : [...registeredToolNames];
104
+ }
105
+ function restrictTools(server, allow) {
106
+ serverToolFilters.set(server, allow);
143
107
  }
144
108
  function envelopeFields(err) {
145
109
  if (!err || typeof err !== "object")
@@ -177,10 +141,60 @@ function defineTool(server, name, description, schema, handler) {
177
141
  };
178
142
  }
179
143
  };
180
- registeredToolNames.add(name);
144
+ const allow = serverToolFilters.get(server);
145
+ if (allow && !allow(name))
146
+ return;
147
+ if (!allow)
148
+ registeredToolNames.add(name);
149
+ const names = serverToolNames.get(server) ?? new Set;
150
+ names.add(name);
151
+ serverToolNames.set(server, names);
181
152
  server.tool(name, description, schema, wrappedHandler);
182
153
  }
183
154
 
155
+ // src/resources.ts
156
+ import {
157
+ ResourceTemplate
158
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
159
+ import {
160
+ isHostedApiUrl as isHostedApiUrl2,
161
+ resolveBaseUrl as resolveBaseUrl2
162
+ } from "@secondlayer/sdk";
163
+ import { CHAIN_TRIGGER_FIELDS } from "@secondlayer/shared";
164
+ import { BOOTSTRAP_STEP } from "@secondlayer/shared/archive/instance-diagnosis";
165
+ import { TRAIT_STANDARDS } from "@secondlayer/stacks/clarity";
166
+ import { camelizeKeys, filterFieldsByType } from "@secondlayer/subgraphs";
167
+ import { TYPE_MAP } from "@secondlayer/subgraphs/schema";
168
+
169
+ // src/lib/format.ts
170
+ function formatSubgraphSummary(s) {
171
+ return {
172
+ name: s.name,
173
+ status: s.status,
174
+ tables: Array.isArray(s.tables) ? s.tables : Object.keys(s.tables),
175
+ lastProcessedBlock: s.lastProcessedBlock
176
+ };
177
+ }
178
+ function withCap(items, cap) {
179
+ return {
180
+ items: items.slice(0, cap),
181
+ truncated: items.length > cap,
182
+ total: items.length
183
+ };
184
+ }
185
+ function jsonResponse(data, isError) {
186
+ return {
187
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
188
+ ...isError && { isError: true }
189
+ };
190
+ }
191
+ function textResponse(text, isError) {
192
+ return {
193
+ content: [{ type: "text", text }],
194
+ ...isError && { isError: true }
195
+ };
196
+ }
197
+
184
198
  // src/resources.ts
185
199
  var TRAIT_BLURBS = {
186
200
  "sip-009": "Non-fungible token (NFT) standard",
@@ -218,7 +232,6 @@ var PRODUCT_BLURBS = {
218
232
  codegen: "ORM schemas for the tables you read",
219
233
  instance: "decoder health and empty-index diagnosis",
220
234
  archive: "signed archive verify, bootstrap/repair, quote, latest pointer",
221
- setup: "one-command self-host onboarding",
222
235
  credits: "archive credits balance at api.secondlayer.tools"
223
236
  };
224
237
  var PRODUCT_ORDER = [
@@ -231,12 +244,11 @@ var PRODUCT_ORDER = [
231
244
  "codegen",
232
245
  "instance",
233
246
  "archive",
234
- "setup",
235
247
  "credits"
236
248
  ];
237
- function buildCapabilities() {
249
+ function buildCapabilities(server) {
238
250
  const byPrefix = new Map;
239
- for (const name of getRegisteredToolNames()) {
251
+ for (const name of getRegisteredToolNames(server)) {
240
252
  const sep = name.indexOf("_");
241
253
  const prefix = sep === -1 ? name : name.slice(0, sep);
242
254
  const tools = byPrefix.get(prefix) ?? [];
@@ -257,7 +269,11 @@ function buildCapabilities() {
257
269
  discoverFirst: "Call index_discover / contracts_find to learn what exists (and which filters each surface accepts) before querying."
258
270
  };
259
271
  }
260
- function readAuthTiers(credential) {
272
+ function readAuthTiers(credential, hosted) {
273
+ if (hosted) {
274
+ const reads = `keyed: every read uses ${credential} and is metered to the account's credits (the first 1M rows each month are free)`;
275
+ return { index: reads, streams: reads };
276
+ }
261
277
  const reads = `keyless while the API is only reachable on loopback; ${credential} is required once it is published past loopback`;
262
278
  return {
263
279
  index: reads,
@@ -268,8 +284,9 @@ function readAuthTiers(credential) {
268
284
  function credentialLabel() {
269
285
  return isHostedApiUrl2(resolveBaseUrl2()) ? "SECONDLAYER_API_KEY (an sk-sl_ account key)" : "INSTANCE_TOKEN";
270
286
  }
287
+ var HOSTED_CREDENTIAL = "the caller's OAuth sign-in (or sk-sl_ account key)";
271
288
  async function buildContext(deps = { clientProvider: getClient }) {
272
- const credential = credentialLabel();
289
+ const credential = deps.hosted ? HOSTED_CREDENTIAL : credentialLabel();
273
290
  const unavailable = `unavailable: set ${credential}`;
274
291
  const orNull = (field) => {
275
292
  if (!field)
@@ -282,7 +299,7 @@ async function buildContext(deps = { clientProvider: getClient }) {
282
299
  const instance = snap?.instance?.value ? snap.instance.value : orNull(snap?.instance);
283
300
  return {
284
301
  authState: {
285
- apiKeySet: Boolean(readApiKey())
302
+ apiKeySet: deps.hosted ? true : Boolean(readApiKey())
286
303
  },
287
304
  whatExists: {
288
305
  account: orNull(snap?.account),
@@ -293,12 +310,13 @@ async function buildContext(deps = { clientProvider: getClient }) {
293
310
  activeOperations: orNull(snap?.activeOperations),
294
311
  instance
295
312
  },
296
- whatYouCanDo: buildCapabilities(),
297
- readAuthTiers: readAuthTiers(credential),
313
+ whatYouCanDo: buildCapabilities(deps.server),
314
+ readAuthTiers: readAuthTiers(credential, Boolean(deps.hosted)),
298
315
  ...snap?.instance?.value?.state === "empty-index" ? { nextStep: BOOTSTRAP_STEP } : {}
299
316
  };
300
317
  }
301
- function registerResources(server) {
318
+ function registerResources(server, deps = { clientProvider: getClient }) {
319
+ const contextDeps = deps.hosted ? { ...deps, server } : deps;
302
320
  server.resource("context", "secondlayer://context", {
303
321
  description: "Live agent context — what exists (your subgraphs, webhooks, account), what you can do, and read-auth tiers. Read this first."
304
322
  }, async () => ({
@@ -306,7 +324,7 @@ function registerResources(server) {
306
324
  {
307
325
  uri: "secondlayer://context",
308
326
  mimeType: "application/json",
309
- text: JSON.stringify(await buildContext(), null, 2)
327
+ text: JSON.stringify(await buildContext(contextDeps), null, 2)
310
328
  }
311
329
  ]
312
330
  }));
@@ -344,24 +362,26 @@ function registerResources(server) {
344
362
  }
345
363
  ]
346
364
  }));
347
- server.resource("chain-triggers", "secondlayer://chain-triggers", {
348
- description: "Chain-webhook trigger types and the filter fields each accepts (for webhooks_create triggers)."
349
- }, async () => ({
350
- contents: [
351
- {
352
- uri: "secondlayer://chain-triggers",
353
- mimeType: "application/json",
354
- text: JSON.stringify(CHAIN_TRIGGER_FIELDS, null, 2)
355
- }
356
- ]
357
- }));
365
+ if (!deps.hosted) {
366
+ server.resource("chain-triggers", "secondlayer://chain-triggers", {
367
+ description: "Chain-webhook trigger types and the filter fields each accepts (for webhooks_create triggers)."
368
+ }, async () => ({
369
+ contents: [
370
+ {
371
+ uri: "secondlayer://chain-triggers",
372
+ mimeType: "application/json",
373
+ text: JSON.stringify(CHAIN_TRIGGER_FIELDS, null, 2)
374
+ }
375
+ ]
376
+ }));
377
+ }
358
378
  server.resource("samples", new ResourceTemplate("secondlayer://samples/{contractId}", {
359
379
  list: undefined
360
380
  }), {
361
381
  description: "Decoded camelCase print samples for a contract (schema + up to 5 events). Use before writing a print_event handler."
362
382
  }, async (uri, variables) => {
363
383
  const contractId = String(Array.isArray(variables.contractId) ? variables.contractId[0] : variables.contractId ?? "");
364
- const body = await buildSamplesResource(contractId);
384
+ const body = await buildSamplesResource(contractId, deps.clientProvider);
365
385
  return {
366
386
  contents: [
367
387
  {
@@ -426,239 +446,32 @@ async function buildSamplesResource(contractId, clientProvider = getClient) {
426
446
 
427
447
  // src/tools/account.ts
428
448
  import { z } from "zod";
429
- function registerAccountTools(server) {
449
+ function registerAccountTools(server, request = apiRequest) {
430
450
  defineTool(server, "account_whoami", "Show the archive account email at api.secondlayer.tools, not the self-host instance.", {}, async () => {
431
- const result = await apiRequest("GET", "/api/accounts/me");
451
+ const result = await request("GET", "/api/accounts/me");
432
452
  return jsonResponse(result);
433
453
  });
434
454
  defineTool(server, "account_create_key", "Mint an account key (sk-sl_*) for archive credits, quote, and fetch at api.secondlayer.tools. The returned `key` is shown ONCE.", {
435
455
  name: z.string().optional().describe("Optional label for the key")
436
- }, async ({ name }) => jsonResponse(await apiRequest("POST", "/api/keys", { name })));
456
+ }, async ({ name }) => jsonResponse(await request("POST", "/api/keys", { name })));
437
457
  }
438
458
 
439
- // src/tools/archive.ts
459
+ // src/tools/archive-cli.ts
440
460
  import { z as z2 } from "zod";
441
- var FLOW = ["bootstrap", "repair"];
442
- function registerArchiveTools(server, clientProvider = getClient, archiveOpsClientProvider = getArchiveOpsClient) {
443
- 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.", {
444
- against: z2.string().describe("Archive manifest URL"),
445
- fromBlock: z2.number().int().nonnegative().optional().describe("First height (inclusive)"),
446
- toBlock: z2.number().int().nonnegative().optional().describe("Last height (inclusive)"),
447
- target: z2.string().optional().describe("Verify target (default raw)")
448
- }, async ({ against, fromBlock, toBlock, target }) => jsonResponse(await clientProvider().archive.verify({
449
- against,
450
- fromBlock,
451
- toBlock,
452
- target
453
- })));
454
- 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.", {
455
- against: z2.string().optional().describe("Override latest.json URL (default official tree)")
456
- }, async ({ against }) => jsonResponse(await archiveOpsClientProvider().archive.latest(against)));
457
- 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.", {
458
- paths: z2.array(z2.string()).describe("Partition paths to price"),
459
- flow: z2.enum(FLOW).describe("bootstrap or repair")
460
- }, async ({ paths, flow }) => jsonResponse(await archiveOpsClientProvider().archive.quote({ paths, flow })));
461
- 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()));
462
- }
463
-
464
- // src/tools/codegen.ts
465
- import {
466
- generateIndexSchema
467
- } from "@secondlayer/subgraphs";
468
- import { z as z3 } from "zod";
469
- var TARGETS = ["prisma", "kysely", "drizzle", "json-schema"];
470
- function registerCodegenTools(server) {
471
- 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.", {
472
- target: z3.enum(TARGETS).optional().describe("Output flavor (default kysely)"),
473
- tables: z3.array(z3.string()).optional().describe("Restrict to these Index tables (default: all)"),
474
- schemaName: z3.string().optional().describe("Postgres schema to qualify table names with"),
475
- datasourceEnv: z3.string().optional().describe("Prisma only: datasource url env var (default DATABASE_URL)")
476
- }, ({ target, tables, schemaName, datasourceEnv }) => textResponse(generateIndexSchema(target ?? "kysely", {
477
- tables,
478
- schemaName,
479
- datasourceEnv
480
- })));
481
- }
482
-
483
- // src/tools/contracts.ts
484
- import { z as z4 } from "zod";
485
- function registerContractTools(server, clientProvider = getClient) {
486
- 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.', {
487
- trait: z4.string().describe('Required. Trait to match (e.g. "sip-010").'),
488
- conformance: z4.enum(["declared", "inferred", "any"]).optional().describe("declared = parsed from source, inferred = ABI shape-match, any = either (default)"),
489
- include: z4.literal("abi").optional().describe(`Set to "abi" to include each contract's full ABI`),
490
- limit: z4.number().optional().describe("Page size, 1–500 (default 100)"),
491
- cursor: z4.string().optional().describe("Opaque cursor from a prior response's next_cursor")
492
- }, async (params) => jsonResponse(await clientProvider().contracts.list(params)));
493
- 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.", {
494
- contractId: z4.string().describe("Fully qualified contract id (e.g. SP….amm-pool-v2-01)")
495
- }, async ({ contractId }) => {
496
- const contract = await clientProvider().contracts.get(contractId, {
497
- includeAbi: true
498
- });
499
- return contract ? jsonResponse(contract) : jsonResponse({
500
- error: {
501
- type: "not_found",
502
- status: 404,
503
- message: `Contract not in registry: ${contractId}`
504
- }
505
- }, true);
506
- });
507
- }
508
-
509
- // src/tools/index.ts
510
- import { DECODED_EVENT_TYPES } from "@secondlayer/shared";
511
- import { z as z5 } from "zod";
512
- var INDEX_EVENT_TYPES = DECODED_EVENT_TYPES;
513
- var rangeFilters = {
514
- contractId: z5.string().optional().describe("Filter by contract id"),
515
- fromHeight: z5.number().optional().describe("Start block height (inclusive)"),
516
- toHeight: z5.number().optional().describe("End block height (inclusive)"),
517
- cursor: z5.string().optional().describe("Opaque cursor from a prior response's next_cursor"),
518
- limit: z5.number().optional().describe("Max rows for this page")
519
- };
520
- var heightFilters = {
521
- fromHeight: rangeFilters.fromHeight,
522
- toHeight: rangeFilters.toHeight,
523
- cursor: rangeFilters.cursor,
524
- limit: rangeFilters.limit
525
- };
526
- function registerIndexTools(server, clientProvider = getClient) {
527
- defineTool(server, "index_ft_transfers", "List decoded SIP-010 fungible-token transfers from Index. Reads are keyless.", {
528
- ...rangeFilters,
529
- sender: z5.string().optional().describe("Filter by sender principal"),
530
- recipient: z5.string().optional().describe("Filter by recipient principal"),
531
- assetIdentifier: z5.string().optional().describe("Filter by asset identifier (contract::asset), e.g. SP…sbtc-token::sbtc-token")
532
- }, async (params) => jsonResponse(await clientProvider().index.ftTransfers.list(params)));
533
- defineTool(server, "index_nft_transfers", "List decoded SIP-009 non-fungible-token transfers from Index. Reads are keyless.", {
534
- ...rangeFilters,
535
- sender: z5.string().optional().describe("Filter by sender principal"),
536
- recipient: z5.string().optional().describe("Filter by recipient principal"),
537
- assetIdentifier: z5.string().optional().describe("Filter by asset identifier (contract::asset)")
538
- }, async (params) => jsonResponse(await clientProvider().index.nftTransfers.list(params)));
539
- 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.", {
540
- eventType: z5.enum(INDEX_EVENT_TYPES).describe("Required. Decoded event type to list."),
541
- ...rangeFilters,
542
- sender: z5.string().optional().describe("Filter by sender principal"),
543
- recipient: z5.string().optional().describe("Filter by recipient principal"),
544
- assetIdentifier: z5.string().optional().describe("Filter by asset identifier where applicable"),
545
- 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."),
546
- 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.")
547
- }, async (params) => jsonResponse(await clientProvider().index.events.list(params)));
548
- 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.", {
549
- ...rangeFilters,
550
- functionName: z5.string().optional().describe("Filter by called function name"),
551
- sender: z5.string().optional().describe("Filter by caller principal"),
552
- trait: z5.string().optional().describe("Match contracts conforming to a trait/standard (e.g. sip-010). Mutually exclusive with contractId.")
553
- }, async (params) => jsonResponse(await clientProvider().index.contractCalls.list(params)));
554
- defineTool(server, "index_blocks", "List decoded blocks from Index. Reads are keyless.", { ...heightFilters }, async (params) => jsonResponse(await clientProvider().index.blocks.list(params)));
555
- defineTool(server, "index_transactions", "List decoded transactions from Index. Filter by type, sender, or contract. Reads are keyless.", {
556
- ...rangeFilters,
557
- type: z5.string().optional().describe("Filter by transaction type"),
558
- sender: z5.string().optional().describe("Filter by sender principal")
559
- }, async (params) => jsonResponse(await clientProvider().index.transactions.list(params)));
560
- 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.", {
561
- contractId: z5.string().describe("Contract id (SP….name) whose print events to profile")
562
- }, async (params) => jsonResponse(await clientProvider().index.printSchema(params.contractId)));
563
- 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()));
564
- 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.", {
565
- requests: z5.array(z5.object({
566
- path: z5.string().describe("Public /v1 read path"),
567
- params: z5.record(z5.string(), z5.string()).optional()
568
- })).min(1).max(10).describe("Read descriptors, executed concurrently")
569
- }, async (params) => jsonResponse(await clientProvider().batch(params.requests)));
570
- }
571
-
572
- // src/tools/instance.ts
573
- function registerInstanceTools(server, clientProvider = getClient) {
574
- 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 () => {
575
- const client = clientProvider();
576
- const status = await client.instance.status();
577
- const diagnosis = await client.instance.diagnose();
578
- return jsonResponse({ status, diagnosis });
579
- });
580
- }
581
-
582
- // src/tools/scaffold.ts
583
- import {
584
- generatePrintSchemaSubgraph,
585
- generateSubgraphCode,
586
- generateTokenSubgraphFromAbi
587
- } from "@secondlayer/scaffold";
588
- import { z as z6 } from "zod";
589
- async function fetchAbi(clientProvider, contractId) {
590
- const contract = await clientProvider().contracts.get(contractId, {
591
- includeAbi: true
592
- });
593
- if (!contract)
594
- throw new Error(`Contract not found: ${contractId}`);
595
- const abi = contract.abi;
596
- if (!abi) {
597
- throw new Error(`No ABI available for ${contractId} (abi_status: ${contract.abi_status})`);
598
- }
599
- return { functions: abi.functions ?? [], abi };
600
- }
601
- function registerScaffoldTools(server, clientProvider = getClient) {
602
- defineTool(server, "subgraphs_scaffold", "Generate a subgraph from observed print events when available; ABI token transfers or public functions only when the contract has never printed. Review the TypeScript, run subgraphs_test until ok: true with written >= 1, read skipped, then subgraphs_deploy. dryRun is DDL only.", {
603
- contractId: z6.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
604
- subgraphName: z6.string().optional().describe("Override the subgraph name (defaults to contract name)")
605
- }, async ({ contractId, subgraphName }) => {
606
- const printSchema = await clientProvider().index.printSchema(contractId);
607
- if (printSchema && printSchema.topics.length > 0) {
608
- const code = generatePrintSchemaSubgraph({
609
- contractId,
610
- name: subgraphName,
611
- topics: printSchema.topics,
612
- sample: printSchema.sample
613
- });
614
- return { content: [{ type: "text", text: code }] };
615
- }
616
- const { functions, abi } = await fetchAbi(clientProvider, contractId);
617
- const token = generateTokenSubgraphFromAbi({
618
- contractId,
619
- abi,
620
- name: subgraphName
621
- });
622
- if (token) {
623
- return { content: [{ type: "text", text: token }] };
624
- }
625
- const code = generateSubgraphCode(contractId, functions, subgraphName);
626
- return { content: [{ type: "text", text: code }] };
627
- });
628
- }
629
-
630
- // src/tools/setup.ts
631
- import { z as z7 } from "zod";
632
461
 
633
462
  // src/lib/exec-cli.ts
634
463
  import {
635
464
  spawn
636
465
  } from "node:child_process";
637
- var ALLOWED = ["setup", "bootstrap", "repair", "verify"];
466
+ var ALLOWED = ["bootstrap", "repair", "verify"];
638
467
  var TIMEOUT_MS = {
639
- setup: 15 * 60 * 1000,
640
468
  bootstrap: 10 * 60 * 1000,
641
469
  repair: 10 * 60 * 1000,
642
470
  verify: 2 * 60 * 1000
643
471
  };
644
- var VALUE_FLAGS = new Set([
645
- "--against",
646
- "--from-block",
647
- "--to-block",
648
- "--network",
649
- "--node-mode",
650
- "--dir"
651
- ]);
652
- var BOOL_FLAGS = new Set(["--yes", "--json", "--apply", "--skip-bootstrap"]);
472
+ var VALUE_FLAGS = new Set(["--against", "--from-block", "--to-block"]);
473
+ var BOOL_FLAGS = new Set(["--yes", "--json", "--apply"]);
653
474
  var FLAGS_BY_CMD = {
654
- setup: new Set([
655
- "--yes",
656
- "--network",
657
- "--node-mode",
658
- "--against",
659
- "--dir",
660
- "--skip-bootstrap"
661
- ]),
662
475
  bootstrap: new Set([
663
476
  "--against",
664
477
  "--yes",
@@ -733,12 +546,8 @@ async function execSecondlayer(subcommand, args, deps = {}) {
733
546
  }
734
547
  assertSafeArgs(subcommand, args);
735
548
  const argv = [subcommand, ...args];
736
- if (subcommand === "setup") {
737
- if (!argv.includes("--yes"))
738
- argv.push("--yes");
739
- } else if (!argv.includes("--json")) {
549
+ if (!argv.includes("--json"))
740
550
  argv.push("--json");
741
- }
742
551
  const env = deps.env ?? process.env;
743
552
  const bin = env.SECONDLAYER_BIN || "secondlayer";
744
553
  const cwd = deps.cwd ?? env.SECONDLAYER_CWD ?? process.cwd();
@@ -786,31 +595,15 @@ timeout after ${timeout}ms`.trim()
786
595
  });
787
596
  }
788
597
 
789
- // src/tools/setup.ts
598
+ // src/tools/archive-cli.ts
790
599
  function cliResponse(result) {
791
600
  return jsonResponse(result, result.code !== 0);
792
601
  }
793
- function registerSetupTools(server, exec = execSecondlayer) {
794
- 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.", {
795
- network: z7.enum(["mainnet", "testnet", "devnet"]).describe("Stacks network"),
796
- nodeMode: z7.enum(["external", "stacks", "full"]).describe("Node mode: external, stacks, or full"),
797
- against: z7.string().optional().describe("Archive manifest URL"),
798
- dir: z7.string().optional().describe("Compose project directory"),
799
- skipBootstrap: z7.boolean().optional().describe("Sync from genesis instead of restoring an archive")
800
- }, async ({ network, nodeMode, against, dir, skipBootstrap }) => {
801
- const args = ["--yes", "--network", network, "--node-mode", nodeMode];
802
- if (against !== undefined)
803
- args.push("--against", against);
804
- if (dir !== undefined)
805
- args.push("--dir", dir);
806
- if (skipBootstrap)
807
- args.push("--skip-bootstrap");
808
- return cliResponse(await exec("setup", args));
809
- });
602
+ function registerArchiveCliTools(server, exec = execSecondlayer) {
810
603
  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.", {
811
- against: z7.string().describe("Archive manifest URL"),
812
- fromBlock: z7.number().int().nonnegative().optional().describe("Restore from this height instead of genesis"),
813
- toBlock: z7.number().int().nonnegative().optional().describe("Stop at this height instead of the archive tip")
604
+ against: z2.string().describe("Archive manifest URL"),
605
+ fromBlock: z2.number().int().nonnegative().optional().describe("Restore from this height instead of genesis"),
606
+ toBlock: z2.number().int().nonnegative().optional().describe("Stop at this height instead of the archive tip")
814
607
  }, async ({ against, fromBlock, toBlock }) => {
815
608
  const args = ["--against", against, "--yes", "--json"];
816
609
  if (fromBlock !== undefined)
@@ -820,10 +613,10 @@ function registerSetupTools(server, exec = execSecondlayer) {
820
613
  return cliResponse(await exec("bootstrap", args));
821
614
  });
822
615
  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.", {
823
- against: z7.string().describe("Archive manifest URL"),
824
- apply: z7.boolean().optional().describe("Write the repair (default plan-only)"),
825
- fromBlock: z7.number().int().nonnegative().optional().describe("First height to consider"),
826
- toBlock: z7.number().int().nonnegative().optional().describe("Last height to consider")
616
+ against: z2.string().describe("Archive manifest URL"),
617
+ apply: z2.boolean().optional().describe("Write the repair (default plan-only)"),
618
+ fromBlock: z2.number().int().nonnegative().optional().describe("First height to consider"),
619
+ toBlock: z2.number().int().nonnegative().optional().describe("Last height to consider")
827
620
  }, async ({ against, apply, fromBlock, toBlock }) => {
828
621
  const args = ["--against", against, "--json"];
829
622
  if (apply)
@@ -836,6 +629,197 @@ function registerSetupTools(server, exec = execSecondlayer) {
836
629
  });
837
630
  }
838
631
 
632
+ // src/tools/archive.ts
633
+ import { z as z3 } from "zod";
634
+ var FLOW = ["bootstrap", "repair"];
635
+ function registerArchiveTools(server, clientProvider = getClient, archiveOpsClientProvider = getArchiveOpsClient) {
636
+ 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.", {
637
+ against: z3.string().describe("Archive manifest URL"),
638
+ fromBlock: z3.number().int().nonnegative().optional().describe("First height (inclusive)"),
639
+ toBlock: z3.number().int().nonnegative().optional().describe("Last height (inclusive)"),
640
+ target: z3.string().optional().describe("Verify target (default raw)")
641
+ }, async ({ against, fromBlock, toBlock, target }) => jsonResponse(await clientProvider().archive.verify({
642
+ against,
643
+ fromBlock,
644
+ toBlock,
645
+ target
646
+ })));
647
+ 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.", {
648
+ against: z3.string().optional().describe("Override latest.json URL (default official tree)")
649
+ }, async ({ against }) => jsonResponse(await archiveOpsClientProvider().archive.latest(against)));
650
+ 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.", {
651
+ paths: z3.array(z3.string()).describe("Partition paths to price"),
652
+ flow: z3.enum(FLOW).describe("bootstrap or repair")
653
+ }, async ({ paths, flow }) => jsonResponse(await archiveOpsClientProvider().archive.quote({ paths, flow })));
654
+ 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()));
655
+ }
656
+
657
+ // src/tools/codegen.ts
658
+ import {
659
+ generateIndexSchema
660
+ } from "@secondlayer/subgraphs";
661
+ import { z as z4 } from "zod";
662
+ var TARGETS = ["prisma", "kysely", "drizzle", "json-schema"];
663
+ function registerCodegenTools(server) {
664
+ 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.", {
665
+ target: z4.enum(TARGETS).optional().describe("Output flavor (default kysely)"),
666
+ tables: z4.array(z4.string()).optional().describe("Restrict to these Index tables (default: all)"),
667
+ schemaName: z4.string().optional().describe("Postgres schema to qualify table names with"),
668
+ datasourceEnv: z4.string().optional().describe("Prisma only: datasource url env var (default DATABASE_URL)")
669
+ }, ({ target, tables, schemaName, datasourceEnv }) => textResponse(generateIndexSchema(target ?? "kysely", {
670
+ tables,
671
+ schemaName,
672
+ datasourceEnv
673
+ })));
674
+ }
675
+
676
+ // src/tools/contracts.ts
677
+ import { z as z5 } from "zod";
678
+ function registerContractTools(server, clientProvider = getClient) {
679
+ 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.', {
680
+ trait: z5.string().describe('Required. Trait to match (e.g. "sip-010").'),
681
+ conformance: z5.enum(["declared", "inferred", "any"]).optional().describe("declared = parsed from source, inferred = ABI shape-match, any = either (default)"),
682
+ include: z5.literal("abi").optional().describe(`Set to "abi" to include each contract's full ABI`),
683
+ limit: z5.number().optional().describe("Page size, 1–500 (default 100)"),
684
+ cursor: z5.string().optional().describe("Opaque cursor from a prior response's next_cursor")
685
+ }, async (params) => jsonResponse(await clientProvider().contracts.list(params)));
686
+ 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.", {
687
+ contractId: z5.string().describe("Fully qualified contract id (e.g. SP….amm-pool-v2-01)")
688
+ }, async ({ contractId }) => {
689
+ const contract = await clientProvider().contracts.get(contractId, {
690
+ includeAbi: true
691
+ });
692
+ return contract ? jsonResponse(contract) : jsonResponse({
693
+ error: {
694
+ type: "not_found",
695
+ status: 404,
696
+ message: `Contract not in registry: ${contractId}`
697
+ }
698
+ }, true);
699
+ });
700
+ }
701
+
702
+ // src/tools/index.ts
703
+ import { DECODED_EVENT_TYPES } from "@secondlayer/shared";
704
+ import { z as z6 } from "zod";
705
+ var INDEX_EVENT_TYPES = DECODED_EVENT_TYPES;
706
+ var rangeFilters = {
707
+ contractId: z6.string().optional().describe("Filter by contract id"),
708
+ fromHeight: z6.number().optional().describe("Start block height (inclusive)"),
709
+ toHeight: z6.number().optional().describe("End block height (inclusive)"),
710
+ cursor: z6.string().optional().describe("Opaque cursor from a prior response's next_cursor"),
711
+ limit: z6.number().optional().describe("Max rows for this page")
712
+ };
713
+ var heightFilters = {
714
+ fromHeight: rangeFilters.fromHeight,
715
+ toHeight: rangeFilters.toHeight,
716
+ cursor: rangeFilters.cursor,
717
+ limit: rangeFilters.limit
718
+ };
719
+ function registerIndexTools(server, clientProvider = getClient) {
720
+ defineTool(server, "index_ft_transfers", "List decoded SIP-010 fungible-token transfers from Index. Reads are keyless.", {
721
+ ...rangeFilters,
722
+ sender: z6.string().optional().describe("Filter by sender principal"),
723
+ recipient: z6.string().optional().describe("Filter by recipient principal"),
724
+ assetIdentifier: z6.string().optional().describe("Filter by asset identifier (contract::asset), e.g. SP…sbtc-token::sbtc-token")
725
+ }, async (params) => jsonResponse(await clientProvider().index.ftTransfers.list(params)));
726
+ defineTool(server, "index_nft_transfers", "List decoded SIP-009 non-fungible-token transfers from Index. Reads are keyless.", {
727
+ ...rangeFilters,
728
+ sender: z6.string().optional().describe("Filter by sender principal"),
729
+ recipient: z6.string().optional().describe("Filter by recipient principal"),
730
+ assetIdentifier: z6.string().optional().describe("Filter by asset identifier (contract::asset)")
731
+ }, async (params) => jsonResponse(await clientProvider().index.nftTransfers.list(params)));
732
+ 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.", {
733
+ eventType: z6.enum(INDEX_EVENT_TYPES).describe("Required. Decoded event type to list."),
734
+ ...rangeFilters,
735
+ sender: z6.string().optional().describe("Filter by sender principal"),
736
+ recipient: z6.string().optional().describe("Filter by recipient principal"),
737
+ assetIdentifier: z6.string().optional().describe("Filter by asset identifier where applicable"),
738
+ trait: z6.string().optional().describe("Match contracts conforming to a trait/standard (e.g. sip-010). Mutually exclusive with contractId; contract-keyed event types only."),
739
+ txContext: z6.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.")
740
+ }, async (params) => jsonResponse(await clientProvider().index.events.list(params)));
741
+ 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.", {
742
+ ...rangeFilters,
743
+ functionName: z6.string().optional().describe("Filter by called function name"),
744
+ sender: z6.string().optional().describe("Filter by caller principal"),
745
+ trait: z6.string().optional().describe("Match contracts conforming to a trait/standard (e.g. sip-010). Mutually exclusive with contractId.")
746
+ }, async (params) => jsonResponse(await clientProvider().index.contractCalls.list(params)));
747
+ defineTool(server, "index_blocks", "List decoded blocks from Index. Reads are keyless.", { ...heightFilters }, async (params) => jsonResponse(await clientProvider().index.blocks.list(params)));
748
+ defineTool(server, "index_transactions", "List decoded transactions from Index. Filter by type, sender, or contract. Reads are keyless.", {
749
+ ...rangeFilters,
750
+ type: z6.string().optional().describe("Filter by transaction type"),
751
+ sender: z6.string().optional().describe("Filter by sender principal")
752
+ }, async (params) => jsonResponse(await clientProvider().index.transactions.list(params)));
753
+ 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.", {
754
+ contractId: z6.string().describe("Contract id (SP….name) whose print events to profile")
755
+ }, async (params) => jsonResponse(await clientProvider().index.printSchema(params.contractId)));
756
+ 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()));
757
+ 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.", {
758
+ requests: z6.array(z6.object({
759
+ path: z6.string().describe("Public /v1 read path"),
760
+ params: z6.record(z6.string(), z6.string()).optional()
761
+ })).min(1).max(10).describe("Read descriptors, executed concurrently")
762
+ }, async (params) => jsonResponse(await clientProvider().batch(params.requests)));
763
+ }
764
+
765
+ // src/tools/instance.ts
766
+ function registerInstanceTools(server, clientProvider = getClient) {
767
+ defineTool(server, "instance_status", "Decoder health and empty-index diagnosis from GET /public/status. If state is empty-index, next tool is archive_bootstrap. 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 () => {
768
+ const client = clientProvider();
769
+ const status = await client.instance.status();
770
+ const diagnosis = await client.instance.diagnose();
771
+ return jsonResponse({ status, diagnosis });
772
+ });
773
+ }
774
+
775
+ // src/tools/scaffold.ts
776
+ import {
777
+ generatePrintSchemaSubgraph,
778
+ generateSubgraphCode,
779
+ generateTokenSubgraphFromAbi
780
+ } from "@secondlayer/scaffold";
781
+ import { z as z7 } from "zod";
782
+ async function fetchAbi(clientProvider, contractId) {
783
+ const contract = await clientProvider().contracts.get(contractId, {
784
+ includeAbi: true
785
+ });
786
+ if (!contract)
787
+ throw new Error(`Contract not found: ${contractId}`);
788
+ const abi = contract.abi;
789
+ if (!abi) {
790
+ throw new Error(`No ABI available for ${contractId} (abi_status: ${contract.abi_status})`);
791
+ }
792
+ return { functions: abi.functions ?? [], abi };
793
+ }
794
+ function registerScaffoldTools(server, clientProvider = getClient) {
795
+ defineTool(server, "subgraphs_scaffold", "Generate a subgraph from observed print events when available; ABI token transfers or public functions only when the contract has never printed. Review the TypeScript, run subgraphs_test until ok: true with written >= 1, read skipped, then subgraphs_deploy. dryRun is DDL only.", {
796
+ contractId: z7.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
797
+ subgraphName: z7.string().optional().describe("Override the subgraph name (defaults to contract name)")
798
+ }, async ({ contractId, subgraphName }) => {
799
+ const printSchema = await clientProvider().index.printSchema(contractId);
800
+ if (printSchema && printSchema.topics.length > 0) {
801
+ const code = generatePrintSchemaSubgraph({
802
+ contractId,
803
+ name: subgraphName,
804
+ topics: printSchema.topics,
805
+ sample: printSchema.sample
806
+ });
807
+ return { content: [{ type: "text", text: code }] };
808
+ }
809
+ const { functions, abi } = await fetchAbi(clientProvider, contractId);
810
+ const token = generateTokenSubgraphFromAbi({
811
+ contractId,
812
+ abi,
813
+ name: subgraphName
814
+ });
815
+ if (token) {
816
+ return { content: [{ type: "text", text: token }] };
817
+ }
818
+ const code = generateSubgraphCode(contractId, functions, subgraphName);
819
+ return { content: [{ type: "text", text: code }] };
820
+ });
821
+ }
822
+
839
823
  // src/tools/streams.ts
840
824
  function registerStreamsTools(server, clientProvider = getClient) {
841
825
  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()));
@@ -1174,40 +1158,109 @@ function registerWebhookTools(server, clientProvider = getClient) {
1174
1158
  }
1175
1159
 
1176
1160
  // src/tool-groups.ts
1161
+ var ENV_TOOL_DEPS = {
1162
+ clientProvider: getClient,
1163
+ archiveOpsClientProvider: getArchiveOpsClient,
1164
+ apiRequest
1165
+ };
1177
1166
  var TOOL_GROUPS = [
1178
- { group: "scaffold", register: registerScaffoldTools },
1179
- { group: "subgraphs", register: registerSubgraphTools },
1180
- { group: "webhooks", register: registerWebhookTools },
1181
- { group: "index", register: registerIndexTools },
1182
- { group: "streams", register: registerStreamsTools },
1183
- { group: "contracts", register: registerContractTools },
1184
- { group: "codegen", register: registerCodegenTools },
1185
- { group: "instance", register: registerInstanceTools },
1186
- { group: "archive", register: registerArchiveTools },
1187
- { group: "setup", register: registerSetupTools },
1188
- { group: "account", register: registerAccountTools, hostedOnly: true }
1167
+ {
1168
+ group: "scaffold",
1169
+ register: (s, d = ENV_TOOL_DEPS) => registerScaffoldTools(s, d.clientProvider),
1170
+ hosted: true
1171
+ },
1172
+ {
1173
+ group: "subgraphs",
1174
+ register: (s, d = ENV_TOOL_DEPS) => registerSubgraphTools(s, d.clientProvider),
1175
+ hosted: false
1176
+ },
1177
+ {
1178
+ group: "webhooks",
1179
+ register: (s, d = ENV_TOOL_DEPS) => registerWebhookTools(s, d.clientProvider),
1180
+ hosted: false
1181
+ },
1182
+ {
1183
+ group: "index",
1184
+ register: (s, d = ENV_TOOL_DEPS) => registerIndexTools(s, d.clientProvider),
1185
+ hosted: true
1186
+ },
1187
+ {
1188
+ group: "streams",
1189
+ register: (s, d = ENV_TOOL_DEPS) => registerStreamsTools(s, d.clientProvider),
1190
+ hosted: true
1191
+ },
1192
+ {
1193
+ group: "contracts",
1194
+ register: (s, d = ENV_TOOL_DEPS) => registerContractTools(s, d.clientProvider),
1195
+ hosted: true
1196
+ },
1197
+ { group: "codegen", register: (s) => registerCodegenTools(s), hosted: false },
1198
+ {
1199
+ group: "instance",
1200
+ register: (s, d = ENV_TOOL_DEPS) => registerInstanceTools(s, d.clientProvider),
1201
+ hosted: false
1202
+ },
1203
+ {
1204
+ group: "archive",
1205
+ register: (s, d = ENV_TOOL_DEPS) => registerArchiveTools(s, d.clientProvider, d.archiveOpsClientProvider),
1206
+ hosted: false
1207
+ },
1208
+ {
1209
+ group: "archive",
1210
+ register: (s) => registerArchiveCliTools(s),
1211
+ hosted: false
1212
+ },
1213
+ {
1214
+ group: "account",
1215
+ register: (s, d = ENV_TOOL_DEPS) => registerAccountTools(s, d.apiRequest),
1216
+ hostedOnly: true,
1217
+ hosted: ["account_whoami"]
1218
+ }
1189
1219
  ];
1220
+ function isHostedTool(group, tool) {
1221
+ return group.hosted === true || Array.isArray(group.hosted) && group.hosted.includes(tool);
1222
+ }
1190
1223
 
1191
1224
  // src/server.ts
1192
1225
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
1193
1226
  var pkg = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
1194
- function createServer() {
1227
+ function createServer(options = {}) {
1195
1228
  const server = new McpServer({
1196
1229
  name: "secondlayer",
1197
1230
  version: pkg.version
1198
1231
  });
1199
- const hosted = isHostedArchiveUrl(resolveBaseUrl3());
1232
+ const deps = {
1233
+ clientProvider: options.clientProvider ?? ENV_TOOL_DEPS.clientProvider,
1234
+ archiveOpsClientProvider: options.archiveOpsClientProvider ?? ENV_TOOL_DEPS.archiveOpsClientProvider,
1235
+ apiRequest: options.apiRequest ?? ENV_TOOL_DEPS.apiRequest
1236
+ };
1237
+ if (options.hosted) {
1238
+ for (const group of TOOL_GROUPS) {
1239
+ if (group.hosted === false)
1240
+ continue;
1241
+ restrictTools(server, (name) => isHostedTool(group, name));
1242
+ group.register(server, deps);
1243
+ }
1244
+ restrictTools(server, () => false);
1245
+ registerResources(server, {
1246
+ clientProvider: deps.clientProvider,
1247
+ hosted: true
1248
+ });
1249
+ return server;
1250
+ }
1251
+ const hostedApi = isHostedArchiveUrl(resolveBaseUrl3());
1200
1252
  for (const { register, hostedOnly } of TOOL_GROUPS) {
1201
- if (hostedOnly && !hosted)
1253
+ if (hostedOnly && !hostedApi)
1202
1254
  continue;
1203
- register(server);
1255
+ register(server, deps);
1204
1256
  }
1205
- registerResources(server);
1257
+ registerResources(server, { clientProvider: deps.clientProvider });
1206
1258
  return server;
1207
1259
  }
1208
1260
  export {
1261
+ createApiRequest,
1209
1262
  createServer
1210
1263
  };
1211
1264
 
1212
- //# debugId=78ED482E1141F79264756E2164756E21
1265
+ //# debugId=F7A822219FC727F864756E2164756E21
1213
1266
  //# sourceMappingURL=index.js.map