@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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/server.ts
2
2
  import { readFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, join as join2 } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
 
@@ -15,10 +15,13 @@ function isHostedArchiveUrl(url) {
15
15
  }
16
16
 
17
17
  // src/resources.ts
18
+ import {
19
+ ResourceTemplate
20
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
18
21
  import { CHAIN_TRIGGER_FIELDS } from "@secondlayer/shared";
19
22
  import { BOOTSTRAP_STEP } from "@secondlayer/shared/archive/instance-diagnosis";
20
23
  import { TRAIT_STANDARDS } from "@secondlayer/stacks/clarity";
21
- import { filterFieldsByType } from "@secondlayer/subgraphs";
24
+ import { camelizeKeys, filterFieldsByType } from "@secondlayer/subgraphs";
22
25
  import { TYPE_MAP } from "@secondlayer/subgraphs/schema";
23
26
 
24
27
  // src/lib/client.ts
@@ -314,6 +317,73 @@ function registerResources(server) {
314
317
  }
315
318
  ]
316
319
  }));
320
+ server.resource("samples", new ResourceTemplate("secondlayer://samples/{contractId}", {
321
+ list: undefined
322
+ }), {
323
+ description: "Decoded camelCase print samples for a contract (schema + up to 5 events). Use before writing a print_event handler."
324
+ }, async (uri, variables) => {
325
+ const contractId = String(Array.isArray(variables.contractId) ? variables.contractId[0] : variables.contractId ?? "");
326
+ const body = await buildSamplesResource(contractId);
327
+ return {
328
+ contents: [
329
+ {
330
+ uri: uri.href,
331
+ mimeType: "application/json",
332
+ text: JSON.stringify(body, null, 2)
333
+ }
334
+ ]
335
+ };
336
+ });
337
+ }
338
+ function jsonSafe(value) {
339
+ if (typeof value === "bigint")
340
+ return value.toString();
341
+ if (value === null || value === undefined)
342
+ return value;
343
+ if (Array.isArray(value))
344
+ return value.map(jsonSafe);
345
+ if (typeof value === "object") {
346
+ const out = {};
347
+ for (const [k, v] of Object.entries(value)) {
348
+ out[k] = jsonSafe(v);
349
+ }
350
+ return out;
351
+ }
352
+ return value;
353
+ }
354
+ async function buildSamplesResource(contractId, clientProvider = getClient) {
355
+ const client = clientProvider();
356
+ const printSchema = await client.index.printSchema(contractId);
357
+ const envelope = await client.index.events.list({
358
+ eventType: "print",
359
+ contractId,
360
+ limit: 5
361
+ });
362
+ const schema = printSchema?.topics.map((t) => ({
363
+ topic: t.topic,
364
+ fields: t.fields.map((f) => ({
365
+ camel: f.camel_name,
366
+ snake: f.name.replace(/-/g, "_"),
367
+ column_type: f.column_type,
368
+ always: f.always_present
369
+ }))
370
+ })) ?? [];
371
+ const samples = (envelope.events ?? []).slice(0, 5).map((ev) => {
372
+ const payload = ev.payload;
373
+ const data = jsonSafe(camelizeKeys(payload?.value));
374
+ return {
375
+ txId: ev.tx_id,
376
+ height: ev.block_height,
377
+ topic: payload?.topic ?? "",
378
+ data: data ?? {}
379
+ };
380
+ });
381
+ return {
382
+ contractId,
383
+ handlerShape: "event.data is camelCase; columns are snake_case",
384
+ schema,
385
+ samples
386
+ };
317
387
  }
318
388
 
319
389
  // src/tools/account.ts
@@ -472,7 +542,11 @@ function registerInstanceTools(server, clientProvider = getClient) {
472
542
  }
473
543
 
474
544
  // src/tools/scaffold.ts
475
- import { generateSubgraphCode } from "@secondlayer/scaffold";
545
+ import {
546
+ generatePrintSchemaSubgraph,
547
+ generateSubgraphCode,
548
+ generateTokenSubgraphFromAbi
549
+ } from "@secondlayer/scaffold";
476
550
  import { z as z6 } from "zod";
