@iicp/client 0.7.100 → 0.7.102
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/LICENSE +83 -61
- package/README.md +2 -2
- package/dist/backends/openai_compat.d.ts +3 -0
- package/dist/backends/openai_compat.d.ts.map +1 -1
- package/dist/backends/openai_compat.js +119 -0
- package/dist/backends/openai_compat.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +264 -39
- package/dist/cli.js.map +1 -1
- package/dist/iicp_tcp.d.ts +38 -0
- package/dist/iicp_tcp.d.ts.map +1 -1
- package/dist/iicp_tcp.js +164 -0
- package/dist/iicp_tcp.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp_negotiation.d.ts +23 -0
- package/dist/mcp_negotiation.d.ts.map +1 -0
- package/dist/mcp_negotiation.js +94 -0
- package/dist/mcp_negotiation.js.map +1 -0
- package/dist/native_call_identity.d.ts +12 -0
- package/dist/native_call_identity.d.ts.map +1 -0
- package/dist/native_call_identity.js +44 -0
- package/dist/native_call_identity.js.map +1 -0
- package/dist/native_response_sequence.d.ts +32 -0
- package/dist/native_response_sequence.d.ts.map +1 -0
- package/dist/native_response_sequence.js +59 -0
- package/dist/native_response_sequence.js.map +1 -0
- package/dist/node.d.ts +5 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +59 -7
- package/dist/node.js.map +1 -1
- package/dist/operator_profile.d.ts +18 -0
- package/dist/operator_profile.d.ts.map +1 -0
- package/dist/operator_profile.js +22 -0
- package/dist/operator_profile.js.map +1 -0
- package/dist/relay_session.d.ts +16 -4
- package/dist/relay_session.d.ts.map +1 -1
- package/dist/relay_session.js +116 -1
- package/dist/relay_session.js.map +1 -1
- package/dist/runtime_health.d.ts +77 -0
- package/dist/runtime_health.d.ts.map +1 -0
- package/dist/runtime_health.js +65 -0
- package/dist/runtime_health.js.map +1 -0
- package/dist/service.d.ts +7 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +44 -0
- package/dist/service.js.map +1 -1
- package/package.json +6 -3
package/dist/cli.js
CHANGED
|
@@ -44,6 +44,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
44
44
|
const readline = require("node:readline/promises");
|
|
45
45
|
const node_process_1 = require("node:process");
|
|
46
46
|
const node_js_1 = require("./node.js");
|
|
47
|
+
const runtime_health_js_1 = require("./runtime_health.js");
|
|
47
48
|
const client_js_1 = require("./client.js");
|
|
48
49
|
const node_log_js_1 = require("./node_log.js");
|
|
49
50
|
const cip_policy_js_1 = require("./cip_policy.js");
|
|
@@ -53,7 +54,10 @@ const index_js_2 = require("./backends/index.js");
|
|
|
53
54
|
const recovery_js_1 = require("./recovery.js");
|
|
54
55
|
const identity_js_1 = require("./identity.js");
|
|
55
56
|
const delegation_js_1 = require("./delegation.js");
|
|
57
|
+
const operator_profile_js_1 = require("./operator_profile.js");
|
|
58
|
+
const updater_js_1 = require("./updater.js");
|
|
56
59
|
const mcp_policy_js_1 = require("./mcp_policy.js");
|
|
60
|
+
const mcp_negotiation_js_1 = require("./mcp_negotiation.js");
|
|
57
61
|
/**
|
|
58
62
|
* Start the default-on provider updater for long-running TS node processes.
|
|
59
63
|
*
|
|
@@ -169,6 +173,7 @@ function printHelp() {
|
|
|
169
173
|
` list List node configs saved under ~/.iicp/nodes/\n` +
|
|
170
174
|
` serve Register and serve a node\n` +
|
|
171
175
|
` doctor Check local health, directory presence, and recovery action\n` +
|
|
176
|
+
` healthcheck Check local runtime liveness/readiness snapshot\n` +
|
|
172
177
|
` query <prompt> Discover mesh nodes and submit a chat task\n` +
|
|
173
178
|
` credits Show your operator wallet plus this node's credit ledger\n` +
|
|
174
179
|
` proxy Run the local OpenAI/Ollama/Anthropic-compat gateway (loopback; no registration)\n` +
|
|
@@ -692,18 +697,6 @@ async function runServe(opts) {
|
|
|
692
697
|
allowCoordinator: true,
|
|
693
698
|
});
|
|
694
699
|
}
|
|
695
|
-
// 2-C: co-host the compat proxy on loopback alongside the node, supervised so a
|
|
696
|
-
// proxy failure logs but never drops the network-facing node. Forced to 127.0.0.1.
|
|
697
|
-
if (opts.withProxy) {
|
|
698
|
-
const pport = envInt("IICP_PROXY_PORT", 9483);
|
|
699
|
-
const pclient = new client_js_1.IicpClient({
|
|
700
|
-
directory_url: opts.directoryUrl,
|
|
701
|
-
region: opts.region,
|
|
702
|
-
});
|
|
703
|
-
const pserver = (0, index_js_1.createProxyServer)(pclient);
|
|
704
|
-
pserver.on("error", (e) => process.stderr.write(`co-hosted proxy error (node continues): ${String(e)}\n`));
|
|
705
|
-
pserver.listen(pport, "127.0.0.1", () => process.stdout.write(`co-hosted proxy → http://127.0.0.1:${pport} (OpenAI/Ollama/Anthropic compat)\n`));
|
|
706
|
-
}
|
|
707
700
|
// Phase 2 (#529/#55) — capture any cached node_token so the node can prove
|
|
708
701
|
// ownership on re-registration (IICP-E050 token path).
|
|
709
702
|
let savedNodeToken;
|
|
@@ -716,6 +709,31 @@ async function runServe(opts) {
|
|
|
716
709
|
savedNodeToken = saved.node_token ?? undefined;
|
|
717
710
|
opts = applySavedNode(opts, saved);
|
|
718
711
|
}
|
|
712
|
+
const _op = (0, identity_js_1.loadOperator)();
|
|
713
|
+
const managedDecision = (0, operator_profile_js_1.evaluateManagedOperator)({
|
|
714
|
+
mode: (process.env.IICP_OPERATOR_PROFILE ?? "convenience").trim().toLowerCase(),
|
|
715
|
+
authentication_configured: !!_op && (0, identity_js_1.operatorIsKeyBacked)(_op),
|
|
716
|
+
identity_storage_protected: !!_op && (0, identity_js_1.operatorIsKeyBacked)(_op) && (0, identity_js_1.operatorIsEncrypted)(_op),
|
|
717
|
+
auto_update_requested: (0, updater_js_1.autoUpdateEnabled)(),
|
|
718
|
+
update_authenticated: envBool("IICP_MANAGED_UPDATE_AUTHENTICATED"),
|
|
719
|
+
rollback_verified: envBool("IICP_MANAGED_ROLLBACK_VERIFIED"),
|
|
720
|
+
upnp_requested: opts.autoDetectNat && !envBool("IICP_SKIP_UPNP"),
|
|
721
|
+
tunnel_requested: opts.tunnel !== false,
|
|
722
|
+
upnp_approved: envBool("IICP_MANAGED_UPNP_APPROVED"),
|
|
723
|
+
tunnel_approved: envBool("IICP_MANAGED_TUNNEL_APPROVED"),
|
|
724
|
+
});
|
|
725
|
+
if (!managedDecision.accepted) {
|
|
726
|
+
process.stderr.write(`ERROR: managed operator startup rejected: ${managedDecision.reason}\n`);
|
|
727
|
+
return 2;
|
|
728
|
+
}
|
|
729
|
+
// Start the optional co-hosted listener only after the operator profile passes.
|
|
730
|
+
if (opts.withProxy) {
|
|
731
|
+
const pport = envInt("IICP_PROXY_PORT", 9483);
|
|
732
|
+
const pclient = new client_js_1.IicpClient({ directory_url: opts.directoryUrl, region: opts.region });
|
|
733
|
+
const pserver = (0, index_js_1.createProxyServer)(pclient);
|
|
734
|
+
pserver.on("error", (e) => process.stderr.write(`co-hosted proxy error (node continues): ${String(e)}\n`));
|
|
735
|
+
pserver.listen(pport, "127.0.0.1", () => process.stdout.write(`co-hosted proxy → http://127.0.0.1:${pport} (OpenAI/Ollama/Anthropic compat)\n`));
|
|
736
|
+
}
|
|
719
737
|
// #410/#414 — built-in backend-url fallback applied LAST (after flag/env/saved-config),
|
|
720
738
|
// so a bare `serve --model x` works without --backend-url. An `anthropic` backend
|
|
721
739
|
// defaults to the Anthropic API, not localhost Ollama. Mirrors Python cli.py ~704.
|
|
@@ -983,7 +1001,6 @@ async function runServe(opts) {
|
|
|
983
1001
|
// #463/#464 — bind the operator identity: issue a delegation FROM the (key-backed) operator
|
|
984
1002
|
// identity for this node and advertise the public display_name. The directory verifies the
|
|
985
1003
|
// delegation (operator_pub == operator_id) and records the operator. Never sends the secret/contact.
|
|
986
|
-
const _op = (0, identity_js_1.loadOperator)();
|
|
987
1004
|
let _opDelegation;
|
|
988
1005
|
let _opDisplayName;
|
|
989
1006
|
let _opCreatedAt;
|
|
@@ -1958,6 +1975,50 @@ async function runDoctor(argv) {
|
|
|
1958
1975
|
process.stdout.write(" Note restart is automatic only when supervised services set IICP_SUPERVISED=1\n");
|
|
1959
1976
|
return 0;
|
|
1960
1977
|
}
|
|
1978
|
+
function runHealthcheck(argv) {
|
|
1979
|
+
const { values } = safeParseArgs({ args: argv, options: { node: { type: "string" }, json: { type: "boolean" }, ready: { type: "boolean" }, help: { type: "boolean", short: "h" } }, allowPositionals: false });
|
|
1980
|
+
if (values.help) {
|
|
1981
|
+
process.stdout.write("usage: iicp-node healthcheck --node NAME [--json] [--ready]\n");
|
|
1982
|
+
return 0;
|
|
1983
|
+
}
|
|
1984
|
+
const node = values.node ?? process.env.IICP_NODE_NAME;
|
|
1985
|
+
if (!node) {
|
|
1986
|
+
process.stderr.write("ERROR: healthcheck requires --node NAME\n");
|
|
1987
|
+
return 2;
|
|
1988
|
+
}
|
|
1989
|
+
let file;
|
|
1990
|
+
try {
|
|
1991
|
+
file = (0, runtime_health_js_1.runtimeHealthPath)(node);
|
|
1992
|
+
}
|
|
1993
|
+
catch (error) {
|
|
1994
|
+
process.stderr.write(`ERROR: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1995
|
+
return 2;
|
|
1996
|
+
}
|
|
1997
|
+
let snapshot;
|
|
1998
|
+
try {
|
|
1999
|
+
snapshot = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2000
|
+
}
|
|
2001
|
+
catch (error) {
|
|
2002
|
+
process.stderr.write(`INDETERMINATE: runtime-health snapshot unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2003
|
+
return 2;
|
|
2004
|
+
}
|
|
2005
|
+
const progress = snapshot.progress;
|
|
2006
|
+
const staleAfter = Number(progress?.runtime?.stale_after_ms ?? 0);
|
|
2007
|
+
const age = Date.now() - fs.statSync(file).mtimeMs;
|
|
2008
|
+
if (age < 0 || !Number.isFinite(staleAfter) || staleAfter <= 0) {
|
|
2009
|
+
process.stderr.write("INDETERMINATE: snapshot freshness could not be established\n");
|
|
2010
|
+
return 2;
|
|
2011
|
+
}
|
|
2012
|
+
if (values.json)
|
|
2013
|
+
process.stdout.write(`${JSON.stringify(snapshot, null, 2)}\n`);
|
|
2014
|
+
else
|
|
2015
|
+
process.stdout.write(`IICP node health — ${node}\n liveness ${String(snapshot.liveness ?? "indeterminate")}\n readiness ${String(snapshot.readiness ?? "not_ready")}\n reasons ${Array.isArray(snapshot.reason_codes) ? snapshot.reason_codes.join(", ") : ""}\n`);
|
|
2016
|
+
if (age > staleAfter)
|
|
2017
|
+
return 1;
|
|
2018
|
+
if (values.ready)
|
|
2019
|
+
return snapshot.readiness === "ready" ? 0 : 1;
|
|
2020
|
+
return snapshot.liveness === "live" ? 0 : snapshot.liveness === "not_live" ? 1 : 2;
|
|
2021
|
+
}
|
|
1961
2022
|
/**
|
|
1962
2023
|
* Resolve a passphrase: $IICP_OPERATOR_PASSPHRASE if set (headless/CI), else an interactive
|
|
1963
2024
|
* readline prompt (this command is operator-run, so a prompt is fine here — only `serve` must
|
|
@@ -2576,7 +2637,10 @@ async function runMcpGateway(argv) {
|
|
|
2576
2637
|
` --allow-dangerous-tools IICP_MCP_ALLOW_DANGEROUS_TOOLS (requires all controls below)\n` +
|
|
2577
2638
|
` --authz-policy ID IICP_MCP_AUTHZ_POLICY\n` +
|
|
2578
2639
|
` --sandbox PROFILE IICP_MCP_SANDBOX (strict/container/sandbox)\n` +
|
|
2579
|
-
` --audit-redaction IICP_MCP_AUDIT_REDACTION\n`
|
|
2640
|
+
` --audit-redaction IICP_MCP_AUDIT_REDACTION\n` +
|
|
2641
|
+
` --mcp-revision REV IICP_MCP_REVISION (legacy default; 2026-07-28 is opt-in)\n` +
|
|
2642
|
+
` --mcp-server-name N IICP_MCP_SERVER_NAME (required for modern MCP)\n` +
|
|
2643
|
+
` --mcp-extensions X IICP_MCP_EXTENSIONS (tasks,skills,apps)\n`);
|
|
2580
2644
|
return 0;
|
|
2581
2645
|
}
|
|
2582
2646
|
const { values } = safeParseArgs({
|
|
@@ -2594,10 +2658,25 @@ async function runMcpGateway(argv) {
|
|
|
2594
2658
|
"authz-policy": { type: "string" },
|
|
2595
2659
|
sandbox: { type: "string" },
|
|
2596
2660
|
"audit-redaction": { type: "boolean" },
|
|
2661
|
+
"mcp-revision": { type: "string" },
|
|
2662
|
+
"mcp-server-name": { type: "string" },
|
|
2663
|
+
"mcp-extensions": { type: "string" },
|
|
2597
2664
|
},
|
|
2598
2665
|
allowPositionals: false,
|
|
2599
2666
|
});
|
|
2600
2667
|
const mcpUrl = (values["mcp-url"] ?? envOr("IICP_MCP_URL") ?? "http://localhost:8001").replace(/\/$/, "");
|
|
2668
|
+
const mcpRevision = values["mcp-revision"] ?? envOr("IICP_MCP_REVISION") ?? mcp_negotiation_js_1.LEGACY_MCP_REVISION;
|
|
2669
|
+
const mcpServerName = (values["mcp-server-name"] ?? envOr("IICP_MCP_SERVER_NAME") ?? "").trim();
|
|
2670
|
+
const mcpExtensions = (values["mcp-extensions"] ?? envOr("IICP_MCP_EXTENSIONS") ?? "")
|
|
2671
|
+
.split(",").map((extension) => extension.trim().toLowerCase()).filter(Boolean);
|
|
2672
|
+
if (![...mcp_negotiation_js_1.SUPPORTED_MCP_REVISIONS].includes(mcpRevision)) {
|
|
2673
|
+
process.stderr.write(`ERROR: unsupported MCP revision ${mcpRevision}.\n`);
|
|
2674
|
+
return 2;
|
|
2675
|
+
}
|
|
2676
|
+
if (mcpRevision === mcp_negotiation_js_1.MODERN_MCP_REVISION && !mcpServerName) {
|
|
2677
|
+
process.stderr.write("ERROR: --mcp-server-name is required with MCP 2026-07-28.\n");
|
|
2678
|
+
return 2;
|
|
2679
|
+
}
|
|
2601
2680
|
const rawTools = (values["tools"] ?? envOr("IICP_MCP_TOOLS") ?? "")
|
|
2602
2681
|
.split(",").map((t) => t.trim()).filter(Boolean);
|
|
2603
2682
|
const toolPolicy = new mcp_policy_js_1.McpToolPolicy({
|
|
@@ -2627,9 +2706,7 @@ async function runMcpGateway(argv) {
|
|
|
2627
2706
|
const publicEndpoint = values["public-endpoint"] ?? envOr("IICP_PUBLIC_ENDPOINT") ?? `http://localhost:${port}`;
|
|
2628
2707
|
const intents = activeTools.map(_toolToIntent);
|
|
2629
2708
|
const capabilities = activeTools.map((tool, index) => ({
|
|
2630
|
-
intent: intents[index],
|
|
2631
|
-
models: [`mcp:${tool}`],
|
|
2632
|
-
max_tokens: 65536,
|
|
2709
|
+
intent: intents[index], models: [`mcp:${tool}`], max_tokens: 65536,
|
|
2633
2710
|
}));
|
|
2634
2711
|
let nodeToken = envOr("IICP_NODE_TOKEN") ?? "";
|
|
2635
2712
|
async function doRegister() {
|
|
@@ -2670,18 +2747,128 @@ async function runMcpGateway(argv) {
|
|
|
2670
2747
|
}
|
|
2671
2748
|
}
|
|
2672
2749
|
let mcpRpcId = 0;
|
|
2673
|
-
|
|
2750
|
+
let legacySessionId;
|
|
2751
|
+
let legacyInitialization;
|
|
2752
|
+
class McpLegacySessionExpired extends Error {
|
|
2753
|
+
}
|
|
2754
|
+
async function mcpResponseJson(response) {
|
|
2755
|
+
const raw = await response.text();
|
|
2756
|
+
if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
2757
|
+
const events = raw.split(/\r?\n/).filter((line) => line.startsWith("data: ")).map((line) => line.slice(6));
|
|
2758
|
+
if (!events.length)
|
|
2759
|
+
throw new Error("MCP server returned an empty event stream");
|
|
2760
|
+
return JSON.parse(events.at(-1) ?? "");
|
|
2761
|
+
}
|
|
2762
|
+
return JSON.parse(raw);
|
|
2763
|
+
}
|
|
2764
|
+
async function initializeLegacySession() {
|
|
2765
|
+
if (legacyInitialization)
|
|
2766
|
+
return legacyInitialization;
|
|
2767
|
+
legacyInitialization = (async () => {
|
|
2768
|
+
const id = ++mcpRpcId;
|
|
2769
|
+
const headers = {
|
|
2770
|
+
"Content-Type": "application/json",
|
|
2771
|
+
"MCP-Protocol-Version": mcp_negotiation_js_1.LEGACY_MCP_REVISION,
|
|
2772
|
+
"Accept": "application/json, text/event-stream",
|
|
2773
|
+
};
|
|
2774
|
+
const init = await fetch(`${mcpUrl}/mcp`, {
|
|
2775
|
+
method: "POST",
|
|
2776
|
+
headers,
|
|
2777
|
+
body: JSON.stringify({
|
|
2778
|
+
jsonrpc: "2.0", id, method: "initialize",
|
|
2779
|
+
params: { protocolVersion: mcp_negotiation_js_1.LEGACY_MCP_REVISION, capabilities: {}, clientInfo: { name: "iicp-mcp-gateway", version: "0.7" } },
|
|
2780
|
+
}),
|
|
2781
|
+
signal: AbortSignal.timeout(30_000),
|
|
2782
|
+
});
|
|
2783
|
+
if (!init.ok)
|
|
2784
|
+
throw new Error(`MCP legacy initialization failed: ${init.status}`);
|
|
2785
|
+
const initData = await mcpResponseJson(init);
|
|
2786
|
+
if (initData["error"])
|
|
2787
|
+
throw new Error("MCP server rejected legacy initialization");
|
|
2788
|
+
const sessionId = init.headers.get("mcp-session-id");
|
|
2789
|
+
if (!sessionId)
|
|
2790
|
+
throw new Error("MCP server did not return a legacy session identifier");
|
|
2791
|
+
const initialized = await fetch(`${mcpUrl}/mcp`, {
|
|
2792
|
+
method: "POST",
|
|
2793
|
+
headers: { ...headers, "Mcp-Session-Id": sessionId },
|
|
2794
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }),
|
|
2795
|
+
signal: AbortSignal.timeout(30_000),
|
|
2796
|
+
});
|
|
2797
|
+
if (!initialized.ok)
|
|
2798
|
+
throw new Error(`MCP legacy initialized notification failed: ${initialized.status}`);
|
|
2799
|
+
legacySessionId = sessionId;
|
|
2800
|
+
})();
|
|
2801
|
+
try {
|
|
2802
|
+
await legacyInitialization;
|
|
2803
|
+
}
|
|
2804
|
+
finally {
|
|
2805
|
+
legacyInitialization = undefined;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
async function legacyToolCall(rpc, replaySafe) {
|
|
2809
|
+
if (!legacySessionId)
|
|
2810
|
+
await initializeLegacySession();
|
|
2811
|
+
const send = async () => {
|
|
2812
|
+
if (!legacySessionId)
|
|
2813
|
+
throw new Error("legacy MCP session identifier is unavailable");
|
|
2814
|
+
const response = await fetch(`${mcpUrl}/mcp`, {
|
|
2815
|
+
method: "POST",
|
|
2816
|
+
headers: {
|
|
2817
|
+
"Content-Type": "application/json",
|
|
2818
|
+
"MCP-Protocol-Version": mcp_negotiation_js_1.LEGACY_MCP_REVISION,
|
|
2819
|
+
"Mcp-Session-Id": legacySessionId,
|
|
2820
|
+
"Accept": "application/json, text/event-stream",
|
|
2821
|
+
},
|
|
2822
|
+
body: JSON.stringify(rpc),
|
|
2823
|
+
signal: AbortSignal.timeout(30_000),
|
|
2824
|
+
});
|
|
2825
|
+
if (response.status === 401 || response.status === 404) {
|
|
2826
|
+
legacySessionId = undefined;
|
|
2827
|
+
throw new McpLegacySessionExpired("MCP legacy session expired");
|
|
2828
|
+
}
|
|
2829
|
+
if (!response.ok)
|
|
2830
|
+
throw new Error(`MCP server unreachable: ${response.status}`);
|
|
2831
|
+
return mcpResponseJson(response);
|
|
2832
|
+
};
|
|
2833
|
+
try {
|
|
2834
|
+
return await send();
|
|
2835
|
+
}
|
|
2836
|
+
catch (error) {
|
|
2837
|
+
if (!(error instanceof McpLegacySessionExpired))
|
|
2838
|
+
throw error;
|
|
2839
|
+
if (!replaySafe)
|
|
2840
|
+
throw new McpLegacySessionExpired("MCP legacy session expired; caller retry required");
|
|
2841
|
+
await initializeLegacySession();
|
|
2842
|
+
try {
|
|
2843
|
+
return await send();
|
|
2844
|
+
}
|
|
2845
|
+
catch (retryError) {
|
|
2846
|
+
if (retryError instanceof McpLegacySessionExpired)
|
|
2847
|
+
throw new Error("MCP legacy session expired after one reinitialization");
|
|
2848
|
+
throw retryError;
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
async function callMcp(toolName, args, replaySafe = false) {
|
|
2674
2853
|
mcpRpcId += 1;
|
|
2675
|
-
const
|
|
2676
|
-
const
|
|
2677
|
-
method: "
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2854
|
+
const params = { name: toolName, arguments: args };
|
|
2855
|
+
const modern = mcpRevision === mcp_negotiation_js_1.MODERN_MCP_REVISION
|
|
2856
|
+
? (0, mcp_negotiation_js_1.buildModernMcpRequest)({ requestId: mcpRpcId, method: "tools/call", name: toolName, params, extensions: mcpExtensions })
|
|
2857
|
+
: null;
|
|
2858
|
+
const rpc = modern?.body ?? { jsonrpc: "2.0", id: mcpRpcId, method: "tools/call", params };
|
|
2859
|
+
const data = modern
|
|
2860
|
+
? await (async () => {
|
|
2861
|
+
const resp = await fetch(`${mcpUrl}/mcp`, {
|
|
2862
|
+
method: "POST", headers: { "Content-Type": "application/json", ...modern.headers },
|
|
2863
|
+
body: JSON.stringify(rpc), signal: AbortSignal.timeout(30_000),
|
|
2864
|
+
});
|
|
2865
|
+
if (!resp.ok)
|
|
2866
|
+
throw new Error(`MCP server unreachable: ${resp.status}`);
|
|
2867
|
+
return resp.json();
|
|
2868
|
+
})()
|
|
2869
|
+
: await legacyToolCall(rpc, replaySafe);
|
|
2870
|
+
if (modern)
|
|
2871
|
+
(0, mcp_negotiation_js_1.validateModernMcpResponse)(data, mcpServerName);
|
|
2685
2872
|
if (data["error"])
|
|
2686
2873
|
throw new Error("MCP tool returned an error");
|
|
2687
2874
|
return data["result"];
|
|
@@ -2757,15 +2944,15 @@ async function runMcpGateway(argv) {
|
|
|
2757
2944
|
}
|
|
2758
2945
|
const taskId = body["task_id"] ?? (0, node_crypto_1.randomUUID)();
|
|
2759
2946
|
try {
|
|
2760
|
-
const result = await callMcp(toolName, args);
|
|
2947
|
+
const result = await callMcp(toolName, args, payload["mcp_replay_safe"] === true);
|
|
2761
2948
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2762
2949
|
res.end(JSON.stringify({ task_id: taskId, status: "completed", result, policy_receipt: toolPolicy.receipt(toolName, "allowed", argumentCount) }));
|
|
2763
2950
|
}
|
|
2764
2951
|
catch (err) {
|
|
2765
2952
|
const msg = err.message ?? "error";
|
|
2766
|
-
const code = msg.includes("unreachable") ? 502 : 422;
|
|
2953
|
+
const code = err instanceof McpLegacySessionExpired ? 409 : msg.includes("unreachable") ? 502 : 422;
|
|
2767
2954
|
res.writeHead(code, { "Content-Type": "application/json" });
|
|
2768
|
-
res.end(JSON.stringify({ error: msg }));
|
|
2955
|
+
res.end(JSON.stringify(err instanceof McpLegacySessionExpired ? { error: "mcp_session_expired_retry_required", retryable: true } : { error: msg }));
|
|
2769
2956
|
}
|
|
2770
2957
|
return;
|
|
2771
2958
|
}
|
|
@@ -2823,7 +3010,8 @@ async function runService(argv) {
|
|
|
2823
3010
|
` --node NAME Saved node name to serve (required)\n` +
|
|
2824
3011
|
` --name NAME Override service label/unit name\n` +
|
|
2825
3012
|
` --platform KIND auto | launchd | systemd (default auto)\n` +
|
|
2826
|
-
` --
|
|
3013
|
+
` --no-start Install and enable without starting the service\n` +
|
|
3014
|
+
` --dry-run Print unit/actions without changing files or service state\n`);
|
|
2827
3015
|
return subcmd ? 0 : 2;
|
|
2828
3016
|
}
|
|
2829
3017
|
if (!["install", "status", "restart", "uninstall"].includes(subcmd)) {
|
|
@@ -2836,6 +3024,7 @@ async function runService(argv) {
|
|
|
2836
3024
|
name: { type: "string" },
|
|
2837
3025
|
platform: { type: "string" },
|
|
2838
3026
|
"dry-run": { type: "boolean" },
|
|
3027
|
+
"no-start": { type: "boolean" },
|
|
2839
3028
|
help: { type: "boolean", short: "h" },
|
|
2840
3029
|
},
|
|
2841
3030
|
allowPositionals: false,
|
|
@@ -2845,17 +3034,37 @@ async function runService(argv) {
|
|
|
2845
3034
|
const node = values.node;
|
|
2846
3035
|
if (!node)
|
|
2847
3036
|
throw new CliError("service requires --node NAME");
|
|
2848
|
-
const { renderServiceUnit } = await import("./service.js");
|
|
3037
|
+
const { managerActions, renderServiceUnit } = await import("./service.js");
|
|
2849
3038
|
const unit = renderServiceUnit(node, values.name, values.platform ?? "auto");
|
|
3039
|
+
const dryRun = values["dry-run"] === true;
|
|
3040
|
+
const noStart = values["no-start"] === true;
|
|
3041
|
+
const printAction = (command, args) => process.stdout.write(`$ ${[command, ...args].map((part) => JSON.stringify(part)).join(" ")}\n`);
|
|
3042
|
+
const execute = (operation) => {
|
|
3043
|
+
for (const action of managerActions(unit, operation, noStart)) {
|
|
3044
|
+
if (dryRun) {
|
|
3045
|
+
printAction(action.command, action.args);
|
|
3046
|
+
continue;
|
|
3047
|
+
}
|
|
3048
|
+
const result = (0, node_child_process_1.spawnSync)(action.command, action.args, { stdio: "inherit" });
|
|
3049
|
+
if (result.error && !action.tolerateFailure)
|
|
3050
|
+
throw new CliError(`${action.command} failed: ${result.error.message}`);
|
|
3051
|
+
if ((result.status ?? (result.error ? 1 : 0)) !== 0 && !action.tolerateFailure) {
|
|
3052
|
+
throw new CliError(`${action.command} exited with status ${result.status ?? "unknown"}`);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
};
|
|
2850
3056
|
if (subcmd === "install") {
|
|
2851
|
-
if (
|
|
3057
|
+
if (dryRun) {
|
|
2852
3058
|
process.stdout.write(`# ${unit.platform} service: ${unit.name}\n# path: ${unit.path}\n${unit.content}`);
|
|
2853
3059
|
}
|
|
2854
3060
|
else {
|
|
2855
3061
|
fs.mkdirSync(path.dirname(unit.path), { recursive: true });
|
|
2856
|
-
|
|
3062
|
+
const temporary = `${unit.path}.tmp-${process.pid}`;
|
|
3063
|
+
fs.writeFileSync(temporary, unit.content, { mode: 0o600 });
|
|
3064
|
+
fs.renameSync(temporary, unit.path);
|
|
2857
3065
|
process.stdout.write(`Installed ${unit.platform} service unit: ${unit.path}\n`);
|
|
2858
3066
|
}
|
|
3067
|
+
execute("install");
|
|
2859
3068
|
process.stdout.write(`status: ${unit.statusHint}\n`);
|
|
2860
3069
|
process.stdout.write(`restart: ${unit.restartHint}\n`);
|
|
2861
3070
|
process.stdout.write(`logs: ${unit.logHint}\n`);
|
|
@@ -2863,11 +3072,25 @@ async function runService(argv) {
|
|
|
2863
3072
|
return 0;
|
|
2864
3073
|
}
|
|
2865
3074
|
if (subcmd === "status")
|
|
2866
|
-
|
|
3075
|
+
execute("status");
|
|
2867
3076
|
if (subcmd === "restart")
|
|
2868
|
-
|
|
2869
|
-
if (subcmd === "uninstall")
|
|
2870
|
-
|
|
3077
|
+
execute("restart");
|
|
3078
|
+
if (subcmd === "uninstall") {
|
|
3079
|
+
execute("uninstall");
|
|
3080
|
+
if (dryRun)
|
|
3081
|
+
process.stdout.write(`$ ${JSON.stringify("rm")} ${JSON.stringify("-f")} ${JSON.stringify(unit.path)}\n`);
|
|
3082
|
+
else
|
|
3083
|
+
fs.rmSync(unit.path, { force: true });
|
|
3084
|
+
if (unit.platform === "systemd") {
|
|
3085
|
+
if (dryRun)
|
|
3086
|
+
printAction("systemctl", ["--user", "daemon-reload"]);
|
|
3087
|
+
else {
|
|
3088
|
+
const result = (0, node_child_process_1.spawnSync)("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
|
|
3089
|
+
if (result.error || result.status !== 0)
|
|
3090
|
+
throw new CliError("systemctl --user daemon-reload failed after uninstall");
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
2871
3094
|
return 0;
|
|
2872
3095
|
}
|
|
2873
3096
|
async function dispatch(argv) {
|
|
@@ -2882,6 +3105,8 @@ async function dispatch(argv) {
|
|
|
2882
3105
|
return runCredits(argv.slice(1));
|
|
2883
3106
|
if (cmd === "doctor")
|
|
2884
3107
|
return runDoctor(argv.slice(1));
|
|
3108
|
+
if (cmd === "healthcheck")
|
|
3109
|
+
return runHealthcheck(argv.slice(1));
|
|
2885
3110
|
if (cmd === "operator")
|
|
2886
3111
|
return runOperator(argv.slice(1));
|
|
2887
3112
|
if (cmd === "proxy")
|