@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-http.js CHANGED
@@ -8,7 +8,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
8
8
 
9
9
  // src/server.ts
10
10
  import { readFileSync } from "node:fs";
11
- import { dirname, join } from "node:path";
11
+ import { dirname, join as join2 } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
 
@@ -23,10 +23,13 @@ function isHostedArchiveUrl(url) {
23
23
  }
24
24
 
25
25
  // src/resources.ts
26
+ import {
27
+ ResourceTemplate
28
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
26
29
  import { CHAIN_TRIGGER_FIELDS } from "@secondlayer/shared";
27
30
  import { BOOTSTRAP_STEP } from "@secondlayer/shared/archive/instance-diagnosis";
28
31
  import { TRAIT_STANDARDS } from "@secondlayer/stacks/clarity";
29
- import { filterFieldsByType } from "@secondlayer/subgraphs";
32
+ import { camelizeKeys, filterFieldsByType } from "@secondlayer/subgraphs";
30
33
  import { TYPE_MAP } from "@secondlayer/subgraphs/schema";
31
34
 
32
35
  // src/lib/client.ts
@@ -322,6 +325,73 @@ function registerResources(server) {
322
325
  }
323
326
  ]
324
327
  }));
328
+ server.resource("samples", new ResourceTemplate("secondlayer://samples/{contractId}", {
329
+ list: undefined
330
+ }), {
331
+ description: "Decoded camelCase print samples for a contract (schema + up to 5 events). Use before writing a print_event handler."
332
+ }, async (uri, variables) => {
333
+ const contractId = String(Array.isArray(variables.contractId) ? variables.contractId[0] : variables.contractId ?? "");
334
+ const body = await buildSamplesResource(contractId);
335
+ return {
336
+ contents: [
337
+ {
338
+ uri: uri.href,
339
+ mimeType: "application/json",
340
+ text: JSON.stringify(body, null, 2)
341
+ }
342
+ ]
343
+ };
344
+ });
345
+ }
346
+ function jsonSafe(value) {
347
+ if (typeof value === "bigint")
348
+ return value.toString();
349
+ if (value === null || value === undefined)
350
+ return value;
351
+ if (Array.isArray(value))
352
+ return value.map(jsonSafe);
353
+ if (typeof value === "object") {
354
+ const out = {};
355
+ for (const [k, v] of Object.entries(value)) {
356
+ out[k] = jsonSafe(v);
357
+ }
358
+ return out;
359
+ }
360
+ return value;
361
+ }
362
+ async function buildSamplesResource(contractId, clientProvider = getClient) {
363
+ const client = clientProvider();
364
+ const printSchema = await client.index.printSchema(contractId);
365
+ const envelope = await client.index.events.list({
366
+ eventType: "print",
367
+ contractId,
368
+ limit: 5
369
+ });
370
+ const schema = printSchema?.topics.map((t) => ({
371
+ topic: t.topic,
372
+ fields: t.fields.map((f) => ({
373
+ camel: f.camel_name,
374
+ snake: f.name.replace(/-/g, "_"),
375
+ column_type: f.column_type,
376
+ always: f.always_present
377
+ }))
378
+ })) ?? [];
379
+ const samples = (envelope.events ?? []).slice(0, 5).map((ev) => {
380
+ const payload = ev.payload;
381
+ const data = jsonSafe(camelizeKeys(payload?.value));
382
+ return {
383
+ txId: ev.tx_id,
384
+ height: ev.block_height,
385
+ topic: payload?.topic ?? "",
386
+ data: data ?? {}
387
+ };
388
+ });
389
+ return {
390
+ contractId,
391
+ handlerShape: "event.data is camelCase; columns are snake_case",
392
+ schema,
393
+ samples
394
+ };
325
395
  }
326
396
 
327
397
  // src/tools/account.ts
@@ -480,7 +550,11 @@ function registerInstanceTools(server, clientProvider = getClient) {
480
550
  }
481
551
 
482
552
  // src/tools/scaffold.ts
483
- import { generateSubgraphCode } from "@secondlayer/scaffold";
553
+ import {
554
+ generatePrintSchemaSubgraph,
555
+ generateSubgraphCode,
556
+ generateTokenSubgraphFromAbi
557
+ } from "@secondlayer/scaffold";
484
558
  import { z as z6 } from "zod";
