@secondlayer/mcp 0.2.0 → 0.3.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 ADDED
@@ -0,0 +1,106 @@
1
+ # @secondlayer/mcp
2
+
3
+ MCP server for the Second Layer indexing platform.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @secondlayer/mcp
9
+ ```
10
+
11
+ ## Quick Start — Stdio (IDE)
12
+
13
+ Add to your Claude Desktop or Cursor config:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "secondlayer": {
19
+ "command": "npx",
20
+ "args": ["@secondlayer/mcp"],
21
+ "env": {
22
+ "SECONDLAYER_API_KEY": "sl_live_..."
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Quick Start — HTTP (Remote)
30
+
31
+ ```bash
32
+ export SECONDLAYER_API_KEY=sl_live_...
33
+ export SECONDLAYER_MCP_SECRET=your-secret
34
+ npx @secondlayer/mcp-http
35
+ # Listening on port 3100
36
+ ```
37
+
38
+ ## Environment Variables
39
+
40
+ | Variable | Required | Default | Description |
41
+ | --- | --- | --- | --- |
42
+ | `SECONDLAYER_API_KEY` | Yes | — | API key |
43
+ | `SECONDLAYER_MCP_PORT` | No | `3100` | HTTP transport port |
44
+ | `SECONDLAYER_MCP_SECRET` | No | — | Bearer token for HTTP auth. Disabled if unset. |
45
+
46
+ ## Tools
47
+
48
+ 22 tools across 5 domains.
49
+
50
+ | Domain | Tools |
51
+ | --- | --- |
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
+ | **Subgraphs** (6) | `subgraphs_list`, `subgraphs_get`, `subgraphs_query`, `subgraphs_reindex`, `subgraphs_delete`, `subgraphs_deploy` |
54
+ | **Scaffold** (2) | `scaffold_from_contract`, `scaffold_from_abi` |
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.
94
+
95
+ ## Programmatic Usage
96
+
97
+ ```typescript
98
+ import { createServer } from "@secondlayer/mcp";
99
+
100
+ const server = createServer();
101
+ // Connect to your own transport
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT
package/dist/bin-http.js CHANGED
@@ -5,6 +5,9 @@ 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
@@ -13,7 +16,20 @@ import { templates, getTemplateById, getTemplatesByCategory } from "@secondlayer
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
@@ -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}`);
@@ -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,6 +470,18 @@ 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
@@ -444,19 +492,34 @@ 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.1.0"
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=EE7F6293C5F1832F64756E2164756E21
768
+ //# debugId=B8BDE1F4EEC0CB2664756E2164756E21
619
769
  //# sourceMappingURL=bin-http.js.map