stitchkit 0.42.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.
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
@@ -1978,8 +2000,14 @@ as one thing. Where they agree, that is what you get; where they disagree — a
1978
2000
  `.refine()` only one of them carries, two different bounds on the same number, an
1979
2001
  enum against a free string — the *constraint* is dropped and the **type** is not.
1980
2002
  A field that is a number in every variant is advertised as a number, not as a
1981
- bare description. Only genuinely different kinds (a string in one variant, a
1982
- 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)
1983
2011
 
1984
2012
  It is **deep** because the projection walks the generated JSON Schema document,
1985
2013
  including objects, arrays, tuples and schema-definition nodes. Structurally
@@ -1987,6 +2015,10 @@ identifiable discriminated object unions are flattened wherever they occur.
1987
2015
  Plain unions and unions hidden behind unresolved external references remain
1988
2016
  unions because Stitchkit cannot soundly invent a discriminator.
1989
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
+
1990
2022
  The flattened form is **lossy but never executable**. Per-variant refinements
1991
2023
  and incompatible constraints are widened in the presentation document; the
1992
2024
  original Zod contract enforces them exactly once inside `executeToolMethod`.
@@ -2287,6 +2319,33 @@ const agentTools = mountAgent([service], {
2287
2319
  })
2288
2320
  ```
2289
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
+
2290
2349
  `transports` defaults to `['MCP', 'AGENT']`; set an explicit subset when an
2291
2350
  operation belongs on only one surface. The configured identity becomes the
2292
2351
  hook/lifecycle `OperationIdentity` and the tool `RequestEvent`
@@ -3443,10 +3502,10 @@ stitchkit answers this at two levels.
3443
3502
  - **The raw hooks** — `LifecycleHooks` and `ToolCallHooks`. Every request and
3444
3503
  every tool call passes through a point you can observe. The lowest level;
3445
3504
  always available. [Jump ↓](#the-raw-hooks)
3446
- - **`stitchkit/observability`** — the audit layer built on those hooks: W3C
3447
- trace context, an `AsyncLocalStorage` request context, payload sanitisation,
3448
- and `createAuditHook` to wire it all into one sink. Your logging becomes a
3449
- 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)
3450
3509
 
3451
3510
  stitchkit still ships no logger and no audit store — those are the app's choice.
3452
3511
  What it ships is the machinery that turns a completed call into a clean,
@@ -3455,52 +3514,55 @@ normalised record.
3455
3514
  ## The observability module
3456
3515
 
3457
3516
  `stitchkit/observability` is server-only. It has three parts — a trace context,
3458
- a request context, and the audit hook — and you usually touch only the last.
3459
-
3460
- ### createAuditHook
3461
-
3462
- `createAuditHook` is the whole module in one call. You give it a `write` sink;
3463
- it gives you back a wrapper for each surface. Every completed call — HTTP
3464
- request, MCP tool call, agent tool call is normalised into one `RequestEvent`
3465
- and handed to `write`.
3466
-
3467
- ```ts
3468
- import { createAuditHook } from 'stitchkit/observability'
3469
-
3470
- export const audit = createAuditHook({
3471
- // The only thing the app supplies — persist one row.
3472
- write: (event) => {
3473
- db.auditLog.create({ data: {
3474
- traceId: event.traceId,
3475
- source: event.source, // 'http' | 'mcp' | 'agent'
3476
- method: event.method, // verb, or 'TOOL'
3477
- path: event.path,
3478
- ok: event.ok,
3479
- statusCode: event.statusCode,
3480
- durationMs: event.durationMs,
3481
- userId: event.userId,
3482
- payload: event.payload, // already sanitised
3483
- }})
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',
3484
3550
  },
3485
- // Optional — keep only the events you care about.
3486
- filter: (event) => event.source !== 'http' || event.method !== 'GET',
3487
3551
  })
3488
3552
  ```
3489
3553
 
3490
- It returns an [`AuditHook`](#audithook) `{ http, toolCall }`:
3554
+ Wire each projection where its completion is owned:
3491
3555
 
3492
3556
  ```ts
3493
- // HTTP — wrap the fetch handler, inside wrapInRequestContext.
3494
- Bun.serve({ fetch: wrapInRequestContext(audit.http(handler)) })
3557
+ createServer({ services, observability: observability.request })
3495
3558
 