485
559
  async function fetchAbi(clientProvider, contractId) {
486
560
  const contract = await clientProvider().contracts.get(contractId, {
@@ -492,14 +566,32 @@ async function fetchAbi(clientProvider, contractId) {
492
566
  if (!abi) {
493
567
  throw new Error(`No ABI available for ${contractId} (abi_status: ${contract.abi_status})`);
494
568
  }
495
- return { functions: abi.functions ?? [] };
569
+ return { functions: abi.functions ?? [], abi };
496
570
  }
497
571
  function registerScaffoldTools(server, clientProvider = getClient) {
498
- 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.", {
572
+ 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.", {
499
573
  contractId: z6.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
500
574
  subgraphName: z6.string().optional().describe("Override the subgraph name (defaults to contract name)")
501
575
  }, async ({ contractId, subgraphName }) => {
502
- const { functions } = await fetchAbi(clientProvider, contractId);
576
+ const printSchema = await clientProvider().index.printSchema(contractId);
577
+ if (printSchema && printSchema.topics.length > 0) {
578
+ const code2 = generatePrintSchemaSubgraph({
579
+ contractId,
580
+ name: subgraphName,
581
+ topics: printSchema.topics,
582
+ sample: printSchema.sample
583
+ });
584
+ return { content: [{ type: "text", text: code2 }] };
585
+ }
586
+ const { functions, abi } = await fetchAbi(clientProvider, contractId);
587
+ const token = generateTokenSubgraphFromAbi({
588
+ contractId,
589
+ abi,
590
+ name: subgraphName
591
+ });
592
+ if (token) {
593
+ return { content: [{ type: "text", text: token }] };
594
+ }
503
595
  const code = generateSubgraphCode(contractId, functions, subgraphName);
504
596
  return { content: [{ type: "text", text: code }] };
505
597
  });
@@ -752,14 +844,42 @@ function registerStreamsTools(server, clientProvider = getClient) {
752
844
  }
753
845
 
754
846
  // src/tools/subgraphs.ts
847
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
848
+ import { tmpdir } from "node:os";
849
+ import { join } from "node:path";
850
+ import { pathToFileURL } from "node:url";
755
851
  import { bundleSubgraphCode } from "@secondlayer/bundler";
852
+ import { DECODED_EVENT_TYPES as DECODED_EVENT_TYPES2 } from "@secondlayer/shared";
853
+ import {
854
+ runSubgraphTest
855
+ } from "@secondlayer/subgraphs/testing";
756
856
  import { z as z9 } from "zod";
857
+ function eventTypeFor(filter) {
858
+ if (filter.type === "contract_deploy")
859
+ return null;
860
+ const candidate = filter.type === "print_event" ? "print" : filter.type;
861
+ return DECODED_EVENT_TYPES2.includes(candidate) ? candidate : null;
862
+ }
863
+ function skipReason(name, filter) {
864
+ 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.`;
865
+ }
866
+ async function loadBundledDefinition(handlerCode) {
867
+ const dir = await mkdtemp(join(tmpdir(), "sl-mcp-sg-test-"));
868
+ try {
869
+ const file = join(dir, `handler-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
870
+ await writeFile(file, handlerCode);
871
+ const mod = await import(pathToFileURL(file).href);
872
+ return mod.default ?? {};
873
+ } finally {
874
+ await rm(dir, { recursive: true, force: true });
875
+ }
876
+ }
757
877
  function registerSubgraphTools(server, clientProvider = getClient) {
758
878
  defineTool(server, "subgraphs_list", "List all deployed subgraphs. Returns summary fields only; read rows with subgraphs_query.", {}, async () => {
759
879
  const { data } = await clientProvider().subgraphs.list();
760
880
  return jsonResponse(data.map(formatSubgraphSummary));
761
881
  });
762
- defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health, and table columns.", { name: z9.string().describe("Subgraph name") }, async ({ name }) => {
882
+ 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 }) => {
763
883
  const detail = await clientProvider().subgraphs.status(name);
764
884
  return jsonResponse(detail);
765
885
  });
@@ -855,10 +975,10 @@ function registerSubgraphTools(server, clientProvider = getClient) {
855
975
  const result = await clientProvider().subgraphs.delete(name);
856
976
  return textResponse(result.message);
857
977
  });
858
- 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.", {
978
+ 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.", {
859
979
  code: z9.string().describe("TypeScript source code containing a defineSubgraph() call"),
860
980
  startBlock: z9.number().int().nonnegative().optional().describe("Override the definition startBlock for this deploy"),
861
- dryRun: z9.boolean().optional().describe("Validate and preview the deploy (schema/DDL) without writing changes")
981
+ dryRun: z9.boolean().optional().describe("Validate schema/DDL only — does not run handlers. Use subgraphs_test to prove a mapping writes rows.")
862
982
  }, async ({ code, startBlock, dryRun }) => {
863
983
  const bundled = await bundleSubgraphCode(code);
864
984
  const result = await clientProvider().subgraphs.deploy({
@@ -874,6 +994,87 @@ function registerSubgraphTools(server, clientProvider = getClient) {
874
994
  });
875
995
  return jsonResponse(result);
876
996
  });
