@ultimat3/mcp 24.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/errors.ts CHANGED
@@ -27,6 +27,15 @@ export const MCP_ERROR_CODES = [
27
27
  'X_MCP_IDEMPOTENCY_KEY_SHADOWED',
28
28
  'X_MCP_SURFACE_OVER_BUDGET',
29
29
  'X_MCP_PATH_DUPLICATE',
30
+ 'X_MCP_SCOPE_UNCOVERED',
31
+ 'X_MCP_CONFIRMATION_PENDING',
32
+ 'X_MCP_CONFIRMATION_EXPIRED',
33
+ 'X_MCP_CONFIRMATION_REJECTED',
34
+ 'X_MCP_CONFIRMATION_DECIDED',
35
+ 'X_MCP_CONFIRMATION_UNKNOWN',
36
+ 'X_MCP_CONFIRMATION_TOOL_UNKNOWN',
37
+ 'X_MCP_CONFIRMATION_CONTESTED',
38
+ 'X_MCP_CONFIRMATION_ARGUMENTS_MISMATCH',
30
39
  ] as const;
31
40
 
32
41
  export type McpErrorCode = (typeof MCP_ERROR_CODES)[number];
@@ -56,6 +65,16 @@ export const MCP_ERROR_TITLES: Readonly<Record<McpErrorCode, string>> = {
56
65
  "an idempotent action's input declares idempotencyKey, the MCP argument reserved for the idempotency key",
57
66
  X_MCP_SURFACE_OVER_BUDGET: "an MCP surface an agent reads is larger than the app's budget for it",
58
67
  X_MCP_PATH_DUPLICATE: 'two MCP endpoints claim one path',
68
+ X_MCP_SCOPE_UNCOVERED: 'an MCP tool is covered by no scope in the scopes map',
69
+ X_MCP_CONFIRMATION_PENDING: 'a gated MCP tool call waits for a person to approve it',
70
+ X_MCP_CONFIRMATION_EXPIRED: 'an MCP confirmation expired before it was used or decided',
71
+ X_MCP_CONFIRMATION_REJECTED: 'a person rejected this MCP tool call',
72
+ X_MCP_CONFIRMATION_DECIDED: 'an MCP confirmation was already decided',
73
+ X_MCP_CONFIRMATION_UNKNOWN: 'no MCP confirmation has that id',
74
+ X_MCP_CONFIRMATION_TOOL_UNKNOWN: 'mcpConfirmations gates a tool this server does not project',
75
+ X_MCP_CONFIRMATION_CONTESTED: 'identical concurrent MCP calls kept racing one confirmation',
76
+ X_MCP_CONFIRMATION_ARGUMENTS_MISMATCH:
77
+ 'an MCP confirmation approval did not carry the arguments the agent sent',
59
78
  };
60
79
 
61
80
  // Titles must be registered for `format()` to render the contract's first line. Every code above is
@@ -65,12 +84,9 @@ registerErrorCodes(
65
84
  Object.fromEntries(Object.entries(MCP_ERROR_TITLES).map(([code, title]) => [code, { title }])),
66
85
  );
67
86
 
68
- // No `docs:` on the subclasses below. `UltimateError` fills it from `describeErrorCode(code).docs`,
69
- // which is `@ultimat3/core`'s `ERROR_DOCS_URL` — one page for every code, never one per code, because
70
- // `wiki/` is the framework's only public documentation surface and a code lives there in a TABLE ROW,
71
- // which has no anchor. The `https://ultimate.dev/errors/<code>` links this file built until 9.x
72
- // answered 404, host included, on every error it has ever thrown; restating the replacement here
73
- // would be the same constant in eight places waiting to drift again.
87
+ // No `docs:` on the subclasses below: `UltimateError` fills it from `describeErrorCode(code).docs`,
88
+ // core's one `ERROR_DOCS_URL` — a code lives in a `wiki/` TABLE ROW, which has no anchor. The
89
+ // per-code links this file built until 9.x answered 404; restating one here would drift again.
74
90
 
