@ai-ecoverse/mcpcat 0.15.0 → 0.15.2

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
@@ -22,7 +22,7 @@ Or via npm:
22
22
  npm install -g @ai-ecoverse/mcpcat
23
23
  ```
24
24
 
25
- mcpcat runs natively on macOS, Linux and Windows (Node ≥ 24), and inside Sliccy Seven as a kernel program, with the same flags, the same output and the same config file in both places. Orphan-free shutdown of stdio server trees in the kernel needs slicc-kernel ≥ 1.74.0; on older kernels only the direct child is signalled. When the kernel sends SIGKILL to mcpcat's process group, no handler can run and the server tree survives; a kernel "parent death signal" spawn option (like Linux `PR_SET_PDEATHSIG`) would cover that case.
25
+ mcpcat runs natively on macOS, Linux and Windows (Node ≥ 24), and inside Sliccy Seven as a kernel program, with the same flags, the same output and the same config file in both places. Orphan-free shutdown of stdio server trees in the kernel needs slicc-kernel ≥ 1.74.0; on older kernels only the direct child is signalled. A SIGKILLed mcpcat runs no handler. On slicc-kernel ≥ 1.88.0, mcpcat spawns stdio servers and served commands with a parent death signal (like Linux `PR_SET_PDEATHSIG`), so the kernel SIGKILLs each server's process group when mcpcat dies. On older kernels, and on the host, where there is no portable equivalent, servers that exit on stdin EOF (most MCP servers) still stop, but servers that ignore it keep running.
26
26
 
27
27
  ## Quick start
28
28
 
@@ -8177,7 +8177,8 @@ var PlatformStdioTransport = class {
8177
8177
  this.process = await this.options.platform.spawn({
8178
8178
  argv: [this.options.command, ...this.options.args ?? []],
8179
8179
  env,
8180
- cwd: this.options.cwd
8180
+ cwd: this.options.cwd,
8181
+ dieWithParent: true
8181
8182
  });
8182
8183
  trackStdio(this.process);
8183
8184
  }
@@ -8343,6 +8344,16 @@ var McpAuthError = class extends Error {
8343
8344
  this.target = target;
8344
8345
  }
8345
8346
  };
8347
+ var TransportRefusalError = class extends Error {
8348
+ status;
8349
+ reason;
8350
+ constructor(status, reason) {
8351
+ super(reason);
8352
+ this.name = "TransportRefusalError";
8353
+ this.status = status;
8354
+ this.reason = reason;
8355
+ }
8356
+ };
8346
8357
  var AuthRetryableError = class extends Error {
8347
8358
  response;
8348
8359
  constructor(response) {
@@ -8485,6 +8496,15 @@ async function createStatelessConnection(options) {
8485
8496
  return h;
8486
8497
  }
8487
8498
  async function handleErrorStatus(res, method) {
8499
+ if (res.status === 403) {
8500
+ const wwwAuth = res.headers.get("www-authenticate") ?? "";
8501
+ if (!wwwAuth) {
8502
+ const body = await res.text();
8503
+ if (body && !body.includes("insufficient_scope")) {
8504
+ throw new TransportRefusalError(403, body.split("\n")[0].slice(0, 200));
8505
+ }
8506
+ }
8507
+ }
8488
8508
  if ((res.status === 401 || res.status === 403) && authProvider)
8489
8509
  throw new AuthRetryableError(res);
8490
8510
  if (res.status === 401 || res.status === 403) throw new McpAuthError(res.status, url);
@@ -8892,7 +8912,8 @@ async function probeStdioStateless(options) {
8892
8912
  const proc = await options.platform.spawn({
8893
8913
  argv: [options.command, ...options.args ?? []],
8894
8914
  env: inheritedEnv,
8895
- cwd: options.cwd
8915
+ cwd: options.cwd,
8916
+ dieWithParent: true
8896
8917
  });
8897
8918
  trackStdio(proc);
8898
8919
  const rpc = createStdioJsonRpc(proc, options.platform, {
@@ -9018,6 +9039,7 @@ async function connect(platform2, options) {
9018
9039
  const isMethodNotFound = err instanceof StatelessRpcError && err.code === -32601 || err instanceof StatelessHttpError && err.status >= 400 && err.status < 500;
9019
9040
  if (options.protocolVersion?.startsWith("2026")) throw err;
9020
9041
  if (err instanceof McpAuthError || err instanceof AuthStepUpError) throw err;
9042
+ if (err instanceof TransportRefusalError) throw err;
9021
9043
  if (err.name === "McpOAuthAuthorizationRequiredError") throw err;
9022
9044
  if (isMethodNotFound) {
9023
9045
  return createPiConnection({
@@ -12509,7 +12531,8 @@ async function runCommand(platform2, cmdPath, argv, childEnv, stdin, timeoutMs,
12509
12531
  proc = await platform2.spawn({
12510
12532
  argv: [cmdPath, ...argv],
12511
12533
  env: childEnv,
12512
- cwd
12534
+ cwd,
12535
+ dieWithParent: true
12513
12536
  });
12514
12537
  } catch (err) {
12515
12538
  return { stdout: "", stderr: `spawn failed: ${err.message}`, exitCode: 127 };
@@ -14449,7 +14472,10 @@ async function sealSecret(platform2, name, value, options) {
14449
14472
  }
14450
14473
  const response = JSON.parse(responseText);
14451
14474
  if (response.error) {
14452
- throw Object.assign(new Error(response.error), { sealErrno: response.errno ?? "EACCES" });
14475
+ const isStructured = typeof response.error === "object" && response.error !== null && response.error.code;
14476
+ const errno = isStructured ? response.error.code : response.errno ?? "EACCES";
14477
+ const message = isStructured ? response.error.message : response.error;
14478
+ throw Object.assign(new Error(String(message)), { sealErrno: errno });
14453
14479
  }
14454
14480
  return { placeholder: response.placeholder };
14455
14481
  }
@@ -14461,6 +14487,7 @@ function sealErrorMessage(errno, name) {
14461
14487
  if (errno === "EEXIST") return `a non-bridge secret named ${name} already exists`;
14462
14488
  if (errno === "ENOTCONN") return "no vault available; use --bearer '${VAR}' or --no-auth";
14463
14489
  if (errno === "EACCES") return "vault locked; use --bearer '${VAR}' or --no-auth";
14490
+ if (errno === "ETIMEDOUT") return "vault did not respond in time";
14464
14491
  return `vault error: ${errno}`;
14465
14492
  }
14466
14493
 
@@ -16088,6 +16115,9 @@ function classifyError(err) {
16088
16115
  if (err instanceof InputRequiredError) {
16089
16116
  return { code: EXIT_INPUT_REQUIRED, kind: "input_required", msg: err.requestJson };
16090
16117
  }
16118
+ if (err instanceof TransportRefusalError) {
16119
+ return { code: EXIT_CONNECTION, kind: "connection", msg: err.reason };
16120
+ }
16091
16121
  if (err instanceof McpTimeoutError2) {
16092
16122
  return { code: EXIT_TIMEOUT, kind: "timeout", msg: err.message };
16093
16123
  }
@@ -16144,6 +16174,14 @@ async function resolveAuthProvider(platform2, hasAuthHeader, url, target, config
16144
16174
  return { provider, credentialSecrets };
16145
16175
  }
16146
16176
  function classifyConnectionError(err, target, interrupted) {
16177
+ if (err instanceof TransportRefusalError) {
16178
+ const hint = "in seven, reach the host via http://host.slicc.internal:<port>; in tests, enable the transport's hostLoopback";
16179
+ return {
16180
+ code: EXIT_CONNECTION,
16181
+ kind: "connection",
16182
+ msg: `${err.reason} (${hint})`
16183
+ };
16184
+ }
16147
16185
  if (err instanceof McpAuthError || err instanceof AuthStepUpError || err instanceof JerkSignInRequiredError) {
16148
16186
  return classifyError(err);
16149
16187
  }
@@ -16649,5 +16687,5 @@ try {
16649
16687
  });
16650
16688
  } catch {
16651
16689
  }
16652
- var code = await run(platform, "0.15.0", args);
16690
+ var code = await run(platform, "0.15.2", args);
16653
16691
  process.exit(code);
@@ -7798,7 +7798,8 @@ var PlatformStdioTransport = class {
7798
7798
  this.process = await this.options.platform.spawn({
7799
7799
  argv: [this.options.command, ...this.options.args ?? []],
7800
7800
  env: env2,
7801
- cwd: this.options.cwd
7801
+ cwd: this.options.cwd,
7802
+ dieWithParent: true
7802
7803
  });
7803
7804
  trackStdio(this.process);
7804
7805
  }
@@ -7964,6 +7965,16 @@ var McpAuthError = class extends Error {
7964
7965
  this.target = target;
7965
7966
  }
7966
7967
  };
7968
+ var TransportRefusalError = class extends Error {
7969
+ status;
7970
+ reason;
7971
+ constructor(status, reason) {
7972
+ super(reason);
7973
+ this.name = "TransportRefusalError";
7974
+ this.status = status;
7975
+ this.reason = reason;
7976
+ }
7977
+ };
7967
7978
  var AuthRetryableError = class extends Error {
7968
7979
  response;
7969
7980
  constructor(response) {
@@ -8106,6 +8117,15 @@ async function createStatelessConnection(options) {
8106
8117
  return h;
8107
8118
  }
8108
8119
  async function handleErrorStatus(res, method) {
8120
+ if (res.status === 403) {
8121
+ const wwwAuth = res.headers.get("www-authenticate") ?? "";
8122
+ if (!wwwAuth) {
8123
+ const body = await res.text();
8124
+ if (body && !body.includes("insufficient_scope")) {
8125
+ throw new TransportRefusalError(403, body.split("\n")[0].slice(0, 200));
8126
+ }
8127
+ }
8128
+ }
8109
8129
  if ((res.status === 401 || res.status === 403) && authProvider)
8110
8130
  throw new AuthRetryableError(res);
8111
8131
  if (res.status === 401 || res.status === 403) throw new McpAuthError(res.status, url);
@@ -8513,7 +8533,8 @@ async function probeStdioStateless(options) {
8513
8533
  const proc = await options.platform.spawn({
8514
8534
  argv: [options.command, ...options.args ?? []],
8515
8535
  env: inheritedEnv,
8516
- cwd: options.cwd
8536
+ cwd: options.cwd,
8537
+ dieWithParent: true
8517
8538
  });
8518
8539
  trackStdio(proc);
8519
8540
  const rpc = createStdioJsonRpc(proc, options.platform, {
@@ -8639,6 +8660,7 @@ async function connect(platform2, options) {
8639
8660
  const isMethodNotFound = err instanceof StatelessRpcError && err.code === -32601 || err instanceof StatelessHttpError && err.status >= 400 && err.status < 500;
8640
8661
  if (options.protocolVersion?.startsWith("2026")) throw err;
8641
8662
  if (err instanceof McpAuthError || err instanceof AuthStepUpError) throw err;
8663
+ if (err instanceof TransportRefusalError) throw err;
8642
8664
  if (err.name === "McpOAuthAuthorizationRequiredError") throw err;
8643
8665
  if (isMethodNotFound) {
8644
8666
  return createPiConnection({
@@ -12130,7 +12152,8 @@ async function runCommand(platform2, cmdPath, argv, childEnv, stdin, timeoutMs,
12130
12152
  proc = await platform2.spawn({
12131
12153
  argv: [cmdPath, ...argv],
12132
12154
  env: childEnv,
12133
- cwd
12155
+ cwd,
12156
+ dieWithParent: true
12134
12157
  });
12135
12158
  } catch (err) {
12136
12159
  return { stdout: "", stderr: `spawn failed: ${err.message}`, exitCode: 127 };
@@ -14070,7 +14093,10 @@ async function sealSecret(platform2, name, value, options) {
14070
14093
  }
14071
14094
  const response = JSON.parse(responseText);
14072
14095
  if (response.error) {
14073
- throw Object.assign(new Error(response.error), { sealErrno: response.errno ?? "EACCES" });
14096
+ const isStructured = typeof response.error === "object" && response.error !== null && response.error.code;
14097
+ const errno = isStructured ? response.error.code : response.errno ?? "EACCES";
14098
+ const message = isStructured ? response.error.message : response.error;
14099
+ throw Object.assign(new Error(String(message)), { sealErrno: errno });
14074
14100
  }
14075
14101
  return { placeholder: response.placeholder };
14076
14102
  }
@@ -14082,6 +14108,7 @@ function sealErrorMessage(errno, name) {
14082
14108
  if (errno === "EEXIST") return `a non-bridge secret named ${name} already exists`;
14083
14109
  if (errno === "ENOTCONN") return "no vault available; use --bearer '${VAR}' or --no-auth";
14084
14110
  if (errno === "EACCES") return "vault locked; use --bearer '${VAR}' or --no-auth";
14111
+ if (errno === "ETIMEDOUT") return "vault did not respond in time";
14085
14112
  return `vault error: ${errno}`;
14086
14113
  }
14087
14114
 
@@ -15709,6 +15736,9 @@ function classifyError(err) {
15709
15736
  if (err instanceof InputRequiredError) {
15710
15737
  return { code: EXIT_INPUT_REQUIRED, kind: "input_required", msg: err.requestJson };
15711
15738
  }
15739
+ if (err instanceof TransportRefusalError) {
15740
+ return { code: EXIT_CONNECTION, kind: "connection", msg: err.reason };
15741
+ }
15712
15742
  if (err instanceof McpTimeoutError2) {
15713
15743
  return { code: EXIT_TIMEOUT, kind: "timeout", msg: err.message };
15714
15744
  }
@@ -15765,6 +15795,14 @@ async function resolveAuthProvider(platform2, hasAuthHeader, url, target, config
15765
15795
  return { provider, credentialSecrets };
15766
15796
  }
15767
15797
  function classifyConnectionError(err, target, interrupted) {
15798
+ if (err instanceof TransportRefusalError) {
15799
+ const hint = "in seven, reach the host via http://host.slicc.internal:<port>; in tests, enable the transport's hostLoopback";
15800
+ return {
15801
+ code: EXIT_CONNECTION,
15802
+ kind: "connection",
15803
+ msg: `${err.reason} (${hint})`
15804
+ };
15805
+ }
15768
15806
  if (err instanceof McpAuthError || err instanceof AuthStepUpError || err instanceof JerkSignInRequiredError) {
15769
15807
  return classifyError(err);
15770
15808
  }
@@ -16059,9 +16097,14 @@ function kernelFs(ctx) {
16059
16097
  };
16060
16098
  }
16061
16099
  var groupSpawnSupported;
16100
+ var deathSignalSupported;
16101
+ var PARENT_DEATH_SIGNAL = "SIGKILL";
16102
+ function isInvalid(err) {
16103
+ return err?.code === "EINVAL";
16104
+ }
16062
16105
  async function kernelSpawn(ctx, options) {
16063
16106
  const tryGroup = groupSpawnSupported !== false;
16064
- const result = await ctx.spawn({
16107
+ const base = {
16065
16108
  argv: options.argv,
16066
16109
  env: options.env,
16067
16110
  cwd: options.cwd,
@@ -16069,7 +16112,20 @@ async function kernelSpawn(ctx, options) {
16069
16112
  stdout: "pipe",
16070
16113
  stderr: "pipe",
16071
16114
  ...tryGroup ? { pgid: 0 } : {}
16072
- });
16115
+ };
16116
+ const tryDeath = options.dieWithParent === true && deathSignalSupported !== false;
16117
+ let result;
16118
+ if (tryDeath) {
16119
+ try {
16120
+ result = await ctx.spawn({ ...base, parentDeathSignal: PARENT_DEATH_SIGNAL });
16121
+ } catch (err) {
16122
+ if (!isInvalid(err)) throw err;
16123
+ deathSignalSupported = false;
16124
+ result = await ctx.spawn(base);
16125
+ }
16126
+ } else {
16127
+ result = await ctx.spawn(base);
16128
+ }
16073
16129
  const childPid = result.pid;
16074
16130
  if (groupSpawnSupported === void 0) {
16075
16131
  try {
@@ -16313,7 +16369,7 @@ async function main(ctx) {
16313
16369
  packageDir: "/node_modules/@ai-ecoverse/mcpcat"
16314
16370
  });
16315
16371
  const args = ctx.argv.slice(1);
16316
- return run(platform2, "0.15.0", args);
16372
+ return run(platform2, "0.15.2", args);
16317
16373
  }
16318
16374
  export {
16319
16375
  main as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/mcpcat",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "A curl for the Model Context Protocol",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -56,7 +56,7 @@
56
56
  "trust:dry": "npx fledgling sync --dry-run -y --skip-publish"
57
57
  },
58
58
  "devDependencies": {
59
- "@ai-ecoverse/slicc-kernel": "1.83.0",
59
+ "@ai-ecoverse/slicc-kernel": "1.88.0",
60
60
  "@ai-ecoverse/slicc-shared-web": "1.13.1",
61
61
  "@biomejs/biome": "2.5.15",
62
62
  "@earendil-works/pi-codemode": "1.2.0",