@secondlayer/mcp 7.4.0 → 7.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -5,7 +5,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
 
6
6
  // src/server.ts
7
7
  import { readFileSync } from "node:fs";
8
- import { dirname, join } from "node:path";
8
+ import { dirname, join as join2 } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
11
 
@@ -20,10 +20,13 @@ function isHostedArchiveUrl(url) {
20
20
  }
21
21
 
22
22
  // src/resources.ts
23
+ import {
24
+ ResourceTemplate
25
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
23
26
  import { CHAIN_TRIGGER_FIELDS } from "@secondlayer/shared";
24
27
  import { BOOTSTRAP_STEP } from "@secondlayer/shared/archive/instance-diagnosis";
25
28
  import { TRAIT_STANDARDS } from "@secondlayer/stacks/clarity";
26
- import { filterFieldsByType } from "@secondlayer/subgraphs";
29
+ import { camelizeKeys, filterFieldsByType } from "@secondlayer/subgraphs";
27
30
  import { TYPE_MAP } from "@secondlayer/subgraphs/schema";
28
31
 
29
32
  // src/lib/client.ts
@@ -319,6 +322,73 @@ function registerResources(server) {
319
322
  }
320
323
  ]
321
324
  }));
325
+ server.resource("samples", new ResourceTemplate("secondlayer://samples/{contractId}", {
326
+ list: undefined
327
+ }), {
328
+ description: "Decoded camelCase print samples for a contract (schema + up to 5 events). Use before writing a print_event handler."
329
+ }, async (uri, variables) => {
330
+ const contractId = String(Array.isArray(variables.contractId) ? variables.contractId[0] : variables.contractId ?? "");
331
+ const body = await buildSamplesResource(contractId);
332
+ return {
333
+ contents: [
334
+ {
335
+ uri: uri.href,
336
+ mimeType: "application/json",
337
+ text: JSON.stringify(body, null, 2)
338
+ }
339
+ ]
340
+ };
341
+ });
342
+ }
343
+ function jsonSafe(value) {
344
+ if (typeof value === "bigint")
345
+ return value.toString();
346
+ if (value === null || value === undefined)
347
+ return value;
348
+ if (Array.isArray(value))
349
+ return value.map(jsonSafe);
350
+ if (typeof value === "object") {
351
+ const out = {};
352
+ for (const [k, v] of Object.entries(value)) {
353
+ out[k] = jsonSafe(v);
354
+ }
355
+ return out;
356
+ }
357
+ return value;
358
+ }
359
+ async function buildSamplesResource(contractId, clientProvider = getClient) {
360
+ const client = clientProvider();
361
+ const printSchema = await client.index.printSchema(contractId);
362
+ const envelope = await client.index.events.list({
363
+ eventType: "print",
364
+ contractId,
365
+ limit: 5
366
+ });
367
+ const schema = printSchema?.topics.map((t) => ({
368
+ topic: t.topic,
369
+ fields: t.fields.map((f) => ({
370
+ camel: f.camel_name,
371
+ snake: f.name.replace(/-/g, "_"),
372
+ column_type: f.column_type,
373
+ always: f.always_present
374
+ }))
375
+ })) ?? [];
376
+ const samples = (envelope.events ?? []).slice(0, 5).map((ev) => {
377
+ const payload = ev.payload;
378
+ const data = jsonSafe(camelizeKeys(payload?.value));
379
+ return {
380
+ txId: ev.tx_id,
381
+ height: ev.block_height,
382
+ topic: payload?.topic ?? "",
383
+ data: data ?? {}
384
+ };
385
+ });
386
+ return {
387
+ contractId,
388
+ handlerShape: "event.data is camelCase; columns are snake_case",
389
+ schema,
390
+ samples
391
+ };
322
392
  }
323
393
 
324
394
  // src/tools/account.ts
@@ -477,7 +547,11 @@ function registerInstanceTools(server, clientProvider = getClient) {
477
547
  }
