@ultimat3/mcp 23.0.0 → 25.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/src/wire.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  // Reference: https://modelcontextprotocol.io/specification (2025-06-18).
7
7
 
8
8
  import { frameworkVersion } from '@ultimat3/core';
9
+ import type { WireJsonSchema } from '@ultimat3/schema';
9
10
 
10
11
  /** MCP protocol version advertised on `initialize`. */
11
12
  export const MCP_PROTOCOL_VERSION = '2025-06-18';
@@ -62,47 +63,10 @@ export const INVALID_PARAMS = -32602;
62
63
  export const INTERNAL_ERROR = -32603;
63
64
 
64
65
  /**
65
- * The JSON Schema subset the framework emits and `validate-args.ts` enforces. Narrow on
66
- * purpose: a tool schema an agent cannot fully understand is a tool it will call wrong.
66
+ * The JSON Schema subset a tool publishes and `validate-args.ts` enforces — `@ultimat3/schema`'s
67
+ * `WireJsonSchema`, declared there so `@ultimat3/action`'s `.tool()` speaks the same one.
67
68
  */
68
- export interface JsonSchema {
69
- readonly type?: 'object' | 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'null';
70
- readonly title?: string;
71
- readonly description?: string;
72
- readonly properties?: Readonly<Record<string, JsonSchema>>;
73
- readonly required?: readonly string[];
74
- readonly additionalProperties?: boolean;
75
- readonly items?: JsonSchema;
76
- readonly enum?: readonly (string | number | boolean | null)[];
77
- readonly const?: string | number | boolean | null;
78
- readonly default?: unknown;
79
- /**
80
- * NOT in the subset, and deliberately absent: `format` NAMES a rule whose meaning lives in
81
- * `@ultimat3/schema` (`uuid`, `email`, `iana-time-zone`, `bcp47-locale`), and this package
82
- * cannot enforce it without a second definition of each one — which would drift from the parse
83
- * the action itself runs and refuse values that surface accepts. So it is dropped by
84
- * `input-schema.ts` rather than published unchecked: an agent told `format: 'uuid'` by a server
85
- * that accepted `"not-a-uuid"` obeyed a rule nothing checked. `pattern` below is the opposite
86
- * case — the rule travels WITH the schema as a source string, so it is enforceable and is kept.
87
- *
88
- * `readonly format?: never;` states it in the type: a narrow() that starts copying `format`
89
- * again fails to compile rather than shipping a silent pass.
90
- */
91
- readonly format?: never;
92
- readonly minimum?: number;
93
- readonly maximum?: number;
94
- readonly minLength?: number;
95
- readonly maxLength?: number;
96
- /**
97
- * A `RegExp.source` string, JSON Schema semantics (a partial match unless it anchors itself).
98
- * In the subset because `validate-args.ts` enforces it: without it `tools/list` published
99
- * `{minLength,maxLength}` for a field whose OpenAPI component carried the pattern, so an HTTP
100
- * client and an MCP agent held different contracts for one declaration and the agent's only
101
- * way to learn the format was `X_INPUT_INVALID` from the action's own parse.
102
- */
103
- readonly pattern?: string;
104
- readonly anyOf?: readonly JsonSchema[];
105
- }
69
+ export type JsonSchema = WireJsonSchema;
106
70
 
107
71
  /** An empty-object schema — the honest shape for a no-argument tool. */
108
72
  export const NO_ARGS: JsonSchema = { type: 'object', properties: {}, additionalProperties: false };
@@ -149,3 +113,20 @@ export function paramsOf(req: JsonRpcRequest): Record<string, unknown> | null {
149
113
  if (typeof req.params !== 'object' || req.params === null) return null;
150
114
  return req.params as Record<string, unknown>;
151
115
  }
