stitchkit 0.41.0 → 0.43.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 (66) hide show
  1. package/README.md +22 -5
  2. package/dist/cli.js +2 -2
  3. package/dist/contract/errors-factory.d.ts +88 -15
  4. package/dist/contract/errors-factory.d.ts.map +1 -1
  5. package/dist/contract/errors.d.ts +4 -4
  6. package/dist/contract/errors.d.ts.map +1 -1
  7. package/dist/contract/factory.d.ts +29 -2
  8. package/dist/contract/factory.d.ts.map +1 -1
  9. package/dist/contract/index.d.ts +2 -2
  10. package/dist/contract/index.d.ts.map +1 -1
  11. package/dist/contract/index.js +2 -2
  12. package/dist/{index-dnkefke9.js → index-310bfer5.js} +1 -1
  13. package/dist/{index-4e1c0hsw.js → index-esqmem78.js} +101 -67
  14. package/dist/{index-sc67e454.js → index-gex6gxhe.js} +7 -4
  15. package/dist/{index-ncqqn1bc.js → index-h2wdcsby.js} +95 -17
  16. package/dist/{index-xax049k6.js → index-r6czv7yt.js} +58 -22
  17. package/dist/{index-809wc1tt.js → index-s4qsmgwe.js} +4 -1
  18. package/dist/index.js +2 -2
  19. package/dist/internal/typed.d.ts +8 -0
  20. package/dist/internal/typed.d.ts.map +1 -1
  21. package/dist/node.js +2 -2
  22. package/dist/observability/audit.d.ts +27 -15
  23. package/dist/observability/audit.d.ts.map +1 -1
  24. package/dist/observability/event.d.ts +2 -2
  25. package/dist/observability/index.d.ts +4 -4
  26. package/dist/observability/index.d.ts.map +1 -1
  27. package/dist/observability/index.js +70 -59
  28. package/dist/react.js +1 -1
  29. package/dist/server/create.d.ts.map +1 -1
  30. package/dist/server/error-hook.d.ts +8 -4
  31. package/dist/server/error-hook.d.ts.map +1 -1
  32. package/dist/server/index.js +7 -6
  33. package/dist/server/logger.d.ts +0 -1
  34. package/dist/server/logger.d.ts.map +1 -1
  35. package/dist/server/types.d.ts +8 -13
  36. package/dist/server/types.d.ts.map +1 -1
  37. package/dist/tools/agent.d.ts +1 -1
  38. package/dist/tools/agent.d.ts.map +1 -1
  39. package/dist/tools/flatten-join.d.ts.map +1 -1
  40. package/dist/tools/list-names.d.ts +8 -6
  41. package/dist/tools/list-names.d.ts.map +1 -1
  42. package/dist/tools/manifest.d.ts +13 -5
  43. package/dist/tools/manifest.d.ts.map +1 -1
  44. package/dist/tools/mcp-handler.d.ts +5 -4
  45. package/dist/tools/mcp-handler.d.ts.map +1 -1
  46. package/dist/tools/mcp-stdio.d.ts +3 -2
  47. package/dist/tools/mcp-stdio.d.ts.map +1 -1
  48. package/dist/tools/mcp.d.ts +49 -9
  49. package/dist/tools/mcp.d.ts.map +1 -1
  50. package/dist/tools/native-mcp.d.ts +4 -18
  51. package/dist/tools/native-mcp.d.ts.map +1 -1
  52. package/dist/tools/runtime-tool.d.ts +32 -1
  53. package/dist/tools/runtime-tool.d.ts.map +1 -1
  54. package/dist/tools/surface.d.ts +37 -0
  55. package/dist/tools/surface.d.ts.map +1 -0
  56. package/dist/tools/toolkit.d.ts +4 -4
  57. package/dist/tools/toolkit.d.ts.map +1 -1
  58. package/dist/tools/transports.d.ts +11 -9
  59. package/dist/tools/transports.d.ts.map +1 -1
  60. package/dist/tools/view-file.d.ts +3 -3
  61. package/dist/tools.d.ts +6 -6
  62. package/dist/tools.d.ts.map +1 -1
  63. package/dist/tools.js +330 -155
  64. package/llms-full.txt +524 -131
  65. package/llms.txt +1 -0
  66. package/package.json +2 -2
package/llms-full.txt CHANGED
@@ -44,7 +44,7 @@ keeping server-only code (`Bun.serve`, the MCP SDK) out of browser bundles.
44
44
  | `stitchkit/node` | server (Node ≥ 22) | `serveNode` + the runtime-agnostic core — the Node mirror of `/server` |
45
45
  | `stitchkit/tools` | server | `createMcpHandler`, `mountMcp`, `mountAgent`, the OAuth provider, native tools |
46
46
  | `stitchkit/cli` | server | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
47
- | `stitchkit/observability` | server | the audit layer — `createAuditHook`, trace context, sanitisation |
47
+ | `stitchkit/observability` | server | request/tool event projections — `createObservability`, trace context, sanitisation |
48
48
  | `stitchkit/react` | browser | `createCursorQuery`, `createCacheBridge` |
49
49
 
50
50
  Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
@@ -168,7 +168,9 @@ actionable error naming the package and the install command — not a bare
168
168
  - [Testing & deployment](./testing-and-deployment.md).
169
169
  - [API reference](../api/reference.md) — every export, by entrypoint.
170
170
 
171
- A complete runnable app is in [`packages/starter`](../../packages/starter).
171
+ For a complete production-shaped app, run `bun create stitchkit my-app`. The
172
+ canonical generated topology is maintained in
173
+ [`packages/create-stitchkit/template`](../../packages/create-stitchkit/template).
172
174
 
173
175
 
174
176
  ==============================================================================
@@ -339,6 +341,19 @@ tools. Narrow it with `expose`:
339
341
  - `expose: ['MCP', 'AGENT']` — a tool only; no HTTP route.
340
342
  - omit `expose` — all transports.
341
343
 
344
+ For a curated or security-sensitive tool surface, opt into explicit tool
345
+ exposure at the scoped factory:
346
+
347
+ ```ts
348
+ const { defineContract } = createContractFactory<AppScope>({
349
+ toolExposure: 'explicit',
350
+ })
351
+ ```
352
+
353
+ With this policy, omitting `expose` materializes `['HTTP']` on the returned
354
+ endpoint. MCP, Agent and CLI then require an explicit endpoint array. The plain
355
+ factory and `defineContract` keep the default-on behaviour above.
356
+
342
357
  Tool transports (`MCP`, `AGENT`) skip three kinds of endpoint automatically:
343
358
  `multipart` (a file upload is not a tool call) and
