@ultimat3/mcp 16.0.0 → 18.0.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/mcp",
3
- "version": "16.0.0",
3
+ "version": "18.0.0",
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",
@@ -24,20 +24,20 @@
24
24
  "LICENSE"
25
25
  ],
26
26
  "engines": {
27
- "bun": ">=1.3.0"
27
+ "bun": ">=1.4.0"
28
28
  },
29
29
  "scripts": {
30
30
  "typecheck": "tsc --noEmit -p tsconfig.json",
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@ultimat3/action": "16.0.0",
35
- "@ultimat3/core": "16.0.0",
36
- "@ultimat3/entity": "16.0.0",
37
- "@ultimat3/http": "16.0.0",
38
- "@ultimat3/jobs": "16.0.0",
39
- "@ultimat3/policy": "16.0.0",
40
- "@ultimat3/query": "16.0.0",
41
- "@ultimat3/schema": "16.0.0"
34
+ "@ultimat3/action": "18.0.0",
35
+ "@ultimat3/core": "18.0.0",
36
+ "@ultimat3/entity": "18.0.0",
37
+ "@ultimat3/http": "18.0.0",
38
+ "@ultimat3/jobs": "18.0.0",
39
+ "@ultimat3/policy": "18.0.0",
40
+ "@ultimat3/query": "18.0.0",
41
+ "@ultimat3/schema": "18.0.0"
42
42
  }
43
43
  }
@@ -2,6 +2,8 @@
2
2
  // it may come back. A cap the caller cannot raise, applied where the tool cannot forget it —
3
3
  // `limit` is an argument, and an argument is a request, never a permission.
4
4
 
5
+ import { finiteCount } from '@ultimat3/core';
6
+
5
7
  /** The ceilings. Frozen because a cap a caller can widen is a default, not a cap. */
