@secondlayer/mcp 0.2.1 → 0.3.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/README.md CHANGED
@@ -45,14 +45,52 @@ npx @secondlayer/mcp-http
45
45
 
46
46
  ## Tools
47
47
 
48
- 19 tools across 4 domains.
48
+ 22 tools across 5 domains.
49
49
 
50
50
  | Domain | Tools |
51
51
  | --- | --- |
52
- | **Streams** (9) | `streams_list`, `streams_get`, `streams_create`, `streams_update`, `streams_delete`, `streams_toggle`, `streams_deliveries`, `streams_pause_all`, `streams_resume_all` |
52
+ | **Streams** (11) | `streams_list`, `streams_get`, `streams_create`, `streams_update`, `streams_delete`, `streams_toggle`, `streams_deliveries`, `streams_pause_all`, `streams_resume_all`, `streams_replay`, `streams_rotate_secret` |
53
53
  | **Subgraphs** (6) | `subgraphs_list`, `subgraphs_get`, `subgraphs_query`, `subgraphs_reindex`, `subgraphs_delete`, `subgraphs_deploy` |
54
54
  | **Scaffold** (2) | `scaffold_from_contract`, `scaffold_from_abi` |
55
55
  | **Templates** (2) | `templates_list`, `templates_get` |
56
+ | **Account** (1) | `account_whoami` |
57
+
58
+ ### `subgraphs_query` enhancements
59
+
60
+ - `fields` — comma-separated column projection (e.g. `"sender,amount_x"`)
61
+ - `count` — boolean, returns row count instead of rows
62
+ - Filter operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `like`
63
+ - Max limit raised from 50 to 200
64
+
65
+ ## Resources
66
+
67
+ 3 MCP resources for agent context:
68
+
69
+ | URI | Description |
70
+ | --- | --- |
71
+ | `secondlayer://filters` | Filter types reference |
72
+ | `secondlayer://column-types` | Column type mappings and options |
73
+ | `secondlayer://templates` | Available subgraph templates |
74
+
75
+ ## Error Handling
76
+
77
+ All tools return structured errors with `isError: true`:
78
+
79
+ ```json
80
+ { "error": { "type": "not_found", "status": 404, "message": "Stream not found" } }
81
+ ```
82
+
83
+ | Error type | Status | When |
84
+ | --- | --- | --- |
85
+ | `unauthorized` | 401 | Invalid or missing API key |
86
+ | `not_found` | 404 | Resource doesn't exist |
87
+ | `rate_limited` | 429 | Too many requests |
88
+ | `server_error` | 5xx | Server-side failure |
89
+ | `error` | other | Validation, bundling, etc. |
90
+
91
+ Bundle/deploy errors use descriptive prefixes: `"Bundle failed:"`, `"Module evaluation failed:"`, `"Validation failed:"`.
92
+
93
+ HTTP transport enforces a 1MB body limit (413) and JSON parse safety (400). Scaffold ABI fetch has a 10s timeout.
56
94
 
57
95
  ## Programmatic Usage
58
96
 
package/dist/bin-http.js CHANGED
@@ -5,15 +5,31 @@ import { createServer as createHttpServer } from "node:http";
5
5
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
6
 
7
7
  // src/server.ts