477
551
  async function fetchAbi(clientProvider, contractId) {
478
552
  const contract = await clientProvider().contracts.get(contractId, {
@@ -484,14 +558,32 @@ async function fetchAbi(clientProvider, contractId) {
484
558
  if (!abi) {
485
559
  throw new Error(`No ABI available for ${contractId} (abi_status: ${contract.abi_status})`);
486
560
  }
487
- return { functions: abi.functions ?? [] };
561
+ return { functions: abi.functions ?? [], abi };
488
562
  }
489
563
  function registerScaffoldTools(server, clientProvider = getClient) {
490
- defineTool(server, "subgraphs_scaffold", "Generate a subgraph scaffold from a deployed Stacks contract. Fetches the ABI automatically. Returns TypeScript source — review it, then pass it to subgraphs_deploy.", {
564
+ 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.", {
491
565
  contractId: z6.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
492
566
  subgraphName: z6.string().optional().describe("Override the subgraph name (defaults to contract name)")
493
567
  }, async ({ contractId, subgraphName }) => {
494
- const { functions } = await fetchAbi(clientProvider, contractId);
568
+ const printSchema = await clientProvider().index.printSchema(contractId);
569
+ if (printSchema && printSchema.topics.length > 0) {
570
+ const code2 = generatePrintSchemaSubgraph({
571
+ contractId,
572
+ name: subgraphName,
573
+ topics: printSchema.topics,
574
+ sample: printSchema.sample
575
+ });
576
+ return { content: [{ type: "text", text: code2 }] };
577
+ }
578
+ const { functions, abi } = await fetchAbi(clientProvider, contractId);
579
+ const token = generateTokenSubgraphFromAbi({
580
+ contractId,
581
+ abi,
582
+ name: subgraphName
583
+ });
584
+ if (token) {
585
+ return { content: [{ type: "text", text: token }] };
586
+ }
495
587
  const code = generateSubgraphCode(contractId, functions, subgraphName);
496
588
  return { content: [{ type: "text", text: code }] };
497
589
  });
@@ -744,14 +836,42 @@ function registerStreamsTools(server, clientProvider = getClient) {
744
836
  }
745
837
 
746
838
  // src/tools/subgraphs.ts
839
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
840
+ import { tmpdir } from "node:os";
841
+ import { join } from "node:path";
842
+ import { pathToFileURL } from "node:url";
747
843
  import { bundleSubgraphCode } from "@secondlayer/bundler";
844
+ import { DECODED_EVENT_TYPES as DECODED_EVENT_TYPES2 } from "@secondlayer/shared";
845
+ import {
846
+ runSubgraphTest
847
+ } from "@secondlayer/subgraphs/testing";
748
848
  import { z as z9 } from "zod";
849
+ function eventTypeFor(filter) {
850
+ if (filter.type === "contract_deploy")
851
+ return null;
852
+ const candidate = filter.type === "print_event" ? "print" : filter.type;
853
+ return DECODED_EVENT_TYPES2.includes(candidate) ? candidate : null;
854
+ }
855
+ function skipReason(name, filter) {
856
+ 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.`;
857
+ }
858
+ async function loadBundledDefinition(handlerCode) {
859
+ const dir = await mkdtemp(join(tmpdir(), "sl-mcp-sg-test-"));
860
+ try {
861
+ const file = join(dir, `handler-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
862
+ await writeFile(file, handlerCode);
863
+ const mod = await import(pathToFileURL(file).href);
864
+ return mod.default ?? {};
865
+ } finally {
866
+ await rm(dir, { recursive: true, force: true });
867
+ }
868
+ }
749
869
  function registerSubgraphTools(server, clientProvider = getClient) {
750
870
  defineTool(server, "subgraphs_list", "List all deployed subgraphs. Returns summary fields only; read rows with subgraphs_query.", {}, async () => {
751
871
  const { data } = await clientProvider().subgraphs.list();
752
872
  return jsonResponse(data.map(formatSubgraphSummary));
753
873
  });
754
- defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health, and table columns.", { name: z9.string().describe("Subgraph name") }, async ({ name }) => {
874
+ 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 }) => {
755
875
  const detail = await clientProvider().subgraphs.status(name);
756
876
  return jsonResponse(detail);
757
877
  });
@@ -847,10 +967,10 @@ function registerSubgraphTools(server, clientProvider = getClient) {
847
967
  const result = await clientProvider().subgraphs.delete(name);
848
968
  return textResponse(result.message);
849
969
  });
