@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 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) return {
1057
- status: "timeout",
1058
- client,
1059
- stderr: stderrBuffer.read(),
1060
- stderrBuffer
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
- appendStderrBlock(lines, stderr);
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
- appendStderrBlock(lines, stderr);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skydiveai/pi-extensions",
3
- "version": "0.1.0-beta.1973",
3
+ "version": "0.1.0-beta.1975",
4
4
  "homepage": "https://skydive.com",
5
5
  "license": "MIT",
6
6
  "author": "Create, Inc.",