116
+
117
+ /**
118
+ * Which wire one message arrived on, so a refusal can name the unit THAT transport counts in.
119
+ * `handle` is transport-independent and stays so — this is a hint for the wording of one fix, never
120
+ * a branch in dispatch. HTTP carries its mounted path because `mcpHttpRoute({ path })` is a knob:
121
+ * a batch refusal that told every client to retry `POST /mcp` was wrong for an app mounted at
122
+ * `/app-mcp`, which is how this came to be threaded rather than spelled.
123
+ */
124
+ export type McpWire =
125
+ | { readonly transport: 'http'; readonly path: string }
126
+ | { readonly transport: 'stdio' };
127
+
128
+ /** The unit a transport counts one request in — or the neutral word when no wire said. */
129
+ export const messageUnitOf = (wire: McpWire | undefined): string => {
130
+ if (wire === undefined) return 'message';
131
+ return wire.transport === 'http' ? `POST ${wire.path}` : 'line';
132
+ };
@@ -1,140 +0,0 @@
1
- // A Standard Schema (`t.object({...})`, Zod, Valibot) -> the JSON Schema subset the wire speaks.
2
- //
3
- // Two `JsonSchema` types exist on purpose. `@ultimat3/schema` emits the full draft-07 vocabulary;
4
- // `wire.ts` declares the narrow subset `validate-args.ts` can actually ENFORCE. Handing an agent a
5
- // keyword the server ignores is worse than omitting it — the agent obeys a rule nothing checks and
6
- // gets a silent pass. So this is a real projection, not a cast: a keyword outside the subset is
7
- // dropped here, and `tools/list` publishes only what the resolver will hold a call to.
8
- //
9
- // `format` is the keyword that rule is easiest to get wrong on: it is expressive, and it is a NAME
10
- // whose meaning lives in `@ultimat3/schema` — enforcing it here would be a second definition of
11
- // `uuid`/`email`/`iana-time-zone` that can only drift from the parse the action itself runs. So it
12
- // is dropped, and `wire.ts` types it `never` so re-adding it does not compile. `pattern` is kept
13
- // for the mirror-image reason: the rule travels with the schema, so this server can hold a call
14
- // to it. A tool needing a format enforced declares a `pattern` beside it.
15
-
16
- import type { JsonSchema as RichJsonSchema } from '@ultimat3/schema';
17
- import { toMcpInputSchema } from '@ultimat3/schema';
18
- import type { JsonSchema } from './wire';
19
-
20
- /** Introspect any Standard Schema and narrow the result to the wire subset. */
21
- export function toWireSchema(schema: unknown): JsonSchema {
22
- return narrow(toMcpInputSchema(schema));
23
- }
24
-
25
- function narrow(source: RichJsonSchema): JsonSchema {
26
- // exactOptionalPropertyTypes: every field is attached only when it is present, never as
27
- // an explicit `undefined` — `tools/list` serialises this object verbatim.
28
- return {
29
- ...(source.type === undefined ? {} : { type: source.type }),
30
- ...(source.title === undefined ? {} : { title: source.title }),
31
- ...(source.description === undefined ? {} : { description: source.description }),
32
- ...(source.properties === undefined ? {} : { properties: narrowProperties(source.properties) }),
33
- ...(source.required === undefined ? {} : { required: source.required }),
34
- ...narrowAdditional(source.additionalProperties),
35
- ...(source.items === undefined ? {} : { items: narrow(source.items) }),
36
- ...(source.enum === undefined ? {} : { enum: source.enum }),
37
- ...(source.const === undefined ? {} : { const: source.const }),
38
- ...(source.default === undefined ? {} : { default: source.default }),
39
- ...(source.minimum === undefined ? {} : { minimum: source.minimum }),
40
- ...(source.maximum === undefined ? {} : { maximum: source.maximum }),
41
- ...(source.minLength === undefined ? {} : { minLength: source.minLength }),
42
- ...(source.maxLength === undefined ? {} : { maxLength: source.maxLength }),
43
- ...(source.pattern === undefined ? {} : { pattern: source.pattern }),
44
- ...(source.anyOf === undefined ? {} : { anyOf: source.anyOf.map(narrow) }),
45
- };
46
- }
47
-
48
- function narrowProperties(
49
- properties: Readonly<Record<string, RichJsonSchema>>,
50
- ): Readonly<Record<string, JsonSchema>> {
51
- const out: Record<string, JsonSchema> = {};
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
- }
64
- return out;
65
- }
66
-
67
- /**
68
- * A record schema says "extra keys, shaped like this". The wire subset can only say yes or no,
69
- * and `validate-args.ts` reads `false` as "reject unknown keys" — so an unrepresentable value
70
- * schema becomes `true` (permit) rather than a rejection the agent was never warned about.
71
- */
72
- function narrowAdditional(value: boolean | RichJsonSchema | undefined): {
73
- readonly additionalProperties?: boolean;
74
- } {
75
- if (value === undefined) return {};
76
- return { additionalProperties: typeof value === 'boolean' ? value : true };
77
- }
78
-
79
- /**
80
- * A Standard Schema → the `outputSchema` a tool publishes, or `undefined` when its root is not an
81
- * object (MCP's `structuredContent` is one) or it cannot be introspected.
82
- *
83
- * STRUCTURE ONLY — type, properties, required, items, enum, const, anyOf, title, description. A
84
- * client validates `structuredContent` against this document and refuses the call on a miss, so
85
- * every keyword here is a promise about a value the server produced: a bound, a pattern or
86
- * `additionalProperties: false` on a query's rows (read from the database, never re-parsed) is a
87
- * promise nothing enforces on the way out. Fewer constraints is the honest projection of an output.
88
- */
89
- export function toOutputSchema(schema: unknown): JsonSchema | undefined {
90
- let rich: RichJsonSchema;
91
- try {
92
- rich = toMcpInputSchema(schema);
93
- } catch {
94
- return undefined;
95
- }
96
- if (rich.type !== 'object') return undefined;
97
- return shape(rich);
98
- }
99
-
100
- /** `{ rows: [<row>] }` — a query's answer as `structuredContent`. `undefined` when no row schema. */
101
- export function toRowsOutputSchema(rows: unknown): JsonSchema | undefined {
102
- if (rows === undefined) return undefined;
103
- const row = toOutputSchema(rows);
104
- if (row === undefined) return undefined;
105
- return {
106
- type: 'object',
107
- properties: { rows: { type: 'array', items: row } },
108
- required: ['rows'],
109
- };
110
- }
111
-
112
- function shape(source: RichJsonSchema): JsonSchema {
113
- return {
114
- ...(source.type === undefined ? {} : { type: source.type }),
115
- ...(source.title === undefined ? {} : { title: source.title }),
116
- ...(source.description === undefined ? {} : { description: source.description }),
117
- ...(source.properties === undefined ? {} : { properties: shapeProperties(source.properties) }),
118
- ...(source.required === undefined ? {} : { required: source.required }),
119
- ...(source.items === undefined ? {} : { items: shape(source.items) }),
120
- ...(source.enum === undefined ? {} : { enum: source.enum }),
121
- ...(source.const === undefined ? {} : { const: source.const }),
122
- ...(source.anyOf === undefined ? {} : { anyOf: source.anyOf.map(shape) }),
123
- };
124
- }
125
-
126
- function shapeProperties(
127
- properties: Readonly<Record<string, RichJsonSchema>>,
128
- ): Readonly<Record<string, JsonSchema>> {
129
- const out: Record<string, JsonSchema> = {};
130
- for (const [key, child] of Object.entries(properties)) {
131
- // `narrowProperties`' reason: `out['__proto__'] = …` re-prototypes instead of adding a key.
132
- Object.defineProperty(out, key, {
133
- value: shape(child),
134
- writable: true,
135
- enumerable: true,
136
- configurable: true,
137
- });
138
- }
139
- return out;
140
- }