478
548
 
479
549
  // src/tools/scaffold.ts
480
- import { generateSubgraphCode } from "@secondlayer/scaffold";
550
+ import {
551
+ generatePrintSchemaSubgraph,
552
+ generateSubgraphCode,
553
+ generateTokenSubgraphFromAbi
554
+ } from "@secondlayer/scaffold";
481
555
  import { z as z6 } from "zod";
482
556
  async function fetchAbi(clientProvider, contractId) {
483
557
  const contract = await clientProvider().contracts.get(contractId, {
@@ -489,14 +563,32 @@ async function fetchAbi(clientProvider, contractId) {
489
563
  if (!abi) {
490
564
  throw new Error(`No ABI available for ${contractId} (abi_status: ${contract.abi_status})`);
491
565
  }
492
- return { functions: abi.functions ?? [] };
566
+ return { functions: abi.functions ?? [], abi };
493
567
  }
494
568
  function registerScaffoldTools(server, clientProvider = getClient) {
495
- defineTool(server, "subgraphs_scaffold", "Generate a subgraph scaffold from a deployed Stacks contract. Fetches the ABI automatically. Returns TypeScript source — review it, then pass it to subgraphs_deploy.", {
569
+ 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.", {
496
570
  contractId: z6.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
497
571
  subgraphName: z6.string().optional().describe("Override the subgraph name (defaults to contract name)")
498
572
  }, async ({ contractId, subgraphName }) => {
499
- const { functions } = await fetchAbi(clientProvider, contractId);
573
+ const printSchema = await clientProvider().index.printSchema(contractId);
574
+ if (printSchema && printSchema.topics.length > 0) {
575
+ const code2 = generatePrintSchemaSubgraph({
576
+ contractId,
577
+ name: subgraphName,
578
+ topics: printSchema.topics,
579
+ sample: printSchema.sample
580
+ });
581
+ return { content: [{ type: "text", text: code2 }] };
582
+ }
583
+ const { functions, abi } = await fetchAbi(clientProvider, contractId);
584
+ const token = generateTokenSubgraphFromAbi({
585
+ contractId,
586
+ abi,
587
+ name: subgraphName
588
+ });
589
+ if (token) {
590
+ return { content: [{ type: "text", text: token }] };
591
+ }
500
592
  const code = generateSubgraphCode(contractId, functions, subgraphName);
501
593
  return { content: [{ type: "text", text: code }] };
502
594
  });
@@ -749,14 +841,42 @@ function registerStreamsTools(server, clientProvider = getClient) {
749
841
  }
750
842
 
751
843
  // src/tools/subgraphs.ts
844
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
845
+ import { tmpdir } from "node:os";
846
+ import { join } from "node:path";
847
+ import { pathToFileURL } from "node:url";
752
848
  import { bundleSubgraphCode } from "@secondlayer/bundler";
849
+ import { DECODED_EVENT_TYPES as DECODED_EVENT_TYPES2 } from "@secondlayer/shared";
850
+ import {
851
+ runSubgraphTest
852
+ } from "@secondlayer/subgraphs/testing";
753
853
  import { z as z9 } from "zod";
854
+ function eventTypeFor(filter) {
855
+ if (filter.type === "contract_deploy")
856
+ return null;
857
+ const candidate = filter.type === "print_event" ? "print" : filter.type;
858
+ return DECODED_EVENT_TYPES2.includes(candidate) ? candidate : null;
859
+ }
860
+ function skipReason(name, filter) {
861
+ return filter.type === "contract_deploy" ? `source "${name}" (contract_deploy) has no Index list endpoint — skipped.` : `source "${name}" (${filter.type}) is not readable from Index — skipped.`;
862
+ }
863
+ async function loadBundledDefinition(handlerCode) {
864
+ const dir = await mkdtemp(join(tmpdir(), "sl-mcp-sg-test-"));
865
+ try {
866
+ const file = join(dir, `handler-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
867
+ await writeFile(file, handlerCode);
868
+ const mod = await import(pathToFileURL(file).href);
869
+ return mod.default ?? {};
870
+ } finally {
871
+ await rm(dir, { recursive: true, force: true });
872
+ }
873
+ }
754
874
  function registerSubgraphTools(server, clientProvider = getClient) {
755
875
  defineTool(server, "subgraphs_list", "List all deployed subgraphs. Returns summary fields only; read rows with subgraphs_query.", {}, async () => {
756
876
  const { data } = await clientProvider().subgraphs.list();
757
877
  return jsonResponse(data.map(formatSubgraphSummary));
758
878
  });
759
- defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health, and table columns.", { name: z9.string().describe("Subgraph name") }, async ({ name }) => {
879
+ defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health (incl. health.emptyMapping when processed events wrote 0 rows), and table columns.", { name: z9.string().describe("Subgraph name") }, async ({ name }) => {
760
880
  const detail = await clientProvider().subgraphs.status(name);
761
881
  return jsonResponse(detail);
762
882
  });
@@ -852,10 +972,10 @@ function registerSubgraphTools(server, clientProvider = getClient) {
852
972
  const result = await clientProvider().subgraphs.delete(name);
853
973
  return textResponse(result.message);
854
974
  });
855
- defineTool(server, "subgraphs_deploy", "Deploy a subgraph from TypeScript code. Pass the full defineSubgraph() source — it will be bundled, validated, and deployed. Optional startBlock overrides the source definition for this deploy. Set dryRun to validate and preview the schema/DDL without writing anything. Call `subgraphs_reindex` separately if you need a forced reindex.", {
975
+ defineTool(server, "subgraphs_deploy", "Deploy a subgraph from TypeScript code. Pass the full defineSubgraph() source — it will be bundled, validated, and deployed. Optional startBlock overrides the source definition for this deploy. dryRun validates schema/DDL only and does not run handlers — call subgraphs_test first and deploy only when it returns ok: true with written >= 1. Call subgraphs_reindex separately if you need a forced reindex.", {
856
976
  code: z9.string().describe("TypeScript source code containing a defineSubgraph() call"),
857
977
  startBlock: z9.number().int().nonnegative().optional().describe("Override the definition startBlock for this deploy"),
858
- dryRun: z9.boolean().optional().describe("Validate and preview the deploy (schema/DDL) without writing changes")
978
+ dryRun: z9.boolean().optional().describe("Validate schema/DDL only — does not run handlers. Use subgraphs_test to prove a mapping writes rows.")
859
979
  }, async ({ code, startBlock, dryRun }) => {
860
980
  const bundled = await bundleSubgraphCode(code);
861
981
  const result = await clientProvider().subgraphs.deploy({
@@ -871,6 +991,87 @@ function registerSubgraphTools(server, clientProvider = getClient) {
871
991
  });
872
992
  return jsonResponse(result);
873
993
  });
994
+ defineTool(server, "subgraphs_test", "Prove a subgraph mapping writes rows against Index data before deploy. Pass full defineSubgraph() source plus a height range. Returns ok/written/tables; EMPTY_MAPPING means events matched but 0 rows were written — read hint for observed event.data keys and do not invent fields. subgraphs_deploy dryRun is DDL only; do not deploy until ok: true with written >= 1.", {
995
+ code: z9.string().describe("TypeScript source containing a defineSubgraph() call"),
996
+ fromHeight: z9.number().int().nonnegative().describe("Start block height (inclusive)"),
997
+ toHeight: z9.number().int().nonnegative().optional().describe("End block height (inclusive); default fromHeight+100"),
998
+ limit: z9.number().int().positive().max(200).optional().describe("Max Index rows per source (default 50, max 200)")
999
+ }, async ({ code, fromHeight, toHeight, limit }) => {
1000
+ const bundled = await bundleSubgraphCode(code);
1001
+ const def = await loadBundledDefinition(bundled.handlerCode);
1002
+ if (!def.handlers || !def.schema) {
1003
+ return jsonResponse({
1004
+ ok: false,
1005
+ code: "NO_SOURCES",
1006
+ matched: 0,
1007
+ written: 0,
1008
+ tables: [],
1009
+ hint: "Bundled module must default-export defineSubgraph() with handlers and schema."
1010
+ }, true);
1011
+ }
1012
+ const sources = bundled.sources ?? {};
1013
+ const to = toHeight ?? fromHeight + 100;
1014
+ const pageLimit = Math.min(limit ?? 50, 200);
1015
+ const index = clientProvider().index;
1016
+ const events = {};
1017
+ const skipped = [];
1018
+ let sourcesTested = 0;
1019
+ for (const [name, filter] of Object.entries(sources)) {
1020
+ if (filter.type === "contract_call") {
1021
+ sourcesTested++;
1022
+ const envelope2 = await index.contractCalls.list({
1023
+ ...filter.contractId ? {
1024
+ contractId: Array.isArray(filter.contractId) ? filter.contractId : filter.contractId
1025
+ } : {},
1026
+ ...filter.functionName ? { functionName: filter.functionName } : {},
1027
+ fromHeight,
1028
+ toHeight: to,
1029
+ limit: pageLimit
1030
+ });
1031
+ events[name] = envelope2.contract_calls;
1032
+ continue;
1033
+ }
1034
+ const eventType = eventTypeFor(filter);
1035
+ if (eventType === null) {
1036
+ skipped.push({ source: name, reason: skipReason(name, filter) });
1037
+ continue;
1038
+ }
1039
+ sourcesTested++;
1040
+ const envelope = await index.events.list({
1041
+ eventType,
1042
+ ...filter.contractId ? {
1043
+ contractId: Array.isArray(filter.contractId) ? filter.contractId : filter.contractId
1044
+ } : {},
1045
+ fromHeight,
1046
+ toHeight: to,
1047
+ limit: pageLimit
1048
+ });
1049
+ events[name] = envelope.events;
1050
+ }
1051
+ if (sourcesTested === 0) {
1052
+ return jsonResponse({
1053
+ ok: false,
1054
+ code: "NO_SOURCES",
1055
+ matched: 0,
1056
+ written: 0,
1057
+ tables: [],
1058
+ hint: "No sources were tested — every source is unreadable from Index.",
1059
+ skipped
1060
+ }, true);
1061
+ }
1062
+ const result = await runSubgraphTest({
1063
+ schema: def.schema,
1064
+ handlers: def.handlers,
1065
+ sources,
1066
+ events
1067
+ });
1068
+ const body = { ...result, skipped };
1069
+ if (skipped.length > 0) {
1070
+ const skipHints = skipped.map((s) => s.reason).join(" ");
1071
+ body.hint = result.hint ? `${result.hint} ${skipHints}` : skipHints;
1072
+ }
1073
+ return jsonResponse(body, !result.ok);
1074
+ });
874
1075
  }
875
1076
 
876
1077
  // src/tools/subscriptions.ts
@@ -977,7 +1178,7 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
977
1178
 
978
1179
  // src/server.ts
979
1180
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
980
- var pkg = JSON.parse(readFileSync(join(__dirname2, "../package.json"), "utf-8"));
1181
+ var pkg = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
981
1182
  function createServer() {
982
1183
  const server = new McpServer({
983
1184
  name: "secondlayer",
@@ -1006,5 +1207,5 @@ var server = createServer();
1006
1207
  var transport = new StdioServerTransport;
1007
1208
  await server.connect(transport);
1008
1209
 
1009
- //# debugId=1DDB49B11F03E33264756E2164756E21
1210
+ //# debugId=C232724CB95A909764756E2164756E21
1010
1211
  //# sourceMappingURL=bin.js.map