stitchkit 0.40.0 → 0.42.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.
Files changed (57) hide show
  1. package/dist/browser/client.d.ts.map +1 -1
  2. package/dist/browser/http.d.ts +1 -1
  3. package/dist/browser/http.d.ts.map +1 -1
  4. package/dist/cli.js +2 -2
  5. package/dist/contract/errors-factory.d.ts +88 -15
  6. package/dist/contract/errors-factory.d.ts.map +1 -1
  7. package/dist/contract/errors.d.ts +4 -4
  8. package/dist/contract/errors.d.ts.map +1 -1
  9. package/dist/contract/index.d.ts +1 -1
  10. package/dist/contract/index.d.ts.map +1 -1
  11. package/dist/contract/index.js +2 -2
  12. package/dist/{index-xax049k6.js → index-11x5dts2.js} +44 -13
  13. package/dist/{index-7rbzbnnf.js → index-181aebw8.js} +10 -15
  14. package/dist/{index-92gs1m5b.js → index-310bfer5.js} +1 -1
  15. package/dist/{index-fyfk537k.js → index-gex6gxhe.js} +25 -5
  16. package/dist/{index-p1x65gk7.js → index-p9vkwns1.js} +11 -13
  17. package/dist/{index-809wc1tt.js → index-s4qsmgwe.js} +4 -1
  18. package/dist/index.js +37 -13
  19. package/dist/internal/errors.d.ts +13 -0
  20. package/dist/internal/errors.d.ts.map +1 -1
  21. package/dist/internal/typed.d.ts +8 -0
  22. package/dist/internal/typed.d.ts.map +1 -1
  23. package/dist/node.js +2 -2
  24. package/dist/observability/index.js +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/server/create.d.ts.map +1 -1
  27. package/dist/server/error-hook.d.ts +8 -4
  28. package/dist/server/error-hook.d.ts.map +1 -1
  29. package/dist/server/index.js +7 -6
  30. package/dist/tools/agent.d.ts +1 -1
  31. package/dist/tools/agent.d.ts.map +1 -1
  32. package/dist/tools/list-names.d.ts +8 -6
  33. package/dist/tools/list-names.d.ts.map +1 -1
  34. package/dist/tools/manifest.d.ts +13 -5
  35. package/dist/tools/manifest.d.ts.map +1 -1
  36. package/dist/tools/mcp-handler.d.ts +5 -4
  37. package/dist/tools/mcp-handler.d.ts.map +1 -1
  38. package/dist/tools/mcp-stdio.d.ts +3 -2
  39. package/dist/tools/mcp-stdio.d.ts.map +1 -1
  40. package/dist/tools/mcp.d.ts +49 -9
  41. package/dist/tools/mcp.d.ts.map +1 -1
  42. package/dist/tools/native-mcp.d.ts +4 -18
  43. package/dist/tools/native-mcp.d.ts.map +1 -1
  44. package/dist/tools/runtime-tool.d.ts +3 -3
  45. package/dist/tools/runtime-tool.d.ts.map +1 -1
  46. package/dist/tools/surface.d.ts +37 -0
  47. package/dist/tools/surface.d.ts.map +1 -0
  48. package/dist/tools/toolkit.d.ts +4 -4
  49. package/dist/tools/toolkit.d.ts.map +1 -1
  50. package/dist/tools/transports.d.ts +11 -9
  51. package/dist/tools/transports.d.ts.map +1 -1
  52. package/dist/tools/view-file.d.ts +3 -3
  53. package/dist/tools.d.ts +5 -5
  54. package/dist/tools.d.ts.map +1 -1
  55. package/dist/tools.js +320 -165
  56. package/llms-full.txt +310 -53
  57. package/package.json +1 -1
