@ultimat3/mcp 19.2.0 → 19.3.1

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/CLAUDE.md CHANGED
@@ -51,6 +51,13 @@ import. The CLI wires it.
51
51
  `exposureOf` to the projected tool — outcome 1's only declaration surface for a projected
52
52
  primitive. Dropping it there silently disables outcome 1 for every projected tool: nothing
53
53
  fails, every caller simply sees every tool.
54
+ - **A thrown value is a framework error only when its `code` matches core's `FRAMEWORK_CODE`,
55
+ never `code.startsWith('X_')`** (`As of 2026-09`). `asFrameworkError` substitutes
56
+ `x errors explain ${code}` when a coded throw carries no `fix:`, so the code lands in a COMMAND
57
+ an agent is told to run — and a prefix test admits `X_$(id)`. A value that is not a code spelled
58
+ the one way takes the `-32603` branch, which leaks nothing of the throw. One pattern, core's,
59
+ because `@ultimat3/core`'s `client-wire.ts` already had to answer the same question for a
60
+ problem document off the wire.
54
61
  - Resolve order is visibility → scope → args → policy. Validating first leaks a schema;
55
62
  running the policy first decides a refusal from attacker-supplied input.
56
63
  - **A key's membership of a declared schema is `Object.hasOwn(properties, key)`, never
@@ -282,6 +289,18 @@ import. The CLI wires it.
282
289
  answered `400 parse error` for a malformed payload and `401` for a well-formed one under the
283
290
  SAME rejected token, which is precisely the oracle the pre-parse 401 exists to remove. The parse
284
291
  error still exists — it is what an authenticated agent gets.
292
+ - **`transport-stdio.ts`'s default `write` is AWAITED, and it has to be** (`As of 2026-09`). It was
293
+ `Bun.stdout.write(chunk)` returning `void`, so every `await write(...)` in `serveStdio` awaited
294
+ nothing: fd 1 is a pipe for every real peer (a local agent LAUNCHED this process), the runtime
295
+ queues past the buffer, and the CLI's exit threw the queue away — measured, a 4,000,236-byte
296
+ frame arrived as 1,388,672 bytes. The same failure `scripts/stdout-truncation.test.ts` pins for
297
+ `--json`, and it needs a CHILD PROCESS to see: in-process it does not exist. Awaiting it is also
298
+ the loop's only back-pressure.
299
+ - **A schema property lands through `Object.defineProperty` too** — `input-schema.ts`'s
300
+ `narrowProperties`, the twin of `validate-args.ts`'s `put` on the schema side. `out[key] = …` for
301
+ a property named `__proto__` runs `Object.prototype`'s setter and re-prototypes the published
302
+ `properties` record instead of adding a key, so a field the tool author declared vanishes from
303
+ `tools/list` and from what the arg validator reads back.
285
304
  - `transport-stdio.ts` never writes stdout except the wire. Diagnostics → stderr. It also **caps one
286
305
  message** at `DEFAULT_STDIO_LINE_LIMIT` (1 MiB, the same figure `transport-http.ts` enforces with
287
306
  `readWithinLimit`) — the peer launched this process and is trusted, a bug in it is not, and a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/mcp",
3
- "version": "19.2.0",
3
+ "version": "19.3.1",
4
4
  "description": "MCP server, dev tools, and the action-to-tool projection \u2014 one authz system, two surfaces",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,13 +31,13 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@ultimat3/action": "19.2.0",
35
- "@ultimat3/core": "19.2.0",
36
- "@ultimat3/entity": "19.2.0",
37
- "@ultimat3/http": "19.2.0",
38
- "@ultimat3/jobs": "19.2.0",
39
- "@ultimat3/policy": "19.2.0",
40
- "@ultimat3/query": "19.2.0",
41
- "@ultimat3/schema": "19.2.0"
34
+ "@ultimat3/action": "19.3.1",
35
+ "@ultimat3/core": "19.3.1",
36
+ "@ultimat3/entity": "19.3.1",
37
+ "@ultimat3/http": "19.3.1",
38
+ "@ultimat3/jobs": "19.3.1",
39
+ "@ultimat3/policy": "19.3.1",
40
+ "@ultimat3/query": "19.3.1",
41
+ "@ultimat3/schema": "19.3.1"
42
42
  }
