@iicp/client 0.7.51 → 0.7.56

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
@@ -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.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAwCA,OAAO,EAcL,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,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;AAoaD,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,SAAS,CAsB9E;AAypBD;;;;;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;AAicD,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBlF"}
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,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;AA+pBD;;;;;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)\n` +
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` +
@@ -619,6 +623,12 @@ async function runServe(opts) {
619
623
  let _opDisplayName;
620
624
  let _opCreatedAt;
621
625
  let _opIntegrityHash;
626
+ const _identityNotice = (0, identity_js_1.noIdentityNotice)(_op);
627
+ if (_identityNotice !== null) {
628
+ // #503 — anonymous registration accrues no founder/recognition standing;
629
+ // say so loudly instead of silently excluding the operator. Non-fatal.
630
+ process.stderr.write(_identityNotice + "\n");
631
+ }
622
632
  if (_op && (0, identity_js_1.operatorIsKeyBacked)(_op)) {
623
633
  _opDelegation = (0, delegation_js_1.issueDelegation)((0, identity_js_1.operatorSigningKey)(_op), nodeId);
624
634
  _opDisplayName = _op.display_name || undefined;
@@ -1206,13 +1216,22 @@ async function runCredits(argv) {
1206
1216
  // Multiple nodes have tokens — show all of them.
1207
1217
  const dir = directoryUrl ?? process.env["IICP_DIRECTORY_URL"] ?? "https://iicp.network/api";
1208
1218
  process.stderr.write(`[iicp-node] no --node given — showing credits for all ${withToken.length} nodes:\n`);
1219
+ // One node failing must not hide the others — show every node,
1220
+ // then exit non-zero if any failed (2026-06-11).
1221
+ let failed = 0;
1209
1222
  for (let i = 0; i < withToken.length; i++) {
1210
1223
  if (i > 0)
1211
1224
  process.stdout.write("\n");
1212
1225
  const n = withToken[i];
1213
1226
  const rc = await fetchAndDisplayCredits(n.directory_url ?? dir, n.node_id, n.node_token, n.name, Boolean(values["json"]), Boolean(values["verify"]));
1214
- if (rc !== 0)
1215
- return rc;
1227
+ if (rc !== 0) {
1228
+ process.stderr.write(`ERROR: credits fetch failed for node '${n.name}' — continuing with remaining nodes\n`);
1229
+ failed++;
1230
+ }
1231
+ }
1232
+ if (failed > 0) {
1233
+ process.stderr.write(`ERROR: ${failed}/${withToken.length} node(s) failed\n`);
1234
+ return 1;
1216
1235
  }
1217
1236
  return 0;
1218
1237
  }
@@ -1252,23 +1271,43 @@ async function runCredits(argv) {
1252
1271
  /** Shared fetch+display logic for one node's credits summary. */
1253
1272
  async function fetchAndDisplayCredits(directoryUrl, nodeId, token, label, asJson, verify) {
1254
1273
  const url = `${directoryUrl.replace(/\/+$/, "")}/v1/credits/summary?node_id=${encodeURIComponent(nodeId)}`;
1274
+ // Transient failures (network error, 5xx, undecodable body) get ONE retry
1275
+ // after a short pause — shared-hosting blips and deploy windows otherwise
1276
+ // surface as one-shot CLI errors (observed 2026-06-11).
1255
1277
  let resp;
1256
- try {
1257
- resp = await fetch(url, {
1258
- headers: { Authorization: `Bearer ${token}` },
1259
- signal: AbortSignal.timeout(15000),
1260
- });
1261
- }
1262
- catch (e) {
1263
- process.stderr.write(`ERROR: request failed: ${e instanceof Error ? e.message : String(e)}\n`);
1264
- return 1;
1265
- }
1266
1278
  let body;
1267
- try {
1268
- body = (await resp.json());
1279
+ let lastErr = "";
1280
+ for (let attempt = 1; attempt <= 2; attempt++) {
1281
+ resp = undefined;
1282
+ body = undefined;
1283
+ try {
1284
+ resp = await fetch(url, {
1285
+ headers: { Authorization: `Bearer ${token}` },
1286
+ signal: AbortSignal.timeout(15000),
1287
+ });
1288
+ }
1289
+ catch (e) {
1290
+ lastErr = `request failed: ${e instanceof Error ? e.message : String(e)}`;
1291
+ }
1292
+ if (resp) {
1293
+ try {
1294
+ body = (await resp.json());
1295
+ }
1296
+ catch {
1297
+ lastErr = `bad response (HTTP ${resp.status})`;
1298
+ }
1299
+ if (body !== undefined && resp.status < 500)
1300
+ break; // success or definitive 4xx
1301
+ if (body !== undefined) {
1302
+ const err = body["error"];
1303
+ lastErr = `HTTP ${resp.status}: ${err?.message ?? "request rejected"}`;
1304
+ }
1305
+ }
1306
+ if (attempt === 1)
1307
+ await new Promise((r) => setTimeout(r, 2000));
1269
1308
  }
1270
- catch {
1271
- process.stderr.write(`ERROR: bad response (HTTP ${resp.status})\n`);
1309
+ if (!resp || body === undefined || resp.status >= 500) {
1310
+ process.stderr.write(`ERROR: ${lastErr}\n`);
1272
1311
  return 1;
1273
1312
  }
1274
1313
  if (!resp.ok) {
@@ -1556,6 +1595,187 @@ async function main(argv = process.argv.slice(2)) {
1556
1595
  throw exc;
1557
1596
  }
1558
1597
  }
1598
+ // ── mcp-gateway (#512) ───────────────────────────────────────────────────────
1599
+ const _MCP_DANGEROUS = new Set(["bash", "shell", "exec", "run_command", "eval"]);
1600
+ function _toolToIntent(name) {
1601
+ const safe = name.toLowerCase().replace(/[^a-z0-9_]/g, "_");
1602
+ return `urn:iicp:intent:mcp:${safe}:v1`;
1603
+ }
1604
+ async function runMcpGateway(argv) {
1605
+ const helpFlag = argv.includes("--help") || argv.includes("-h");
1606
+ if (helpFlag) {
1607
+ process.stdout.write(`usage: iicp-node mcp-gateway [options]\n\n` +
1608
+ `Bridge a local MCP server into the IICP mesh as a registered provider node.\n\n` +
1609
+ `Options:\n` +
1610
+ ` --mcp-url URL IICP_MCP_URL — MCP server base URL (default http://localhost:8001)\n` +
1611
+ ` --tools NAMES IICP_MCP_TOOLS — comma-separated tool names to advertise (required)\n` +
1612
+ ` --node-id ID IICP_NODE_ID — auto-generated if absent\n` +
1613
+ ` --public-endpoint U IICP_PUBLIC_ENDPOINT — externally reachable URL of this gateway\n` +
1614
+ ` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api/v1)\n` +
1615
+ ` --region REGION IICP_REGION (default local)\n` +
1616
+ ` --port N IICP_PORT (default 9484)\n` +
1617
+ ` --host HOST IICP_HOST (default ::)\n`);
1618
+ return 0;
1619
+ }
1620
+ const { values } = safeParseArgs({
1621
+ args: argv,
1622
+ options: {
1623
+ "mcp-url": { type: "string" },
1624
+ tools: { type: "string" },
1625
+ "node-id": { type: "string" },
1626
+ "public-endpoint": { type: "string" },
1627
+ "directory-url": { type: "string" },
1628
+ region: { type: "string" },
1629
+ port: { type: "string" },
1630
+ host: { type: "string" },
1631
+ },
1632
+ allowPositionals: false,
1633
+ });
1634
+ const mcpUrl = (values["mcp-url"] ?? envOr("IICP_MCP_URL") ?? "http://localhost:8001").replace(/\/$/, "");
1635
+ const rawTools = (values["tools"] ?? envOr("IICP_MCP_TOOLS") ?? "")
1636
+ .split(",").map((t) => t.trim()).filter(Boolean);
1637
+ const activeTools = rawTools.filter((t) => !_MCP_DANGEROUS.has(t.toLowerCase()));
1638
+ if (activeTools.length === 0) {
1639
+ process.stderr.write(`ERROR: --tools is required. Provide a comma-separated list of MCP tool names.\n` +
1640
+ ` Example: iicp-node mcp-gateway --tools read_file,list_dir --mcp-url http://localhost:8001\n`);
1641
+ return 2;
1642
+ }
1643
+ const nodeId = values["node-id"] ?? envOr("IICP_NODE_ID") ?? `gateway-mcp-${(0, node_crypto_1.randomBytes)(4).toString("hex")}`;
1644
+ const directoryUrl = (values["directory-url"] ?? envOr("IICP_DIRECTORY_URL") ?? "https://iicp.network/api/v1").replace(/\/$/, "");
1645
+ const region = values["region"] ?? envOr("IICP_REGION") ?? "local";
1646
+ const port = parsePort(values["port"], 9484);
1647
+ const host = values["host"] ?? envOr("IICP_HOST") ?? "::";
1648
+ const publicEndpoint = values["public-endpoint"] ?? envOr("IICP_PUBLIC_ENDPOINT") ?? `http://localhost:${port}`;
1649
+ const intents = activeTools.map(_toolToIntent);
1650
+ let nodeToken = envOr("IICP_NODE_TOKEN") ?? "";
1651
+ async function doRegister() {
1652
+ const payload = { node_id: nodeId, region, endpoint: publicEndpoint, intents, mcp_tools: activeTools, protocol_version: "1.0" };
1653
+ const headers = { "Content-Type": "application/json" };
1654
+ if (nodeToken)
1655
+ headers["Authorization"] = `Bearer ${nodeToken}`;
1656
+ const resp = await fetch(`${directoryUrl}/register`, { method: "POST", headers, body: JSON.stringify(payload), signal: AbortSignal.timeout(10_000) });
1657
+ if (!resp.ok)
1658
+ throw new Error(`register ${resp.status}`);
1659
+ const data = await resp.json();
1660
+ return data["node_token"] ?? nodeToken;
1661
+ }
1662
+ async function doHeartbeat() {
1663
+ const payload = { node_id: nodeId, intents, load: 0.0, status: "active" };
1664
+ try {
1665
+ await fetch(`${directoryUrl}/heartbeat`, {
1666
+ method: "POST",
1667
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${nodeToken}` },
1668
+ body: JSON.stringify(payload),
1669
+ signal: AbortSignal.timeout(10_000),
1670
+ });
1671
+ }
1672
+ catch {
1673
+ // heartbeat failures are non-fatal
1674
+ }
1675
+ }
1676
+ let mcpRpcId = 0;
1677
+ async function callMcp(toolName, args) {
1678
+ mcpRpcId += 1;
1679
+ const rpc = { jsonrpc: "2.0", id: mcpRpcId, method: "tools/call", params: { name: toolName, arguments: args } };
1680
+ const resp = await fetch(`${mcpUrl}/mcp`, {
1681
+ method: "POST",
1682
+ headers: { "Content-Type": "application/json" },
1683
+ body: JSON.stringify(rpc),
1684
+ signal: AbortSignal.timeout(30_000),
1685
+ });
1686
+ if (!resp.ok)
1687
+ throw new Error(`MCP server unreachable: ${resp.status}`);
1688
+ const data = await resp.json();
1689
+ if (data["error"])
1690
+ throw new Error(data["error"]["message"] ?? "MCP error");
1691
+ return data["result"];
1692
+ }
1693
+ try {
1694
+ nodeToken = await doRegister();
1695
+ process.stdout.write(`iicp-node mcp-gateway registered as '${nodeId}' with ${activeTools.length} tool(s): ${activeTools.join(", ")}\n` +
1696
+ ` IICP endpoint: ${publicEndpoint}\n MCP server: ${mcpUrl}\n`);
1697
+ }
1698
+ catch (err) {
1699
+ process.stderr.write(`WARNING: directory registration failed (${err.message}) — running without listing\n`);
1700
+ }
1701
+ const hbInterval = setInterval(() => { void doHeartbeat(); }, 30_000);
1702
+ const server = http.createServer(async (req, res) => {
1703
+ if (req.method === "GET" && req.url === "/iicp/health") {
1704
+ const body = JSON.stringify({ status: "ok", node_id: nodeId, active_tools: activeTools, mcp_server: mcpUrl, timestamp: Math.floor(Date.now() / 1000) });
1705
+ res.writeHead(200, { "Content-Type": "application/json" });
1706
+ res.end(body);
1707
+ return;
1708
+ }
1709
+ if (req.method === "POST" && req.url === "/v1/task") {
1710
+ const auth = req.headers["authorization"] ?? "";
1711
+ if (!nodeToken || auth !== `Bearer ${nodeToken}`) {
1712
+ res.writeHead(401, { "Content-Type": "application/json" });
1713
+ res.end(JSON.stringify({ error: "Unauthorized" }));
1714
+ return;
1715
+ }
1716
+ const chunks = [];
1717
+ for await (const chunk of req)
1718
+ chunks.push(chunk);
1719
+ let body;
1720
+ try {
1721
+ body = JSON.parse(Buffer.concat(chunks).toString());
1722
+ }
1723
+ catch {
1724
+ res.writeHead(400, { "Content-Type": "application/json" });
1725
+ res.end(JSON.stringify({ error: "invalid JSON" }));
1726
+ return;
1727
+ }
1728
+ const payload = (body["payload"] ?? {});
1729
+ let toolName = payload["tool_name"] ?? "";
1730
+ if (!toolName) {
1731
+ const m = /urn:iicp:intent:mcp:([^:]+):v1/.exec(body["intent"] ?? "");
1732
+ if (m)
1733
+ toolName = m[1];
1734
+ }
1735
+ if (!toolName) {
1736
+ res.writeHead(400, { "Content-Type": "application/json" });
1737
+ res.end(JSON.stringify({ error: "Cannot determine tool name" }));
1738
+ return;
1739
+ }
1740
+ if (_MCP_DANGEROUS.has(toolName.toLowerCase())) {
1741
+ res.writeHead(403, { "Content-Type": "application/json" });
1742
+ res.end(JSON.stringify({ error: "Tool not permitted" }));
1743
+ return;
1744
+ }
1745
+ if (activeTools.length && !activeTools.includes(toolName)) {
1746
+ res.writeHead(404, { "Content-Type": "application/json" });
1747
+ res.end(JSON.stringify({ error: "Tool not available" }));
1748
+ return;
1749
+ }
1750
+ const taskId = body["task_id"] ?? (0, node_crypto_1.randomUUID)();
1751
+ try {
1752
+ const result = await callMcp(toolName, (payload["arguments"] ?? {}));
1753
+ res.writeHead(200, { "Content-Type": "application/json" });
1754
+ res.end(JSON.stringify({ task_id: taskId, status: "completed", result }));
1755
+ }
1756
+ catch (err) {
1757
+ const msg = err.message ?? "error";
1758
+ const code = msg.includes("unreachable") ? 502 : 422;
1759
+ res.writeHead(code, { "Content-Type": "application/json" });
1760
+ res.end(JSON.stringify({ error: msg }));
1761
+ }
1762
+ return;
1763
+ }
1764
+ res.writeHead(404, { "Content-Type": "application/json" });
1765
+ res.end(JSON.stringify({ error: "not found" }));
1766
+ });
1767
+ await new Promise((resolve) => {
1768
+ server.listen(port, host === "::" ? "::" : host, () => {
1769
+ process.stdout.write(` Listening on ${host}:${port}\n`);
1770
+ resolve();
1771
+ });
1772
+ });
1773
+ await new Promise((resolve) => {
1774
+ process.on("SIGINT", () => { clearInterval(hbInterval); server.close(); resolve(); });
1775
+ process.on("SIGTERM", () => { clearInterval(hbInterval); server.close(); resolve(); });
1776
+ });
1777
+ return 0;
1778
+ }
1559
1779
  /** Command dispatch — separated so main() can wrap parse failures as clean CliError output. */
1560
1780
  async function dispatch(argv) {
1561
1781
  const cmd = argv[0];
@@ -1571,6 +1791,8 @@ async function dispatch(argv) {
1571
1791
  return runOperator(argv.slice(1));
1572
1792
  if (cmd === "proxy")
1573
1793
  return runProxyCmd(argv.slice(1));
1794
+ if (cmd === "mcp-gateway")
1795
+ return runMcpGateway(argv.slice(1));
1574
1796
  if (cmd !== "serve") {
1575
1797
  process.stderr.write(`unknown command: ${cmd}\n`);
1576
1798
  printHelp();