@ultimat3/mcp 15.0.0 → 17.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 +9 -9
- package/src/query-limits.ts +14 -4
- package/src/transport-http.ts +12 -2
- package/src/transport-stdio.ts +11 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "17.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",
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/action": "
|
|
35
|
-
"@ultimat3/core": "
|
|
36
|
-
"@ultimat3/entity": "
|
|
37
|
-
"@ultimat3/http": "
|
|
38
|
-
"@ultimat3/jobs": "
|
|
39
|
-
"@ultimat3/policy": "
|
|
40
|
-
"@ultimat3/query": "
|
|
41
|
-
"@ultimat3/schema": "
|
|
34
|
+
"@ultimat3/action": "17.0.0",
|
|
35
|
+
"@ultimat3/core": "17.0.0",
|
|
36
|
+
"@ultimat3/entity": "17.0.0",
|
|
37
|
+
"@ultimat3/http": "17.0.0",
|
|
38
|
+
"@ultimat3/jobs": "17.0.0",
|
|
39
|
+
"@ultimat3/policy": "17.0.0",
|
|
40
|
+
"@ultimat3/query": "17.0.0",
|
|
41
|
+
"@ultimat3/schema": "17.0.0"
|
|
42
42
|
}
|
|
43
43
|
}
|
package/src/query-limits.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
102
|
+
for (const row of source.rows.slice(0, maxRows)) {
|
|
93
103
|
const size = rowBytes(row);
|
|
94
|
-
if (bytes + size >
|
|
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:${
|
|
121
|
+
guards: [...source.guards, `cap:${maxRows} rows`, `cap:${maxBytes} bytes`],
|
|
112
122
|
};
|
|
113
123
|
}
|
package/src/transport-http.ts
CHANGED
|
@@ -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
|
-
|
|
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();
|
package/src/transport-stdio.ts
CHANGED
|
@@ -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
|
-
|
|
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
|