6
8
  export interface QueryLimits {
7
9
  /** Hard row ceiling. A larger `limit` argument is clamped, not honoured. */
@@ -84,14 +86,22 @@ function rowBytes(row: readonly unknown[]): number {
84
86
  * what to do next: select fewer columns.
85
87
  */
86
88
  export function capQueryRows(source: QueryRows, limits: QueryLimits): QueryResult {
87
- const overRows = source.rows.length > limits.maxRows;
89
+ // `QueryLimits` is a PUBLIC type and this is the one path `resolveQueryLimits` cannot defend: a
90
+ // ceilings object built by hand. `slice(0, NaN)` is `[]` and `length > NaN` is false, so the
91
+ // agent would be handed zero rows with `truncated: false` — an empty table reported as the
92
+ // complete answer — and `bytes + size > NaN` would switch the 256 KiB context guard off entirely.
93
+ // `maxRows` floors at 1 because `resolveQueryLimits` already refuses to build anything lower;
94
+ // `maxBytes` floors at 0, where nothing in this package claims a floor.
95
+ const maxRows = finiteCount('capQueryRows', 'limits.maxRows', limits.maxRows, 1);
96
+ const maxBytes = finiteCount('capQueryRows', 'limits.maxBytes', limits.maxBytes, 0);
97
+ const overRows = source.rows.length > maxRows;
88
98
  const kept: (readonly unknown[])[] = [];
89
99
  let bytes = 2; // the enclosing `[]`
90
100
  let overBytes = false;
91
101
 
92
- for (const row of source.rows.slice(0, limits.maxRows)) {
102
+ for (const row of source.rows.slice(0, maxRows)) {
93
103
  const size = rowBytes(row);
94
- if (bytes + size > limits.maxBytes) {
104
+ if (bytes + size > maxBytes) {
95
105
  overBytes = true;
96
106
  break;
97
107
  }
@@ -108,6 +118,6 @@ export function capQueryRows(source: QueryRows, limits: QueryLimits): QueryResul
108
118
  // ceiling would also have applied further down.
109
119
  truncatedBy: overBytes ? 'bytes' : overRows ? 'rows' : null,
110
120
  bytes,
111
- guards: [...source.guards, `cap:${limits.maxRows} rows`, `cap:${limits.maxBytes} bytes`],
121
+ guards: [...source.guards, `cap:${maxRows} rows`, `cap:${maxBytes} bytes`],
112
122
  };
113
123
  }
@@ -20,7 +20,7 @@
20
20
  // refuse what a human would be allowed.
21
21
 
22
22
  import type { Actor, Clock } from '@ultimat3/core';
23
- import { readWithinLimit, systemClock } from '@ultimat3/core';
23
+ import { finiteCount, readWithinLimit, systemClock } from '@ultimat3/core';
24
24
  import type { RateLimitStore } from '@ultimat3/http';
25
25
  import { memoryRateLimitStore, toBucket } from '@ultimat3/http';
26
26
  import { McpRateLimitedError } from './errors';
@@ -91,7 +91,17 @@ const JSON_HEADERS = { 'content-type': 'application/json' } as const;
91
91
 
92
92
  export function mcpHttpRoute(input: McpHttpTransportInput): McpRouteDescriptor {
93
93
  const { server } = input;
94
- const bodyLimitBytes = input.bodyLimitBytes ?? DEFAULT_MCP_BODY_LIMIT_BYTES;
94
+ // At CONSTRUCTION, beside the two buckets and for the same reason: `readWithinLimit` refuses a
95
+ // non-finite cap, but it does so on the first request an agent makes and only after
96
+ // `resolveToken` has run — a number that cannot be enforced must fail where an author can act on
97
+ // it. Floor of 0 because `@ultimat3/http`'s `bodyLimitBytes` accepts 0, and these two must not
98
+ // answer one question two ways.
99
+ const bodyLimitBytes = finiteCount(
100
+ 'mcpHttpRoute',
101
+ 'bodyLimitBytes',
102
+ input.bodyLimitBytes ?? DEFAULT_MCP_BODY_LIMIT_BYTES,
103
+ 0,
104
+ );
95
105
  const limits = input.rateLimits ?? MCP_RATE_LIMITS;
96
106
  const clock = input.clock ?? systemClock;
97
107
  const store = input.rateLimitStore ?? memoryRateLimitStore();
@@ -8,6 +8,7 @@
8
8
  // stdout is the WIRE. Anything logged there corrupts the protocol, so diagnostics go to
9
9
  // stderr and this file never calls `console.log`.
10
10
 
11
+ import { finiteCount } from '@ultimat3/core';
11
12
  import type { McpCaller } from './registry';
12
13
  import type { McpServer } from './server';
13
14
  import { errorResponse, INVALID_REQUEST, PARSE_ERROR } from './wire';
@@ -41,7 +42,16 @@ export interface StdioTransportInput {
41
42
  export async function serveStdio(config: StdioTransportInput): Promise<void> {
42
43
  const stream = config.input ?? Bun.stdin.stream();
43
44
  const write = config.write ?? defaultWrite;
44
- const limit = config.lineLimitBytes ?? DEFAULT_STDIO_LINE_LIMIT;
45
+ // Before a byte of stdin is read, because `line.length > NaN` is false for EVERY line: the cap
46
+ // would not be a large one, it would be absent, and the buffer it bounds grows until the process
47
+ // dies. `??` cannot screen it — `NaN` is not nullish. Floor of 1: a cap of 0 refuses every
48
+ // message there is, which is a dead transport rather than a bound.
49
+ const limit = finiteCount(
50
+ 'serveStdio',
51
+ 'lineLimitBytes',
52
+ config.lineLimitBytes ?? DEFAULT_STDIO_LINE_LIMIT,
53
+ 1,
54
+ );
45
55
  const decoder = new TextDecoder();
46
56
  let buffer = '';
47
57
  // The rest of an over-long message is dropped, not parsed: whatever follows it on the same line