3496
- // MCP & agents pass as the tool-call hooks.
3497
- createMcpHandler({ /* … */ hooks: audit.toolCall })
3498
- mountAgent(service, { hooks: audit.toolCall })
3559
+ createMcpHandler({ /* */ hooks: observability.toolCall })
3560
+ mountAgent(service, { hooks: observability.toolCall })
3499
3561
  ```
3500
3562
 
3501
- One `createAuditHook`, one sink, every surface. The sink runs
3502
- fire-and-forget and its errors are swallowed a slow or failing audit write
3503
- 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.
3504
3566
 
3505
3567
  ### RequestEvent
3506
3568
 
@@ -3519,32 +3581,26 @@ queryable across all three:
3519
3581
  | `ok` / `statusCode` | outcome — real HTTP status, or `200`/`400` for a tool |
3520
3582
  | `durationMs` / `startedAt` | timing |
3521
3583
  | `errorCode` / `errorMessage` / `errorDetail` | failures only — `errorDetail` carries the structure the message flattens (e.g. Zod issues) |
3522
- | `payload` | the request body / tool arguments sanitised |
3584
+ | `payload` | sanitised tool arguments; HTTP is `null` unless request `includePayload` is enabled |
3523
3585
  | `resultSize` / `responseBytes` | result item count + serialised size |
3524
3586
  | `userId` / `ipAddress` / `userAgent` | identity |
3525
3587
 
3526
3588
  ### Request context
3527
3589
 
3528
- `createAuditHook`'s `http` wrapper reads a request context — trace ids, timing,
3529
- identity from `AsyncLocalStorage`. `wrapInRequestContext` establishes it, and
3530
- 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:
3531
3594
 
3532
3595
  ```ts
3533
- import { getTraceId, wrapInRequestContext } from 'stitchkit/observability'
3534
-
3535
- Bun.serve({
3536
- fetch: wrapInRequestContext(audit.http(handler)),
3537
- })
3596
+ createServer({ services, logging, observability: observability.request })
3538
3597
  ```
3539
3598
 
3540
- `createServer` and `serveNode` build their own `fetch`, so compose through
3541
- **`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:
3542
3601
 
3543
3602
  ```ts
3544
- createServer({
3545
- services,
3546
- wrapFetch: (fetch) => wrapInRequestContext(audit.http(fetch)),
3547
- })
3603
+ Bun.serve({ fetch: wrapInRequestContext(customFetch) })
3548
3604
  ```
3549
3605
 
3550
3606
  Some fields are filled in late. Set them from the hooks that know:
@@ -3679,8 +3735,8 @@ every tool call underneath it. With no inbound header a fresh root trace is
3679
3735
  minted. Each tool call opens a [`childSpan`](#trace-context) of the request it
3680
3736
  runs in.
3681
3737
 
3682
- You rarely call the trace functions directly — `wrapInRequestContext` and
3683
- `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`,
3684
3740
  `parseTraceparent`, `formatTraceparent`, `childSpan`) for when you need to
3685
3741
  propagate a `traceparent` onward to another service.
3686
3742
 
@@ -3715,12 +3771,19 @@ A payload goes into an audit row only after `sanitizePayload`:
3715
3771
  never the bytes;
3716
3772
  - the result is **capped** — anything over the byte limit becomes a preview.
3717
3773
 
3718
- `createAuditHook` runs it on every event; tune it through `sanitize`:
3774
+ `createObservability` runs it on every emitted event; tune each sink separately:
3719
3775
 
3720
3776
  ```ts
3721
- createAuditHook({
3722
- write,
3723
- 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
+ },
3724
3787
  })
3725
3788
  ```
3726
3789
 
@@ -3729,7 +3792,7 @@ need to sanitise something outside the audit path.
3729
3792
 
3730
3793
  ## The raw hooks
3731
3794
 