850
- defineTool(server, "subgraphs_deploy", "Deploy a subgraph from TypeScript code. Pass the full defineSubgraph() source — it will be bundled, validated, and deployed. Optional startBlock overrides the source definition for this deploy. Set dryRun to validate and preview the schema/DDL without writing anything. Call `subgraphs_reindex` separately if you need a forced reindex.", {
970
+ 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.", {
851
971
  code: z9.string().describe("TypeScript source code containing a defineSubgraph() call"),
852
972
  startBlock: z9.number().int().nonnegative().optional().describe("Override the definition startBlock for this deploy"),
853
- dryRun: z9.boolean().optional().describe("Validate and preview the deploy (schema/DDL) without writing changes")
973
+ dryRun: z9.boolean().optional().describe("Validate schema/DDL only — does not run handlers. Use subgraphs_test to prove a mapping writes rows.")
854
974
  }, async ({ code, startBlock, dryRun }) => {
855
975
  const bundled = await bundleSubgraphCode(code);
856
976
  const result = await clientProvider().subgraphs.deploy({
@@ -866,6 +986,87 @@ function registerSubgraphTools(server, clientProvider = getClient) {
866
986
  });
867
987
  return jsonResponse(result);
868
988
  });
989
+ 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.", {
990
+ code: z9.string().describe("TypeScript source containing a defineSubgraph() call"),
991
+ fromHeight: z9.number().int().nonnegative().describe("Start block height (inclusive)"),
992
+ toHeight: z9.number().int().nonnegative().optional().describe("End block height (inclusive); default fromHeight+100"),
993
+ limit: z9.number().int().positive().max(200).optional().describe("Max Index rows per source (default 50, max 200)")
994
+ }, async ({ code, fromHeight, toHeight, limit }) => {
995
+ const bundled = await bundleSubgraphCode(code);
996
+ const def = await loadBundledDefinition(bundled.handlerCode);
997
+ if (!def.handlers || !def.schema) {
998
+ return jsonResponse({
999
+ ok: false,
1000
+ code: "NO_SOURCES",
1001
+ matched: 0,
1002
+ written: 0,
1003
+ tables: [],
1004
+ hint: "Bundled module must default-export defineSubgraph() with handlers and schema."
1005
+ }, true);
1006
+ }
1007
+ const sources = bundled.sources ?? {};
1008
+ const to = toHeight ?? fromHeight + 100;
1009
+ const pageLimit = Math.min(limit ?? 50, 200);
1010
+ const index = clientProvider().index;
1011
+ const events = {};
1012
+ const skipped = [];
1013
+ let sourcesTested = 0;
1014
+ for (const [name, filter] of Object.entries(sources)) {
1015
+ if (filter.type === "contract_call") {
1016
+ sourcesTested++;
1017
+ const envelope2 = await index.contractCalls.list({
1018
+ ...filter.contractId ? {
1019
+ contractId: Array.isArray(filter.contractId) ? filter.contractId : filter.contractId
1020
+ } : {},
1021
+ ...filter.functionName ? { functionName: filter.functionName } : {},
1022
+ fromHeight,
1023
+ toHeight: to,
1024
+ limit: pageLimit
1025
+ });
1026
+ events[name] = envelope2.contract_calls;
1027
+ continue;
1028
+ }
1029
+ const eventType = eventTypeFor(filter);
1030
+ if (eventType === null) {
1031
+ skipped.push({ source: name, reason: skipReason(name, filter) });
1032
+ continue;
1033
+ }
1034
+ sourcesTested++;
1035
+ const envelope = await index.events.list({
1036
+ eventType,
1037
+ ...filter.contractId ? {
1038
+ contractId: Array.isArray(filter.contractId) ? filter.contractId : filter.contractId
1039
+ } : {},
1040
+ fromHeight,
1041
+ toHeight: to,
1042
+ limit: pageLimit
1043
+ });
1044
+ events[name] = envelope.events;
1045
+ }
1046
+ if (sourcesTested === 0) {
1047
+ return jsonResponse({
1048
+ ok: false,
1049
+ code: "NO_SOURCES",
1050
+ matched: 0,
1051
+ written: 0,
1052
+ tables: [],
1053
+ hint: "No sources were tested — every source is unreadable from Index.",
1054
+ skipped
1055
+ }, true);
1056
+ }
1057
+ const result = await runSubgraphTest({
1058
+ schema: def.schema,
1059
+ handlers: def.handlers,
1060
+ sources,
1061
+ events
1062
+ });
1063
+ const body = { ...result, skipped };
1064
+ if (skipped.length > 0) {
1065
+ const skipHints = skipped.map((s) => s.reason).join(" ");
1066
+ body.hint = result.hint ? `${result.hint} ${skipHints}` : skipHints;
1067
+ }
1068
+ return jsonResponse(body, !result.ok);
1069
+ });
869
1070
  }
870
1071
 
871
1072
  // src/tools/subscriptions.ts
@@ -972,7 +1173,7 @@ function registerSubscriptionTools(server, clientProvider = getClient) {
972
1173
 
973
1174
  // src/server.ts
974
1175
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
975
- var pkg = JSON.parse(readFileSync(join(__dirname2, "../package.json"), "utf-8"));
1176
+ var pkg = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
976
1177
  function createServer() {
977
1178
  const server = new McpServer({
978
1179
  name: "secondlayer",
@@ -999,5 +1200,5 @@ export {
999
1200
  createServer
1000
1201
  };
1001
1202
 
1002
- //# debugId=8023DE628F04195164756E2164756E21
1203
+ //# debugId=F1D65205F9CD817664756E2164756E21
1003
1204
  //# sourceMappingURL=index.js.map