@ultimat3/mcp 25.0.0 → 25.1.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 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 must never import `db`), the parse here, and the caps here.
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.0.0",
3
+ "version": "25.1.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.0.0",
37
- "@ultimat3/core": "25.0.0",
38
- "@ultimat3/entity": "25.0.0",
39
- "@ultimat3/http": "25.0.0",
40
- "@ultimat3/jobs": "25.0.0",
41
- "@ultimat3/policy": "25.0.0",
42
- "@ultimat3/query": "25.0.0",
43
- "@ultimat3/schema": "25.0.0"
36
+ "@ultimat3/action": "25.1.0",
37
+ "@ultimat3/core": "25.1.0",
38
+ "@ultimat3/db": "25.1.0",
39
+ "@ultimat3/entity": "25.1.0",
40
+ "@ultimat3/http": "25.1.0",
41
+ "@ultimat3/jobs": "25.1.0",
42
+ "@ultimat3/policy": "25.1.0",
43
+ "@ultimat3/query": "25.1.0",
44
+ "@ultimat3/schema": "25.1.0"
44
45
  }
45
46
  }
@@ -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: `validate-args.ts` enforces no
8
- // `maxItems`, and a step list the schema cannot bound is one the CLI refuses whole — never trims,
9
- // because dropping a step changes what the picture is of. The schema's job is the SHAPE of a step
10
- // (`anyOf` over five one-key objects); the CLI's is the count, the lengths and the policies.
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,
@@ -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: `validate-args.ts` enforces
234
- * no `maxItems`, and a bound the catalog cannot state is one the handler must apply and then
235
- * confess to. Shared with `ui.interact`, whose `inspect` block is these same four fields.
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']);
@@ -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. Only word boundaries matter
220
- * downstream, so collapsing each run to one space is enough.
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
- const end = sql.indexOf('*/', i + 2);
238
- if (end === -1) {
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 + 2;
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
- // Glued to an identifier, `$` is part of the NAME (`a$b$` is one identifier to Postgres), so
264
- // it opens nothing — `@ultimat3/db`'s `dollarTagAt` makes the same check.
265
- if (char === '$' && !IDENTIFIER_CHAR.test(sql[i - 1] ?? '')) {
266
- // Postgres's own tag grammar: an identifier that does not start with a digit — letters,
267
- // `_`, digits after the first, and any non-ASCII character. `[a-z_]*` read `$a1$` as text,
268
- // and the `'` it then saw hid a `pg_sleep` Postgres runs outside every quote.
269
- const tag = DOLLAR_TAG.exec(sql.slice(i));
270
- if (tag !== null) {
271
- const marker = tag[0];
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
- );
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
@@ -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));