3732
- `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
3733
3796
  metric, a custom log line, anything that is not a full audit row.
3734
3797
 
3735
3798
  | Surface | Hook | Fires |
@@ -3803,7 +3866,7 @@ observe. (This is also why it lives on `ToolCallHooks` rather than being an
3803
3866
  object must stay assignable to `ToolLifecycle`.)
3804
3867
 
3805
3868
  **Do not reach for `setRequestError` here.** It writes to the *request* context,
3806
- 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
3807
3870
  `errorCode` / `errorMessage` / `errorDetail` from the `ToolResult`, and only
3808
3871
  identity and `dimensions` from the context. Calling it in `onToolError` would
3809
3872
  leave the tool row exactly as scrubbed as before. It is right for the **HTTP**
@@ -3829,7 +3892,7 @@ validation failure or a `beforeToolCall` rejection leaves it `undefined`, becaus
3829
3892
  neither ever had a raw value to lose. Consumers destructure only the fields they
3830
3893
  use; future optional fields do not change callback arity.
3831
3894
 
3832
- `createAuditHook` uses it already. Where the envelope was scrubbed to
3895
+ `createObservability({ tools })` uses it already. Where the envelope was scrubbed to
3833
3896
  `INTERNAL_SERVER_ERROR`, the row's `errorMessage` becomes the real message
3834
3897
  instead of the placeholder; a truthful envelope (a thrown `AppError`, a
3835
3898
  `ZodError`) is left alone, `errorCode` and `errorDetail` are untouched, and the
@@ -3842,7 +3905,7 @@ sink of your own (a tracker, a stack, an alert), `afterToolCall` for the record.
3842
3905
 
3843
3906
  ### Keying a row on (service, action)
3844
3907
 
3845
- `createAuditHook` already keys every event by **service** and **action**
3908
+ Built-in observability keys every event by **service** and **action**
3846
3909
  (`event.serviceName` / `event.action`, → ADR 0029) — reach for the raw hook only
3847
3910
  when you also need the handler **output**, which the audit wrapper never sees. For
3848
3911
  that, read the endpoint identity off the `OperationIdentity` the tool hook
@@ -3870,11 +3933,11 @@ hooks: {
3870
3933
  }
3871
3934
  ```
3872
3935
 
3873
- > **Why the HTTP audit is a wrapper, not a lifecycle hook.** `LifecycleHooks`
3874
- > has a single `onError` an audit built on it would compete with the app's own
3875
- > error handler. `createAuditHook`'s `http` wrapper sees the final `Response`
3876
- > instead, success and error alike, and never contends for a hook. The raw
3877
- > 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.
3878
3941
 
3879
3942
  Keep any sink **asynchronous and self-contained**: a slow or failing write must
3880
3943
  never block or break the request. Swallow the sink's own errors.
@@ -4023,7 +4086,8 @@ Notes for a Node host:
4023
4086
  stitchkit serves the API. A SPA front-end is built and hosted separately — a
4024
4087
  static host or CDN in production, its own dev server in development. The backend