8
+ import { readFileSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { fileURLToPath } from "node:url";
8
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
12
 
10
13
  // src/tools/templates.ts
11
- import { z } from "zod";
14
+ import { z } from "zod/v4";
12
15
  import { templates, getTemplateById, getTemplatesByCategory } from "@secondlayer/subgraphs/templates";
13
16
 
14
17
  // src/lib/tool.ts
15
18
  function defineTool(server, name, description, schema, handler) {
16
- server.tool(name, description, schema, handler);
19
+ const wrappedHandler = async (args) => {
20
+ try {
21
+ return await handler(args);
22
+ } catch (err) {
23
+ const message = err instanceof Error ? err.message : String(err);
24
+ const status = err instanceof Error && "status" in err ? err.status : 0;
25
+ const type = status === 401 ? "unauthorized" : status === 404 ? "not_found" : status === 429 ? "rate_limited" : status >= 500 ? "server_error" : "error";
26
+ return {
27
+ content: [{ type: "text", text: JSON.stringify({ error: { type, status, message } }) }],
28
+ isError: true
29
+ };
30
+ }
31
+ };
32
+ server.tool(name, description, schema, wrappedHandler);
17
33
  }
18
34
 
19
35
  // src/tools/templates.ts
@@ -47,7 +63,7 @@ function registerTemplateTools(server) {
47
63
  }
48
64
 
49
65
  // src/tools/scaffold.ts
50
- import { z as z2 } from "zod";
66
+ import { z as z2 } from "zod/v4";
51
67
 
52
68
  // src/lib/scaffold-generate.ts
53
69
  function isAbiBuffer(t) {
@@ -228,7 +244,9 @@ ${handlersBlock}
228
244
  // src/tools/scaffold.ts
229
245
  var API_BASE = process.env.SECONDLAYER_API_URL || "https://api.secondlayer.tools";
230
246
  async function fetchAbi(contractId) {
231
- const res = await fetch(`${API_BASE}/api/node/contracts/${contractId}/abi`);
247
+ const res = await fetch(`${API_BASE}/api/node/contracts/${contractId}/abi`, {
248
+ signal: AbortSignal.timeout(1e4)
249
+ });
232
250
  if (!res.ok) {
233
251
  if (res.status === 404)
234
252
  throw new Error(`Contract not found: ${contractId}`);
@@ -266,7 +284,7 @@ function registerScaffoldTools(server) {
266
284
  }
267
285
 
268
286
  // src/tools/streams.ts
269
- import { z as z3 } from "zod";
287
+ import { z as z3 } from "zod/v4";
270
288
 
271
289
  // src/lib/client.ts
272
290
  import { SecondLayer } from "@secondlayer/sdk";
@@ -281,6 +299,24 @@ function getClient() {
281
299
  }
282
300
  return instance;
283
301
  }
302
+ async function apiRequest(method, path, body) {
303
+ const apiKey = process.env.SECONDLAYER_API_KEY;
304
+ if (!apiKey)
305
+ throw new Error("SECONDLAYER_API_KEY required");
306
+ const baseUrl = process.env.SECONDLAYER_API_URL || "https://api.secondlayer.tools";
307
+ const res = await fetch(`${baseUrl}${path}`, {
308
+ method,
309
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
310
+ body: body ? JSON.stringify(body) : undefined
311
+ });
312
+ if (!res.ok) {
313
+ const text = await res.text().catch(() => "");
314
+ throw Object.assign(new Error(text || `HTTP ${res.status}`), { status: res.status });
315
+ }
316
+ if (res.status === 204)
317
+ return;
318
+ return res.json();
319
+ }
284
320
 
285
321
  // src/lib/format.ts
286
322
  function formatStreamSummary(s) {
@@ -434,29 +470,56 @@ function registerStreamTools(server) {
434
470
  }]
435
471
  };
436
472
  });
473
+ defineTool(server, "streams_replay", "Replay blocks through a stream, re-delivering events for a block range.", {
474
+ id: z3.string().describe("Stream UUID or prefix"),
475
+ fromBlock: z3.number().describe("Start block height (inclusive)"),
476
+ toBlock: z3.number().describe("End block height (inclusive)")
477
+ }, async ({ id, fromBlock, toBlock }) => {
478
+ const result = await apiRequest("POST", `/api/streams/${id}/replay`, { fromBlock, toBlock });
479
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
480
+ });
481
+ defineTool(server, "streams_rotate_secret", "Rotate the signing secret for a stream. Returns the new secret.", { id: z3.string().describe("Stream UUID or prefix") }, async ({ id }) => {
482
+ const result = await getClient().streams.rotateSecret(id);
483
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
484
+ });
437
485
  }
438
486
 
439
487
  // src/tools/subgraphs.ts
440
- import { z as z4 } from "zod";
488
+ import { z as z4 } from "zod/v4";
441
489
 
442
490
  // src/lib/bundle.ts
443
491
  import esbuild from "esbuild";
444
492
  import { validateSubgraphDefinition } from "@secondlayer/subgraphs/validate";
445
493
  import { sourceKey } from "@secondlayer/subgraphs";
