@ultimat3/mcp 25.0.0 → 25.2.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/CLAUDE.md +2 -1
- package/package.json +10 -9
- package/src/dev-server.ts +1 -1
- package/src/dev-ui-interact.ts +5 -4
- package/src/dev-ui-tools.ts +3 -3
- package/src/readonly-sql.ts +23 -25
- package/src/validate-args.ts +9 -0
package/CLAUDE.md
CHANGED
|
@@ -228,7 +228,8 @@ import. The CLI wires it.
|
|
|
228
228
|
- Banned SQL functions are matched as a **prefix of a CALLED function name** — add a family, never
|
|
229
229
|
a name (`pg_sleep_for` passed an exact `pg_sleep` ban); the call unit and the three named bans: README.
|
|
230
230
|
- `db.query` is defended four ways: a SELECT-only role and `BEGIN READ ONLY` in `@ultimat3/db`
|
|
231
|
-
(the CLI wires them — this package
|
|
231
|
+
(the CLI wires them — this package never opens a `db` connection; it imports one lexer rule,
|
|
232
|
+
`endOfBlockComment`, so a nested comment ends where Postgres ends it), the parse here, the caps here.
|
|
232
233
|
`limit` is a request, never a permission: `resolveQueryLimits` clamps it into a hard 1000.
|
|
233
234
|
- The caps run in the **tool**, not the host. A host that forgets them answers a million rows
|
|
234
235
|
into a model's context. `guards` names the layers that engaged; a layer that could not engage
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/mcp",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.2.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",
|
|
@@ -33,13 +33,14 @@
|
|
|
33
33
|
"test": "bun test"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@ultimat3/action": "25.
|
|
37
|
-
"@ultimat3/core": "25.
|
|
38
|
-
"@ultimat3/
|
|
39
|
-
"@ultimat3/
|
|
40
|
-
"@ultimat3/
|
|
41
|
-
"@ultimat3/
|
|
42
|
-
"@ultimat3/
|
|
43
|
-
"@ultimat3/
|
|
36
|
+
"@ultimat3/action": "25.2.0",
|
|
37
|
+
"@ultimat3/core": "25.2.0",
|
|
38
|
+
"@ultimat3/db": "25.2.0",
|
|
39
|
+
"@ultimat3/entity": "25.2.0",
|
|
40
|
+
"@ultimat3/http": "25.2.0",
|
|
41
|
+
"@ultimat3/jobs": "25.2.0",
|
|
42
|
+
"@ultimat3/policy": "25.2.0",
|
|
43
|
+
"@ultimat3/query": "25.2.0",
|
|
44
|
+
"@ultimat3/schema": "25.2.0"
|
|
44
45
|
}
|
|
45
46
|
}
|
package/src/dev-server.ts
CHANGED
|
@@ -188,7 +188,7 @@ export function devTools(host: DevHost): readonly AnyMcpTool[] {
|
|
|
188
188
|
return [
|
|
189
189
|
read(
|
|
190
190
|
'routes.list',
|
|
191
|
-
'Route table:
|
|
191
|
+
'Route table as `x routes --json` prints it: pages, then every action, query and runtime.ts route.',
|
|
192
192
|
NO_ARGS,
|
|
193
193
|
() => jsonResult(host.routes()),
|
|
194
194
|
),
|
package/src/dev-ui-interact.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// navigation. Its own file for the reason `dev-ui-tools.ts` has one: that file stands near the
|
|
5
5
|
// 500-line ceiling, and this tool's step schema alone is a screen. `dev-server.ts` spreads both.
|
|
6
6
|
//
|
|
7
|
-
// Every bound lives in the HANDLER's contract, not the schema
|
|
8
|
-
// `
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// Every bound lives in the HANDLER's contract, not the schema — a placement, no longer a limit:
|
|
8
|
+
// `validate-args.ts` enforces `maxItems` now, so the count COULD move into the schema; it has not.
|
|
9
|
+
// The CLI refuses an over-long step list whole — never trims, because dropping a step changes what
|
|
10
|
+
// the picture is of. The schema's job is the SHAPE of a step (`anyOf` over five one-key objects);
|
|
11
|
+
// the CLI's is the count, the lengths and the policies.
|
|
11
12
|
|
|
12
13
|
import type {
|
|
13
14
|
UiColorScheme,
|
package/src/dev-ui-tools.ts
CHANGED
|
@@ -230,9 +230,9 @@ export interface InspectSpecRead {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
/**
|
|
233
|
-
* The four `ui.inspect` fields, bounded HERE and not in the schema:
|
|
234
|
-
*
|
|
235
|
-
*
|
|
233
|
+
* The four `ui.inspect` fields, bounded HERE and not in the schema: the handler TRIMS and confesses
|
|
234
|
+
* (`trimmed`, `droppedStyles`), where a schema `maxItems` — which `validate-args.ts` enforces —
|
|
235
|
+
* would refuse the whole call. Shared with `ui.interact`, whose `inspect` block is these four.
|
|
236
236
|
*/
|
|
237
237
|
export function inspectSpecOf(args: ToolArgs): InspectSpecRead {
|
|
238
238
|
const wanted = strings(args['selectors']);
|
package/src/readonly-sql.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// anywhere at statement level (which also catches a data-modifying CTE — `WITH x AS
|
|
7
7
|
// (INSERT ...) SELECT`, which reads like a SELECT and is not one).
|
|
8
8
|
|
|
9
|
+
import { dollarTagAt, endOfBlockComment } from '@ultimat3/db';
|
|
9
10
|
import { McpNotBranchDbError, McpQueryRejectedError } from './errors';
|
|
10
11
|
import { calledFunctions, forbiddenFamily } from './readonly-sql-calls';
|
|
11
12
|
|
|
@@ -216,8 +217,9 @@ function notBranch(cause: string, fix: string): McpNotBranchDbError {
|
|
|
216
217
|
/**
|
|
217
218
|
* Replace string/identifier literals and comments with spaces so keyword scanning cannot be
|
|
218
219
|
* fooled by `SELECT 'delete'` (a harmless literal that looks like a write) and cannot be
|
|
219
|
-
* evaded by hiding a second statement behind a block comment
|
|
220
|
-
*
|
|
220
|
+
* evaded by hiding a second statement behind a block comment — ended where Postgres ends it,
|
|
221
|
+
* nesting counted (`@ultimat3/db`'s `endOfBlockComment`), or a comment closed early hides one
|
|
222
|
+
* instead. Only word boundaries matter downstream, so collapsing each run to one space is enough.
|
|
221
223
|
*
|
|
222
224
|
* `identifiers: 'keep'` unwraps a double-quoted identifier to its content instead — the call
|
|
223
225
|
* scan needs it, because `"pg_advisory_lock"(1)` calls the function the blanked form hides.
|
|
@@ -234,11 +236,13 @@ function stripLiteralsAndComments(sql: string, identifiers: 'blank' | 'keep' = '
|
|
|
234
236
|
continue;
|
|
235
237
|
}
|
|
236
238
|
if (two === '/*') {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
// Nested, as Postgres reads it: ending at the FIRST `*/` let `/* /* */ ' */` open a literal
|
|
240
|
+
// here that swallowed the real statement after the comment.
|
|
241
|
+
const end = endOfBlockComment(sql, i);
|
|
242
|
+
if (end === null) {
|
|
239
243
|
throw unterminated('a /* block comment', 'close it with */, or use -- to the end of line');
|
|
240
244
|
}
|
|
241
|
-
i = end
|
|
245
|
+
i = end;
|
|
242
246
|
out += ' ';
|
|
243
247
|
continue;
|
|
244
248
|
}
|
|
@@ -260,26 +264,21 @@ function stripLiteralsAndComments(sql: string, identifiers: 'blank' | 'keep' = '
|
|
|
260
264
|
i = end;
|
|
261
265
|
continue;
|
|
262
266
|
}
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
throw unterminated(
|
|
275
|
-
`a ${marker} dollar-quoted body`,
|
|
276
|
-
`close it with the same tag: ${marker} … ${marker}`,
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
i = end + marker.length;
|
|
280
|
-
out += ' ';
|
|
281
|
-
continue;
|
|
267
|
+
// `@ultimat3/db`'s lexer rule, the one copy: Postgres's tag grammar (non-ASCII included, never
|
|
268
|
+
// a leading digit — `[a-z_]*` read `$a1$` as text and hid a `pg_sleep` behind the `'` after
|
|
269
|
+
// it), and a `$` that continues an identifier (`é$x$` is one name) opens nothing.
|
|
270
|
+
const marker = char === '$' ? dollarTagAt(sql, i) : null;
|
|
271
|
+
if (marker !== null) {
|
|
272
|
+
const end = sql.indexOf(marker, i + marker.length);
|
|
273
|
+
if (end === -1) {
|
|
274
|
+
throw unterminated(
|
|
275
|
+
`a ${marker} dollar-quoted body`,
|
|
276
|
+
`close it with the same tag: ${marker} … ${marker}`,
|
|
277
|
+
);
|
|
282
278
|
}
|
|
279
|
+
i = end + marker.length;
|
|
280
|
+
out += ' ';
|
|
281
|
+
continue;
|
|
283
282
|
}
|
|
284
283
|
out += char;
|
|
285
284
|
i += 1;
|
|
@@ -289,7 +288,6 @@ function stripLiteralsAndComments(sql: string, identifiers: 'blank' | 'keep' = '
|
|
|
289
288
|
|
|
290
289
|
const IDENTIFIER_CHAR = /[A-Za-z0-9_$\u0080-\uffff]/;
|
|
291
290
|
const WORD = /[a-z_\u0080-\uffff][a-z0-9_$\u0080-\uffff]*/g;
|
|
292
|
-
const DOLLAR_TAG = /^\$(?:[A-Za-z_\u0080-\uffff][A-Za-z0-9_\u0080-\uffff]*)?\$/;
|
|
293
291
|
|
|
294
292
|
/**
|
|
295
293
|
* `U&"` opening at a token boundary — the lexer's `xuistart`, with no space inside. `menu&"m"` is
|
package/src/validate-args.ts
CHANGED
|
@@ -121,6 +121,15 @@ function array(schema: JsonSchema, input: unknown, path: string, issues: ArgIssu
|
|
|
121
121
|
issues.push({ path, message: 'must be an array' });
|
|
122
122
|
return input;
|
|
123
123
|
}
|
|
124
|
+
// `@ultimat3/schema` publishes `t.array(items, { min, max })` as these two; a bound on the wire
|
|
125
|
+
// that nothing here checks is an agent obeying a rule a hand-written tool never enforced.
|
|
126
|
+
const count = (n: number): string => `${n} item${n === 1 ? '' : 's'}`;
|
|
127
|
+
if (schema.minItems !== undefined && input.length < schema.minItems) {
|
|
128
|
+
issues.push({ path, message: `must have at least ${count(schema.minItems)}` });
|
|
129
|
+
}
|
|
130
|
+
if (schema.maxItems !== undefined && input.length > schema.maxItems) {
|
|
131
|
+
issues.push({ path, message: `must have at most ${count(schema.maxItems)}` });
|
|
132
|
+
}
|
|
124
133
|
const items = schema.items;
|
|
125
134
|
if (items === undefined) return input;
|
|
126
135
|
return input.map((item, index) => walk(items, item, `${path}[${index}]`, issues));
|