997
+ 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.", {
998
+ code: z9.string().describe("TypeScript source containing a defineSubgraph() call"),
999
+ fromHeight: z9.number().int().nonnegative().describe("Start block height (inclusive)"),
1000
+ toHeight: z9.number().int().nonnegative().optional().describe("End block height (inclusive); default fromHeight+100"),
1001
+ limit: z9.number().int().positive().max(200).optional().describe("Max Index rows per source (default 50, max 200)")
1002
+ }, async ({ code, fromHeight, toHeight, limit }) => {
1003
+ const bundled = await bundleSubgraphCode(code);
1004
+ const def = await loadBundledDefinition(bundled.handlerCode);
1005
+ if (!def.handlers || !def.schema) {
1006
+ return jsonResponse({
1007
+ ok: false,
1008
+ code: "NO_SOURCES",
1009
+ matched: 0,
1010
+ written: 0,
1011
+ tables: [],
1012
+ hint: "Bundled module must default-export defineSubgraph() with handlers and schema."
1013
+ }, true);
1014
+ }
1015
+ const sources = bundled.sources ?? {};
1016
+ const to = toHeight ?? fromHeight + 100;
1017
+ const pageLimit = Math.min(limit ?? 50, 200);
1018
+ const index = clientProvider().index;
1019
+ const events = {};
1020
+ const skipped = [];
1021
+ let sourcesTested = 0;
1022
+ for (const [name, filter] of Object.entries(sources)) {
1023
+ if (filter.type === "contract_call") {
1024
+ sourcesTested++;
1025
+ const envelope2 = await index.contractCalls.list({
1026
+ ...filter.contractId ? {
1027
+ contractId: Array.isArray(filter.contractId) ? filter.contractId : filter.contractId
1028
+ } : {},
1029
+ ...filter.functionName ? { functionName: filter.functionName } : {},
1030
+ fromHeight,
1031
+ toHeight: to,
1032
+ limit: pageLimit
1033
+ });
1034
+ events[name] = envelope2.contract_calls;
1035
+ continue;
1036
+ }
1037
+ const eventType = eventTypeFor(filter);
1038
+ if (eventType === null) {
1039
+ skipped.push({ source: name, reason: skipReason(name, filter) });
1040
+ continue;
1041
+ }
1042
+ sourcesTested++;
1043
+ const envelope = await index.events.list({
1044
+ eventType,
1045
+ ...filter.contractId ? {
1046
+ contractId: Array.isArray(filter.contractId) ? filter.contractId : filter.contractId
1047
+ } : {},
1048
+ fromHeight,
1049
+ toHeight: to,
1050
+ limit: pageLimit
1051
+ });
1052
+ events[name] = envelope.events;
1053
+ }
1054
+ if (sourcesTested === 0) {
1055
+ return jsonResponse({
1056
+ ok: false,
1057
+ code: "NO_SOURCES",
1058
+ matched: 0,
1059
+ written: 0,
1060
+ tables: [],
1061
+ hint: "No sources were tested — every source is unreadable from Index.",
1062
+ skipped
1063
+ }, true);
1064
+ }
1065
+ const result = await runSubgraphTest({
1066
+ schema: def.schema,
1067
+ handlers: def.handlers,
1068
+ sources,
1069
+ events
1070
+ });
1071
+ const body = { ...result, skipped };
1072
+ if (skipped.length > 0) {
1073
+ const skipHints = skipped.map((s) => s.reason).join(" ");
1074
+ body.hint = result.hint ? `${result.hint} ${skipHints}` : skipHints;
1075
+ }
1076
+ return jsonResponse(body, !result.ok);
1077
+ });
877
1078
  }
878
1079
 
879
1080
  // src/tools/subscriptions.ts
@@ -980,7 +1181,7 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
980
1181
 
981
1182
  // src/server.ts
982
1183
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
983
- var pkg = JSON.parse(readFileSync(join(__dirname2, "../package.json"), "utf-8"));
1184
+ var pkg = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
984
1185
  function createServer() {
985
1186
  const server = new McpServer({
986
1187
  name: "secondlayer",
@@ -1090,5 +1291,5 @@ httpServer.listen(port, () => {
1090
1291
  console.error("Warning: SECONDLAYER_MCP_SECRET not set, authentication disabled");
1091
1292
  });
1092
1293
 
1093
- //# debugId=978D99EF4EC8A61364756E2164756E21
1294
+ //# debugId=0C640F6B0FB7B21064756E2164756E21
1094
1295
  //# sourceMappingURL=bin-http.js.map