344
359
  [`rawResponse`](./server.md#raw-response-endpoints) (its answer is bytes, which
@@ -542,9 +557,11 @@ export const { defineContract } = createContractFactory<'public' | 'user' | 'adm
542
557
  export const users = defineContract({ prefix: 'users', scope: 'user' }, { … })
543
558
  ```
544
559
 
545
- The vocabulary is yours; each returned `ContractDef` retains its concrete scope
546
- literal, so scope-aware registries select the exact config without another
547
- wrapper.
560
+ The vocabulary is yours; each returned `ScopedContractDef` retains its concrete
561
+ scope literal as a required `meta.scope` field, so scope-aware registries select
562
+ the exact config without `NonNullable` or another wrapper. Plain `ContractDef`
563
+ keeps optional scope because ordinary `defineContract` still supports its
564
+ default-public overload.
548
565
 
549
566
  ## One source of truth
550
567
 
@@ -1664,6 +1681,11 @@ The server side is [`streamSSE`](./server.md#sse-streaming).
1664
1681
 
1665
1682
  # MCP & AI agents
1666
1683
 
1684
+ The application generated by `bun create stitchkit` includes a working
1685
+ stateless MCP endpoint and a separate `bun run tools` manifest command. Both
1686
+ are assembled from the same implemented project contracts as HTTP and CLI, so
1687
+ the generated project is the canonical end-to-end example for this chapter.
1688
+
1667
1689
  The same contract that drives the HTTP API also drives AI tooling. An endpoint
1668
1690
  exposed on `MCP` becomes a [Model Context Protocol](https://modelcontextprotocol.io)
1669
1691
  tool — callable from Claude, Cursor and other MCP clients. An endpoint exposed on
@@ -1703,16 +1725,16 @@ call —
1703
1725
  ### Pinning tool names — `listToolNames`
1704
1726
 
1705
1727
  Derived tool names are part of your public surface — an MCP client config or an
1706
- agent prompt refers to them by string. `listToolNames(services)` resolves every
1707
- tool name your services expose (the exact resolver the mounts use), with its
1708
- `(service, method)` identity and transports, sorted — a stable shape to
1709
- snapshot:
1728
+ agent prompt refers to them by string. `listToolNames({ services, runtimeTools })`
1729
+ resolves the complete mixed surface (the exact resolver the mounts use), with
1730
+ its origin, `(service, method)` identity and transports, sorted — a stable shape
1731
+ to snapshot:
1710
1732
 
1711
1733
  ```ts
1712
1734
  import { listToolNames } from 'stitchkit/tools'
1713
1735
 
1714
1736
  test('tool names have not drifted', () => {
1715
- expect(listToolNames(services)).toMatchSnapshot()
1737
+ expect(listToolNames({ services, runtimeTools })).toMatchSnapshot()
1716
1738
  })
1717
1739
  ```
1718
1740
 
@@ -1806,13 +1828,16 @@ createServer({
1806
1828
  | `serverInfo` | MCP server identity — `{ name, version }` |
1807
1829
  | `auth` | `(req) => identity \| null` — `null` rejects with 401 |
1808
1830
  | `services` | the services to expose — an array, or `(auth) => ServiceDef[]` |
1831
+ | `runtimeTools` | managed pathless operations — an array, or `(auth) => RuntimeToolDefinition[]` |
1832
+ | `surfaces` | finite `{ key: { services, runtimeTools } }` registry prepared eagerly |
1833
+ | `selectSurface` | `(auth) => declaredKey` — required with `surfaces` |
1809
1834
  | `context` | `(auth) => {…}` — values merged into every tool handler's `ctx` |
1810
1835
  | `lifecycle` | `beforeHandle` / `afterHandle` — the tool-side auth gate (see below) |
1811
1836
  | `hooks` | tool-call observability hooks — `afterToolCall` fires on every result |
1812
1837
  | `extend` | extra advertised arguments resolved into handler context |
1813
1838
  | `schemaValidation` | compatibility policy, typed-property guard and portable-format guard |
1814
1839
  | `logger` | a `StitchLogger` for the `'warn'` policy |
1815
- | `nativeTools` | `({ registerTool, rawServer }, auth) => …` — protected native registration plus an explicit raw SDK escape hatch |
1840
+ | `rawTools` | `(server, auth) => …` — explicit SDK registration without framework guarantees |
1816
1841
  | `resources` | MCP Apps `ui://` resources mounted on every server |
1817
1842
  | `instructions` | a short host-facing usage hint, surfaced to MCP tool-search |
1818
1843
  | `coerceJsonArgs` | coerce JSON-stringified object/array arguments (default `true`) |
@@ -1822,14 +1847,37 @@ createServer({
1822
1847
  | `protectedResource` | RFC 9728 metadata used by HTTP `401` responses |
1823
1848
  | `sessionMode` | `'stateless'` (default) or explicit `'stateful'` session/SSE continuity |
1824
1849
 
1825
- `services`, `context` and `nativeTools` all receive the resolved identity, so a
1826
- tenant can be shown only its own tools and every handler can read `ctx.tenantId`.
1850
+ Direct `services` / `runtimeTools` factories, `context`, `selectSurface` and
1851
+ `rawTools` receive the resolved identity, so a tenant can be shown only its own
1852
+ tools and every handler can read `ctx.tenantId`.
1853
+
1854
+ Use direct factories only when the definitions are genuinely arbitrary per
1855
+ identity. For a bounded role/plan set, declare a finite registry instead:
1856
+
1857
+ ```ts
1858
+ const handleMcp = createMcpHandler({
1859
+ serverInfo,
1860
+ auth,
1861
+ surfaces: {
1862
+ admin: { services: allServices, runtimeTools: [renderPreview] },
1863
+ member: { services: memberServices, runtimeTools: [renderPreview] },
1864
+ },
1865
+ selectSurface: (identity) => identity.isAdmin ? 'admin' : 'member',
1866
+ context: (identity) => ({ identity }),
1867
+ })
1868
+ ```
1869
+
1870
+ Every declared entry is schema-validated and prepared once when the handler is
1871
+ constructed. The selected immutable descriptors are shared; the SDK server,
1872
+ transport, auth-derived context, lifecycle runner and tool-call context are
1873
+ fresh for every stateless request (or every stateful session). Unknown keys
1874
+ fail before the server connects. The registry never retains auth values.
1827
1875
 
1828
1876
  ### Stateless by default; stateful only when required
1829
1877
 
1830
1878
  The default `sessionMode: 'stateless'` creates a fresh SDK server, transport,
1831
- resolved auth/context and runner for each HTTP request. Static contract schemas
1832
- are still prepared once when the handler is constructed. There is no session
1879
+ resolved auth/context and runner for each HTTP request. Static direct and finite
1880
+ registry schemas are still prepared once when the handler is constructed. There is no session
1833
1881
  map, event store, sweep timer or `Mcp-Session-Id`, so process replacement and
1834
1882
  load balancing cannot strand a client on an in-memory session.
1835
1883
 
@@ -1952,8 +2000,14 @@ as one thing. Where they agree, that is what you get; where they disagree — a
1952
2000
  `.refine()` only one of them carries, two different bounds on the same number, an
1953
2001
  enum against a free string — the *constraint* is dropped and the **type** is not.
1954
2002
  A field that is a number in every variant is advertised as a number, not as a
1955
- bare description. Only genuinely different kinds (a string in one variant, a
1956
- number in another) fall back to unconstrained. ADR 0044
2003
+ bare description. When the kinds genuinely differ, the flat projection keeps
2004
+ every provable kind in a deterministic JSON Schema type array for example
2005
+ `type: ['string', 'array']` — while dropping constraints that are not sound for
2006
+ all branches. Nested `oneOf` / `anyOf` values still contribute their provable
2007
+ base kinds without reintroducing union keywords. Only a branch whose kind is
2008
+ actually unknowable, such as a free-form schema or unresolved reference, leaves
2009
+ the collision unconstrained. → ADRs [0044](../decisions/0044-a-collided-field-keeps-its-type.md)
2010
+ and [0065](../decisions/0065-flat-collisions-preserve-every-known-kind.md)
1957
2011
 
1958
2012
  It is **deep** because the projection walks the generated JSON Schema document,
1959
2013
  including objects, arrays, tuples and schema-definition nodes. Structurally
@@ -1961,6 +2015,10 @@ identifiable discriminated object unions are flattened wherever they occur.
1961
2015
  Plain unions and unions hidden behind unresolved external references remain
1962
2016
  unions because Stitchkit cannot soundly invent a discriminator.
1963
2017
 
2018
+ Use the default `flattenUnionInput: false` when the model must see the exact
2019
+ relationship between a discriminator and each branch. The flat type array is a
2020
+ sound set of possible JSON kinds, not a reconstruction of those correlations.
2021
+
1964
2022
  The flattened form is **lossy but never executable**. Per-variant refinements
1965
2023
  and incompatible constraints are widened in the presentation document; the
1966
2024
  original Zod contract enforces them exactly once inside `executeToolMethod`.
@@ -2006,8 +2064,8 @@ than a per-request `(req) => …`. Keep all logging on **stderr**: stdout is the
2006
2064
  JSON-RPC channel.
2007
2065
 
2008
2066
  Both transports build the server through the shared `buildMcpServer` — same
2009
- contract pipeline, same `services` / `context` / `hooks` / `nativeTools` /
2010
- `instructions`.
2067
+ contract/runtime pipeline, same surface selection, context, hooks, raw escape
2068
+ hatch and instructions.
2011
2069
 
2012
2070
  ## OAuth 2.1 — a native remote connector
2013
2071
 
@@ -2249,9 +2307,9 @@ const handleMcp = createMcpHandler({
2249
2307
  serverInfo: { name: 'my-app', version: '1.0.0' },
2250
2308
  auth,
2251
2309
  services: [service],
2310
+ runtimeTools: [renderPreview],
2252
2311
  lifecycle: { beforeHandle: authHook },
2253
2312
  hooks: audit.toolCall,
2254
- nativeTools: ({ registerTool }) => registerTool(renderPreview),
2255
2313
  })
2256
2314
 
2257
2315
  const agentTools = mountAgent([service], {
@@ -2261,6 +2319,33 @@ const agentTools = mountAgent([service], {
2261
2319
  })
2262
2320
  ```
2263
2321
 
2322
+ When several runtime tools share application context and identity, bind both
2323
+ once with `createRuntimeToolFactory`. Its Zod context schema is parsed once per
2324
+ call inside the same runner; the authored handler receives validated context and
2325
+ parsed `input`, while lifecycle, hooks, output validation and presenters remain
2326
+ unchanged:
2327
+
2328
+ ```ts
2329
+ const knowledgeTools = createRuntimeToolFactory({
2330
+ serviceName: 'agentKnowledge',
2331
+ scope: 'user',
2332
+ context: z.object({ userId: z.string(), tz: z.string() }),
2333
+ })
2334
+
2335
+ const countRecords = knowledgeTools.define({
2336
+ name: 'count_records',
2337
+ action: 'countRecords',
2338
+ method: 'GET',
2339
+ description: 'Count records',
2340
+ input: z.object({ kind: z.string() }),
2341
+ output: z.object({ count: z.number() }),
2342
+ handler: async ({ userId, tz, input }) => countFor(userId, tz, input.kind),
2343
+ })
2344
+ ```
2345
+
2346
+ `serviceName` and `scope` cannot be overridden by one definition. Use standalone
2347
+ `defineRuntimeTool` when tools do not share a context schema or identity.
2348
+
2264
2349
  `transports` defaults to `['MCP', 'AGENT']`; set an explicit subset when an
2265
2350
  operation belongs on only one surface. The configured identity becomes the
2266
2351
  hook/lifecycle `OperationIdentity` and the tool `RequestEvent`
@@ -2281,7 +2366,7 @@ use the same validation, lifecycle and hook path as contract tools.
2281
2366
 
2282
2367
  ### Explicit raw SDK registration
2283
2368
 
2284
- `rawServer` is deliberately named as an escape hatch. A tool registered there
2369
+ `rawTools` is deliberately named as an escape hatch. A tool registered there
2285
2370
  does **not** receive stitchkit schema policy, lifecycle, per-call context or
2286
2371
  hooks. The built-in `mountViewFile` helper remains raw for callers that choose
2287
2372
  that boundary; it fetches media with SSRF and path-traversal defenses:
@@ -2293,14 +2378,51 @@ const handleMcp = createMcpHandler({
2293
2378
  serverInfo: { name: 'my-app', version: '1.0.0' },
2294
2379
  auth,
2295
2380
  services: [service],
2296
- nativeTools: ({ rawServer }) =>
2297
- mountViewFile(rawServer, { baseDir: '/srv/uploads' }),
2381
+ rawTools: (server) => mountViewFile(server, { baseDir: '/srv/uploads' }),
2298
2382
  })
2299
2383
  ```
2300
2384
 
2301
2385
  Use raw registration only when opting out is intentional. For a protected
2302
- `view_file`, define it through `registerTool` and call the exported
2303
- `resolveMedia` core from its handler.
2386
+ `view_file`, define it with `defineRuntimeTool`, include it in `runtimeTools`,
2387
+ and call the exported `resolveMedia` core from its handler.
2388
+
2389
+ ## Introspecting the complete tool surface
2390
+
2391
+ Deferred tool search, name snapshots and boot diagnostics accept the same
2392
+ object-shaped surface as the mounts. Contract operations are followed by
2393
+ runtime definitions, matching mount order; exposure filters and collisions are
2394
+ resolved by Stitchkit rather than by consumer code:
2395
+
2396
+ ```ts
2397
+ import {
2398
+ buildToolManifest,
2399
+ listToolNames,
2400
+ summarizeTransports,
2401
+ } from 'stitchkit/tools'
2402
+
2403
+ const surface = { services, runtimeTools: [renderPreview] }
2404
+
2405
+ const manifest = buildToolManifest({
2406
+ ...surface,
2407
+ transport: 'AGENT',
2408
+ flattenUnionInput: true,
2409
+ })
2410
+
2411
+ const names = listToolNames(surface)
2412
+ const summary = summarizeTransports(surface)
2413
+ ```
2414
+
2415
+ `buildToolManifest` returns the exact immutable presentation schema shown to
2416
+ the selected MCP or Agent transport. It does not execute validation effects and
2417
+ fails first on duplicate contract/runtime names. There is no runtime-only
2418
+ manifest helper or public mount adapter: the framework owns merging and schema
2419
+ projection.
2420
+
2421
+ `listToolNames` remains diagnostic and reports `kind: 'contract' | 'runtime'`
2422
+ for every identity. `summarizeTransports` returns `contractServices`,
2423
+ `runtimeTools`, aggregate `totals`, and a `sources` breakdown. Runtime tools
2424
+ contribute only to MCP/Agent according to their `transports`; they never inflate
2425
+ HTTP or CLI counts.
2304
2426
 
2305
2427
  ## Logging tool calls — `createToolLogger`
2306
2428
 
@@ -2320,9 +2442,9 @@ Pass `log` to redirect the line, or `onRecord` to feed a metrics sink the
2320
2442
  structured `ToolCallRecord`. That record carries `traceId` whenever an
2321
2443
  observability context is active, so a tool call made inside an HTTP request
2322
2444
  joins that request's log line on one key — see
2323
- [Observability](./observability.md). For a boot-time picture of what is exposed where,
2324
- `summarizeTransports(services)` returns per-transport operation counts (HTTP /
2325
- MCP / AGENT / CLI) for you to log.
2445
+ [Observability](./observability.md). For a boot-time picture of what is exposed
2446
+ where, `summarizeTransports({ services, runtimeTools })` returns the mixed
2447
+ per-transport counts for you to log.
2326
2448
 
2327
2449
  ## One handler, three callers
2328
2450
 
@@ -3253,27 +3375,46 @@ onError: (ctx, err) => {
3253
3375
 
3254
3376
  ## Domain errors — `defineErrors`
3255
3377
 
3256
- Declaring your app's error codes once gives you typed throwers on the server and
3257
- a code table the client matches with autocomplete — instead of reading the raw
3258
- `message` string (which breaks the moment a code expects a string but gets an
3259
- object):
3378
+ Declare each domain code, HTTP status and optional structured-details schema in
3379
+ one immutable registry. The generated functions construct typed branded
3380
+ `AppError` instances; ordinary `throw` remains explicit at the call site:
3260
3381
 
3261
3382
  ```ts
3262
- export const { errors, codes, isCode } = defineErrors({
3263
- SESSION_NOT_FOUND: 404,
3264
- QUOTA_EXCEEDED: 429,
3383
+ import { z } from 'zod'
3384
+
3385
+ export const { errors, codes, definitions, isCode } = defineErrors({
3386
+ SESSION_NOT_FOUND: { status: 404 },
3387
+ QUOTA_EXCEEDED: {
3388
+ status: 429,
3389
+ details: z.object({ retryAfterSeconds: z.number().int().positive() }),
3390
+ },
3391
+ })
3392
+
3393
+ throw errors.SESSION_NOT_FOUND({ message: 'No such session' })
3394
+ throw errors.QUOTA_EXCEEDED({
3395
+ message: 'Try later',
3396
+ details: { retryAfterSeconds: 30 },
3397
+ hint: 'Wait for the current window to expire',
3265
3398
  })
3266
3399
 
3267
- // server a typed thrower, the right HTTP status baked in
3268
- throw errors.SESSION_NOT_FOUND('no such session')
3400
+ // Construction without throwing is useful for composition or inspection.
3401
+ const error = errors.QUOTA_EXCEEDED({ details: { retryAfterSeconds: 30 } })
3402
+ definitions.QUOTA_EXCEEDED.status // 429 — same source, no copied status map
3269
3403
 
3270
3404
  // client — match the code, never a magic string
3271
3405
  if (err instanceof ApiError && err.code === codes.SESSION_NOT_FOUND) { … }
3272
3406
  ```
3273
3407
 
3274
- The `code` rides through unchanged in both the HTTP envelope and the MCP tool
3275
- result, so one vocabulary covers every transport. The codes are yours; the core
3276
- stays domain-free.
3408
+ With no `details` schema, the options object forbids `details`. A required
3409
+ `z.object` makes `details` required; `z.object(...).optional()` makes it
3410
+ optional. Supplied details are parsed when the error is constructed, before any
3411
+ transport sees them. `code` remains literal and the parsed details type is
3412
+ retained on the returned `AppError`.
3413
+
3414
+ HTTP renders the complete code/status/message/details/hint. Tool transports keep
3415
+ their established model-facing projection (code/details/hint, without HTTP
3416
+ status), while `invokeOrThrow` recovers the exact normalized `AppError`. The
3417
+ codes and schemas remain application-owned; Stitchkit stays domain-free.
3277
3418
 
3278
3419
  ## `createErrorHook`
3279
3420
 
@@ -3305,6 +3446,27 @@ createServer({ services, hooks: { onError } })
3305
3446
  Codes you threw yourself (not stitchkit's) pass through `codeMap` unchanged; the
3306
3447
  `satisfies Record<StitchErrorCode, …>` keeps the map exhaustive across upgrades.
3307
3448
 
3449
+ Both `onError` and `render` may be asynchronous and receive the matched endpoint
3450
+ as their final argument. The observer is awaited before rendering, so it can
3451
+ resolve identity or enrich the request context even for failures raised before
3452
+ `beforeHandle`:
3453
+
3454
+ ```ts
3455
+ const onError = createErrorHook({
3456
+ onError: async (_error, _info, ctx, endpoint) => {
3457
+ await attributeFailedRequest(ctx, endpoint)
3458
+ },
3459
+ render: (info, ctx) => ({
3460
+ error: { code: info.code },
3461
+ actorId: ctx.actorId,
3462
+ }),
3463
+ })
3464
+ ```
3465
+
3466
+ `endpoint` is `undefined` when the failure happened before route resolution.
3467
+ Synchronous callbacks and renderers that declare fewer parameters continue to
3468
+ work normally.
3469
+
3308
3470
  Invalid input (a `ZodError`) is classified as `VALIDATION_ERROR` 400 before it
3309
3471
  reaches `render` — a client fault is an honest 400, not a 500 — and the
3310
3472
  offending fields arrive as structured `info.details.issues`, so your `render`
@@ -3340,10 +3502,10 @@ stitchkit answers this at two levels.
3340
3502
  - **The raw hooks** — `LifecycleHooks` and `ToolCallHooks`. Every request and
3341
3503
  every tool call passes through a point you can observe. The lowest level;
3342
3504
  always available. [Jump ↓](#the-raw-hooks)
3343
- - **`stitchkit/observability`** — the audit layer built on those hooks: W3C
3344
- trace context, an `AsyncLocalStorage` request context, payload sanitisation,
3345
- and `createAuditHook` to wire it all into one sink. Your logging becomes a
3346
- table plus a `write` function. [Start here ↓](#the-observability-module)
3505
+ - **`stitchkit/observability`** — framework-owned HTTP completion plus canonical
3506
+ tool hooks: W3C trace context, an `AsyncLocalStorage` request context, payload
3507
+ sanitisation and `createObservability` with independent request/tool sinks.
3508
+ [Start here ↓](#the-observability-module)
3347
3509
 
3348
3510
  stitchkit still ships no logger and no audit store — those are the app's choice.
3349
3511
  What it ships is the machinery that turns a completed call into a clean,
@@ -3352,52 +3514,55 @@ normalised record.
3352
3514
  ## The observability module
3353
3515
 
3354
3516
  `stitchkit/observability` is server-only. It has three parts — a trace context,
3355
- a request context, and the audit hook — and you usually touch only the last.
3356
-
3357
- ### createAuditHook
3358
-
3359
- `createAuditHook` is the whole module in one call. You give it a `write` sink;
3360
- it gives you back a wrapper for each surface. Every completed call — HTTP
3361
- request, MCP tool call, agent tool call is normalised into one `RequestEvent`
3362
- and handed to `write`.
3363
-
3364
- ```ts
3365
- import { createAuditHook } from 'stitchkit/observability'
3366
-
3367
- export const audit = createAuditHook({
3368
- // The only thing the app supplies — persist one row.
3369
- write: (event) => {
3370
- db.auditLog.create({ data: {
3371
- traceId: event.traceId,
3372
- source: event.source, // 'http' | 'mcp' | 'agent'
3373
- method: event.method, // verb, or 'TOOL'
3374
- path: event.path,
3375
- ok: event.ok,
3376
- statusCode: event.statusCode,
3377
- durationMs: event.durationMs,
3378
- userId: event.userId,
3379
- payload: event.payload, // already sanitised
3380
- }})
3517
+ a request context, and event projections — and you usually touch only the last.
3518
+
3519
+ ### createObservability
3520
+
3521
+ `createObservability` configures request and tool projections independently.
3522
+ Every completed call is normalised into one `RequestEvent`; HTTP completion is
3523
+ owned directly by `createHandler`, while MCP/Agent completion uses the canonical
3524
+ `ToolCallHooks` runner. There is no nested HTTP audit wrapper.
3525
+
3526
+ ```ts
3527
+ import { createObservability } from 'stitchkit/observability'
3528
+
3529
+ const write = (event) => db.auditLog.create({ data: {
3530
+ traceId: event.traceId,
3531
+ source: event.source,
3532
+ method: event.method,
3533
+ path: event.path,
3534
+ ok: event.ok,
3535
+ statusCode: event.statusCode,
3536
+ durationMs: event.durationMs,
3537
+ userId: event.userId,
3538
+ payload: event.payload,
3539
+ }})
3540
+
3541
+ export const observability = createObservability({
3542
+ request: {
3543
+ write,
3544
+ includePayload: false, // default: no Request.clone(), payload is null
3545
+ filter: (event) => event.method !== 'GET',
3546
+ },
3547
+ tools: {
3548
+ write,
3549
+ filter: (event) => event.source === 'mcp' || event.source === 'agent',
3381
3550
  },
3382
- // Optional — keep only the events you care about.
3383
- filter: (event) => event.source !== 'http' || event.method !== 'GET',
3384
3551
  })
3385
3552
  ```
3386
3553
 
3387
- It returns an [`AuditHook`](#audithook) `{ http, toolCall }`:
3554
+ Wire each projection where its completion is owned:
3388
3555
 
3389
3556
  ```ts
3390
- // HTTP — wrap the fetch handler, inside wrapInRequestContext.
3391
- Bun.serve({ fetch: wrapInRequestContext(audit.http(handler)) })
3557
+ createServer({ services, observability: observability.request })
3392
3558
 
3393
- // MCP & agents pass as the tool-call hooks.
3394
- createMcpHandler({ /* … */ hooks: audit.toolCall })
3395
- mountAgent(service, { hooks: audit.toolCall })
3559
+ createMcpHandler({ /* */ hooks: observability.toolCall })
3560
+ mountAgent(service, { hooks: observability.toolCall })
3396
3561
  ```
3397
3562
 
3398
- One `createAuditHook`, one sink, every surface. The sink runs
3399
- fire-and-forget and its errors are swallowed a slow or failing audit write
3400
- can never block or break the request it observes.
3563
+ Each sink runs fire-and-forget and fails independently: a slow or broken request
3564
+ sink cannot block the response, suppress operational logging or break the tool
3565
+ sink.
3401
3566
 
3402
3567
  ### RequestEvent
3403
3568
 
@@ -3416,32 +3581,26 @@ queryable across all three:
3416
3581
  | `ok` / `statusCode` | outcome — real HTTP status, or `200`/`400` for a tool |
3417
3582
  | `durationMs` / `startedAt` | timing |
3418
3583
  | `errorCode` / `errorMessage` / `errorDetail` | failures only — `errorDetail` carries the structure the message flattens (e.g. Zod issues) |
3419
- | `payload` | the request body / tool arguments sanitised |
3584
+ | `payload` | sanitised tool arguments; HTTP is `null` unless request `includePayload` is enabled |
3420
3585
  | `resultSize` / `responseBytes` | result item count + serialised size |
3421
3586
  | `userId` / `ipAddress` / `userAgent` | identity |
3422
3587
 
3423
3588
  ### Request context
3424
3589
 
3425
- `createAuditHook`'s `http` wrapper reads a request context — trace ids, timing,
3426
- identity from `AsyncLocalStorage`. `wrapInRequestContext` establishes it, and
3427
- must be the **outermost** wrapper of your fetch handler:
3590
+ When request observability is configured, `createHandler` establishes the
3591
+ `AsyncLocalStorage` request context itself and uses the same completion snapshot
3592
+ for operational logging and `RequestEvent`. No `wrapFetch` composition is
3593
+ needed:
3428
3594
 
3429
3595
  ```ts
3430
- import { getTraceId, wrapInRequestContext } from 'stitchkit/observability'
3431
-
3432
- Bun.serve({
3433
- fetch: wrapInRequestContext(audit.http(handler)),
3434
- })
3596
+ createServer({ services, logging, observability: observability.request })
3435
3597
  ```
3436
3598
 
3437
- `createServer` and `serveNode` build their own `fetch`, so compose through
3438
- **`wrapFetch`** instead same order, the context outermost:
3599
+ `wrapInRequestContext` remains available for a custom fetch pipeline that does
3600
+ not use `createHandler`; it is no longer part of built-in HTTP audit wiring:
3439
3601
 
3440
3602
  ```ts
3441
- createServer({
3442
- services,
3443
- wrapFetch: (fetch) => wrapInRequestContext(audit.http(fetch)),
3444
- })
3603
+ Bun.serve({ fetch: wrapInRequestContext(customFetch) })
3445
3604
  ```
3446
3605
 
3447
3606
  Some fields are filled in late. Set them from the hooks that know:
@@ -3477,6 +3636,12 @@ context carries nothing yet, so your value always wins.
3477
3636
  `event.serviceName` / `event.action` are present on every event, including a
3478
3637
  pre-handler 400. Nothing to wire.
3479
3638
 
3639
+ When failure attribution itself is asynchronous, use `createErrorHook`'s
3640
+ `onError(error, info, ctx, endpoint)` observer. The framework awaits it before
3641
+ calling `render`, so identity or audit enrichment is visible to both the final
3642
+ error envelope and the request event. `endpoint` is the matched `MethodDef`, or
3643
+ `undefined` when routing failed before a method was selected.
3644
+
3480
3645
  **Domain dimensions** — attach your own tenant / project / entity id with
3481
3646
  `setRequestDimensions`. It is an opaque `Record<string, string>` the core gives no
3482
3647
  meaning to (→ ADR 0021). Resolve it cheaply from `ctx.params` / headers in
@@ -3570,8 +3735,8 @@ every tool call underneath it. With no inbound header a fresh root trace is
3570
3735
  minted. Each tool call opens a [`childSpan`](#trace-context) of the request it
3571
3736
  runs in.
3572
3737
 
3573
- You rarely call the trace functions directly — `wrapInRequestContext` and
3574
- `createAuditHook` use them for you. They are exported (`resolveTraceContext`,
3738
+ You rarely call the trace functions directly — `createHandler` request
3739
+ observability and `wrapInRequestContext` use them for you. They are exported (`resolveTraceContext`,
3575
3740
  `parseTraceparent`, `formatTraceparent`, `childSpan`) for when you need to
3576
3741
  propagate a `traceparent` onward to another service.
3577
3742
 
@@ -3606,12 +3771,19 @@ A payload goes into an audit row only after `sanitizePayload`:
3606
3771
  never the bytes;
3607
3772
  - the result is **capped** — anything over the byte limit becomes a preview.
3608
3773
 
3609
- `createAuditHook` runs it on every event; tune it through `sanitize`:
3774
+ `createObservability` runs it on every emitted event; tune each sink separately:
3610
3775
 
3611
3776
  ```ts
3612
- createAuditHook({
3613
- write,
3614
- sanitize: { maxBytes: 8_000, sensitiveKeys: /password|token|pin/i },
3777
+ createObservability({
3778
+ request: {
3779
+ write,
3780
+ includePayload: true,
3781
+ sanitize: { maxBytes: 8_000, sensitiveKeys: /password|token|pin/i },
3782
+ },
3783
+ tools: {
3784
+ write,
3785
+ sanitize: { maxBytes: 8_000, sensitiveKeys: /password|token|pin/i },
3786
+ },
3615
3787
  })
3616
3788
  ```
3617
3789
 
@@ -3620,7 +3792,7 @@ need to sanitise something outside the audit path.
3620
3792
 
3621
3793
  ## The raw hooks
3622
3794
 
3623
- `createAuditHook` is built on hooks you can also use directly — for a one-off
3795
+ Tool observability is built on hooks you can also use directly — for a one-off
3624
3796
  metric, a custom log line, anything that is not a full audit row.
3625
3797
 
3626
3798
  | Surface | Hook | Fires |
@@ -3694,7 +3866,7 @@ observe. (This is also why it lives on `ToolCallHooks` rather than being an
3694
3866
  object must stay assignable to `ToolLifecycle`.)
3695
3867
 
3696
3868
  **Do not reach for `setRequestError` here.** It writes to the *request* context,
3697
- which `createAuditHook`'s **tool** row does not read: a tool event takes
3869
+ which the built-in **tool** row does not read: a tool event takes
3698
3870
  `errorCode` / `errorMessage` / `errorDetail` from the `ToolResult`, and only
3699
3871
  identity and `dimensions` from the context. Calling it in `onToolError` would
3700
3872
  leave the tool row exactly as scrubbed as before. It is right for the **HTTP**
@@ -3720,7 +3892,7 @@ validation failure or a `beforeToolCall` rejection leaves it `undefined`, becaus
3720
3892
  neither ever had a raw value to lose. Consumers destructure only the fields they
3721
3893
  use; future optional fields do not change callback arity.
3722
3894
 
3723
- `createAuditHook` uses it already. Where the envelope was scrubbed to
3895
+ `createObservability({ tools })` uses it already. Where the envelope was scrubbed to
3724
3896
  `INTERNAL_SERVER_ERROR`, the row's `errorMessage` becomes the real message
3725
3897
  instead of the placeholder; a truthful envelope (a thrown `AppError`, a
3726
3898
  `ZodError`) is left alone, `errorCode` and `errorDetail` are untouched, and the
@@ -3733,7 +3905,7 @@ sink of your own (a tracker, a stack, an alert), `afterToolCall` for the record.
3733
3905
 
3734
3906
  ### Keying a row on (service, action)
3735
3907
 
3736
- `createAuditHook` already keys every event by **service** and **action**
3908
+ Built-in observability keys every event by **service** and **action**
3737
3909
  (`event.serviceName` / `event.action`, → ADR 0029) — reach for the raw hook only
3738
3910
  when you also need the handler **output**, which the audit wrapper never sees. For
3739
3911
  that, read the endpoint identity off the `OperationIdentity` the tool hook
@@ -3761,11 +3933,11 @@ hooks: {
3761
3933
  }
3762
3934
  ```
3763
3935
 
3764
- > **Why the HTTP audit is a wrapper, not a lifecycle hook.** `LifecycleHooks`
3765
- > has a single `onError` an audit built on it would compete with the app's own
3766
- > error handler. `createAuditHook`'s `http` wrapper sees the final `Response`
3767
- > instead, success and error alike, and never contends for a hook. The raw
3768
- > lifecycle hooks remain yours for everything else.
3936
+ > **Why HTTP observability is framework-owned, not a lifecycle hook.**
3937
+ > `LifecycleHooks` has a single `onError`; an audit built on it would compete
3938
+ > with the app's error renderer and miss raw/unmatched exits. `createHandler`
3939
+ > sees the final response on every path and emits one completion without
3940
+ > consuming an application hook.
3769
3941
 
3770
3942
  Keep any sink **asynchronous and self-contained**: a slow or failing write must
3771
3943
  never block or break the request. Swallow the sink's own errors.
@@ -3914,7 +4086,8 @@ Notes for a Node host:
3914
4086
  stitchkit serves the API. A SPA front-end is built and hosted separately — a
3915
4087
  static host or CDN in production, its own dev server in development. The backend
3916
4088
  does not serve static files (`staticRoute` exists for the occasional asset, not
3917
- a whole app). See [`packages/starter`](../../packages/starter) for the split.
4089
+ a whole app). `bun create stitchkit my-app` demonstrates the supported split:
4090
+ an independently built Next.js frontend and Bun/Stitchkit API.
3918
4091
 
3919
4092
  ### MCP
3920
4093
 
@@ -4051,6 +4224,76 @@ rule read it the same way on both surfaces — one contract, every surface, no
4051
4224
  per-transport tenant plumbing.
4052
4225
 
4053
4226
 
4227
+ ==============================================================================
4228
+ # Guide: Frontend integrations (docs/guide/frontend-integrations.md)
4229
+ ==============================================================================
4230
+
4231
+ ---
4232
+ title: Frontend integrations
4233
+ description: Compose Stitchkit with Next.js, React Router or a separate Vite development server
4234
+ type: architecture
4235
+ status: active
4236
+ created: 2026-08-08
4237
+ updated: 2026-08-08
4238
+ ---
4239
+
4240
+ # Frontend integrations
4241
+
4242
+ The official `bun create stitchkit` application uses Next.js with a separate
4243
+ Bun API. Stitchkit remains a Fetch-native backend and does not own frontend
4244
+ routing, SSR or HMR.
4245
+
4246
+ ## Theme boundary in the official starter
4247
+
4248
+ The generated Next.js application uses `@wrksz/themes`, not a Stitchkit-owned
4249
+ theme abstraction. Its root `ThemeProvider` comes from `@wrksz/themes/next` and
4250
+ lives directly in the server layout so Next 16 can inject the first-paint script
4251
+ through `useServerInsertedHTML`. The default `hybrid` storage reads a cookie
4252
+ during SSR and mirrors changes to localStorage for cross-tab synchronization.
4253
+
4254
+ Client components import typed hooks from the fine-grained
4255
+ `@wrksz/themes/client/*` entrypoints. Nested visual examples use
4256
+ `ClientThemeProvider` with a scoped target and `storage="none"`; they never
4257
+ become a second global provider. Applications may add account-backed theme
4258
+ preferences, CSP nonces or consent-aware storage, but those policies remain
4259
+ application concerns.
4260
+
4261
+ Theme state and theme animation are intentionally separate. `@wrksz/themes`
4262
+ owns selection, resolution, SSR prepaint and persistence. The generated app's
4263
+ `theme/transition.ts` wraps an interactive `setTheme` call with the native View
4264
+ Transition API and exposes typed style, duration, easing and origin settings.
4265
+ The default 250 ms crossfade matches the starter's visual language; the
4266
+ catalogue also demonstrates a radial reveal. The runner bypasses animation when
4267
+ the browser lacks the API or `prefers-reduced-motion: reduce` is active.
4268
+
4269
+ ## React Router
4270
+
4271
+ Mount a `createHandler()` result in a catch-all resource route and pass the
4272
+ incoming `Request` through unchanged. Mount MCP as a second resource route. An
4273
+ SSR request creates its own typed client using that request's origin and auth;
4274
+ do not share request identity in a module singleton.
4275
+
4276
+ ```ts
4277
+ export async function loader({ request }: LoaderFunctionArgs) {
4278
+ return apiHandler(request);
4279
+ }
4280
+
4281
+ export async function action({ request }: ActionFunctionArgs) {
4282
+ return apiHandler(request);
4283
+ }
4284
+ ```
4285
+
4286
+ ## Vite
4287
+
4288
+ Run Vite and the Stitchkit API as separate development processes. Declare one
4289
+ proxy for `/api`, `/mcp` and `/socket.io`; browser code still calls the typed
4290
+ client with same-origin paths. Production serves the static Vite output from a
4291
+ static host or reverse proxy and routes those backend paths to Stitchkit.
4292
+
4293
+ Do not duplicate DTOs or handwritten API wrappers in either integration. The
4294
+ shared contract remains the only transport schema source.
4295
+
4296
+
4054
4297
  ==============================================================================
4055
4298
  # Guide: Upgrading (docs/guide/upgrading.md)
4056
4299
  ==============================================================================
@@ -4119,6 +4362,123 @@ current one *up to* your target, and apply each snippet.
4119
4362
 
4120
4363
  ## Unreleased breaking migrations
4121
4364
 
4365
+ HTTP observability now completes inside the framework handler instead of a
4366
+ nested fetch wrapper. Configure request and tool sinks explicitly:
4367
+
4368
+ ```ts
4369
+ // before
4370
+ const audit = createAuditHook({ write })
4371
+ createServer({
4372
+ services,
4373
+ wrapFetch: (handler) => wrapInRequestContext(audit.http(handler)),
4374
+ })
4375
+ mountAgent(services, { hooks: audit.toolCall })
4376
+
4377
+ // after
4378
+ const observability = createObservability({
4379
+ request: { write, includePayload: true },
4380
+ tools: { write },
4381
+ })
4382
+ createServer({ services, observability: observability.request })
4383
+ mountAgent(services, { hooks: observability.toolCall })
4384
+ ```
4385
+
4386
+ Body capture changed from always-on for body methods to opt-in. Set
4387
+ `includePayload: true` only when the request sink needs the sanitized JSON body.
4388
+ There is no `createAuditHook` or `audit.http` compatibility path;
4389
+ `wrapInRequestContext` remains only for custom fetch pipelines.
4390
+
4391
+ Tool introspection now accepts one object-shaped contract/runtime surface. Stop
4392
+ calling the internal contract collector or merging a locally converted runtime
4393
+ manifest:
4394
+
4395
+ ```ts
4396
+ // before
4397
+ buildToolManifest(services.flatMap((service) => collectTools(service, 'AGENT')))
4398
+ listToolNames(services)
4399
+ summarizeTransports(services)
4400
+
4401
+ // after
4402
+ const surface = { services, runtimeTools }
4403
+ buildToolManifest({ ...surface, transport: 'AGENT' })
4404
+ listToolNames(surface)
4405
+ summarizeTransports(surface)
4406
+ ```
4407
+
4408
+ `ToolNameEntry` adds `kind: 'contract' | 'runtime'`. `TransportSummary` is now
4409
+ `{ contractServices, runtimeTools, totals, sources }`; replace `services` and
4410
+ `perService` reads with the explicit counts and mixed-source breakdown. There
4411
+ is no positional overload and no `buildRuntimeToolManifest`: Stitchkit owns the
4412
+ combined order, transport filtering, canonical presentation schema and
4413
+ cross-origin collision checks.
4414
+
4415
+ `defineErrors` now uses one Zod-first definition object and returns constructors
4416
+ instead of positional throwers. Add explicit `throw`, move message/details/hint
4417
+ into one options object, and declare a details schema when that code carries
4418
+ structured context:
4419
+
4420
+ ```ts
4421
+ // before
4422
+ const { errors } = defineErrors({ QUOTA_EXCEEDED: 429 })
4423
+ errors.QUOTA_EXCEEDED('Try later', { retryAfterSeconds: 30 }, 'Wait')
4424
+
4425
+ // after
4426
+ const { errors, definitions } = defineErrors({
4427
+ QUOTA_EXCEEDED: {
4428
+ status: 429,
4429
+ details: z.object({ retryAfterSeconds: z.number().positive() }),
4430
+ },
4431
+ })
4432
+ throw errors.QUOTA_EXCEEDED({
4433
+ message: 'Try later',
4434
+ details: { retryAfterSeconds: 30 },
4435
+ hint: 'Wait',
4436
+ })
4437
+ ```
4438
+
4439
+ There is no positional overload. A code without `details` forbids them; use an
4440
+ optional object schema when the details object itself is optional. Read status
4441
+ and schemas from the frozen `definitions` registry instead of maintaining a
4442
+ parallel map.
4443
+
4444
+ Managed MCP runtime tools are now declared as immutable data. Move protected
4445
+ registrar calls to `runtimeTools`; rename deliberate raw SDK registration to
4446
+ `rawTools`. There is no registrar alias:
4447
+
4448
+ ```ts
4449
+ // before — protected
4450
+ createMcpHandler({
4451
+ services,
4452
+ nativeTools: ({ registerTool }) => registerTool(preview),
4453
+ })
4454
+
4455
+ // after — protected and prepared with the rest of the surface
4456
+ createMcpHandler({ services, runtimeTools: [preview] })
4457
+
4458
+ // before — deliberate raw SDK opt-out
4459
+ nativeTools: ({ rawServer }, auth) => mountRaw(rawServer, auth)
4460
+
4461
+ // after — still a deliberate raw SDK opt-out
4462
+ rawTools: (server, auth) => mountRaw(server, auth)
4463
+ ```
4464
+
4465
+ When identity selects from a bounded set, replace a repeatedly prepared
4466
+ `services(auth)` factory with a finite registry:
4467
+
4468
+ ```ts
4469
+ createMcpHandler({
4470
+ surfaces: {
4471
+ admin: { services: allServices, runtimeTools: [preview] },
4472
+ member: { services: memberServices, runtimeTools: [preview] },
4473
+ },
4474
+ selectSurface: (auth) => auth.isAdmin ? 'admin' : 'member',
4475
+ })
4476
+ ```
4477
+
4478
+ Keep direct `services(auth)` / `runtimeTools(auth)` only for genuinely
4479
+ unbounded definitions; Stitchkit intentionally does not cache arbitrary auth
4480
+ values.
4481
+
4122
4482
  Contract success bodies are now determined by the presence of `output`, not by
4123
4483
  the handler's runtime value. A nullable output returns JSON `null` with status
4124
4484
  `200`; `undefined` with a declared output and non-null data without an output
@@ -4222,7 +4582,7 @@ const preview = defineRuntimeTool({
4222
4582
  }),
4223
4583
  },
4224
4584
  })
4225
- nativeTools: ({ registerTool }) => registerTool(preview)
4585
+ runtimeTools: [preview]
4226
4586
  ```
4227
4587
 
4228
4588
  The removed `NativeMcp*` types have no aliases. Use `RuntimeToolDefinition`,
@@ -4421,7 +4781,7 @@ name derivation, diff them mechanically:
4421
4781
 
4422
4782
  ```ts
4423
4783
  import { listToolNames } from 'stitchkit/tools'
4424
- console.log(JSON.stringify(listToolNames(services), null, 2))
4784
+ console.log(JSON.stringify(listToolNames({ services, runtimeTools }), null, 2))
4425
4785
  ```
4426
4786
 
4427
4787
  `listToolNames` never throws on an illegal name — that is deliberate, so it can
@@ -4529,6 +4889,11 @@ from the root `stitchkit`.
4529
4889
  |--------|------|---------|
4530
4890
  | `defineContract` | function | declare a contract — [guide](../guide/contracts.md#definecontract) |
4531
4891
  | `createContractFactory` | function | a `defineContract` with a required allowed scope that retains each concrete literal — [guide](../guide/contracts.md#scope) |
4892
+ | `ContractFactoryConfig` | _type_ | optional scoped-factory policy, including explicit tool exposure |
4893
+ | `ContractFactoryToolExposure` | _type_ | `'explicit'` — omitted endpoint exposure materializes as HTTP-only |
4894
+ | `ExplicitScopedDefineContract` | _type_ | scoped factory authoring with explicit tool opt-in |
4895
+ | `ExplicitToolExposureEndpoints` | _type_ | endpoint map after missing exposure is materialized as `['HTTP']` |
4896
+ | `ScopedContractDef` | _type_ | a factory-defined contract whose `meta.scope` is the required concrete literal |
4532
4897
  | `ScopedDefineContract` | _type_ | the `defineContract` `createContractFactory` returns |
4533
4898
  | `ALL_TRANSPORTS` | constant | `['HTTP', 'MCP', 'AGENT', 'CLI']` |
4534
4899
  | `ContractDef` | _type_ | a defined contract |
@@ -4570,9 +4935,17 @@ from the root `stitchkit`.
4570
4935
  | `conflict` | function | throw `409 CONFLICT` |
4571
4936
  | `rateLimited` | function | throw `429 RATE_LIMITED` |
4572
4937
  | `appError` | function | throw an `AppError` for any code |
4573
- | `defineErrors` | function | declare domain error codes → typed throwers + a code table — [guide](../guide/auth-and-errors.md#domain-errors--defineerrors) |
4574
- | `DefinedErrors` | _type_ | the `{ errors, codes, isCode }` handle `defineErrors` returns |
4575
- | `ErrorThrower` | _type_ | one `defineErrors` thrower `(message?, details?, hint?) => never` |
4938
+ | `defineErrors` | function | declare immutable domain error definitions → typed `AppError` constructors, codes and schemas — [guide](../guide/auth-and-errors.md#domain-errors--defineerrors) |
4939
+ | `DefinedErrors` | _type_ | the `{ errors, codes, definitions, isCode }` handle `defineErrors` returns |
4940
+ | `DefinedAppError` | _type_ | literal-code error instance with schema-refined details |
4941
+ | `ErrorDefinition` | _type_ | `{ status, details? }` definition for one domain code |
4942
+ | `ErrorDefinitions` | _type_ | string-keyed domain error definition registry |
4943
+ | `ErrorDetailsSchema` | _type_ | required or optional Zod object accepted for structured details |
4944
+ | `ErrorDetailsOutput` | _type_ | parsed details inferred from one definition |
4945
+ | `ErrorFactoryArguments` | _type_ | options tuple with forbidden/required/optional details inferred per code |
4946
+ | `ErrorFactory` | _type_ | one typed `AppError` constructor |
4947
+ | `ErrorFactories` | _type_ | mapped constructor registry derived from all definitions |
4948
+ | `FrozenErrorDefinitions` | _type_ | read-only definition registry returned to consumers |
4576
4949
  | `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) |
4577
4950
  | `StitchErrorCode` | _type_ | a code stitchkit itself emits (`keyof STITCH_ERROR_STATUS`) |
4578
4951
  | `isStitchErrorCode` | function | type guard — is a code one of stitchkit's own? |
@@ -4640,8 +5013,8 @@ Also re-exports the error helpers from `stitchkit/contract`.
4640
5013
  | Export | Kind | Summary |
4641
5014
  |--------|------|---------|
4642
5015
  | `createAuthHook` | function | a scope-enforcing `beforeHandle` hook — [guide](../guide/auth-and-errors.md#createauthhook) |
4643
- | `createErrorHook` | function | an `onError` hook from a code map + envelope renderer — [guide](../guide/auth-and-errors.md#createerrorhook) |
4644
- | `ErrorHookConfig` | _type_ | config for `createErrorHook` |
5016
+ | `createErrorHook` | function | an async-capable, endpoint-aware `onError` hook from a code map + envelope renderer — [guide](../guide/auth-and-errors.md#createerrorhook) |
5017
+ | `ErrorHookConfig` | _type_ | async observer/renderer config for `createErrorHook` |
4645
5018
  | `ResolvedError` | _type_ | the normalised error handed to `createErrorHook`'s `render` |
4646
5019
  | `createBearerResolver` | function | a bearer-token identity resolver |
4647
5020
  | `signJwt` | function | sign an HS256 JWT |
@@ -4731,14 +5104,18 @@ Server-only. The audit layer one level above the raw hooks — W3C trace context
4731
5104
  an `AsyncLocalStorage` request context, payload sanitisation and a normalised
4732
5105
  audit event. See the [Observability guide](../guide/observability.md).
4733
5106
 
4734
- ### Audit
5107
+ ### Events
4735
5108
 
4736
5109
  | Export | Kind | Summary |
4737
5110
  |--------|------|---------|
4738
- | `createAuditHook` | function | wire both surfaces into one sink — [guide](../guide/observability.md#createaudithook) |
5111
+ | `createObservability` | function | configure framework-owned request completion and canonical tool event sinks — [guide](../guide/observability.md#createobservability) |
4739
5112
  | `RequestEvent` | _type_ | the normalised audit event handed to the sink |
4740
- | `AuditConfig` | _type_ | config for `createAuditHook` |
4741
- | `AuditHook` | _type_ | the `{ http, toolCall }` the hook returns |
5113
+ | `ObservabilityConfig` | _type_ | independent request and tool sink configuration |
5114
+ | `Observability` | _type_ | the `{ request?, toolCall }` wiring result |
5115
+ | `RequestEventSinkConfig` | _type_ | `write`, `filter` and sanitisation for one event surface |
5116
+ | `RequestObservabilityConfig` | _type_ | request sink plus opt-in payload capture |
5117
+ | `HttpRequestCompletion` | _type_ | the single framework-owned HTTP outcome projected to logging and request events |
5118
+ | `HttpRequestObserver` | _type_ | server-facing projection consumed by `HandlerConfig.observability` |
4742
5119
 
4743
5120
  ### Request context
4744
5121
 
@@ -4790,31 +5167,44 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
4790
5167
  |--------|------|---------|
4791
5168
  | `createMcpHandler` | function | a complete Streamable-HTTP MCP server — [guide](../guide/mcp-and-agents.md#mcp--createmcphandler) |
4792
5169
  | `createStdioMcpServer` | function | a complete stdio MCP server — [guide](../guide/mcp-and-agents.md#mcp-over-stdio--createstdiomcpserver) |
4793
- | `buildMcpServer` | function | build an `McpServer` from contracts — the transport-neutral core |
5170
+ | `buildMcpServer` | function | build an `McpServer` from contract/runtime surfaces — the transport-neutral core |
4794
5171
  | `mountMcp` | function | add contract tools to an existing `McpServer` — [guide](../guide/mcp-and-agents.md#mountmcp) |
4795
5172
  | `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
4796
5173
  | `mountAgent` | function | a Vercel AI SDK `ToolSet` from a service — [guide](../guide/mcp-and-agents.md#ai-agents--mountagent) |
4797
5174
  | `defineRuntimeTool` | function | define one validated pathless operation for MCP, Agent or both — [guide](../guide/mcp-and-agents.md#pathless-runtime-tools-and-multimodal-results) |
5175
+ | `createRuntimeToolFactory` | function | bind shared identity and Zod-validated per-call context for runtime tools — [guide](../guide/mcp-and-agents.md#pathless-runtime-tools-and-multimodal-results) |
4798
5176
  | `createToolInvoker` | function | compile an exposure-aware in-process dispatcher over the canonical tool runner — [guide](../guide/mcp-and-agents.md#in-process-calls--createtoolinvoker) |
4799
5177
  | `createCli` | function | a command-line program from contracts — [guide](../guide/cli.md) (also on `stitchkit/cli`) |
4800
5178
  | `createToolkit` | function | context-typed tool mounts — [guide](../guide/cli.md#typed-context) |
4801
5179
  | `mountViewFile` | function | a native multimodal "view file" MCP tool |
4802
5180
  | `resolveMedia` | function | resolve a media reference for a tool result |
4803
5181
  | `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)) |
4804
- | `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) |
5182
+ | `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) |
4805
5183
  | `McpHandlerConfig` | _type_ | config for `createMcpHandler` |
5184
+ | `McpHttpConfig` | _type_ | HTTP auth, protected-resource and session options composed into `McpHandlerConfig` |
4806
5185
  | `McpSessionMode` | _type_ | `'stateless' \| 'stateful'`; HTTP defaults to request-isolated stateless mode |
4807
5186
  | `StdioMcpServerConfig` | _type_ | config for `createStdioMcpServer` |
4808
5187
  | `McpServerBuildConfig` | _type_ | shared config for `buildMcpServer` |
5188
+ | `McpServerSharedConfig` | _type_ | transport-neutral options shared by direct and finite surface configs |
5189
+ | `DirectMcpSurfaceConfig` | _type_ | static or identity-dynamic `services` / `runtimeTools` source |
5190
+ | `FiniteMcpSurfaceConfig` | _type_ | bounded `surfaces` registry plus typed selector |
5191
+ | `McpSurfaceDefinition` | _type_ | one immutable `{ services, runtimeTools }` MCP surface |
5192
+ | `McpSurfaceRegistry` | _type_ | finite keyed surface registry for eager preparation |
5193
+ | `StdioAuthConfig` | _type_ | startup identity composed into `StdioMcpServerConfig` |
4809
5194
  | `ImplementRemoteOptions` | _type_ | options for `implementRemote` |
4810
5195
  | `McpMountConfig` | _type_ | config for `mountMcp` |
4811
5196
  | `McpSchemaValidationConfig` | _type_ | shared `{ policy, requireTypedProperties, allowUntyped, requirePortableFormats, allowFormats }` profile |
4812
5197
  | `ValidateMcpSchemasConfig` | _type_ | standalone validation profile plus `services`, `extend`, flattening and logger |
4813
- | `NativeMcpRegistrar` | _type_ | protected `registerTool` plus explicit unprotected `rawServer` access |
4814
5198
  | `RuntimeToolDefinition` | _type_ | transport-neutral pathless operation with identity, schemas, handler and optional presenters |
4815
5199
  | `RuntimeToolDefinitionBase` | _type_ | common name, identity, input, exposure and MCP metadata fields |
4816
5200
  | `RuntimeToolDefinitionWithOutput` | _type_ | runtime definition whose handler and presenters share a validated output type |
4817
5201
  | `RuntimeToolDefinitionWithoutOutput` | _type_ | runtime definition with a void handler and no presentation callbacks |
5202
+ | `RuntimeToolFactory` | _type_ | identity/context-bound runtime-tool definition factory |
5203
+ | `RuntimeToolFactoryConfig` | _type_ | factory service identity and context schema |
5204
+ | `RuntimeToolFactoryDefinitionWithOutput` | _type_ | factory-authored runtime tool with a validated output schema |
5205
+ | `RuntimeToolFactoryDefinitionWithoutOutput` | _type_ | factory-authored void runtime tool without presenters |
5206
+ | `RuntimeToolFactoryHandlerContext` | _type_ | parsed factory context plus parsed tool input |
5207
+ | `RuntimeToolFactoryIdentityFields` | _type_ | per-tool action, semantic method and optional identity metadata |
4818
5208
  | `RuntimeToolIdentity` | _type_ | `{ serviceName, action, scope?, method, meta? }` for runtime lifecycle/audit |
4819
5209
  | `RuntimeToolHandlerContext` | _type_ | runtime context with the definition's parsed input |
4820
5210
  | `RuntimeToolOutput` | _type_ | output inferred from a runtime tool's optional Zod schema |
@@ -4850,7 +5240,7 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
4850
5240
  | `findNonPortableFormats` | function | deep finder for formats outside the portable MCP/AJV baseline |
4851
5241
  | `NonPortableFormat` | _type_ | one `{ path, format }` portability finding |
4852
5242
  | `PORTABLE_JSON_SCHEMA_FORMATS` | constant | portable-format baseline used by MCP validation |
4853
- | `ToolNameEntry` | _type_ | one `listToolNames` row — `{ name, service, method, transports }` |
5243
+ | `ToolNameEntry` | _type_ | one `listToolNames` row — `{ kind, name, service, method, transports }` |
4854
5244
  | `IncompatibleSchemaPolicy` | _type_ | `'throw' \| 'skip' \| 'warn'` |
4855
5245
  | `McpMediaContent` | _type_ | a multimodal MCP content item |
4856
5246
 
@@ -4909,14 +5299,17 @@ Advanced building blocks — the shared machinery the mounts are built on.
4909
5299
  |--------|------|---------|
4910
5300
  | `collectTools` | function | resolve a service's methods to mountable tools (the shared resolver) |
4911
5301
  | `createToolLogger` | function | a ready `afterToolCall` that logs every tool call — [guide](../guide/mcp-and-agents.md#logging-tool-calls--createtoollogger) |
4912
- | `summarizeTransports` | function | per-transport operation counts for a boot-time summary |
4913
- | `buildToolManifest` | function | a searchable `{ name, description, inputSchema }` manifest for a `tool_search` tool |
5302
+ | `summarizeTransports` | function | mixed contract/runtime operation counts and per-source breakdown for a boot-time summary |
5303
+ | `buildToolManifest` | function | transport-aware searchable `{ name, description, inputSchema }` rows from a mixed surface |
4914
5304
  | `ToolLoggerConfig` | _type_ | config for `createToolLogger` |
4915
5305
  | `ToolCallRecord` | _type_ | the structured record `createToolLogger` passes to `onRecord` |
4916
- | `TransportSummary` | _type_ | the result of `summarizeTransports` |
5306
+ | `TransportSummary` | _type_ | `{ contractServices, runtimeTools, totals, sources }` from `summarizeTransports` |
4917
5307
  | `TransportCounts` | _type_ | per-transport counts (`{ HTTP, MCP, AGENT, CLI }`) |
5308
+ | `ToolSurfaceDefinition` | _type_ | shared object-shaped `{ services?, runtimeTools? }` introspection surface |
5309
+ | `ToolSurfaceTransport` | _type_ | tool collector transport: `'MCP' \| 'AGENT' \| 'CLI'` |
5310
+ | `ToolManifestConfig` | _type_ | mixed surface plus required model-facing `transport` and presentation options |
4918
5311
  | `coerceJsonArgs` | function | coerce JSON-stringified array/object tool arguments |
4919
- | `flattenToolJsonSchema` | function | project structurally identifiable discriminated unions in a JSON Schema document into conservative object joins; never executes validation |
5312
+ | `flattenToolJsonSchema` | function | project structurally identifiable discriminated unions into conservative object joins; divergent fields retain every provable base kind in a deterministic `type` array, and the projection never executes validation |
4920
5313
  | `ToolPresentationSchema` | _type_ | immutable model-facing JSON Schema document shared by tool transports |
4921
5314
  | `MountableTool` | _type_ | one operation with separate executable CLI argument schema and model-facing presentation schema |
4922
5315
  | `ToolManifestEntry` | _type_ | one `buildToolManifest` row |