package/llms-full.txt CHANGED
@@ -234,7 +234,7 @@ export const users = defineContract({ prefix: 'users' }, {
234
234
  | `desc` | yes | human description — also the MCP / agent tool description |
235
235
  | `params` | no | Zod schema for **path params** (`:id`, …) |
236
236
  | `input` | no | Zod schema for the **request body** (or query, for GET/DELETE) |
237
- | `output` | no | Zod schema for the **response body** |
237
+ | `output` | no | Zod schema for the **response body**; its presence declares a JSON result (`null` is data, `undefined` is not) |
238
238
  | `scope` | no | access scope for this endpoint — see [Auth & errors](./auth-and-errors.md) |
239
239
  | `expose` | no | which transports carry this endpoint — see [below](#transports) |
240
240
  | `toolName` | no | explicit MCP / agent tool name (default: a verb-aware derivation, see below — not a literal `prefix_key`) |
@@ -256,6 +256,13 @@ headers, while Stitchkit guarantees an empty wire body even if the returned
256
256
 
257
257
  ## `params` vs `input` vs `output`
258
258
 
259
+ `output` owns response presence across HTTP and tool transports. With a schema,
260
+ the handler must return a value that validates and can be represented as JSON;
261
+ a nullable schema may return `null`, but `undefined` is always a contract
262
+ violation. Without `output`, the handler is empty (`undefined`/`null`) and may
263
+ not return undeclared data. On HTTP this means a default `200` JSON response
264
+ with output and a default bodyless `204` without it.
265
+
259
266
  The three schemas are distinct on purpose:
260
267
 
261
268
  - **`params`** — values in the URL path. `path: '/:id'` ⇒
@@ -910,9 +917,13 @@ HTTP-only, but its typed client method still resolves to `AuthUser` — not
910
917
  declared `output` schema exactly once.
911
918
 
912
919
  `responseMeta.status` is static contract metadata and OpenAPI publishes the same
913
- 2xx code. Without it, data keeps status `200` and no-data keeps `204`. Bodyless
914
- `204`/`205` cannot be combined with `output`. Redirects, streams, files and
915
- handler-owned status/body logic remain [`rawResponse: true`](#raw-response-endpoints).
920
+ 2xx code. Without it, an endpoint with `output` uses `200`; an endpoint without
921
+ `output` uses `204`. A nullable output returns JSON `null` with status `200` —
922
+ the runtime value never turns a declared response body into no content.
923
+ `undefined` violates a declared output, while returning non-null data without an
924
+ output schema is also a server fault. Bodyless `204`/`205` cannot be combined
925
+ with `output`. Redirects, streams, files and handler-owned status/body logic
926
+ remain [`rawResponse: true`](#raw-response-endpoints).
916
927
 
917
928
  Collected headers are merged only after the complete success pipeline. A
918
929
  handler, hook or output-validation failure discards them. `Content-Type`,
@@ -1352,6 +1363,13 @@ await api.update({ id: '1', name: 'M' }) // PUT /users/1 body: { name }
1352
1363
  await api.delete({ id: '1' }) // DELETE /users/1
1353
1364
  ```
1354
1365
 
1366
+ The client follows response presence from the contract, not from the HTTP
1367
+ status or a truthy runtime value. An endpoint with nullable `output` resolves
1368
+ JSON `null` as `null`; an endpoint without `output` resolves `undefined`,
1369
+ including an explicitly declared empty `200` or `205`. A missing body for a
1370
+ declared output, or a body for an endpoint with no output, fails loudly instead
1371
+ of changing the typed result.
1372
+
1355
1373
  An explicit contract `HEAD` operation is exposed like any other typed method.
1356
1374
  Because HEAD endpoints are `rawResponse`, it resolves to the untouched
1357
1375
  `Response`, giving the caller direct access to status and headers without JSON
@@ -1685,16 +1703,16 @@ call —
1685
1703
  ### Pinning tool names — `listToolNames`
1686
1704
 
1687
1705
  Derived tool names are part of your public surface — an MCP client config or an
1688
- agent prompt refers to them by string. `listToolNames(services)` resolves every
1689
- tool name your services expose (the exact resolver the mounts use), with its
1690
- `(service, method)` identity and transports, sorted — a stable shape to
1691
- snapshot:
1706
+ agent prompt refers to them by string. `listToolNames({ services, runtimeTools })`
1707
+ resolves the complete mixed surface (the exact resolver the mounts use), with
1708
+ its origin, `(service, method)` identity and transports, sorted — a stable shape
1709
+ to snapshot:
1692
1710
 
1693
1711
  ```ts
1694
1712
  import { listToolNames } from 'stitchkit/tools'
1695
1713
 
1696
1714
  test('tool names have not drifted', () => {
1697
- expect(listToolNames(services)).toMatchSnapshot()
1715
+ expect(listToolNames({ services, runtimeTools })).toMatchSnapshot()
1698
1716
  })
1699
1717
  ```
1700
1718
 
@@ -1788,13 +1806,16 @@ createServer({
1788
1806
  | `serverInfo` | MCP server identity — `{ name, version }` |
1789
1807
  | `auth` | `(req) => identity \| null` — `null` rejects with 401 |
1790
1808
  | `services` | the services to expose — an array, or `(auth) => ServiceDef[]` |
1809
+ | `runtimeTools` | managed pathless operations — an array, or `(auth) => RuntimeToolDefinition[]` |
1810
+ | `surfaces` | finite `{ key: { services, runtimeTools } }` registry prepared eagerly |
1811
+ | `selectSurface` | `(auth) => declaredKey` — required with `surfaces` |
1791
1812
  | `context` | `(auth) => {…}` — values merged into every tool handler's `ctx` |
1792
1813
  | `lifecycle` | `beforeHandle` / `afterHandle` — the tool-side auth gate (see below) |
1793
1814
  | `hooks` | tool-call observability hooks — `afterToolCall` fires on every result |
1794
1815
  | `extend` | extra advertised arguments resolved into handler context |
1795
1816
  | `schemaValidation` | compatibility policy, typed-property guard and portable-format guard |
1796
1817
  | `logger` | a `StitchLogger` for the `'warn'` policy |
1797
- | `nativeTools` | `({ registerTool, rawServer }, auth) => …` — protected native registration plus an explicit raw SDK escape hatch |
1818
+ | `rawTools` | `(server, auth) => …` — explicit SDK registration without framework guarantees |
1798
1819
  | `resources` | MCP Apps `ui://` resources mounted on every server |
1799
1820
  | `instructions` | a short host-facing usage hint, surfaced to MCP tool-search |
1800
1821
  | `coerceJsonArgs` | coerce JSON-stringified object/array arguments (default `true`) |
@@ -1804,14 +1825,37 @@ createServer({
1804
1825
  | `protectedResource` | RFC 9728 metadata used by HTTP `401` responses |
1805
1826
  | `sessionMode` | `'stateless'` (default) or explicit `'stateful'` session/SSE continuity |
1806
1827
 
1807
- `services`, `context` and `nativeTools` all receive the resolved identity, so a
1808
- tenant can be shown only its own tools and every handler can read `ctx.tenantId`.
1828
+ Direct `services` / `runtimeTools` factories, `context`, `selectSurface` and
1829
+ `rawTools` receive the resolved identity, so a tenant can be shown only its own
1830
+ tools and every handler can read `ctx.tenantId`.
1831
+
1832
+ Use direct factories only when the definitions are genuinely arbitrary per
1833
+ identity. For a bounded role/plan set, declare a finite registry instead:
1834
+
1835
+ ```ts
1836
+ const handleMcp = createMcpHandler({
1837
+ serverInfo,
1838
+ auth,
1839
+ surfaces: {
1840
+ admin: { services: allServices, runtimeTools: [renderPreview] },
1841
+ member: { services: memberServices, runtimeTools: [renderPreview] },
1842
+ },
1843
+ selectSurface: (identity) => identity.isAdmin ? 'admin' : 'member',
1844
+ context: (identity) => ({ identity }),
1845
+ })
1846
+ ```
1847
+
1848
+ Every declared entry is schema-validated and prepared once when the handler is
1849
+ constructed. The selected immutable descriptors are shared; the SDK server,
1850
+ transport, auth-derived context, lifecycle runner and tool-call context are
1851
+ fresh for every stateless request (or every stateful session). Unknown keys
1852
+ fail before the server connects. The registry never retains auth values.
1809
1853
 
1810
1854
  ### Stateless by default; stateful only when required
1811
1855
 
1812
1856
  The default `sessionMode: 'stateless'` creates a fresh SDK server, transport,
1813
- resolved auth/context and runner for each HTTP request. Static contract schemas
1814
- are still prepared once when the handler is constructed. There is no session
1857
+ resolved auth/context and runner for each HTTP request. Static direct and finite
1858
+ registry schemas are still prepared once when the handler is constructed. There is no session
1815
1859
  map, event store, sweep timer or `Mcp-Session-Id`, so process replacement and
1816
1860
  load balancing cannot strand a client on an in-memory session.
1817
1861
 
@@ -1988,8 +2032,8 @@ than a per-request `(req) => …`. Keep all logging on **stderr**: stdout is the
1988
2032
  JSON-RPC channel.
1989
2033
 
1990
2034
  Both transports build the server through the shared `buildMcpServer` — same
1991
- contract pipeline, same `services` / `context` / `hooks` / `nativeTools` /
1992
- `instructions`.
2035
+ contract/runtime pipeline, same surface selection, context, hooks, raw escape
2036
+ hatch and instructions.
1993
2037
 
1994
2038
  ## OAuth 2.1 — a native remote connector
1995
2039
 
@@ -2231,9 +2275,9 @@ const handleMcp = createMcpHandler({
2231
2275
  serverInfo: { name: 'my-app', version: '1.0.0' },
2232
2276
  auth,
2233
2277
  services: [service],
2278
+ runtimeTools: [renderPreview],
2234
2279
  lifecycle: { beforeHandle: authHook },
2235
2280
  hooks: audit.toolCall,
2236
- nativeTools: ({ registerTool }) => registerTool(renderPreview),
2237
2281
  })
2238
2282
 
2239
2283
  const agentTools = mountAgent([service], {
@@ -2263,7 +2307,7 @@ use the same validation, lifecycle and hook path as contract tools.
2263
2307
 
2264
2308
  ### Explicit raw SDK registration
2265
2309
 
2266
- `rawServer` is deliberately named as an escape hatch. A tool registered there
2310
+ `rawTools` is deliberately named as an escape hatch. A tool registered there
2267
2311
  does **not** receive stitchkit schema policy, lifecycle, per-call context or
2268
2312
  hooks. The built-in `mountViewFile` helper remains raw for callers that choose
2269
2313
  that boundary; it fetches media with SSRF and path-traversal defenses:
@@ -2275,14 +2319,51 @@ const handleMcp = createMcpHandler({
2275
2319
  serverInfo: { name: 'my-app', version: '1.0.0' },
2276
2320
  auth,
2277
2321
  services: [service],
2278
- nativeTools: ({ rawServer }) =>
2279
- mountViewFile(rawServer, { baseDir: '/srv/uploads' }),
2322
+ rawTools: (server) => mountViewFile(server, { baseDir: '/srv/uploads' }),
2280
2323
  })
2281
2324
  ```
2282
2325
 
2283
2326
  Use raw registration only when opting out is intentional. For a protected
2284
- `view_file`, define it through `registerTool` and call the exported
2285
- `resolveMedia` core from its handler.
2327
+ `view_file`, define it with `defineRuntimeTool`, include it in `runtimeTools`,
2328
+ and call the exported `resolveMedia` core from its handler.
2329
+
2330
+ ## Introspecting the complete tool surface
2331
+
2332
+ Deferred tool search, name snapshots and boot diagnostics accept the same
2333
+ object-shaped surface as the mounts. Contract operations are followed by
2334
+ runtime definitions, matching mount order; exposure filters and collisions are
2335
+ resolved by Stitchkit rather than by consumer code:
2336
+
2337
+ ```ts
2338
+ import {
2339
+ buildToolManifest,
2340
+ listToolNames,
2341
+ summarizeTransports,
2342
+ } from 'stitchkit/tools'
2343
+
2344
+ const surface = { services, runtimeTools: [renderPreview] }
2345
+
2346
+ const manifest = buildToolManifest({
2347
+ ...surface,
2348
+ transport: 'AGENT',
2349
+ flattenUnionInput: true,
2350
+ })
2351
+
2352
+ const names = listToolNames(surface)
2353
+ const summary = summarizeTransports(surface)
2354
+ ```
2355
+
2356
+ `buildToolManifest` returns the exact immutable presentation schema shown to
2357
+ the selected MCP or Agent transport. It does not execute validation effects and
2358
+ fails first on duplicate contract/runtime names. There is no runtime-only
2359
+ manifest helper or public mount adapter: the framework owns merging and schema
2360
+ projection.
2361
+
2362
+ `listToolNames` remains diagnostic and reports `kind: 'contract' | 'runtime'`
2363
+ for every identity. `summarizeTransports` returns `contractServices`,
2364
+ `runtimeTools`, aggregate `totals`, and a `sources` breakdown. Runtime tools
2365
+ contribute only to MCP/Agent according to their `transports`; they never inflate
2366
+ HTTP or CLI counts.
2286
2367
 
2287
2368
  ## Logging tool calls — `createToolLogger`
2288
2369
 
@@ -2302,9 +2383,9 @@ Pass `log` to redirect the line, or `onRecord` to feed a metrics sink the
2302
2383
  structured `ToolCallRecord`. That record carries `traceId` whenever an
2303
2384
  observability context is active, so a tool call made inside an HTTP request
2304
2385
  joins that request's log line on one key — see
2305
- [Observability](./observability.md). For a boot-time picture of what is exposed where,
2306
- `summarizeTransports(services)` returns per-transport operation counts (HTTP /
2307
- MCP / AGENT / CLI) for you to log.
2386
+ [Observability](./observability.md). For a boot-time picture of what is exposed
2387
+ where, `summarizeTransports({ services, runtimeTools })` returns the mixed
2388
+ per-transport counts for you to log.
2308
2389
 
2309
2390
  ## One handler, three callers
2310
2391
 
@@ -3235,27 +3316,46 @@ onError: (ctx, err) => {
3235
3316
 
3236
3317
  ## Domain errors — `defineErrors`
3237
3318
 
3238
- Declaring your app's error codes once gives you typed throwers on the server and
3239
- a code table the client matches with autocomplete — instead of reading the raw
3240
- `message` string (which breaks the moment a code expects a string but gets an
3241
- object):
3319
+ Declare each domain code, HTTP status and optional structured-details schema in
3320
+ one immutable registry. The generated functions construct typed branded
3321
+ `AppError` instances; ordinary `throw` remains explicit at the call site:
3242
3322
 
3243
3323
  ```ts
3244
- export const { errors, codes, isCode } = defineErrors({
3245
- SESSION_NOT_FOUND: 404,
3246
- QUOTA_EXCEEDED: 429,
3324
+ import { z } from 'zod'
3325
+
3326
+ export const { errors, codes, definitions, isCode } = defineErrors({
3327
+ SESSION_NOT_FOUND: { status: 404 },
3328
+ QUOTA_EXCEEDED: {
3329
+ status: 429,
3330
+ details: z.object({ retryAfterSeconds: z.number().int().positive() }),
3331
+ },
3247
3332
  })
3248
3333
 
3249
- // server a typed thrower, the right HTTP status baked in
3250
- throw errors.SESSION_NOT_FOUND('no such session')
3334
+ throw errors.SESSION_NOT_FOUND({ message: 'No such session' })
3335
+ throw errors.QUOTA_EXCEEDED({
3336
+ message: 'Try later',
3337
+ details: { retryAfterSeconds: 30 },
3338
+ hint: 'Wait for the current window to expire',
3339
+ })
3340
+
3341
+ // Construction without throwing is useful for composition or inspection.
3342
+ const error = errors.QUOTA_EXCEEDED({ details: { retryAfterSeconds: 30 } })
3343
+ definitions.QUOTA_EXCEEDED.status // 429 — same source, no copied status map
3251
3344
 
3252
3345
  // client — match the code, never a magic string
3253
3346
  if (err instanceof ApiError && err.code === codes.SESSION_NOT_FOUND) { … }
3254
3347
  ```
3255
3348
 
3256
- The `code` rides through unchanged in both the HTTP envelope and the MCP tool
3257
- result, so one vocabulary covers every transport. The codes are yours; the core
3258
- stays domain-free.
3349
+ With no `details` schema, the options object forbids `details`. A required
3350
+ `z.object` makes `details` required; `z.object(...).optional()` makes it
3351
+ optional. Supplied details are parsed when the error is constructed, before any
3352
+ transport sees them. `code` remains literal and the parsed details type is
3353
+ retained on the returned `AppError`.
3354
+
3355
+ HTTP renders the complete code/status/message/details/hint. Tool transports keep
3356
+ their established model-facing projection (code/details/hint, without HTTP
3357
+ status), while `invokeOrThrow` recovers the exact normalized `AppError`. The
3358
+ codes and schemas remain application-owned; Stitchkit stays domain-free.
3259
3359
 
3260
3360
  ## `createErrorHook`
3261
3361
 
@@ -3287,6 +3387,27 @@ createServer({ services, hooks: { onError } })
3287
3387
  Codes you threw yourself (not stitchkit's) pass through `codeMap` unchanged; the
3288
3388
  `satisfies Record<StitchErrorCode, …>` keeps the map exhaustive across upgrades.
3289
3389
 
3390
+ Both `onError` and `render` may be asynchronous and receive the matched endpoint
3391
+ as their final argument. The observer is awaited before rendering, so it can
3392
+ resolve identity or enrich the request context even for failures raised before
3393
+ `beforeHandle`:
3394
+
3395
+ ```ts
3396
+ const onError = createErrorHook({
3397
+ onError: async (_error, _info, ctx, endpoint) => {
3398
+ await attributeFailedRequest(ctx, endpoint)
3399
+ },
3400
+ render: (info, ctx) => ({
3401
+ error: { code: info.code },
3402
+ actorId: ctx.actorId,
3403
+ }),
3404
+ })
3405
+ ```
3406
+
3407
+ `endpoint` is `undefined` when the failure happened before route resolution.
3408
+ Synchronous callbacks and renderers that declare fewer parameters continue to
3409
+ work normally.
3410
+
3290
3411
  Invalid input (a `ZodError`) is classified as `VALIDATION_ERROR` 400 before it
3291
3412
  reaches `render` — a client fault is an honest 400, not a 500 — and the
3292
3413
  offending fields arrive as structured `info.details.issues`, so your `render`
@@ -3459,6 +3580,12 @@ context carries nothing yet, so your value always wins.
3459
3580
  `event.serviceName` / `event.action` are present on every event, including a
3460
3581
  pre-handler 400. Nothing to wire.
3461
3582
 
3583
+ When failure attribution itself is asynchronous, use `createErrorHook`'s
3584
+ `onError(error, info, ctx, endpoint)` observer. The framework awaits it before
3585
+ calling `render`, so identity or audit enrichment is visible to both the final
3586
+ error envelope and the request event. `endpoint` is the matched `MethodDef`, or
3587
+ `undefined` when routing failed before a method was selected.
3588
+
3462
3589
  **Domain dimensions** — attach your own tenant / project / entity id with
3463
3590
  `setRequestDimensions`. It is an opaque `Record<string, string>` the core gives no
3464
3591
  meaning to (→ ADR 0021). Resolve it cheaply from `ctx.params` / headers in
@@ -4101,6 +4228,119 @@ current one *up to* your target, and apply each snippet.
4101
4228
 
4102
4229
  ## Unreleased breaking migrations
4103
4230
 
4231
+ Tool introspection now accepts one object-shaped contract/runtime surface. Stop
4232
+ calling the internal contract collector or merging a locally converted runtime
4233
+ manifest:
4234
+
4235
+ ```ts
4236
+ // before
4237
+ buildToolManifest(services.flatMap((service) => collectTools(service, 'AGENT')))
4238
+ listToolNames(services)
4239
+ summarizeTransports(services)
4240
+
4241
+ // after
4242
+ const surface = { services, runtimeTools }
4243
+ buildToolManifest({ ...surface, transport: 'AGENT' })
4244
+ listToolNames(surface)
4245
+ summarizeTransports(surface)
4246
+ ```
4247
+
4248
+ `ToolNameEntry` adds `kind: 'contract' | 'runtime'`. `TransportSummary` is now
4249
+ `{ contractServices, runtimeTools, totals, sources }`; replace `services` and
4250
+ `perService` reads with the explicit counts and mixed-source breakdown. There
4251
+ is no positional overload and no `buildRuntimeToolManifest`: Stitchkit owns the
4252
+ combined order, transport filtering, canonical presentation schema and
4253
+ cross-origin collision checks.
4254
+
4255
+ `defineErrors` now uses one Zod-first definition object and returns constructors
4256
+ instead of positional throwers. Add explicit `throw`, move message/details/hint
4257
+ into one options object, and declare a details schema when that code carries
4258
+ structured context:
4259
+
4260
+ ```ts
4261
+ // before
4262
+ const { errors } = defineErrors({ QUOTA_EXCEEDED: 429 })
4263
+ errors.QUOTA_EXCEEDED('Try later', { retryAfterSeconds: 30 }, 'Wait')
4264
+
4265
+ // after
4266
+ const { errors, definitions } = defineErrors({
4267
+ QUOTA_EXCEEDED: {
4268
+ status: 429,
4269
+ details: z.object({ retryAfterSeconds: z.number().positive() }),
4270
+ },
4271
+ })
4272
+ throw errors.QUOTA_EXCEEDED({
4273
+ message: 'Try later',
4274
+ details: { retryAfterSeconds: 30 },
4275
+ hint: 'Wait',
4276
+ })
4277
+ ```
4278
+
4279
+ There is no positional overload. A code without `details` forbids them; use an
4280
+ optional object schema when the details object itself is optional. Read status
4281
+ and schemas from the frozen `definitions` registry instead of maintaining a
4282
+ parallel map.
4283
+
4284
+ Managed MCP runtime tools are now declared as immutable data. Move protected
4285
+ registrar calls to `runtimeTools`; rename deliberate raw SDK registration to
4286
+ `rawTools`. There is no registrar alias:
4287
+
4288
+ ```ts
4289
+ // before — protected
4290
+ createMcpHandler({
4291
+ services,
4292
+ nativeTools: ({ registerTool }) => registerTool(preview),
4293
+ })
4294
+
4295
+ // after — protected and prepared with the rest of the surface
4296
+ createMcpHandler({ services, runtimeTools: [preview] })
4297
+
4298
+ // before — deliberate raw SDK opt-out
4299
+ nativeTools: ({ rawServer }, auth) => mountRaw(rawServer, auth)
4300
+
4301
+ // after — still a deliberate raw SDK opt-out
4302
+ rawTools: (server, auth) => mountRaw(server, auth)
4303
+ ```
4304
+
4305
+ When identity selects from a bounded set, replace a repeatedly prepared
4306
+ `services(auth)` factory with a finite registry:
4307
+
4308
+ ```ts
4309
+ createMcpHandler({
4310
+ surfaces: {
4311
+ admin: { services: allServices, runtimeTools: [preview] },
4312
+ member: { services: memberServices, runtimeTools: [preview] },
4313
+ },
4314
+ selectSurface: (auth) => auth.isAdmin ? 'admin' : 'member',
4315
+ })
4316
+ ```
4317
+
4318
+ Keep direct `services(auth)` / `runtimeTools(auth)` only for genuinely
4319
+ unbounded definitions; Stitchkit intentionally does not cache arbitrary auth
4320
+ values.
4321
+
4322
+ Contract success bodies are now determined by the presence of `output`, not by
4323
+ the handler's runtime value. A nullable output returns JSON `null` with status
4324
+ `200`; `undefined` with a declared output and non-null data without an output
4325
+ schema are contract violations:
4326
+
4327
+ ```ts
4328
+ // nullable JSON data: 200 with body `null`
4329
+ session: {
4330
+ method: 'GET', path: '/session', desc: 'Current session',
4331
+ output: SessionSchema.nullable(),
4332
+ }
4333
+
4334
+ // bodyless operation: 204 with no body
4335
+ logout: {
4336
+ method: 'POST', path: '/logout', desc: 'End the session',
4337
+ }
4338
+ ```
4339
+
4340
+ Add an output schema to every handler that returns data. Omit `output` and
4341
+ return nothing for bodyless operations; runtime tools follow the same rule and
4342
+ type no-output handlers as `void`.
4343
+
4104
4344
  `createToolInvoker` now separates immutable registry preparation from per-call
4105
4345
  runtime state. Move source/context/lifecycle/hooks/output-strip reporting from
4106
4346
  the factory config to the third invocation argument. Use `invokeOrThrow` when a
@@ -4182,7 +4422,7 @@ const preview = defineRuntimeTool({
4182
4422
  }),
4183
4423
  },
4184
4424
  })
4185
- nativeTools: ({ registerTool }) => registerTool(preview)
4425
+ runtimeTools: [preview]
4186
4426
  ```
4187
4427
 
4188
4428
  The removed `NativeMcp*` types have no aliases. Use `RuntimeToolDefinition`,
@@ -4381,7 +4621,7 @@ name derivation, diff them mechanically:
4381
4621
 
4382
4622
  ```ts
4383
4623
  import { listToolNames } from 'stitchkit/tools'
4384
- console.log(JSON.stringify(listToolNames(services), null, 2))
4624
+ console.log(JSON.stringify(listToolNames({ services, runtimeTools }), null, 2))
4385
4625
  ```
4386
4626
 
4387
4627
  `listToolNames` never throws on an illegal name — that is deliberate, so it can
@@ -4493,7 +4733,7 @@ from the root `stitchkit`.
4493
4733
  | `ALL_TRANSPORTS` | constant | `['HTTP', 'MCP', 'AGENT', 'CLI']` |
4494
4734
  | `ContractDef` | _type_ | a defined contract |
4495
4735
  | `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` and `meta` (a default every endpoint shallow-merges over) |
4496
- | `EndpointDef` | _type_ | a single endpoint definition |
4736
+ | `EndpointDef` | _type_ | a single endpoint definition; `output` declares JSON response presence (`null` is data, `undefined` is invalid) |
4497
4737
  | `HeadEndpointDef` | _type_ | explicit HTTP-only, bodyless `HEAD` endpoint definition |
4498
4738
  | `EndpointResponseMeta` | _type_ | static success metadata declared by an HTTP-only typed-data endpoint |
4499
4739
  | `ResponseMetadata` | _type_ | per-request outbound collector exposed as `ctx.response` only for a `responseMeta` endpoint |
@@ -4530,9 +4770,17 @@ from the root `stitchkit`.
4530
4770
  | `conflict` | function | throw `409 CONFLICT` |
4531
4771
  | `rateLimited` | function | throw `429 RATE_LIMITED` |
4532
4772
  | `appError` | function | throw an `AppError` for any code |
4533
- | `defineErrors` | function | declare domain error codes → typed throwers + a code table — [guide](../guide/auth-and-errors.md#domain-errors--defineerrors) |
4534
- | `DefinedErrors` | _type_ | the `{ errors, codes, isCode }` handle `defineErrors` returns |
4535
- | `ErrorThrower` | _type_ | one `defineErrors` thrower `(message?, details?, hint?) => never` |
4773
+ | `defineErrors` | function | declare immutable domain error definitions → typed `AppError` constructors, codes and schemas — [guide](../guide/auth-and-errors.md#domain-errors--defineerrors) |
4774
+ | `DefinedErrors` | _type_ | the `{ errors, codes, definitions, isCode }` handle `defineErrors` returns |
4775
+ | `DefinedAppError` | _type_ | literal-code error instance with schema-refined details |
4776
+ | `ErrorDefinition` | _type_ | `{ status, details? }` definition for one domain code |
4777
+ | `ErrorDefinitions` | _type_ | string-keyed domain error definition registry |
4778
+ | `ErrorDetailsSchema` | _type_ | required or optional Zod object accepted for structured details |
4779
+ | `ErrorDetailsOutput` | _type_ | parsed details inferred from one definition |
4780
+ | `ErrorFactoryArguments` | _type_ | options tuple with forbidden/required/optional details inferred per code |
4781
+ | `ErrorFactory` | _type_ | one typed `AppError` constructor |
4782
+ | `ErrorFactories` | _type_ | mapped constructor registry derived from all definitions |
4783
+ | `FrozenErrorDefinitions` | _type_ | read-only definition registry returned to consumers |
4536
4784
  | `STITCH_ERROR_STATUS` | const | `code → HTTP status` map for stitchkit's own error codes — [guide](../guide/auth-and-errors.md#stitch-codes-vs-your-codes) |
4537
4785
  | `StitchErrorCode` | _type_ | a code stitchkit itself emits (`keyof STITCH_ERROR_STATUS`) |
4538
4786
  | `isStitchErrorCode` | function | type guard — is a code one of stitchkit's own? |
@@ -4600,8 +4848,8 @@ Also re-exports the error helpers from `stitchkit/contract`.
4600
4848
  | Export | Kind | Summary |
4601
4849
  |--------|------|---------|
4602
4850
  | `createAuthHook` | function | a scope-enforcing `beforeHandle` hook — [guide](../guide/auth-and-errors.md#createauthhook) |
4603
- | `createErrorHook` | function | an `onError` hook from a code map + envelope renderer — [guide](../guide/auth-and-errors.md#createerrorhook) |
4604
- | `ErrorHookConfig` | _type_ | config for `createErrorHook` |
4851
+ | `createErrorHook` | function | an async-capable, endpoint-aware `onError` hook from a code map + envelope renderer — [guide](../guide/auth-and-errors.md#createerrorhook) |
4852
+ | `ErrorHookConfig` | _type_ | async observer/renderer config for `createErrorHook` |
4605
4853
  | `ResolvedError` | _type_ | the normalised error handed to `createErrorHook`'s `render` |
4606
4854
  | `createBearerResolver` | function | a bearer-token identity resolver |
4607
4855
  | `signJwt` | function | sign an HS256 JWT |
@@ -4750,7 +4998,7 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
4750
4998
  |--------|------|---------|
4751
4999
  | `createMcpHandler` | function | a complete Streamable-HTTP MCP server — [guide](../guide/mcp-and-agents.md#mcp--createmcphandler) |
4752
5000
  | `createStdioMcpServer` | function | a complete stdio MCP server — [guide](../guide/mcp-and-agents.md#mcp-over-stdio--createstdiomcpserver) |
4753
- | `buildMcpServer` | function | build an `McpServer` from contracts — the transport-neutral core |
5001
+ | `buildMcpServer` | function | build an `McpServer` from contract/runtime surfaces — the transport-neutral core |
4754
5002
  | `mountMcp` | function | add contract tools to an existing `McpServer` — [guide](../guide/mcp-and-agents.md#mountmcp) |
4755
5003
  | `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
4756
5004
  | `mountAgent` | function | a Vercel AI SDK `ToolSet` from a service — [guide](../guide/mcp-and-agents.md#ai-agents--mountagent) |
@@ -4761,20 +5009,26 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
4761
5009
  | `mountViewFile` | function | a native multimodal "view file" MCP tool |
4762
5010
  | `resolveMedia` | function | resolve a media reference for a tool result |
4763
5011
  | `validateMcpSchemas` | function | object-shaped assertion over the exact advertised schema surface — compatibility, typed properties and portable formats ([guide](../guide/mcp-and-agents.md#mcp-schema-validation-profile)) |
4764
- | `listToolNames` | function | every mounted tool name with its `(service, method)` identity — for name-baseline snapshots — [guide](../guide/mcp-and-agents.md#pinning-tool-names--listtoolnames) |
5012
+ | `listToolNames` | function | every contract/runtime tool name with origin, identity and transports — for stable snapshots — [guide](../guide/mcp-and-agents.md#pinning-tool-names--listtoolnames) |
4765
5013
  | `McpHandlerConfig` | _type_ | config for `createMcpHandler` |
5014
+ | `McpHttpConfig` | _type_ | HTTP auth, protected-resource and session options composed into `McpHandlerConfig` |
4766
5015
  | `McpSessionMode` | _type_ | `'stateless' \| 'stateful'`; HTTP defaults to request-isolated stateless mode |
4767
5016
  | `StdioMcpServerConfig` | _type_ | config for `createStdioMcpServer` |
4768
5017
  | `McpServerBuildConfig` | _type_ | shared config for `buildMcpServer` |
5018
+ | `McpServerSharedConfig` | _type_ | transport-neutral options shared by direct and finite surface configs |
5019
+ | `DirectMcpSurfaceConfig` | _type_ | static or identity-dynamic `services` / `runtimeTools` source |
5020
+ | `FiniteMcpSurfaceConfig` | _type_ | bounded `surfaces` registry plus typed selector |
5021
+ | `McpSurfaceDefinition` | _type_ | one immutable `{ services, runtimeTools }` MCP surface |
5022
+ | `McpSurfaceRegistry` | _type_ | finite keyed surface registry for eager preparation |
5023
+ | `StdioAuthConfig` | _type_ | startup identity composed into `StdioMcpServerConfig` |
4769
5024
  | `ImplementRemoteOptions` | _type_ | options for `implementRemote` |
4770
5025
  | `McpMountConfig` | _type_ | config for `mountMcp` |
4771
5026
  | `McpSchemaValidationConfig` | _type_ | shared `{ policy, requireTypedProperties, allowUntyped, requirePortableFormats, allowFormats }` profile |
4772
5027
  | `ValidateMcpSchemasConfig` | _type_ | standalone validation profile plus `services`, `extend`, flattening and logger |
4773
- | `NativeMcpRegistrar` | _type_ | protected `registerTool` plus explicit unprotected `rawServer` access |
4774
5028
  | `RuntimeToolDefinition` | _type_ | transport-neutral pathless operation with identity, schemas, handler and optional presenters |
4775
5029
  | `RuntimeToolDefinitionBase` | _type_ | common name, identity, input, exposure and MCP metadata fields |
4776
5030
  | `RuntimeToolDefinitionWithOutput` | _type_ | runtime definition whose handler and presenters share a validated output type |
4777
- | `RuntimeToolDefinitionWithoutOutput` | _type_ | runtime definition without output validation or presentation callbacks |
5031
+ | `RuntimeToolDefinitionWithoutOutput` | _type_ | runtime definition with a void handler and no presentation callbacks |
4778
5032
  | `RuntimeToolIdentity` | _type_ | `{ serviceName, action, scope?, method, meta? }` for runtime lifecycle/audit |
4779
5033
  | `RuntimeToolHandlerContext` | _type_ | runtime context with the definition's parsed input |
4780
5034
  | `RuntimeToolOutput` | _type_ | output inferred from a runtime tool's optional Zod schema |
@@ -4810,7 +5064,7 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
4810
5064
  | `findNonPortableFormats` | function | deep finder for formats outside the portable MCP/AJV baseline |
4811
5065
  | `NonPortableFormat` | _type_ | one `{ path, format }` portability finding |
4812
5066
  | `PORTABLE_JSON_SCHEMA_FORMATS` | constant | portable-format baseline used by MCP validation |
4813
- | `ToolNameEntry` | _type_ | one `listToolNames` row — `{ name, service, method, transports }` |
5067
+ | `ToolNameEntry` | _type_ | one `listToolNames` row — `{ kind, name, service, method, transports }` |
4814
5068
  | `IncompatibleSchemaPolicy` | _type_ | `'throw' \| 'skip' \| 'warn'` |
4815
5069
  | `McpMediaContent` | _type_ | a multimodal MCP content item |
4816
5070
 
@@ -4869,12 +5123,15 @@ Advanced building blocks — the shared machinery the mounts are built on.
4869
5123
  |--------|------|---------|
4870
5124
  | `collectTools` | function | resolve a service's methods to mountable tools (the shared resolver) |
4871
5125
  | `createToolLogger` | function | a ready `afterToolCall` that logs every tool call — [guide](../guide/mcp-and-agents.md#logging-tool-calls--createtoollogger) |
4872
- | `summarizeTransports` | function | per-transport operation counts for a boot-time summary |
4873
- | `buildToolManifest` | function | a searchable `{ name, description, inputSchema }` manifest for a `tool_search` tool |
5126
+ | `summarizeTransports` | function | mixed contract/runtime operation counts and per-source breakdown for a boot-time summary |
5127
+ | `buildToolManifest` | function | transport-aware searchable `{ name, description, inputSchema }` rows from a mixed surface |
4874
5128
  | `ToolLoggerConfig` | _type_ | config for `createToolLogger` |
4875
5129
  | `ToolCallRecord` | _type_ | the structured record `createToolLogger` passes to `onRecord` |
4876
- | `TransportSummary` | _type_ | the result of `summarizeTransports` |
5130
+ | `TransportSummary` | _type_ | `{ contractServices, runtimeTools, totals, sources }` from `summarizeTransports` |
4877
5131
  | `TransportCounts` | _type_ | per-transport counts (`{ HTTP, MCP, AGENT, CLI }`) |
5132
+ | `ToolSurfaceDefinition` | _type_ | shared object-shaped `{ services?, runtimeTools? }` introspection surface |
5133
+ | `ToolSurfaceTransport` | _type_ | tool collector transport: `'MCP' \| 'AGENT' \| 'CLI'` |
5134
+ | `ToolManifestConfig` | _type_ | mixed surface plus required model-facing `transport` and presentation options |
4878
5135
  | `coerceJsonArgs` | function | coerce JSON-stringified array/object tool arguments |
4879
5136
  | `flattenToolJsonSchema` | function | project structurally identifiable discriminated unions in a JSON Schema document into conservative object joins; never executes validation |
4880
5137
  | `ToolPresentationSchema` | _type_ | immutable model-facing JSON Schema document shared by tool transports |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",