@skydiveai/pi-extensions 0.1.0-beta.1973 → 0.1.0-beta.1975
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/dist/index.d.mts +2 -0
- package/dist/index.mjs +103 -17
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { A2AOptions, Middleware, ProtocolsOptions } from "@skydiveai/pi-server";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
4
|
import { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
3
5
|
import { Logger } from "pino";
|
|
4
6
|
|
package/dist/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { pathToFileURL } from "node:url";
|
|
|
9
9
|
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
10
10
|
import { Type } from "typebox";
|
|
11
11
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
12
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
12
|
+
import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
13
13
|
import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
14
14
|
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
15
15
|
import { Check, Errors } from "typebox/value";
|
|
@@ -957,6 +957,47 @@ function createStderrBuffer({ maxBytes }) {
|
|
|
957
957
|
const DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
|
|
958
958
|
const STDERR_BUFFER_BYTES = 4096;
|
|
959
959
|
/**
|
|
960
|
+
* Sandbox egress variables a stdio MCP server needs to make outbound HTTPS
|
|
961
|
+
* calls. The SDK's default child environment is a tiny whitelist (HOME,
|
|
962
|
+
* LOGNAME, PATH, SHELL, TERM, USER on Linux), which drops NODE_EXTRA_CA_CERTS
|
|
963
|
+
* — but the sandbox's egress proxy terminates TLS with a private CA, so a
|
|
964
|
+
* child spawned without it fails every HTTPS request with a bare
|
|
965
|
+
* `fetch failed` (cause: UNABLE_TO_VERIFY_LEAF_SIGNATURE) that looks like the
|
|
966
|
+
* remote service is down. Same story for SSL_CERT_FILE (OpenSSL-based
|
|
967
|
+
* runtimes) and the *_PROXY set. Inherit them from this process so a stdio
|
|
968
|
+
* server gets working egress like every other process in the sandbox.
|
|
969
|
+
*/
|
|
970
|
+
const INHERITED_EGRESS_ENV_VARS = [
|
|
971
|
+
"NODE_EXTRA_CA_CERTS",
|
|
972
|
+
"SSL_CERT_FILE",
|
|
973
|
+
"SSL_CERT_DIR",
|
|
974
|
+
"REQUESTS_CA_BUNDLE",
|
|
975
|
+
"CURL_CA_BUNDLE",
|
|
976
|
+
"HTTP_PROXY",
|
|
977
|
+
"HTTPS_PROXY",
|
|
978
|
+
"NO_PROXY",
|
|
979
|
+
"http_proxy",
|
|
980
|
+
"https_proxy",
|
|
981
|
+
"no_proxy"
|
|
982
|
+
];
|
|
983
|
+
/**
|
|
984
|
+
* Environment for a stdio MCP server child: the SDK's safe defaults, plus the
|
|
985
|
+
* sandbox's egress/TLS variables, plus (last, so it wins) the server's own
|
|
986
|
+
* configured `env`. Building the merge here — instead of only when `env` is
|
|
987
|
+
* unset — also fixes the workaround trap where supplying any `env` in
|
|
988
|
+
* mcp.config.json silently replaced the ENTIRE default set, so a config that
|
|
989
|
+
* added one API key lost PATH/HOME and the server failed to spawn at all.
|
|
990
|
+
*/
|
|
991
|
+
function buildStdioEnv(configured, processEnv = process.env) {
|
|
992
|
+
const env = { ...getDefaultEnvironment() };
|
|
993
|
+
for (const key of INHERITED_EGRESS_ENV_VARS) {
|
|
994
|
+
const value = processEnv[key];
|
|
995
|
+
if (value !== void 0) env[key] = value;
|
|
996
|
+
}
|
|
997
|
+
if (configured) Object.assign(env, configured);
|
|
998
|
+
return env;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
960
1001
|
* undici's fetch throws `TypeError: fetch failed` with the actual
|
|
961
1002
|
* network error hung off `.cause` (e.g. `getaddrinfo ENOTFOUND ...`,
|
|
962
1003
|
* `ECONNREFUSED`, TLS errors). Surfacing only `err.message` makes
|
|
@@ -988,6 +1029,15 @@ function formatError(err) {
|
|
|
988
1029
|
}
|
|
989
1030
|
return parts.join(": ");
|
|
990
1031
|
}
|
|
1032
|
+
function probeAlive(pid) {
|
|
1033
|
+
if (pid === null) return null;
|
|
1034
|
+
try {
|
|
1035
|
+
process.kill(pid, 0);
|
|
1036
|
+
return true;
|
|
1037
|
+
} catch {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
991
1041
|
async function connectHttp(_id, config, client) {
|
|
992
1042
|
const transport = new StreamableHTTPClientTransport(new URL(config.url), { ...config.headers !== null ? { requestInit: { headers: config.headers } } : {} });
|
|
993
1043
|
try {
|
|
@@ -1022,9 +1072,9 @@ async function connectClient(id, config, opts = {}) {
|
|
|
1022
1072
|
const params = {
|
|
1023
1073
|
command: config.command,
|
|
1024
1074
|
args: config.args,
|
|
1025
|
-
stderr: "pipe"
|
|
1075
|
+
stderr: "pipe",
|
|
1076
|
+
env: buildStdioEnv(config.env)
|
|
1026
1077
|
};
|
|
1027
|
-
if (config.env !== null) params.env = config.env;
|
|
1028
1078
|
if (config.cwd !== null) params.cwd = config.cwd;
|
|
1029
1079
|
const transport = new StdioClientTransport(params);
|
|
1030
1080
|
const stderrBuffer = createStderrBuffer({ maxBytes: STDERR_BUFFER_BYTES });
|
|
@@ -1036,6 +1086,10 @@ async function connectClient(id, config, opts = {}) {
|
|
|
1036
1086
|
resolve();
|
|
1037
1087
|
};
|
|
1038
1088
|
});
|
|
1089
|
+
let stdoutMessages = 0;
|
|
1090
|
+
transport.onmessage = () => {
|
|
1091
|
+
if (stdoutMessages < 1e4) stdoutMessages += 1;
|
|
1092
|
+
};
|
|
1039
1093
|
const connectPromise = client.connect(transport);
|
|
1040
1094
|
const TIMEOUT_SENTINEL = Symbol("timeout");
|
|
1041
1095
|
const result = await Promise.race([
|
|
@@ -1053,12 +1107,20 @@ async function connectClient(id, config, opts = {}) {
|
|
|
1053
1107
|
error: `server "${id}" exited before completing initialize`,
|
|
1054
1108
|
stderr: stderrBuffer.read()
|
|
1055
1109
|
};
|
|
1056
|
-
if (result === TIMEOUT_SENTINEL)
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1110
|
+
if (result === TIMEOUT_SENTINEL) {
|
|
1111
|
+
const pid = transport.pid;
|
|
1112
|
+
return {
|
|
1113
|
+
status: "timeout",
|
|
1114
|
+
client,
|
|
1115
|
+
stderr: stderrBuffer.read(),
|
|
1116
|
+
stderrBuffer,
|
|
1117
|
+
diagnostics: {
|
|
1118
|
+
pid,
|
|
1119
|
+
alive: probeAlive(pid),
|
|
1120
|
+
stdoutMessages
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1062
1124
|
try {
|
|
1063
1125
|
await client.close();
|
|
1064
1126
|
} catch {}
|
|
@@ -1543,7 +1605,8 @@ var McpExtension = class {
|
|
|
1543
1605
|
},
|
|
1544
1606
|
serverStatus: {
|
|
1545
1607
|
status: "timeout",
|
|
1546
|
-
stderr: result.stderr
|
|
1608
|
+
stderr: result.stderr,
|
|
1609
|
+
diagnostics: result.diagnostics
|
|
1547
1610
|
},
|
|
1548
1611
|
change,
|
|
1549
1612
|
error: null,
|
|
@@ -1590,7 +1653,8 @@ var McpExtension = class {
|
|
|
1590
1653
|
},
|
|
1591
1654
|
serverStatus: {
|
|
1592
1655
|
status: "timeout",
|
|
1593
|
-
stderr: retry.stderr
|
|
1656
|
+
stderr: retry.stderr,
|
|
1657
|
+
diagnostics: retry.diagnostics
|
|
1594
1658
|
},
|
|
1595
1659
|
change: null,
|
|
1596
1660
|
error: null,
|
|
@@ -1623,7 +1687,8 @@ var McpExtension = class {
|
|
|
1623
1687
|
store: connected,
|
|
1624
1688
|
serverStatus: {
|
|
1625
1689
|
status: "timeout",
|
|
1626
|
-
stderr
|
|
1690
|
+
stderr,
|
|
1691
|
+
diagnostics: null
|
|
1627
1692
|
},
|
|
1628
1693
|
change: null,
|
|
1629
1694
|
error: null,
|
|
@@ -1844,9 +1909,21 @@ function pendingAuthEntries(summary) {
|
|
|
1844
1909
|
function timeoutEntries(summary) {
|
|
1845
1910
|
return Object.entries(summary.servers).flatMap(([id, status]) => status.status === "timeout" ? [{
|
|
1846
1911
|
id,
|
|
1847
|
-
stderr: status.stderr
|
|
1912
|
+
stderr: status.stderr,
|
|
1913
|
+
diagnostics: status.diagnostics
|
|
1848
1914
|
}] : []);
|
|
1849
1915
|
}
|
|
1916
|
+
/**
|
|
1917
|
+
* One line describing what the connect probe learned about a timed-out
|
|
1918
|
+
* stdio child, so an empty stderr is no longer a dead end. Empty array
|
|
1919
|
+
* when the entry was re-surfaced without a fresh probe.
|
|
1920
|
+
*/
|
|
1921
|
+
function timeoutDiagnosticLines(diagnostics) {
|
|
1922
|
+
if (diagnostics === null) return [];
|
|
1923
|
+
const alive = diagnostics.alive === null ? "unknown (no pid)" : diagnostics.alive ? "alive" : "DEAD";
|
|
1924
|
+
const spoke = diagnostics.stdoutMessages === 0 ? "no JSON-RPC received on stdout (initialize never answered — slow startup, or blocked e.g. on OAuth)" : `${diagnostics.stdoutMessages} JSON-RPC message(s) received on stdout (server spoke but the handshake stalled)`;
|
|
1925
|
+
return [` child process: ${alive}${diagnostics.pid !== null ? ` (pid ${diagnostics.pid})` : ""}; ${spoke}`];
|
|
1926
|
+
}
|
|
1850
1927
|
function failedEntries(summary) {
|
|
1851
1928
|
return Object.entries(summary.servers).flatMap(([id, status]) => status.status === "failed" ? [{
|
|
1852
1929
|
id,
|
|
@@ -1854,6 +1931,13 @@ function failedEntries(summary) {
|
|
|
1854
1931
|
stderr: status.stderr
|
|
1855
1932
|
}] : []);
|
|
1856
1933
|
}
|
|
1934
|
+
function appendTimeoutStderrBlock(lines, stderr) {
|
|
1935
|
+
if (!stderr) {
|
|
1936
|
+
lines.push(" stderr: (empty — the child wrote nothing to stderr)");
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
appendStderrBlock(lines, stderr);
|
|
1940
|
+
}
|
|
1857
1941
|
function appendStderrBlock(lines, stderr) {
|
|
1858
1942
|
if (!stderr) return;
|
|
1859
1943
|
lines.push(" stderr:");
|
|
@@ -1892,11 +1976,12 @@ function summaryText(summary) {
|
|
|
1892
1976
|
lines.push(` run \`${cliHint}\` in the shell to authenticate.`);
|
|
1893
1977
|
appendStderrBlock(lines, stderr);
|
|
1894
1978
|
}
|
|
1895
|
-
for (const { id, stderr } of timeoutEntries(summary)) {
|
|
1979
|
+
for (const { id, stderr, diagnostics } of timeoutEntries(summary)) {
|
|
1896
1980
|
lines.push(` TIMED OUT ${id}:`);
|
|
1897
1981
|
lines.push(` bridge spawned but did not complete initialize in time`);
|
|
1898
1982
|
lines.push(` (usually mcp-remote mid-OAuth — see captured stderr).`);
|
|
1899
|
-
|
|
1983
|
+
lines.push(...timeoutDiagnosticLines(diagnostics));
|
|
1984
|
+
appendTimeoutStderrBlock(lines, stderr);
|
|
1900
1985
|
}
|
|
1901
1986
|
for (const { id, error, stderr } of failedEntries(summary)) {
|
|
1902
1987
|
lines.push(` FAILED ${id}: ${error}`);
|
|
@@ -1941,9 +2026,10 @@ function formatMcpUpdateMessage(summary) {
|
|
|
1941
2026
|
if (timedOut.length > 0) {
|
|
1942
2027
|
lines.push("");
|
|
1943
2028
|
lines.push("Servers that timed out during initialize (bridge is still alive in the background; usually mcp-remote-style stdio bridges mid-OAuth):");
|
|
1944
|
-
for (const { id, stderr } of timedOut) {
|
|
2029
|
+
for (const { id, stderr, diagnostics } of timedOut) {
|
|
1945
2030
|
lines.push(` - ${id}:`);
|
|
1946
|
-
|
|
2031
|
+
lines.push(...timeoutDiagnosticLines(diagnostics));
|
|
2032
|
+
appendTimeoutStderrBlock(lines, stderr);
|
|
1947
2033
|
}
|
|
1948
2034
|
lines.push("If the bridge prints an auth URL in its stderr, share it with the user. Then call `reload_mcp` once they've finished.");
|
|
1949
2035
|
}
|