43
43
  }
@@ -49,7 +49,18 @@ function narrowProperties(
49
49
  properties: Readonly<Record<string, RichJsonSchema>>,
50
50
  ): Readonly<Record<string, JsonSchema>> {
51
51
  const out: Record<string, JsonSchema> = {};
52
- for (const [key, child] of Object.entries(properties)) out[key] = narrow(child);
52
+ for (const [key, child] of Object.entries(properties)) {
53
+ // `out[key] = …` is not an assignment for exactly one name — `__proto__` runs
54
+ // `Object.prototype`'s setter and RE-PROTOTYPES this record instead of adding a key, so a
55
+ // field an author declared disappears from `tools/list` and from what `validate-args.ts`
56
+ // reads back. The twin of `validate-args.ts`'s `put`, on the schema side.
57
+ Object.defineProperty(out, key, {
58
+ value: narrow(child),
59
+ writable: true,
60
+ enumerable: true,
61
+ configurable: true,
62
+ });
63
+ }
53
64
  return out;
54
65
  }
55
66
 
package/src/server.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // and an already-resolved caller, and returns a response or `null` for a notification.
4
4
  // Both transports (http, stdio) and every test drive this one function.
5
5
 
6
- import { singleLine, stringField } from '@ultimat3/core';
6
+ import { FRAMEWORK_CODE, singleLine, stringField } from '@ultimat3/core';
7
7
  import { formatIssues } from '@ultimat3/schema';
8
8
  import { auditResourceRead, auditToolCall, outcomeForCode, outcomeForResult } from './audit';
9
9
  import { McpScopeDeniedError } from './errors';
@@ -322,7 +322,11 @@ function asFrameworkError(error: unknown): FrameworkError | undefined {
322
322
  // raises here leaves the JSON-RPC request with no response at all — not even the `-32603` the
323
323
  // header promises for a genuine bug.
324
324
  const code = stringField(error, 'code');
325
- if (code === undefined || !code.startsWith('X_')) return undefined;
325
+ // `FRAMEWORK_CODE`, never `startsWith('X_')`: the substituted `fix:` below interpolates this
326
+ // value into a COMMAND an agent is told to run, and `X_$(id)` passes a prefix test. A code is
327
+ // `X_SCREAMING_SNAKE` and nothing else, so a value that is not one is not a framework error —
328
+ // it takes the `-32603` branch, which leaks nothing of the throw.
329
+ if (code === undefined || !FRAMEWORK_CODE.test(code)) return undefined;
326
330
  return {
327
331
  code,
328
332
  title: stringField(error, 'title') ?? '',
@@ -128,6 +128,13 @@ async function handleLine(
128
128
  await write(`${JSON.stringify(response)}\n`);
129
129
  }
130
130
 
131
- function defaultWrite(chunk: string): void {
132
- Bun.stdout.write(chunk);
131
+ /**
132
+ * `Bun.stdout.write` is ASYNCHRONOUS when fd 1 is a pipe — which it always is here, because the
133
+ * peer LAUNCHED this process — and it answers the count it accepted, never the count it wrote.
134
+ * Dropping that promise made every `await write(...)` above await nothing: the CLI's exit ran with
135
+ * the tail still queued and the peer lost it. Measured: a 4,000,236-byte frame arrived as 1,388,672
136
+ * bytes. Awaiting it is also the back-pressure this loop otherwise has none of.
137
+ */
138
+ async function defaultWrite(chunk: string): Promise<void> {
139
+ await Bun.stdout.write(chunk);
133
140
  }