@iicp/client 0.7.54 → 0.7.57
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 +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +260 -3
- package/dist/cli.js.map +1 -1
- package/dist/node.d.ts +10 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +0 -0
- package/dist/node.js.map +1 -1
- package/dist/relay_session.d.ts +58 -2
- package/dist/relay_session.d.ts.map +1 -1
- package/dist/relay_session.js +140 -3
- package/dist/relay_session.js.map +1 -1
- package/dist/relay_worker_client.d.ts +3 -1
- package/dist/relay_worker_client.d.ts.map +1 -1
- package/dist/relay_worker_client.js +5 -1
- package/dist/relay_worker_client.js.map +1 -1
- package/dist/tunnel.d.ts +50 -0
- package/dist/tunnel.d.ts.map +1 -0
- package/dist/tunnel.js +179 -0
- package/dist/tunnel.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -317,6 +317,8 @@ const node = new IicpNode({
|
|
|
317
317
|
});
|
|
318
318
|
```
|
|
319
319
|
|
|
320
|
+
> **Security note**: relay bind authentication is pending ([#510](https://github.com/RobLe3/iicp.network/issues/510)) — only run a relay accept port on networks you trust until the signed-bind mechanism ships.
|
|
321
|
+
|
|
320
322
|
### Opt-out / override
|
|
321
323
|
|
|
322
324
|
```bash
|
package/dist/cli.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface ServeOpts {
|
|
|
20
20
|
externalIpProbeUrl: string;
|
|
21
21
|
relayWorkerEndpoint: string;
|
|
22
22
|
relayCapable?: boolean;
|
|
23
|
+
/** #520 rung 5 tri-state: true=forced, false=disabled, undefined=auto. */
|
|
24
|
+
tunnel?: boolean;
|
|
23
25
|
relayAcceptPort?: number;
|
|
24
26
|
node: string;
|
|
25
27
|
logDir?: string;
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAyCA,OAAO,EAeL,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAGvB,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,qGAAqG;IACrG,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,OAAO,CAAC;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iGAAiG;IACjG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAuaD,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,SAAS,CAsB9E;AA0uBD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA+D5D;AA8dD,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBlF"}
|
package/dist/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
26
26
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
27
|
const SDK_VERSION = require("../package.json").version;
|
|
28
28
|
const net = require("node:net");
|
|
29
|
+
const http = require("node:http");
|
|
29
30
|
const node_child_process_1 = require("node:child_process");
|
|
30
31
|
const readline = require("node:readline/promises");
|
|
31
32
|
const node_process_1 = require("node:process");
|
|
@@ -64,6 +65,7 @@ function printHelp() {
|
|
|
64
65
|
` query <prompt> Discover mesh nodes and submit a chat task\n` +
|
|
65
66
|
` credits Show this node's earned / spent / balance credits\n` +
|
|
66
67
|
` proxy Run the local OpenAI/Ollama/Anthropic-compat gateway (loopback; no registration)\n` +
|
|
68
|
+
` mcp-gateway Bridge a local MCP server as an IICP provider node (registers + serves)\n` +
|
|
67
69
|
` operator rename <name> Change your public display_name (signed by your operator key)\n` +
|
|
68
70
|
` operator encrypt Password-encrypt the operator secret at rest ($IICP_OPERATOR_PASSPHRASE)\n` +
|
|
69
71
|
` operator decrypt Remove at-rest encryption of the operator secret\n\n` +
|
|
@@ -90,7 +92,9 @@ function printHelp() {
|
|
|
90
92
|
` --external-ip-probe-url U IICP_EXTERNAL_IP_PROBE_URL — fallback IPv4 probe\n` +
|
|
91
93
|
` --relay-worker-endpoint H IICP_RELAY_WORKER_ENDPOINT — <host>:<port> of a relay node (R2 last-resort)\n` +
|
|
92
94
|
` --relay-capable IICP_RELAY_CAPABLE — advertise as relay server for CGNAT/tier-4 operators\n` +
|
|
93
|
-
` --relay-accept-port N IICP_RELAY_ACCEPT_PORT — TCP port for relay accept server (default 9485)
|
|
95
|
+
` --relay-accept-port N IICP_RELAY_ACCEPT_PORT — TCP port for relay accept server (default 9485).\n` +
|
|
96
|
+
` Note: relay bind authentication is pending (#510) — only run a relay\n` +
|
|
97
|
+
` accept port on networks you trust until the signed-bind mechanism ships.\n` +
|
|
94
98
|
` --log-dir DIR IICP_LOG_DIR — directory for persistent log files (<node_id>.log + events.jsonl)\n` +
|
|
95
99
|
` --with-proxy IICP_WITH_PROXY — also run the loopback compat proxy (127.0.0.1:9483) in-process\n\n` +
|
|
96
100
|
`query optional:\n` +
|
|
@@ -541,6 +545,29 @@ async function runServe(opts) {
|
|
|
541
545
|
}
|
|
542
546
|
}
|
|
543
547
|
let publicEndpoint = opts.publicEndpoint || `http://localhost:${opts.port}`;
|
|
548
|
+
// #520 rung 5 — Quick Tunnel escalation state (see src/tunnel.ts).
|
|
549
|
+
const tunnelPref = opts.tunnel;
|
|
550
|
+
let tunnelHandle = null;
|
|
551
|
+
const openTunnelRung = async (localPort, forced) => {
|
|
552
|
+
const { INSTALL_HINT, cloudflaredPath, openQuickTunnel } = await import("./tunnel.js");
|
|
553
|
+
if (!cloudflaredPath()) {
|
|
554
|
+
// eslint-disable-next-line no-console
|
|
555
|
+
console.warn(`[iicp-node] ${INSTALL_HINT}`);
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
const t = await openQuickTunnel(localPort);
|
|
560
|
+
// eslint-disable-next-line no-console
|
|
561
|
+
console.log(`[iicp-node] NAT rung 5${forced ? " (forced)" : ""}: public https endpoint via ` +
|
|
562
|
+
`Quick Tunnel — ${t.url} (zero-account; URL rotates on restart)`);
|
|
563
|
+
return t;
|
|
564
|
+
}
|
|
565
|
+
catch (exc) {
|
|
566
|
+
// eslint-disable-next-line no-console
|
|
567
|
+
console.warn(`[iicp-node] Quick Tunnel failed to start: ${exc instanceof Error ? exc.message : exc} — continuing without it`);
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
544
571
|
// ADR-043 §5 / #343 — Tier-0 IPv6 pinhole attempt. Runs unconditionally
|
|
545
572
|
// when the operator's public_endpoint is bracketed-IPv6, even without
|
|
546
573
|
// --auto-detect-nat. Mirrors Python's cli.py path: try AddPinhole on
|
|
@@ -596,10 +623,22 @@ async function runServe(opts) {
|
|
|
596
623
|
// eslint-disable-next-line no-console
|
|
597
624
|
console.log(`[iicp-node] auto-elected relay: ${relayHost}:${relayPort}`);
|
|
598
625
|
}
|
|
626
|
+
else if (tunnelPref !== false) {
|
|
627
|
+
// #520 rung 5: no relay anywhere → Quick Tunnel (zero-account),
|
|
628
|
+
// unless disabled via --no-tunnel / IICP_TUNNEL=0.
|
|
629
|
+
tunnelHandle = await openTunnelRung(opts.port, false);
|
|
630
|
+
if (tunnelHandle)
|
|
631
|
+
publicEndpoint = tunnelHandle.url;
|
|
632
|
+
if (!tunnelHandle) {
|
|
633
|
+
// eslint-disable-next-line no-console
|
|
634
|
+
console.warn(`[iicp-node] NAT tier=${natProfile.tier}: no relay-capable peers and no tunnel ` +
|
|
635
|
+
`available. Set IICP_RELAY_WORKER_ENDPOINT=<host>:<port> to specify a relay manually.`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
599
638
|
else {
|
|
600
639
|
// eslint-disable-next-line no-console
|
|
601
|
-
console.warn(`[iicp-node] NAT tier=${natProfile.tier}: no relay-capable peers in directory
|
|
602
|
-
`Set IICP_RELAY_WORKER_ENDPOINT
|
|
640
|
+
console.warn(`[iicp-node] NAT tier=${natProfile.tier}: no relay-capable peers in directory ` +
|
|
641
|
+
`(tunnel escalation disabled). Set IICP_RELAY_WORKER_ENDPOINT to specify a relay.`);
|
|
603
642
|
}
|
|
604
643
|
}
|
|
605
644
|
}
|
|
@@ -609,6 +648,14 @@ async function runServe(opts) {
|
|
|
609
648
|
console.warn(`[iicp-node] NAT auto-detect failed: ${msg} — continuing with configured endpoint`);
|
|
610
649
|
}
|
|
611
650
|
}
|
|
651
|
+
// #520 — `--tunnel` forces rung 5 regardless of NAT tier (e.g. an operator
|
|
652
|
+
// who KNOWS they're unreachable, or wants an https endpoint for browser
|
|
653
|
+
// consumers without touching the router).
|
|
654
|
+
if (tunnelPref === true && tunnelHandle === null) {
|
|
655
|
+
tunnelHandle = await openTunnelRung(opts.port, true);
|
|
656
|
+
if (tunnelHandle)
|
|
657
|
+
publicEndpoint = tunnelHandle.url;
|
|
658
|
+
}
|
|
612
659
|
const backendFlavor = await detectBackendFlavor(opts.backendUrl, opts.backendApiKey, opts.backendType);
|
|
613
660
|
process.stderr.write(`backend detected: ${backendFlavor}\n`);
|
|
614
661
|
// #463/#464 — bind the operator identity: issue a delegation FROM the (key-backed) operator
|
|
@@ -820,6 +867,22 @@ async function runServe(opts) {
|
|
|
820
867
|
(0, node_log_js_1.writeNodeEvent)(nodeId, "serve_start", `port=${opts.port} model=${opts.model} intent=${opts.intent}`, logDir);
|
|
821
868
|
// serve() returns a stop() handle but never resolves on its own; we wait for
|
|
822
869
|
// SIGINT/SIGTERM to terminate.
|
|
870
|
+
if (tunnelHandle) {
|
|
871
|
+
// The endpoint was already the tunnel URL at construction; mark the
|
|
872
|
+
// transport and arm the watchdog (URL rotates per process → re-register).
|
|
873
|
+
node["_cfg"].transportMethod = "external_tunnel";
|
|
874
|
+
tunnelHandle.watch((url) => {
|
|
875
|
+
node["_cfg"].endpoint = url;
|
|
876
|
+
void node.register().catch((exc) => {
|
|
877
|
+
// eslint-disable-next-line no-console
|
|
878
|
+
console.warn(`[iicp-node] tunnel re-register failed: ${exc instanceof Error ? exc.message : exc}`);
|
|
879
|
+
});
|
|
880
|
+
}, () => {
|
|
881
|
+
// eslint-disable-next-line no-console
|
|
882
|
+
console.error("[iicp-node] Quick Tunnel permanently down — this node is no longer " +
|
|
883
|
+
"publicly reachable. Restart `iicp-node serve` to recover.");
|
|
884
|
+
});
|
|
885
|
+
}
|
|
823
886
|
const stop = node.serve(handler, { host: opts.host, port: opts.port, nodeToken: token });
|
|
824
887
|
await new Promise((resolve) => {
|
|
825
888
|
const shutdown = async (sig) => {
|
|
@@ -844,6 +907,7 @@ async function runServe(opts) {
|
|
|
844
907
|
console.warn(`[iicp-node] deregister failed: ${deregMsg}`);
|
|
845
908
|
(0, node_log_js_1.writeNodeEvent)(nodeId, "deregister_fail", `error=${deregMsg}`, logDir);
|
|
846
909
|
}
|
|
910
|
+
tunnelHandle?.close(); // #520 — tear the Quick Tunnel down with the node
|
|
847
911
|
stop();
|
|
848
912
|
instanceLock.release(); // #405 — free the pidfile on shutdown
|
|
849
913
|
resolve();
|
|
@@ -1591,6 +1655,187 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1591
1655
|
throw exc;
|
|
1592
1656
|
}
|
|
1593
1657
|
}
|
|
1658
|
+
// ── mcp-gateway (#512) ───────────────────────────────────────────────────────
|
|
1659
|
+
const _MCP_DANGEROUS = new Set(["bash", "shell", "exec", "run_command", "eval"]);
|
|
1660
|
+
function _toolToIntent(name) {
|
|
1661
|
+
const safe = name.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
1662
|
+
return `urn:iicp:intent:mcp:${safe}:v1`;
|
|
1663
|
+
}
|
|
1664
|
+
async function runMcpGateway(argv) {
|
|
1665
|
+
const helpFlag = argv.includes("--help") || argv.includes("-h");
|
|
1666
|
+
if (helpFlag) {
|
|
1667
|
+
process.stdout.write(`usage: iicp-node mcp-gateway [options]\n\n` +
|
|
1668
|
+
`Bridge a local MCP server into the IICP mesh as a registered provider node.\n\n` +
|
|
1669
|
+
`Options:\n` +
|
|
1670
|
+
` --mcp-url URL IICP_MCP_URL — MCP server base URL (default http://localhost:8001)\n` +
|
|
1671
|
+
` --tools NAMES IICP_MCP_TOOLS — comma-separated tool names to advertise (required)\n` +
|
|
1672
|
+
` --node-id ID IICP_NODE_ID — auto-generated if absent\n` +
|
|
1673
|
+
` --public-endpoint U IICP_PUBLIC_ENDPOINT — externally reachable URL of this gateway\n` +
|
|
1674
|
+
` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api/v1)\n` +
|
|
1675
|
+
` --region REGION IICP_REGION (default local)\n` +
|
|
1676
|
+
` --port N IICP_PORT (default 9484)\n` +
|
|
1677
|
+
` --host HOST IICP_HOST (default ::)\n`);
|
|
1678
|
+
return 0;
|
|
1679
|
+
}
|
|
1680
|
+
const { values } = safeParseArgs({
|
|
1681
|
+
args: argv,
|
|
1682
|
+
options: {
|
|
1683
|
+
"mcp-url": { type: "string" },
|
|
1684
|
+
tools: { type: "string" },
|
|
1685
|
+
"node-id": { type: "string" },
|
|
1686
|
+
"public-endpoint": { type: "string" },
|
|
1687
|
+
"directory-url": { type: "string" },
|
|
1688
|
+
region: { type: "string" },
|
|
1689
|
+
port: { type: "string" },
|
|
1690
|
+
host: { type: "string" },
|
|
1691
|
+
},
|
|
1692
|
+
allowPositionals: false,
|
|
1693
|
+
});
|
|
1694
|
+
const mcpUrl = (values["mcp-url"] ?? envOr("IICP_MCP_URL") ?? "http://localhost:8001").replace(/\/$/, "");
|
|
1695
|
+
const rawTools = (values["tools"] ?? envOr("IICP_MCP_TOOLS") ?? "")
|
|
1696
|
+
.split(",").map((t) => t.trim()).filter(Boolean);
|
|
1697
|
+
const activeTools = rawTools.filter((t) => !_MCP_DANGEROUS.has(t.toLowerCase()));
|
|
1698
|
+
if (activeTools.length === 0) {
|
|
1699
|
+
process.stderr.write(`ERROR: --tools is required. Provide a comma-separated list of MCP tool names.\n` +
|
|
1700
|
+
` Example: iicp-node mcp-gateway --tools read_file,list_dir --mcp-url http://localhost:8001\n`);
|
|
1701
|
+
return 2;
|
|
1702
|
+
}
|
|
1703
|
+
const nodeId = values["node-id"] ?? envOr("IICP_NODE_ID") ?? `gateway-mcp-${(0, node_crypto_1.randomBytes)(4).toString("hex")}`;
|
|
1704
|
+
const directoryUrl = (values["directory-url"] ?? envOr("IICP_DIRECTORY_URL") ?? "https://iicp.network/api/v1").replace(/\/$/, "");
|
|
1705
|
+
const region = values["region"] ?? envOr("IICP_REGION") ?? "local";
|
|
1706
|
+
const port = parsePort(values["port"], 9484);
|
|
1707
|
+
const host = values["host"] ?? envOr("IICP_HOST") ?? "::";
|
|
1708
|
+
const publicEndpoint = values["public-endpoint"] ?? envOr("IICP_PUBLIC_ENDPOINT") ?? `http://localhost:${port}`;
|
|
1709
|
+
const intents = activeTools.map(_toolToIntent);
|
|
1710
|
+
let nodeToken = envOr("IICP_NODE_TOKEN") ?? "";
|
|
1711
|
+
async function doRegister() {
|
|
1712
|
+
const payload = { node_id: nodeId, region, endpoint: publicEndpoint, intents, mcp_tools: activeTools, protocol_version: "1.0" };
|
|
1713
|
+
const headers = { "Content-Type": "application/json" };
|
|
1714
|
+
if (nodeToken)
|
|
1715
|
+
headers["Authorization"] = `Bearer ${nodeToken}`;
|
|
1716
|
+
const resp = await fetch(`${directoryUrl}/register`, { method: "POST", headers, body: JSON.stringify(payload), signal: AbortSignal.timeout(10_000) });
|
|
1717
|
+
if (!resp.ok)
|
|
1718
|
+
throw new Error(`register ${resp.status}`);
|
|
1719
|
+
const data = await resp.json();
|
|
1720
|
+
return data["node_token"] ?? nodeToken;
|
|
1721
|
+
}
|
|
1722
|
+
async function doHeartbeat() {
|
|
1723
|
+
const payload = { node_id: nodeId, intents, load: 0.0, status: "active" };
|
|
1724
|
+
try {
|
|
1725
|
+
await fetch(`${directoryUrl}/heartbeat`, {
|
|
1726
|
+
method: "POST",
|
|
1727
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${nodeToken}` },
|
|
1728
|
+
body: JSON.stringify(payload),
|
|
1729
|
+
signal: AbortSignal.timeout(10_000),
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
catch {
|
|
1733
|
+
// heartbeat failures are non-fatal
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
let mcpRpcId = 0;
|
|
1737
|
+
async function callMcp(toolName, args) {
|
|
1738
|
+
mcpRpcId += 1;
|
|
1739
|
+
const rpc = { jsonrpc: "2.0", id: mcpRpcId, method: "tools/call", params: { name: toolName, arguments: args } };
|
|
1740
|
+
const resp = await fetch(`${mcpUrl}/mcp`, {
|
|
1741
|
+
method: "POST",
|
|
1742
|
+
headers: { "Content-Type": "application/json" },
|
|
1743
|
+
body: JSON.stringify(rpc),
|
|
1744
|
+
signal: AbortSignal.timeout(30_000),
|
|
1745
|
+
});
|
|
1746
|
+
if (!resp.ok)
|
|
1747
|
+
throw new Error(`MCP server unreachable: ${resp.status}`);
|
|
1748
|
+
const data = await resp.json();
|
|
1749
|
+
if (data["error"])
|
|
1750
|
+
throw new Error(data["error"]["message"] ?? "MCP error");
|
|
1751
|
+
return data["result"];
|
|
1752
|
+
}
|
|
1753
|
+
try {
|
|
1754
|
+
nodeToken = await doRegister();
|
|
1755
|
+
process.stdout.write(`iicp-node mcp-gateway registered as '${nodeId}' with ${activeTools.length} tool(s): ${activeTools.join(", ")}\n` +
|
|
1756
|
+
` IICP endpoint: ${publicEndpoint}\n MCP server: ${mcpUrl}\n`);
|
|
1757
|
+
}
|
|
1758
|
+
catch (err) {
|
|
1759
|
+
process.stderr.write(`WARNING: directory registration failed (${err.message}) — running without listing\n`);
|
|
1760
|
+
}
|
|
1761
|
+
const hbInterval = setInterval(() => { void doHeartbeat(); }, 30_000);
|
|
1762
|
+
const server = http.createServer(async (req, res) => {
|
|
1763
|
+
if (req.method === "GET" && req.url === "/iicp/health") {
|
|
1764
|
+
const body = JSON.stringify({ status: "ok", node_id: nodeId, active_tools: activeTools, mcp_server: mcpUrl, timestamp: Math.floor(Date.now() / 1000) });
|
|
1765
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1766
|
+
res.end(body);
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
if (req.method === "POST" && req.url === "/v1/task") {
|
|
1770
|
+
const auth = req.headers["authorization"] ?? "";
|
|
1771
|
+
if (!nodeToken || auth !== `Bearer ${nodeToken}`) {
|
|
1772
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1773
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
const chunks = [];
|
|
1777
|
+
for await (const chunk of req)
|
|
1778
|
+
chunks.push(chunk);
|
|
1779
|
+
let body;
|
|
1780
|
+
try {
|
|
1781
|
+
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
1782
|
+
}
|
|
1783
|
+
catch {
|
|
1784
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1785
|
+
res.end(JSON.stringify({ error: "invalid JSON" }));
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
const payload = (body["payload"] ?? {});
|
|
1789
|
+
let toolName = payload["tool_name"] ?? "";
|
|
1790
|
+
if (!toolName) {
|
|
1791
|
+
const m = /urn:iicp:intent:mcp:([^:]+):v1/.exec(body["intent"] ?? "");
|
|
1792
|
+
if (m)
|
|
1793
|
+
toolName = m[1];
|
|
1794
|
+
}
|
|
1795
|
+
if (!toolName) {
|
|
1796
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1797
|
+
res.end(JSON.stringify({ error: "Cannot determine tool name" }));
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
if (_MCP_DANGEROUS.has(toolName.toLowerCase())) {
|
|
1801
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1802
|
+
res.end(JSON.stringify({ error: "Tool not permitted" }));
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (activeTools.length && !activeTools.includes(toolName)) {
|
|
1806
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1807
|
+
res.end(JSON.stringify({ error: "Tool not available" }));
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
const taskId = body["task_id"] ?? (0, node_crypto_1.randomUUID)();
|
|
1811
|
+
try {
|
|
1812
|
+
const result = await callMcp(toolName, (payload["arguments"] ?? {}));
|
|
1813
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1814
|
+
res.end(JSON.stringify({ task_id: taskId, status: "completed", result }));
|
|
1815
|
+
}
|
|
1816
|
+
catch (err) {
|
|
1817
|
+
const msg = err.message ?? "error";
|
|
1818
|
+
const code = msg.includes("unreachable") ? 502 : 422;
|
|
1819
|
+
res.writeHead(code, { "Content-Type": "application/json" });
|
|
1820
|
+
res.end(JSON.stringify({ error: msg }));
|
|
1821
|
+
}
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1825
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
1826
|
+
});
|
|
1827
|
+
await new Promise((resolve) => {
|
|
1828
|
+
server.listen(port, host === "::" ? "::" : host, () => {
|
|
1829
|
+
process.stdout.write(` Listening on ${host}:${port}\n`);
|
|
1830
|
+
resolve();
|
|
1831
|
+
});
|
|
1832
|
+
});
|
|
1833
|
+
await new Promise((resolve) => {
|
|
1834
|
+
process.on("SIGINT", () => { clearInterval(hbInterval); server.close(); resolve(); });
|
|
1835
|
+
process.on("SIGTERM", () => { clearInterval(hbInterval); server.close(); resolve(); });
|
|
1836
|
+
});
|
|
1837
|
+
return 0;
|
|
1838
|
+
}
|
|
1594
1839
|
/** Command dispatch — separated so main() can wrap parse failures as clean CliError output. */
|
|
1595
1840
|
async function dispatch(argv) {
|
|
1596
1841
|
const cmd = argv[0];
|
|
@@ -1606,6 +1851,8 @@ async function dispatch(argv) {
|
|
|
1606
1851
|
return runOperator(argv.slice(1));
|
|
1607
1852
|
if (cmd === "proxy")
|
|
1608
1853
|
return runProxyCmd(argv.slice(1));
|
|
1854
|
+
if (cmd === "mcp-gateway")
|
|
1855
|
+
return runMcpGateway(argv.slice(1));
|
|
1609
1856
|
if (cmd !== "serve") {
|
|
1610
1857
|
process.stderr.write(`unknown command: ${cmd}\n`);
|
|
1611
1858
|
printHelp();
|
|
@@ -1628,6 +1875,8 @@ async function dispatch(argv) {
|
|
|
1628
1875
|
port: { type: "string" },
|
|
1629
1876
|
host: { type: "string" },
|
|
1630
1877
|
"skip-registration": { type: "boolean" },
|
|
1878
|
+
tunnel: { type: "boolean" },
|
|
1879
|
+
"no-tunnel": { type: "boolean" },
|
|
1631
1880
|
force: { type: "boolean" },
|
|
1632
1881
|
"auto-detect-nat": { type: "boolean" },
|
|
1633
1882
|
// Parity with Python's BooleanOptionalAction: an explicit off-switch for NAT
|
|
@@ -1683,6 +1932,14 @@ async function dispatch(argv) {
|
|
|
1683
1932
|
?? envOr("IICP_EXTERNAL_IP_PROBE_URL")
|
|
1684
1933
|
?? "https://api.ipify.org",
|
|
1685
1934
|
relayWorkerEndpoint: values["relay-worker-endpoint"] ?? envOr("IICP_RELAY_WORKER_ENDPOINT") ?? "",
|
|
1935
|
+
// #520 rung 5 tri-state: --tunnel > --no-tunnel > IICP_TUNNEL env > auto (undefined).
|
|
1936
|
+
tunnel: values["tunnel"]
|
|
1937
|
+
? true
|
|
1938
|
+
: values["no-tunnel"]
|
|
1939
|
+
? false
|
|
1940
|
+
: process.env.IICP_TUNNEL !== undefined
|
|
1941
|
+
? envBool("IICP_TUNNEL")
|
|
1942
|
+
: undefined,
|
|
1686
1943
|
relayCapable: values["relay-capable"] !== undefined
|
|
1687
1944
|
? Boolean(values["relay-capable"])
|
|
1688
1945
|
: envBool("IICP_RELAY_CAPABLE"),
|