446
494
  async function bundleSubgraphCode(code) {
447
- const result = await esbuild.build({
448
- stdin: { contents: code, loader: "ts", resolveDir: process.cwd() },
449
- bundle: true,
450
- platform: "node",
451
- format: "esm",
452
- external: ["@secondlayer/subgraphs"],
453
- write: false
454
- });
495
+ let result;
496
+ try {
497
+ result = await esbuild.build({
498
+ stdin: { contents: code, loader: "ts", resolveDir: process.cwd() },
499
+ bundle: true,
500
+ platform: "node",
501
+ format: "esm",
502
+ external: ["@secondlayer/subgraphs"],
503
+ write: false
504
+ });
505
+ } catch (err) {
506
+ throw new Error(`Bundle failed: ${err instanceof Error ? err.message : String(err)}`);
507
+ }
455
508
  const handlerCode = new TextDecoder().decode(result.outputFiles[0].contents);
456
- const dataUri = `data:text/javascript;base64,${Buffer.from(handlerCode).toString("base64")}`;
457
- const mod = await import(dataUri);
509
+ let mod;
510
+ try {
511
+ const dataUri = `data:text/javascript;base64,${Buffer.from(handlerCode).toString("base64")}`;
512
+ mod = await import(dataUri);
513
+ } catch (err) {
514
+ throw new Error(`Module evaluation failed: ${err instanceof Error ? err.message : String(err)}`);
515
+ }
458
516
  const def = mod.default ?? mod;
459
- const validated = validateSubgraphDefinition(def);
517
+ let validated;
518
+ try {
519
+ validated = validateSubgraphDefinition(def);
520
+ } catch (err) {
521
+ throw new Error(`Validation failed: ${err instanceof Error ? err.message : String(err)}`);
522
+ }
460
523
  return {
461
524
  name: validated.name,
462
525
  version: validated.version,
@@ -479,23 +542,31 @@ function registerSubgraphTools(server) {
479
542
  const detail = await getClient().subgraphs.get(name);
480
543
  return { content: [{ type: "text", text: JSON.stringify(detail, null, 2) }] };
481
544
  });
482
- defineTool(server, "subgraphs_query", "Query rows from a subgraph table (max 50 rows).", {
545
+ defineTool(server, "subgraphs_query", 'Query rows from a subgraph table (max 200 rows). Filters support operators: "amount.gte": "1000", "sender.neq": "SP...", "name.like": "%token%". Available operators: eq, neq, gt, gte, lt, lte, like.', {
483
546
  name: z4.string().describe("Subgraph name"),
484
547
  table: z4.string().describe("Table name"),
485
- filters: z4.record(z4.string(), z4.string()).optional().describe('Column filters as key-value pairs (e.g. {"sender": "SP..."})'),
548
+ filters: z4.record(z4.string(), z4.string()).optional().describe('Column filters plain values or with operators (e.g. {"amount.gte": "1000", "sender": "SP..."})'),
486
549
  sort: z4.string().optional().describe("Column to sort by"),
487
550
  order: z4.enum(["asc", "desc"]).optional().describe("Sort order"),
488
- limit: z4.number().max(50).optional().describe("Max rows (default 50)"),
489
- offset: z4.number().optional().describe("Offset for pagination")
490
- }, async ({ name, table, filters, sort, order, limit, offset }) => {
551
+ limit: z4.number().max(200).optional().describe("Max rows (default 50, max 200)"),
552
+ offset: z4.number().optional().describe("Offset for pagination"),
553
+ fields: z4.string().optional().describe('Comma-separated column list to return (e.g. "sender,amount")'),
554
+ count: z4.boolean().optional().describe("If true, return row count instead of rows")
555
+ }, async ({ name, table, filters, sort, order, limit, offset, fields, count }) => {
556
+ if (count) {
557
+ const result2 = await getClient().subgraphs.queryTableCount(name, table, { filters, sort, order });
558
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
559
+ }
491
560
  const rows = await getClient().subgraphs.queryTable(name, table, {
492
561
  filters,
493
562
  sort,
494
563
  order,
495
564
  limit: limit ?? 50,
496
- offset
565
+ offset,
566
+ fields
497
567
  });
498
- const result = withCap(rows, 50);
568
+ const cap = limit ?? 50;
569
+ const result = withCap(rows, cap > 200 ? 200 : cap);
499
570
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
500
571
  });
501
572
  defineTool(server, "subgraphs_reindex", "Reindex a subgraph from a specific block range.", {
@@ -530,16 +601,81 @@ function registerSubgraphTools(server) {
530
601
  });
531
602
  }
532
603
 
604
+ // src/tools/account.ts
605
+ function registerAccountTools(server) {
606
+ defineTool(server, "account_whoami", "Show the authenticated account's email and plan.", {}, async () => {
607
+ const result = await apiRequest("GET", "/api/accounts/me");
608
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
609
+ });
610
+ }
611
+
612
+ // src/resources.ts
613
+ import { templates as templates2 } from "@secondlayer/subgraphs/templates";
614
+ var FILTERS_REFERENCE = [
615
+ { type: "stx_transfer", fields: ["sender", "recipient", "minAmount", "maxAmount"] },
616
+ { type: "stx_mint", fields: ["recipient", "minAmount"] },
617
+ { type: "stx_burn", fields: ["sender", "minAmount"] },
618
+ { type: "stx_lock", fields: ["lockedAddress", "minAmount"] },
619
+ { type: "ft_transfer", fields: ["sender", "recipient", "assetIdentifier", "minAmount"] },
620
+ { type: "ft_mint", fields: ["recipient", "assetIdentifier", "minAmount"] },
621
+ { type: "ft_burn", fields: ["sender", "assetIdentifier", "minAmount"] },
622
+ { type: "nft_transfer", fields: ["sender", "recipient", "assetIdentifier", "tokenId"] },
623
+ { type: "nft_mint", fields: ["recipient", "assetIdentifier", "tokenId"] },
624
+ { type: "nft_burn", fields: ["sender", "assetIdentifier", "tokenId"] },
625
+ { type: "contract_call", fields: ["contractId", "functionName", "caller"] },
626
+ { type: "contract_deploy", fields: ["deployer", "contractName"] },
627
+ { type: "print_event", fields: ["contractId", "topic", "contains"] }
628
+ ];
629
+ var COLUMN_TYPES = [
630
+ { type: "uint", sqlType: "bigint", description: "Unsigned integer (Clarity uint)" },
631
+ { type: "int", sqlType: "bigint", description: "Signed integer (Clarity int)" },
632
+ { type: "text", sqlType: "text", description: "UTF-8 string" },
633
+ { type: "principal", sqlType: "text", description: "Stacks address (standard or contract)" },
634
+ { type: "bool", sqlType: "boolean", description: "Boolean value" },
635
+ { type: "json", sqlType: "jsonb", description: "Arbitrary JSON data" },
636
+ {
637
+ options: ["nullable", "indexed", "search"],
638
+ description: "Column options: nullable allows NULL, indexed creates a B-tree index, search enables full-text search"
639
+ }
640
+ ];
641
+ function registerResources(server) {
642
+ server.resource("filters", "secondlayer://filters", { description: "Stream filter types and their available fields" }, async () => ({
643
+ contents: [{
644
+ uri: "secondlayer://filters",
645
+ mimeType: "application/json",
646
+ text: JSON.stringify(FILTERS_REFERENCE, null, 2)
647
+ }]
648
+ }));
649
+ server.resource("column-types", "secondlayer://column-types", { description: "Subgraph column types, SQL mappings, and options" }, async () => ({
650
+ contents: [{
651
+ uri: "secondlayer://column-types",
652
+ mimeType: "application/json",
653
+ text: JSON.stringify(COLUMN_TYPES, null, 2)
654
+ }]
655
+ }));
656
+ server.resource("templates", "secondlayer://templates", { description: "Available subgraph templates with descriptions and categories" }, async () => ({
657
+ contents: [{
658
+ uri: "secondlayer://templates",
659
+ mimeType: "application/json",
660
+ text: JSON.stringify(templates2.map(({ id, name, description, category }) => ({ id, name, description, category })), null, 2)
661
+ }]
662
+ }));
663
+ }
664
+
533
665
  // src/server.ts
666
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
667
+ var pkg = JSON.parse(readFileSync(join(__dirname2, "../package.json"), "utf-8"));
534
668
  function createServer() {
535
669
  const server = new McpServer({
536
670
  name: "secondlayer",
537
- version: "0.2.1"
671
+ version: pkg.version
538
672
  });
539
673
  registerTemplateTools(server);
540
674
  registerScaffoldTools(server);
541
675
  registerStreamTools(server);
542
676
  registerSubgraphTools(server);
677
+ registerAccountTools(server);
678
+ registerResources(server);
543
679
  return server;
544
680
  }
545
681
 
@@ -564,10 +700,24 @@ var httpServer = createHttpServer(async (req, res) => {
564
700
  }
565
701
  const sessionId = req.headers["mcp-session-id"];
566
702
  if (req.method === "POST") {
703
+ const MAX_BODY = 1048576;
567
704
  const chunks = [];
568
- for await (const chunk of req)
705
+ let totalSize = 0;
706
+ for await (const chunk of req) {
707
+ totalSize += chunk.length;
708
+ if (totalSize > MAX_BODY) {
709
+ res.writeHead(413).end(JSON.stringify({ error: "Request body too large" }));
710
+ return;
711
+ }
569
712
  chunks.push(chunk);
570
- const body = JSON.parse(Buffer.concat(chunks).toString());
713
+ }
714
+ let body;
715
+ try {
716
+ body = JSON.parse(Buffer.concat(chunks).toString());
717
+ } catch {
718
+ res.writeHead(400).end(JSON.stringify({ error: "Invalid JSON" }));
719
+ return;
720
+ }
571
721
  const isInitialize = Array.isArray(body) ? body.some((m) => m.method === "initialize") : body.method === "initialize";
572
722
  if (isInitialize) {
573
723
  const transport = new StreamableHTTPServerTransport({
@@ -615,5 +765,5 @@ httpServer.listen(port, () => {
615
765
  console.error("Warning: SECONDLAYER_MCP_SECRET not set, authentication disabled");
616
766
  });
617
767
 
618
- //# debugId=51EC2A8F2A8ABB6E64756E2164756E21
768
+ //# debugId=21D5DE1DFDBB85A364756E2164756E21
619
769
  //# sourceMappingURL=bin-http.js.map