@ultimat3/mcp 19.1.3 → 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 +19 -0
- package/package.json +9 -9
- package/src/input-schema.ts +12 -1
- package/src/server.ts +6 -2
- package/src/transport-stdio.ts +9 -2
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.1
|
|
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.1
|
|
35
|
-
"@ultimat3/core": "19.1
|
|
36
|
-
"@ultimat3/entity": "19.1
|
|
37
|
-
"@ultimat3/http": "19.1
|
|
38
|
-
"@ultimat3/jobs": "19.1
|
|
39
|
-
"@ultimat3/policy": "19.1
|
|
40
|
-
"@ultimat3/query": "19.1
|
|
41
|
-
"@ultimat3/schema": "19.1
|
|
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
|
}
|
package/src/input-schema.ts
CHANGED
|
@@ -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))
|
|
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
|
-
|
|
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') ?? '',
|
package/src/transport-stdio.ts
CHANGED
|
@@ -128,6 +128,13 @@ async function handleLine(
|
|
|
128
128
|
await write(`${JSON.stringify(response)}\n`);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
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
|
}
|