75
91
  /**
76
92
  * The one instruction that is SAFE on both of outcome 1's branches. `server.ts` appends it to the
@@ -183,14 +199,9 @@ export class McpToolUndeclaredError extends UltimateError {
183
199
  }
184
200
  }
185
201
 
186
- /**
187
- * Two primitives project to one tool name. Caught at boot rather than at first call: an agent
188
- * asking for the name reaches whichever copy won, which is the worst failure mode available —
189
- * a call that succeeds against the wrong handler and reports nothing.
190
- */
191
202
  /**
192
203
  * `config.ai.mcp.expose` is `true` — the DEFAULT — and the web role found nothing to mount at
193
- * `config.ai.mcp.path`. Two causes, one code: no `apps/<app>/mcp.ts` exports an `mcp`, or the one
204
+ * any `defineAppMcp` path. Two causes, one code: no `apps/<app>/mcp.ts` exports an `mcp`, or the one
194
205
  * that does was built without `resolveToken`, so `defineAppMcp` built no `route` (a token resolver
195
206
  * is what turns a bearer header into an actor, and an endpoint with no way to name its caller is
196
207
  * not one the framework will serve). Logged once per boot by the web role, never thrown: an app
@@ -224,6 +235,10 @@ export class McpAppUnmountedError extends UltimateError {
224
235
  }
225
236
  }
226
237
 
238
+ /**
239
+ * Two primitives project to one tool name. Refused at boot, not at first call: the agent would
240
+ * reach whichever copy won — a call that succeeds against the wrong handler and reports nothing.
241
+ */
227
242
  export class McpToolDuplicateError extends UltimateError {
228
243
  /**
229
244
  * Which declaration each copy came from, when the projector knows — carried the way
@@ -233,10 +248,8 @@ export class McpToolDuplicateError extends UltimateError {
233
248
  readonly declaredBy: readonly string[];
234
249
 
235
250
  constructor(input: { name: string; declaredBy?: readonly string[] | undefined }) {
236
- // The old `fix:` named "the primitive's export name, or the `tools` record key" for every
237
- // raiser. On `@ultimat3/admin`'s path the colliding string is an `AdminAction.name` and
238
- // neither of those exists, so the reader was sent to two places that do not hold it. Where
239
- // the projector knows the sources, the fix names THEM instead of guessing.
251
+ // Where the projector knows the sources the fix names THEM: on `@ultimat3/admin`'s path the
252
+ // colliding string is an `AdminAction.name`, neither an export name nor a `tools` key.
240
253
  const sites = input.declaredBy ?? [];
241
254
  const from = sites.length > 0 ? ` (declared by ${sites.join(' and ')})` : '';
242
255
  super({
@@ -311,6 +324,21 @@ export class McpScopeConflictError extends UltimateError {
311
324
  }
312
325
  }
313
326
 
327
+ /** `scopes:` names none of these tools, so every token ran them — while a bearer mount over the
328
+ * same map serves them to nobody. Refused at boot, every offender at once. */
329
+ export class McpScopeUncoveredError extends UltimateError {
330
+ readonly uncovered: readonly string[];
331
+ constructor(input: { uncovered: readonly string[] }) {
332
+ const names = input.uncovered.map((name) => `"${name}"`).join(', ');
333
+ super({
334
+ code: 'X_MCP_SCOPE_UNCOVERED',
335
+ cause: `defineAppMcp was given a scopes map that names no scope for ${names}, so no token scope gates them`,
336
+ fix: `[${input.uncovered.map((name) => `'${name}'`).join(', ')}] — append each to the list of the scope a token must hold for it, in the scopes map passed to defineAppMcp; or drop it from include/actions/queries/tools`,
337
+ });
338
+ this.uncovered = input.uncovered;
339
+ }
340
+ }
341
+
314
342
  /** A malformed envelope or an unsupported method — a client bug, not an authz outcome. */
315
343
  export class McpProtocolError extends UltimateError {
316
344
  constructor(input: { cause: string; fix?: string }) {
@@ -363,92 +391,6 @@ export class McpNotBranchDbError extends UltimateError {
363
391
  }
364
392
  }
365
393
 
366
- /**
367
- * One message larger than the transport holds. Its own code, for the reason `X_MCP_RATE_LIMITED`
368
- * below is not `X_RATE_LIMITED`: `@ultimat3/http`'s `X_BODY_INVALID` names `bodyLimitBytes` on the
369
- * HTTP pipeline, and this route never passes through it — the knob is `mcpHttpRoute`'s (or
370
- * `defineAppMcp`'s, which forwards it), and over stdio it is `serveStdio({ lineLimitBytes })`, a
371
- * ceiling in characters rather than bytes. One class for both transports, because the condition is
372
- * one: the peer sent more in one message than this end agreed to hold, and the two answers must
373
- * name the same code so an agent learns it once.
374
- *
375
- * Measured through ai-maxxing's `POST /mcp` on 2026-09-07: the 413 was a bare `-32600` reading
376
- * `request body is at least N bytes, limit is M` — two numbers and no next step, while the 401,
377
- * 403 and 429 beside it all carried `{ code, cause, fix }`. A box agent sending a large
378
- * `promptSession` had nothing to act on. The fix names both moves, send less or raise the cap,
379
- * because which one is right is the author's call: a prompt can be split, a paged result cannot
380
- * always be, and a cap set by default was never a decision anyone made about this app.
381
- */
382
- export class McpBodyTooLargeError extends UltimateError {
383
- readonly limit: number;
384
-
385
- constructor(input: { transport: 'http' | 'stdio'; limit: number; over?: number | undefined }) {
386
- const http = input.transport === 'http';
387
- // A NUMBER in the fix, never a `<n>`: the contract is a one-line edit that runs as written. The
388
- // cap itself would run and change nothing; twice the larger of the cap and what arrived is a
389
- // value that admits this message, whichever of the two was the shorter measure. Both are
390
- // finite non-negative integers by the transports' own screens, so the product is too.
391
- const raised = 2 * Math.max(input.limit, input.over ?? 0);
392
- super({
393
- code: 'X_MCP_BODY_TOO_LARGE',
394
- cause: http
395
- ? `request body is at least ${input.over ?? input.limit} bytes, limit is ${input.limit}`
396
- : `one message exceeded ${input.limit} characters and was dropped`,
397
- fix: http
398
- ? `send less in one request — page a large read, split a long prompt across calls — or raise the cap where the route is built: mcpHttpRoute({ bodyLimitBytes: ${raised} }) or defineAppMcp({ bodyLimitBytes: ${raised} })`
399
- : `send one JSON-RPC message per line, each under the cap — split a large tool result into paged calls — or raise it where the transport is started: serveStdio({ lineLimitBytes: ${raised} })`,
400
- // The numbers as FIELDS, not only prose: a `--json` reader and the wire's `data` both want
401
- // the limit without re-parsing a sentence.
402
- meta: {
403
- transport: input.transport,
404
- limit: input.limit,
405
- ...(input.over === undefined ? {} : { over: input.over }),
406
- },
407
- });
408
- this.limit = input.limit;
409
- }
410
- }
411
-
412
- /**
413
- * The transport's own throttle, and its own CODE rather than `@ultimat3/http`'s `X_RATE_LIMITED`
414
- * — not because the maths differ (they are the same `Bucket`, the same store and the same
415
- * `rateLimitDecision`) but because the KNOB does. `X_RATE_LIMITED` tells an operator to raise
416
- * `rateLimit.buckets` through `configureHttp()`, which governs the HTTP pipeline and not this route: all
417
- * MCP traffic is one URL and the class comes from the parsed body, so `mcpHttpRoute` meters itself.
418
- * A fix line that runs and changes nothing is worse than none — the same rule
419
- * `@ultimat3/realtime`'s `SubscriptionLimitError` follows when it names the knob over the default.
420
- *
421
- * The bucket key names the ACTOR and never reaches the caller: a 429 is provokable by anyone
422
- * holding a valid token, and an org id or an actor id in the body of one is a leak wearing a
423
- * throttle's clothes.
424
- */
425
- export class McpRateLimitedError extends UltimateError {
426
- constructor(input: { verbClass: string; limit: number; retryAfterSeconds: number }) {
427
- super({
428
- code: 'X_MCP_RATE_LIMITED',
429
- cause: `this caller has spent its ${input.limit} ${input.verbClass} requests per minute; the bucket refills in ${input.retryAfterSeconds}s`,
430
- fix: `wait ${input.retryAfterSeconds}s — the Retry-After header carries the same number — or raise it where the route is built: mcpHttpRoute({ rateLimits: { ${input.verbClass}: <n> } }), or defineAppMcp({ rateLimits })`,
431
- // why `meta` and not only the sentence: `transport-http.ts` writes `Retry-After` by hand, so
432
- // this route is correct today. `@ultimat3/http`'s `retryAfterOf` reads the header off
433
- // `meta.retryAfterSeconds` — so the day an MCP host is mounted inside that pipeline, an error
434
- // carrying the number only in its prose sheds a caller with no delay to honour, which is the
435
- // stampede `admit` exists to spread.
436
- meta: { retryAfterSeconds: input.retryAfterSeconds },
437
- });
438
- }
439
- }
440
-
441
- /** `defineAppMcp({ oauth })` refused at boot — see `oauth-metadata.ts`. */
442
- export class McpOAuthInvalidError extends UltimateError {
443
- constructor(reason: string) {
444
- super({
445
- code: 'X_MCP_OAUTH_INVALID',
446
- cause: `defineAppMcp({ oauth }) cannot be published as protected-resource metadata: ${reason}`,
447
- fix: "defineAppMcp({ ..., oauth: { authorizationServers: ['https://www.example.com'] } }) — at least one absolute https (or http://localhost) issuer URL; resource and resourceDocumentation, when set, absolute URLs with no fragment",
448
- });
449
- }
450
- }
451
-
452
394
  /**
453
395
  * An idempotent action's tool carries the reserved `idempotencyKey` argument, and this action's
454
396
  * own input already names a field that. One argument cannot be both the input a handler reads and
@@ -98,7 +98,7 @@ export function forAudience(error: FrameworkError, audience: ErrorAudience): Fra
98
98
  * second one here. Dropping the title would be a second rendering of the same contract, and the two
99
99
  * would drift.
100
100
  *
101
- * `audience` is the server's (`createMcpServer({ errorAudience })`): an app's server answers remote
101
+ * `audience` is the server's (`mcpServer({ errorAudience })`): an app's server answers remote
102
102
  * agents, who cannot run `x policy explain`, so it renders `callerFix` where the error has one.
103
103
  *
104
104
  * The bare-`code` head is the fallback for a foreign thrown object that carries `code`/`cause`
@@ -14,18 +14,19 @@
14
14
  // other tools that same token capability covers, never guessed from the action.
15
15
  //
16
16
  // `toWireSchema` from `@ultimat3/schema`, reached through `projectable.ts`, owns the schema half
17
- // of what THIS server publishes; this file owns the execution half. `@ultimat3/action`'s
18
- // `.tool()` publishes through the same function, so the two cannot describe one declaration
19
- // differently — `packages/mcp/src/cross-surface.test.ts` holds them to it.
17
+ // of what THIS server publishes; this file owns the execution half. It is the ONE tool projection:
18
+ // `@ultimat3/action`'s `.tool()` and `@ultimat3/query`'s were a second, built a tier below, and
19
+ // disagreed with what `tools/list` served — deleted in 25.0.0 (O-tool), because tier 3 cannot
20
+ // import this one to return it. `cross-surface.test.ts` pins `toolFrom` to the served entry.
20
21
 
21
- import type { Actor } from '@ultimat3/core';
22
+ import type { Actor, McpExposureDeclaration } from '@ultimat3/core';
22
23
  import { isMcpExposed } from '@ultimat3/core';
23
24
  import { McpToolUndeclaredError } from './errors';
24
- import type { McpListParams } from './list-params';
25
+ import type { ListedPrimitive } from './projectable';
26
+ import { asProjectable } from './projectable';
25
27
  import type {
26
28
  AnyMcpTool,
27
29
  McpCaller,
28
- McpRole,
29
30
  McpToolAnnotations,
30
31
  McpToolResult,
31
32
  ToolArgs,
@@ -34,34 +35,6 @@ import { jsonResult, structuredResult } from './registry';
34
35
  import type { JsonSchema } from './wire';
35
36
  import { NO_ARGS } from './wire';
36
37
 
37
- /**
38
- * MCP exposure as declared on an action/query (`mcp: { expose: true, description }`).
39
- * `visibleTo` restricts which roles may enumerate the tool; it is a catalog concern, not
40
- * an authz one — the policy still decides.
41
- */
42
- export interface McpExposure {
43
- readonly expose?: boolean;
44
- /**
45
- * Contract text, not UI text: the same string is the OpenAPI operation `summary`, and
46
- * `buildOpenApi`'s bytes are what `x verify` diffs. Routing it through the ambient,
47
- * request-scoped `t()` would make that artifact locale-dependent — see
48
- * `ActionMcp.description` in @ultimat3/action.
49
- */
50
- readonly description?: string;
51
- readonly visibleTo?: readonly McpRole[];
52
- /** Override the projected tool name. Defaults to the primitive's own name. */
53
- readonly name?: string;
54
- /** Display name for a client's UI. Contract text, like `description`. Absent: none published. */
55
- readonly title?: string;
56
- /**
57
- * Overrides of the derived hints, key by key — see `deriveAnnotations`. The one declaration an
58
- * app writes for a write that destroys nothing: `annotations: { destructiveHint: false }`.
59
- */
60
- readonly annotations?: McpToolAnnotations;
61
- /** A list query's whitelist, carried to the tool for the meta surface. See `McpListParams`. */
62
- readonly listParams?: McpListParams;
63
- }
64
-
65
38
  /**
66
39
  * The surface this projection needs from a primitive. Structurally satisfied by `Action`
67
40
  * and by `query`'s runnable handle from @ultimat3/action / @ultimat3/query — restated here
@@ -71,7 +44,8 @@ export interface McpExposure {
71
44
  export interface ProjectablePrimitive {
72
45
  readonly name: string;
73
46
  readonly description?: string;
74
- readonly mcp?: McpExposure;
47
+ /** Core's ONE `mcp` block, as the action or query declared it — never a restatement here. */
48
+ readonly mcp?: McpExposureDeclaration;
75
49
  /** JSON Schema of the input, narrowed to the wire subset by `toWireSchema`. */
76
50
  readonly inputJsonSchema?: JsonSchema;
77
51
  /** True for a mutation. Drives the rate-limit bucket; queries set it false. */
@@ -98,20 +72,27 @@ export interface ProjectablePrimitive {
98
72
  }
99
73
 
100
74
  /**
101
- * True when the primitive opted into MCP. Opt-in, never opt-out: silence exposes nothing.
102
- * `isMcpExposed` from `@ultimat3/core` is the one predicate — the manifest fact, the OpenAPI
103
- * hint and both tool projections all ask it, so none of them can answer differently.
75
+ * True when the primitive opted into MCP — `@ultimat3/core`'s `isMcpExposed`, the one predicate
76
+ * the manifest fact and the OpenAPI hint ask too. Private: the exported wrapper was a second name
77
+ * for core's function (plan 101, M3), and an app asks core's directly.
104
78
  */
105
- export function isExposed(primitive: ProjectablePrimitive): boolean {
79
+ function exposed(primitive: ProjectablePrimitive): boolean {
106
80
  return isMcpExposed(primitive.mcp);
107
81
  }
108
82
 
109
83
  /**
110
- * Project one primitive. Throws nothing, and does not re-check exposure: the two list
111
- * projections below decide what an un-exposed primitive means — skip it, or refuse it.
84
+ * Project one primitive — an `action()`, a `query()`, or a hand-built `ProjectablePrimitive` —
85
+ * into the tool this server serves. THE projection: `toolListEntry(toolFrom(publishPost))`
86
+ * is, field for field, the entry `tools/list` answers for it. Does not re-check exposure: the two
87
+ * list projections below decide what an un-exposed primitive means — skip it, or refuse it. A
88
+ * real action or query that was never registered throws `X_ACTION_UNREGISTERED` /
89
+ * `X_QUERY_UNREGISTERED`, rather than projecting a tool called `''`.
112
90
  */
113
- export function toolFromAction(primitive: ProjectablePrimitive): AnyMcpTool {
114
- const name = primitive.mcp?.name ?? primitive.name;
91
+ export function toolFrom(listed: ListedPrimitive): AnyMcpTool {
92
+ const primitive = asProjectable(listed);
93
+ // The primitive's own name and nothing else: `mcp.name` was a second naming field no
94
+ // declaration could set, deleted in 25.0.0 (plan 101, M4).
95
+ const name = primitive.name;
115
96
  const description =
116
97
  primitive.mcp?.description ?? primitive.description ?? `Run the "${primitive.name}" action.`;
117
98
  const mutates = primitive.mutates ?? true;
@@ -185,9 +166,6 @@ function definedOnly(annotations: McpToolAnnotations | undefined): McpToolAnnota
185
166
  };
186
167
  }
187
168
 
188
- /** Alias that reads correctly at a query call site — same projection, same guarantees. */
189
- export const toolFromQuery = toolFromAction;
190
-
191
169
  /**
192
170
  * Project every exposed primitive, SKIPPING the rest. Stable name order.
193
171
  *
@@ -197,8 +175,8 @@ export const toolFromQuery = toolFromAction;
197
175
  */
198
176
  export function toolsFrom(primitives: readonly ProjectablePrimitive[]): readonly AnyMcpTool[] {
199
177
  return primitives
200
- .filter(isExposed)
201
- .map(toolFromAction)
178
+ .filter(exposed)
179
+ .map(toolFrom)
202
180
  .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
203
181
  }
204
182
 
@@ -211,9 +189,9 @@ export function toolsFrom(primitives: readonly ProjectablePrimitive[]): readonly
211
189
  * throwing so one edit closes all of them.
212
190
  */
213
191
  export function toolsListed(primitives: readonly ProjectablePrimitive[]): readonly AnyMcpTool[] {
214
- const undeclared = primitives.filter((primitive) => !isExposed(primitive));
192
+ const undeclared = primitives.filter((primitive) => !exposed(primitive));
215
193
  if (undeclared.length > 0) {
216
- // The primitive's own name, not `mcp.name`: it is what the author greps for.
194
+ // The primitive's own name — the tool's, and what the author greps for.
217
195
  throw new McpToolUndeclaredError({ names: undeclared.map((primitive) => primitive.name) });
218
196
  }
219
197
  return toolsFrom(primitives);
package/src/index.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  // Public API of @ultimat3/mcp. Explicit — nothing is re-exported by wildcard, so the
2
2
  // surface an app or an agent can reach is exactly this list.
3
3
 
4
+ /** `@ultimat3/http`'s `RequestFacts`, under the name this package's resolver docs use. One shape. */
5
+ export type { RequestFacts as McpRequestFacts } from '@ultimat3/http';
4
6
  /** Re-exported so a `defineAppMcp` file needs one import, not two. Same object as schema's. */
5
7
  export type { Infer } from '@ultimat3/schema';
6
- export { formatIssues, t } from '@ultimat3/schema';
8
+ export { t } from '@ultimat3/schema';
7
9
  export type {
8
10
  AnyAppToolDefinition,
9
11
  AppToolArgs,
@@ -14,8 +16,48 @@ export type { AppMcp, AppToolSchemas, DefineAppMcpInput } from './app-tools';
14
16
  export { defineAppMcp } from './app-tools';
15
17
  export type { McpAuditEntry, McpOutcome, McpResourceAuditEntry } from './audit';
16
18
  export { auditResourceRead, auditToolCall, outcomeForCode } from './audit';
17
- export type { CreateDevServerInput } from './dev-host';
18
- export { createDevServer, devHost, frameworkIntrospection } from './dev-host';
19
+ export type {
20
+ McpAuditEvent,
21
+ McpAuditHook,
22
+ McpAuditor,
23
+ McpAuditorInput,
24
+ McpAuthRefusal,
25
+ } from './audit-hook';
26
+ export { mcpAuditor } from './audit-hook';
27
+ export {
28
+ McpConfirmationArgumentsMismatchError,
29
+ McpConfirmationContestedError,
30
+ McpConfirmationDecidedError,
31
+ McpConfirmationExpiredError,
32
+ McpConfirmationPendingError,
33
+ McpConfirmationRejectedError,
34
+ McpConfirmationToolUnknownError,
35
+ McpConfirmationUnknownError,
36
+ } from './confirmation-errors';
37
+ export {
38
+ inputDigest,
39
+ MCP_CONFIRMATION_ARGUMENTS_PURPOSE,
40
+ MCP_CONFIRMATION_DIGEST_PURPOSE,
41
+ withConfirmations,
42
+ } from './confirmation-gate';
43
+ export type { PostgresConfirmationStoreOptions } from './confirmation-postgres';
44
+ export { postgresConfirmationStore } from './confirmation-postgres';
45
+ // The confirmation table's DDL is `@ultimat3/mcp/schema`'s alone — one import path for one statement.
46
+ export type {
47
+ McpConfirmation,
48
+ McpConfirmationDraft,
49
+ McpConfirmationStatus,
50
+ McpConfirmationStore,
51
+ } from './confirmation-store';
52
+ export { memoryConfirmationStore } from './confirmation-store';
53
+ export type {
54
+ McpConfirmationDecision,
55
+ McpConfirmations,
56
+ McpConfirmationsInput,
57
+ } from './confirmations';
58
+ export { DEFAULT_MCP_CONFIRMATION_TTL_MS, mcpConfirmations } from './confirmations';
59
+ export type { DevMcpServerInput } from './dev-host';
60
+ export { devHost, devMcpServer, frameworkIntrospection } from './dev-host';
19
61
  export type {
20
62
  DevCapabilities,
21
63
  DevHost,
@@ -62,16 +104,14 @@ export {
62
104
  MCP_ERROR_TITLES,
63
105
  McpAppUnmountedError,
64
106
  McpArgsInvalidError,
65
- McpBodyTooLargeError,
66
107
  McpIdempotencyKeyShadowedError,
67
108
  McpNotBranchDbError,
68
- McpOAuthInvalidError,
69
109
  McpProtocolError,
70
110
  McpQueryRejectedError,
71
- McpRateLimitedError,
72
111
  McpResourceDuplicateError,
73
112
  McpScopeConflictError,
74
113
  McpScopeDeniedError,
114
+ McpScopeUncoveredError,
75
115
  McpScopeUnknownError,
76
116
  McpSurfaceOverBudgetError,
77
117
  McpToolDuplicateError,
@@ -79,16 +119,14 @@ export {
79
119
  McpToolUnknownError,
80
120
  McpToolUnsafeError,
81
121
  } from './errors';
82
- export { exposedPrimitives } from './exposed';
83
- export type { McpExposure, ProjectablePrimitive } from './from-action';
84
122
  export {
85
- deriveAnnotations,
86
- isExposed,
87
- toolFromAction,
88
- toolFromQuery,
89
- toolsFrom,
90
- toolsListed,
91
- } from './from-action';
123
+ McpBodyTooLargeError,
124
+ McpOAuthInvalidError,
125
+ McpRateLimitedError,
126
+ } from './errors-transport';
127
+ export { exposedPrimitives } from './exposed';
128
+ export type { ProjectablePrimitive } from './from-action';
129
+ export { deriveAnnotations, toolFrom, toolsFrom, toolsListed } from './from-action';
92
130
  export { MCP_IDEMPOTENCY_KEY_ARG } from './idempotency-arg';
93
131
  export type { ListFilterOp, McpListParams } from './list-params';
94
132
  export { DEFAULT_LIST_MAX_LIMIT, listParamsSchema } from './list-params';
@@ -173,8 +211,8 @@ export {
173
211
  } from './resources';
174
212
  export type { McpScopes } from './scopes';
175
213
  export { withScopes } from './scopes';
176
- export type { CreateMcpServerInput, McpWire } from './server';
177
- export { createMcpServer, McpServer } from './server';
214
+ export type { McpServer, McpServerInput, McpWire } from './server';
215
+ export { mcpServer } from './server';
178
216
  export type { McpInstructions, McpServerVoice } from './server-voice';
179
217
  export type { McpSurfaceBudget, McpSurfaceSize } from './surface-budget';
180
218
  export { assertMcpSurfaceBudget, measureMcpSurface } from './surface-budget';
package/src/meta-call.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // its flat name. Split from `server.ts` for the size ceiling only — it holds no gate of its own:
3
3
  // every call it runs goes back through the server's `resolve` → `dispatch`, the flat path.
4
4
 
5
- import { auditToolCall } from './audit';
5
+ import type { McpAuditor } from './audit-hook';
6
6
  import { META_UNKNOWN_FIX } from './meta-errors';
7
7
  import type { MetaSurface } from './meta-surface';
8
8
  import {
@@ -21,6 +21,7 @@ import { errorResponse, METHOD_NOT_FOUND, resultResponse } from './wire';
21
21
  /** The server's own two steps, handed in so this file cannot grow a second path. */
22
22
  export interface MetaHost {
23
23
  readonly tools: ToolRegistry;
24
+ readonly audit: McpAuditor;
24
25
  dispatch(
25
26
  id: JsonRpcId,
26
27
  name: string,
@@ -53,21 +54,21 @@ export async function metaCall(
53
54
  return host.dispatch(id, name, { kind: 'invalid-args', name, issues: args.issues }, caller);
54
55
  }
55
56
  if (name === LIST_RESOURCES) {
56
- auditToolCall({ tool: name, outcome: 'ok', caller });
57
+ host.audit.toolCall({ tool: name, outcome: 'ok', caller });
57
58
  return resultResponse(id, textResult(renderCatalog(meta.listResources(caller))));
58
59
  }
59
60
  if (name === DESCRIBE_RESOURCE) {
60
61
  const described = meta.describe(args.value['resources'] as readonly string[], caller);
61
62
  if (!described.ok) {
62
63
  // Absent and hidden are one answer, with no `data`, exactly as for a tool.
63
- auditToolCall({ tool: name, outcome: 'hidden', caller, code: 'X_MCP_TOOL_UNKNOWN' });
64
+ host.audit.toolCall({ tool: name, outcome: 'hidden', caller, code: 'X_MCP_TOOL_UNKNOWN' });
64
65
  return errorResponse(
65
66
  id,
66
67
  METHOD_NOT_FOUND,
67
68
  `resource not found: ${described.name} — ${META_UNKNOWN_FIX}`,
68
69
  );
69
70
  }
70
- auditToolCall({ tool: name, outcome: 'ok', caller });
71
+ host.audit.toolCall({ tool: name, outcome: 'ok', caller });
71
72
  return resultResponse(id, jsonResult({ resources: described.resources }));
72
73
  }
73
74
  // `manage_resource`: address → the SAME resolve and dispatch a flat call takes, audited under