@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 +106 -0
- package/dist/bin-http.js +174 -24
- package/dist/bin-http.js.map +13 -11
- package/dist/bin.js +158 -22
- package/dist/bin.js.map +12 -10
- package/dist/index.js +158 -22
- package/dist/index.js.map +12 -10
- package/package.json +5 -2
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
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
|
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(
|
|
489
|
-
offset: z4.number().optional().describe("Offset for pagination")
|
|
490
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
768
|
+
//# debugId=B8BDE1F4EEC0CB2664756E2164756E21
|
|
619
769
|
//# sourceMappingURL=bin-http.js.map
|