4025
4088
  does not serve static files (`staticRoute` exists for the occasional asset, not
4026
- 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.
4027
4091
 
4028
4092
  ### MCP
4029
4093
 
@@ -4160,6 +4224,76 @@ rule read it the same way on both surfaces — one contract, every surface, no
4160
4224
  per-transport tenant plumbing.
4161
4225
 
4162
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
+
4163
4297
  ==============================================================================
4164
4298
  # Guide: Upgrading (docs/guide/upgrading.md)
4165
4299
  ==============================================================================
@@ -4228,6 +4362,32 @@ current one *up to* your target, and apply each snippet.
4228
4362
 
4229
4363
  ## Unreleased breaking migrations
4230
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
+
4231
4391
  Tool introspection now accepts one object-shaped contract/runtime surface. Stop
4232
4392
  calling the internal contract collector or merging a locally converted runtime
4233
4393
  manifest:
@@ -4729,6 +4889,11 @@ from the root `stitchkit`.
4729
4889
  |--------|------|---------|
4730
4890
  | `defineContract` | function | declare a contract — [guide](../guide/contracts.md#definecontract) |
4731
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 |
4732
4897
  | `ScopedDefineContract` | _type_ | the `defineContract` `createContractFactory` returns |
4733
4898
  | `ALL_TRANSPORTS` | constant | `['HTTP', 'MCP', 'AGENT', 'CLI']` |
4734
4899
  | `ContractDef` | _type_ | a defined contract |
@@ -4939,14 +5104,18 @@ Server-only. The audit layer one level above the raw hooks — W3C trace context
4939
5104
  an `AsyncLocalStorage` request context, payload sanitisation and a normalised
4940
5105
  audit event. See the [Observability guide](../guide/observability.md).
4941
5106
 
4942
- ### Audit
5107
+ ### Events
4943
5108
 
4944
5109
  | Export | Kind | Summary |
4945
5110
  |--------|------|---------|
4946
- | `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) |
4947
5112
  | `RequestEvent` | _type_ | the normalised audit event handed to the sink |
4948
- | `AuditConfig` | _type_ | config for `createAuditHook` |
4949
- | `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` |
4950
5119
 
4951
5120
  ### Request context
4952
5121
 
@@ -5003,6 +5172,7 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
5003
5172
  | `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
5004
5173
  | `mountAgent` | function | a Vercel AI SDK `ToolSet` from a service — [guide](../guide/mcp-and-agents.md#ai-agents--mountagent) |
5005
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) |
5006
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) |
5007
5177
  | `createCli` | function | a command-line program from contracts — [guide](../guide/cli.md) (also on `stitchkit/cli`) |
5008
5178
  | `createToolkit` | function | context-typed tool mounts — [guide](../guide/cli.md#typed-context) |
@@ -5029,6 +5199,12 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
5029
5199
  | `RuntimeToolDefinitionBase` | _type_ | common name, identity, input, exposure and MCP metadata fields |
5030
5200
  | `RuntimeToolDefinitionWithOutput` | _type_ | runtime definition whose handler and presenters share a validated output type |
5031
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 |
5032
5208
  | `RuntimeToolIdentity` | _type_ | `{ serviceName, action, scope?, method, meta? }` for runtime lifecycle/audit |
5033
5209
  | `RuntimeToolHandlerContext` | _type_ | runtime context with the definition's parsed input |
5034
5210
  | `RuntimeToolOutput` | _type_ | output inferred from a runtime tool's optional Zod schema |
@@ -5133,7 +5309,7 @@ Advanced building blocks — the shared machinery the mounts are built on.
5133
5309
  | `ToolSurfaceTransport` | _type_ | tool collector transport: `'MCP' \| 'AGENT' \| 'CLI'` |
5134
5310
  | `ToolManifestConfig` | _type_ | mixed surface plus required model-facing `transport` and presentation options |
5135
5311
  | `coerceJsonArgs` | function | coerce JSON-stringified array/object tool arguments |
5136
- | `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 |
5137
5313
  | `ToolPresentationSchema` | _type_ | immutable model-facing JSON Schema document shared by tool transports |
5138
5314
  | `MountableTool` | _type_ | one operation with separate executable CLI argument schema and model-facing presentation schema |
5139
5315
  | `ToolManifestEntry` | _type_ | one `buildToolManifest` row |
package/llms.txt CHANGED
@@ -16,6 +16,7 @@ Build with stitchkit: define a contract once, then `implement` it and serve it (
16
16
  - [Observability](https://github.com/max-listov/stitchkit/blob/master/docs/guide/observability.md): request and tool-call logging via hooks, W3C trace context, createAuditHook
17
17
  - [Testing & deployment](https://github.com/max-listov/stitchkit/blob/master/docs/guide/testing-and-deployment.md): in-process testing; deploying on Bun and on Node (serveNode)
18
18
  - [Multi-tenant](https://github.com/max-listov/stitchkit/blob/master/docs/guide/multi-tenant.md): a /tenants/:id/… scenario end-to-end — scopePrefixes, scoped client, extend
19
+ - [Frontend integrations](https://github.com/max-listov/stitchkit/blob/master/docs/guide/frontend-integrations.md): React Router resource routes and a separate Vite development proxy
19
20
  - [Upgrading](https://github.com/max-listov/stitchkit/blob/master/docs/guide/upgrading.md): moving a project across stitchkit versions; how breaking changes are marked
20
21
 
21
22
  ## Reference
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.42.0",
3
+ "version": "0.43.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",
@@ -160,7 +160,7 @@
160
160
  "@types/json-schema": "^7.0.15",
161
161
  "@types/react": "^19.2.18",
162
162
  "@typescript/typescript6": "^6.0.2",
163
- "ai": "^7.0.56",
163
+ "ai": "^7.0.58",
164
164
  "react": "^19.2.8",
165
165
  "react-query-kit": "^3.3.4",
166
166
  "socket.io": "^4.8.3",