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