stitchkit 0.36.0 → 0.37.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 (81) hide show
  1. package/README.md +4 -2
  2. package/dist/browser/http.d.ts +2 -2
  3. package/dist/browser/http.d.ts.map +1 -1
  4. package/dist/cli.js +3 -3
  5. package/dist/contract/define.d.ts +28 -2
  6. package/dist/contract/define.d.ts.map +1 -1
  7. package/dist/contract/errors.d.ts.map +1 -1
  8. package/dist/contract/index.js +1 -1
  9. package/dist/{index-dvrn81q4.js → index-4pvtq6h2.js} +83 -34
  10. package/dist/{index-0ed3bx43.js → index-frfyw9fa.js} +2 -2
  11. package/dist/{index-pmftwk2a.js → index-g3jrbd0z.js} +26 -0
  12. package/dist/{index-kgga1n8d.js → index-s6yhmg1k.js} +563 -342
  13. package/dist/{index-bx49hskg.js → index-xq45akyd.js} +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/node.d.ts +1 -1
  16. package/dist/node.d.ts.map +1 -1
  17. package/dist/node.js +7 -3
  18. package/dist/observability/index.js +3 -3
  19. package/dist/server/bun.d.ts +32 -0
  20. package/dist/server/bun.d.ts.map +1 -0
  21. package/dist/server/context.d.ts +1 -1
  22. package/dist/server/context.d.ts.map +1 -1
  23. package/dist/server/create.d.ts +2 -3
  24. package/dist/server/create.d.ts.map +1 -1
  25. package/dist/server/implement.d.ts.map +1 -1
  26. package/dist/server/index.d.ts +3 -2
  27. package/dist/server/index.d.ts.map +1 -1
  28. package/dist/server/index.js +25 -5
  29. package/dist/server/logger.d.ts +5 -5
  30. package/dist/server/logger.d.ts.map +1 -1
  31. package/dist/server/logging.d.ts +4 -1
  32. package/dist/server/logging.d.ts.map +1 -1
  33. package/dist/server/middleware/auth.d.ts +2 -2
  34. package/dist/server/middleware/auth.d.ts.map +1 -1
  35. package/dist/server/request-body.d.ts +4 -0
  36. package/dist/server/request-body.d.ts.map +1 -0
  37. package/dist/server/router.d.ts +3 -3
  38. package/dist/server/router.d.ts.map +1 -1
  39. package/dist/server/socket-io-config.d.ts +20 -0
  40. package/dist/server/socket-io-config.d.ts.map +1 -0
  41. package/dist/server/socket-io-node.d.ts +14 -0
  42. package/dist/server/socket-io-node.d.ts.map +1 -0
  43. package/dist/server/socket-io.d.ts +5 -32
  44. package/dist/server/socket-io.d.ts.map +1 -1
  45. package/dist/server/types.d.ts +54 -49
  46. package/dist/server/types.d.ts.map +1 -1
  47. package/dist/tools/agent.d.ts.map +1 -1
  48. package/dist/tools/execute.d.ts +60 -15
  49. package/dist/tools/execute.d.ts.map +1 -1
  50. package/dist/tools/flatten-join.d.ts +8 -0
  51. package/dist/tools/flatten-join.d.ts.map +1 -0
  52. package/dist/tools/flatten.d.ts +7 -44
  53. package/dist/tools/flatten.d.ts.map +1 -1
  54. package/dist/tools/json-schema.d.ts +1 -1
  55. package/dist/tools/json-schema.d.ts.map +1 -1
  56. package/dist/tools/manifest.d.ts.map +1 -1
  57. package/dist/tools/mcp-handler.d.ts +7 -13
  58. package/dist/tools/mcp-handler.d.ts.map +1 -1
  59. package/dist/tools/mcp.d.ts +61 -24
  60. package/dist/tools/mcp.d.ts.map +1 -1
  61. package/dist/tools/mount.d.ts +9 -6
  62. package/dist/tools/mount.d.ts.map +1 -1
  63. package/dist/tools/names.d.ts +1 -1
  64. package/dist/tools/native-mcp.d.ts +57 -0
  65. package/dist/tools/native-mcp.d.ts.map +1 -0
  66. package/dist/tools/portable-formats.d.ts +15 -0
  67. package/dist/tools/portable-formats.d.ts.map +1 -0
  68. package/dist/tools/presentation.d.ts +15 -0
  69. package/dist/tools/presentation.d.ts.map +1 -0
  70. package/dist/tools/remote.d.ts.map +1 -1
  71. package/dist/tools/schema.d.ts +3 -8
  72. package/dist/tools/schema.d.ts.map +1 -1
  73. package/dist/tools/tool-logger.d.ts.map +1 -1
  74. package/dist/tools/view-file.d.ts +5 -3
  75. package/dist/tools/view-file.d.ts.map +1 -1
  76. package/dist/tools.d.ts +7 -4
  77. package/dist/tools.d.ts.map +1 -1
  78. package/dist/tools.js +357 -95
  79. package/llms-full.txt +374 -77
  80. package/package.json +14 -11
  81. /package/dist/{index-17rdjw68.js → index-e497hxcy.js} +0 -0
package/llms-full.txt CHANGED
@@ -239,10 +239,12 @@ export const users = defineContract({ prefix: 'users' }, {
239
239
  | `expose` | no | which transports carry this endpoint — see [below](#transports) |
240
240
  | `toolName` | no | explicit MCP / agent tool name (default: a verb-aware derivation, see below — not a literal `prefix_key`) |
241
241
  | `multipart` | no | field name of a file upload — see [below](#file-uploads) |
242
+ | `maxJsonBodyBytes` | no | per-route JSON body ceiling; overrides the server default |
242
243
  | `timeout` | no | per-endpoint client timeout in ms, for slow endpoints |
243
244
  | `idempotent` | no | safe to call twice with the same input (like `PUT`/`DELETE`); a retrying transport reads it — see [Realtime](./realtime.md#bring-your-own-transport) |
244
245
  | `meta` | no | opaque app metadata — read in hooks / on tool mounts, never in OpenAPI ([below](#endpoint-metadata-meta)) |
245
246
  | `rawResponse` | no | the handler returns the `Response` itself — a download, a file, an SSE stream. HTTP-only, never a tool, no `output`. See [Raw-response endpoints](./server.md#raw-response-endpoints) |
247
+ | `rawBody` | no | retain original JSON text for a signed HTTP webhook. See [Signed JSON webhooks](./server.md#signed-json-webhooks) |
246
248
  | `contentType` | no | documented response media type of a `rawResponse` endpoint (OpenAPI only) |
247
249
 
248
250
  ## `params` vs `input` vs `output`
@@ -617,6 +619,7 @@ server. See [Testing & deployment](./testing-and-deployment.md).
617
619
  | `scopePrefixes` | `scope → path prefix` map — mount `services` by `service.scope` (see below) |
618
620
  | `rawRoutes` | non-contract routes (see below) |
619
621
  | `maxUploadBytes` | default multipart upload cap (bytes); per-route `EndpointDef.maxUploadBytes` overrides |
622
+ | `maxJsonBodyBytes` | optional JSON body cap (bytes); per-route value overrides; unset preserves existing behaviour |
620
623
  | `port` / `hostname` | listen address — port defaults to `3000` |
621
624
  | `cors` | CORS policy — `{ origin, credentials, methods, headers, exposeHeaders }` |
622
625
  | `hooks` | lifecycle hooks (see below) |
@@ -682,10 +685,18 @@ is not involved.
682
685
  way to see what `enrich` and the request context actually put on the record;
683
686
  changing `NODE_ENV` is not needed, and neither is deploying.
684
687
 
685
- Three more things about `enrich`: it runs at close, when the request body is
686
- already consumed; framework fields (`traceId`, `status`, `path`, …) always win a
687
- key collision; and a throw in `skip` or `enrich` is swallowed — neither can fail
688
- a request.
688
+ Three more things about `enrich`:
689
+
690
+ - It runs at close, when the request body is already consumed.
691
+ - The framework owns `traceId`, `method`, `path`, `status`, `durationMs`,
692
+ `errorCode` and `ip` in both structured sinks; the built-in JSON line also
693
+ owns `ts`, `level` and `msg`. Its value wins a collision, and a discarded key
694
+ warns once per handler rather than disappearing silently.
695
+ - `errorCode` has one outcome-aware exception: enrichment may supply it for a
696
+ `4xx`/`5xx` response when the framework derived no code, such as an error
697
+ `Response` returned by a raw route. It cannot add one to a `2xx`/`3xx` or
698
+ replace a framework-derived code.
699
+ - A throw in `skip` or `enrich` is swallowed; neither can fail a request.
689
700
 
690
701
  With an observability context active, the structured line also carries `userId`,
691
702
  `serviceName`, `action` and `dimensions` for free. See
@@ -804,6 +815,40 @@ createServer({
804
815
  Hooks see `RuntimeContext` (loose types); handlers see `HandlerContext` (typed).
805
816
  That split is deliberate — see [ADR 0003](../decisions/0003-two-context-types.md).
806
817
 
818
+ ## Signed JSON webhooks
819
+
820
+ A provider signs the original JSON text, not `JSON.stringify(ctx.input)`.
821
+ Declare `rawBody: true` to retain the same decoded text the router reads while
822
+ keeping normal Zod validation:
823
+
824
+ ```ts
825
+ const webhooks = defineContract(
826
+ { prefix: 'webhooks' },
827
+ {
828
+ receive: {
829
+ method: 'POST', path: '/provider', desc: 'Receive a signed event',
830
+ rawBody: true,
831
+ maxJsonBodyBytes: 256 * 1024,
832
+ input: ProviderEventSchema,
833
+ output: z.object({ accepted: z.boolean() }),
834
+ },
835
+ },
836
+ )
837
+
838
+ receive: async (ctx) => {
839
+ const signature = ctx.req.headers.get('x-signature')
840
+ await verifyWebhookHmac(ctx.rawBody, signature) // guaranteed string
841
+ return { accepted: true }
842
+ }
843
+ ```
844
+
845
+ This endpoint is HTTP-only and cannot be multipart or exposed as a tool. The
846
+ router sets `ctx.rawBody` before JSON/Zod validation, so `onError` can inspect it
847
+ after malformed JSON or a schema failure. Endpoints without `rawBody: true` do
848
+ not retain the text. `maxJsonBodyBytes` may also be set once on `createServer` /
849
+ `createHandler`; a route value wins. Both limits are opt-in and abort an
850
+ oversized stream before it is fully buffered. → ADR 0051
851
+
807
852
  ## Raw-response endpoints
808
853
 
809
854
  An endpoint that answers with **bytes rather than data** — a PDF download, a
@@ -853,8 +898,9 @@ them — see [`cors.exposeHeaders`](#serving-files--range-requests).
853
898
  code. A raw-response *endpoint* stays in the contract: only its response is
854
899
  raw — it is still routed, gated, typed and documented like every other endpoint.
855
900
  A raw *route* is outside the contract entirely — no schemas, no auth gate, no
856
- client — which is what you want for an OAuth redirect or a webhook, and what you
857
- do not want for a download.
901
+ client — which is what you want for an OAuth redirect or a non-JSON webhook. A
902
+ signed JSON webhook can stay validated through
903
+ [`rawBody: true`](#signed-json-webhooks).
858
904
 
859
905
  ⚠️ **Delete the old raw route when you move an endpoint into the contract.** Raw
860
906
  routes are matched **first**, so a leftover one keeps serving the bytes and the
@@ -1432,9 +1478,8 @@ and after, compare.
1432
1478
  ## MCP — `createMcpHandler`
1433
1479
 
1434
1480
  `createMcpHandler` builds a complete Streamable-HTTP MCP server as a single
1435
- `Request → Response` handler. It owns the whole MCP lifecycle the SSE event
1436
- store, per-session transports, the server instances — so your app never imports
1437
- `@modelcontextprotocol/sdk` itself.
1481
+ `Request → Response` handler. It owns the SDK server and transport lifecycle, so
1482
+ your app never imports `@modelcontextprotocol/sdk` itself.
1438
1483
 
1439
1484
  ```ts
1440
1485
  import { createMcpHandler } from 'stitchkit/tools'
@@ -1465,14 +1510,34 @@ createServer({
1465
1510
  | `context` | `(auth) => {…}` — values merged into every tool handler's `ctx` |
1466
1511
  | `lifecycle` | `beforeHandle` / `afterHandle` — the tool-side auth gate (see below) |
1467
1512
  | `hooks` | tool-call observability hooks — `afterToolCall` fires on every result |
1468
- | `onIncompatibleSchema` | `'throw'` (default) · `'skip'` · `'warn'` see below |
1513
+ | `extend` | extra advertised arguments resolved into handler context |
1514
+ | `schemaValidation` | compatibility policy, typed-property guard and portable-format guard |
1469
1515
  | `logger` | a `StitchLogger` for the `'warn'` policy |
1470
- | `nativeTools` | `(server, auth) => …` — register non-contract tools directly on the `McpServer`; receives the resolved identity, but is **not** a scope gate (`lifecycle` does not run for native tools) |
1516
+ | `nativeTools` | `({ registerTool, rawServer }, auth) => …` — protected native registration plus an explicit raw SDK escape hatch |
1517
+ | `resources` | MCP Apps `ui://` resources mounted on every server |
1471
1518
  | `instructions` | a short host-facing usage hint, surfaced to MCP tool-search |
1519
+ | `coerceJsonArgs` | coerce JSON-stringified object/array arguments (default `true`) |
1520
+ | `flattenUnionInput` | advertise discriminated unions as one object (default `false`) |
1521
+ | `errorHint` | add a project-owned hint to failed tool results |
1522
+ | `onOutputStrip` | observe output keys removed by contract validation |
1523
+ | `protectedResource` | RFC 9728 metadata used by HTTP `401` responses |
1524
+ | `sessionMode` | `'stateless'` (default) or explicit `'stateful'` session/SSE continuity |
1472
1525
 
1473
1526
  `services`, `context` and `nativeTools` all receive the resolved identity, so a
1474
1527
  tenant can be shown only its own tools and every handler can read `ctx.tenantId`.
1475
1528
 
1529
+ ### Stateless by default; stateful only when required
1530
+
1531
+ The default `sessionMode: 'stateless'` creates a fresh SDK server, transport,
1532
+ resolved auth/context and runner for each HTTP request. Static contract schemas
1533
+ are still prepared once when the handler is constructed. There is no session
1534
+ map, event store, sweep timer or `Mcp-Session-Id`, so process replacement and
1535
+ load balancing cannot strand a client on an in-memory session.
1536
+
1537
+ Opt into `sessionMode: 'stateful'` only when the client needs server-initiated
1538
+ messages, cross-request progress or resumable SSE. That mode issues a server
1539
+ session id and retains the bounded session/event stores until idle expiry.
1540
+
1476
1541
  ### Guarding tools — `lifecycle`
1477
1542
 
1478
1543
  A tool call runs the same handler an HTTP request would. `lifecycle` makes it
@@ -1490,15 +1555,20 @@ Without it, a tool call bypasses the HTTP `beforeHandle` — the contract's
1490
1555
  `buildMcpServer` take `lifecycle` too.
1491
1556
 
1492
1557
  The observability `hooks` are symmetric with the HTTP side too: `beforeToolCall`
1493
- and `afterToolCall` receive the resolved **`MethodDef`** as their last argument
1558
+ and `afterToolCall` receive the resolved **`MethodDef`** as `endpoint`
1494
1559
  the tool-side twin of `afterHandle(ctx, result, endpoint)`. Read
1495
1560
  `endpoint.serviceName` / `.key` / `.meta` directly for an audit row; you do not
1496
1561
  need to rebuild a `toolName → identity` map:
1497
1562
 
1498
1563
  ```ts
1499
1564
  hooks: {
1500
- afterToolCall: (toolName, args, result, ms, ctx, endpoint) => {
1501
- audit({ service: endpoint.serviceName, action: endpoint.key, ok: result.ok, ms })
1565
+ afterToolCall: ({ result, durationMs, endpoint }) => {
1566
+ audit({
1567
+ service: endpoint.serviceName,
1568
+ action: endpoint.key,
1569
+ ok: result.ok,
1570
+ ms: durationMs,
1571
+ })
1502
1572
  },
1503
1573
  }
1504
1574
  ```
@@ -1509,10 +1579,10 @@ hooks: {
1509
1579
  > has no identity and **fails closed**. See
1510
1580
  > [Auth on the tool surface](./auth-and-errors.md#auth-on-the-tool-surface--resolvefromcontext).
1511
1581
 
1512
- ### Incompatible schemas `onIncompatibleSchema`
1582
+ ### MCP schema validation profile
1513
1583
 
1514
1584
  A contract schema that JSON Schema cannot represent (a `z.date()`, a `z.map()`)
1515
- cannot become a tool. `onIncompatibleSchema` decides what happens:
1585
+ cannot become a tool. `schemaValidation.policy` decides what happens:
1516
1586
 
1517
1587
  - `'throw'` (default) — fail the build, listing every offending tool. A static
1518
1588
  `services` array is checked when `createMcpHandler` is constructed, so a bad
@@ -1521,9 +1591,16 @@ cannot become a tool. `onIncompatibleSchema` decides what happens:
1521
1591
  - `'warn'` — log through `logger` and drop the tool.
1522
1592
  - `'skip'` — drop the tool silently.
1523
1593
 
1524
- `validateMcpSchemas(services)` runs the same check on its own — useful in a
1594
+ `validateMcpSchemas({ services })` runs the same check on its own — useful in a
1525
1595
  startup assertion or a test.
1526
1596
 
1597
+ For a static `services` array, collection, schema conversion and every enabled
1598
+ validation guard run once when the handler is created. Each HTTP request or
1599
+ stateful session still receives a fresh `McpServer`, runner, context and native
1600
+ registration over that immutable prepared surface. A `services(auth)` factory
1601
+ is deliberately prepared after resolving each identity because its tool set may
1602
+ change by tenant.
1603
+
1527
1604
  #### Is every property actually usable by a model?
1528
1605
 
1529
1606
  A tool schema is the only instruction a model gets about the shape of its
@@ -1533,17 +1610,30 @@ mount succeeds, the tool is advertised, and the model then guesses, retrying the
1533
1610
  same wrong guess because the error does not say what the right one would be.
1534
1611
 
1535
1612
  ```ts
1536
- validateMcpSchemas(services, 'throw', logger, {
1537
- flattenUnionInput: true, // mirror the live mount
1613
+ validateMcpSchemas({
1614
+ services,
1615
+ policy: 'throw',
1616
+ logger,
1617
+ extend,
1618
+ flattenUnionInput: true,
1538
1619
  requireTypedProperties: true,
1539
1620
  allowUntyped: ['docs_create.payload'], // deliberately free-form
1621
+ requirePortableFormats: true,
1622
+ allowFormats: [],
1540
1623
  })
1541
1624
  ```
1542
1625
 
1543
1626
  Off by default, because a contract may legitimately declare `z.unknown()`.
1544
1627
  `allowUntyped` takes dotted `tool.property` paths — an entry there is a decision,
1545
- anything else is a finding. Pass the **same** `extend` / `flattenUnionInput` the
1546
- mount uses, or the check vets a different document than the one advertised.
1628
+ anything else is a finding. On `createMcpHandler`, put the policy under
1629
+ `schemaValidation`; the handler supplies its real `extend` and
1630
+ `flattenUnionInput`, so the check cannot vet a different document from the one
1631
+ advertised.
1632
+
1633
+ `requirePortableFormats` rejects custom `format` values common MCP/AJV clients
1634
+ do not know, such as the `cuid2` emitted by `z.cuid2()`. Use a portable
1635
+ schema/pattern, or list it in `allowFormats` only when every client supports it.
1636
+ stitchkit never removes or rewrites the keyword.
1547
1637
 
1548
1638
  `findUntypedProperties(jsonSchema)` is the same walk, exported on its own if you
1549
1639
  want to assert on a schema you built elsewhere.
@@ -1566,25 +1656,19 @@ A field that is a number in every variant is advertised as a number, not as a
1566
1656
  bare description. Only genuinely different kinds (a string in one variant, a
1567
1657
  number in another) fall back to unconstrained. → ADR 0044
1568
1658
 
1569
- It is **deep**: unions are flattened at every depth top level, object fields,
1570
- array items, and through `optional` / `nullable` / `default` / intersection
1571
- wrappers so no `oneOf` survives anywhere (e.g. a `content.parts[]` that is an
1572
- array of a discriminated union). Schemas a transform cannot safely rebuild
1573
- (refined / piped / lazy / plain non-discriminated unions) are left as-is.
1574
-
1575
- The flattened form is **lossy but never destructive**. Lossy: per-variant
1576
- strictness and object-level refinements are not advertised the original schemas
1577
- enforce them in `executeToolMethod`. Not destructive: every object keeps its own
1578
- **key policy** (`.strict()` stays strict, `.loose()` / `.catchall()` still keep
1579
- extra keys), because the advertised schema is not advertised-only — the MCP and
1580
- AI SDKs parse the caller's arguments *with it* and hand the handler the parsed
1581
- result. An object advertised without its policy would silently delete keys the
1582
- contract would have rejected. → [ADR 0034](../decisions/0034-advertised-schema-key-policy.md).
1583
-
1584
- A consequence worth knowing when you read logs: a `.strict()` violation is caught
1585
- by the SDK **before** the tool callback runs, so it comes back as an MCP
1586
- `InvalidParams` protocol error rather than a stitchkit `VALIDATION_ERROR`
1587
- envelope, and `beforeToolCall` / `afterToolCall` do not fire for it.
1659
+ It is **deep** because the projection walks the generated JSON Schema document,
1660
+ including objects, arrays, tuples and schema-definition nodes. Structurally
1661
+ identifiable discriminated object unions are flattened wherever they occur.
1662
+ Plain unions and unions hidden behind unresolved external references remain
1663
+ unions because Stitchkit cannot soundly invent a discriminator.
1664
+
1665
+ The flattened form is **lossy but never executable**. Per-variant refinements
1666
+ and incompatible constraints are widened in the presentation document; the
1667
+ original Zod contract enforces them exactly once inside `executeToolMethod`.
1668
+ MCP and AI adapters forward the raw argument object unchanged, so defaults,
1669
+ coercions and transforms cannot run before Stitchkit. Strict violations return
1670
+ the normal `VALIDATION_ERROR` tool envelope and fire `beforeToolCall` /
1671
+ `afterToolCall`. [ADR 0050](../decisions/0050-presentation-schema-is-not-a-parser.md).
1588
1672
 
1589
1673
  ## `mountMcp`
1590
1674
 
@@ -1820,10 +1904,60 @@ so one handler serves both surfaces. Pair `extend` with `lifecycle` (your
1820
1904
 
1821
1905
  ## Native multimodal tools
1822
1906
 
1823
- Contract tools return JSON. For a tool that returns an image or other
1824
- multimodal content, register a native tool. `mountViewFile` is the built-in one
1825
- it lets a model fetch and view a file (with SSRF and path-traversal
1826
- defenses):
1907
+ Contract tools return JSON. A native tool can return MCP text/image/audio/
1908
+ resource content directly while still using stitchkit's input/output validation,
1909
+ isolated per-call context, lifecycle/RBAC and tool hooks:
1910
+
1911
+ ```ts
1912
+ import { createMcpHandler } from 'stitchkit/tools'
1913
+ import { z } from 'zod'
1914
+
1915
+ const handleMcp = createMcpHandler({
1916
+ serverInfo: { name: 'my-app', version: '1.0.0' },
1917
+ auth,
1918
+ services: [service],
1919
+ lifecycle: { beforeHandle: authHook },
1920
+ hooks: audit.toolCall,
1921
+ nativeTools: ({ registerTool }, identity) => {
1922
+ registerTool({
1923
+ name: 'render_preview',
1924
+ description: 'Render and inspect a preview',
1925
+ identity: {
1926
+ serviceName: 'mediaTools',
1927
+ action: 'renderPreview',
1928
+ scope: 'admin',
1929
+ method: 'POST',
1930
+ },
1931
+ input: z.object({ prompt: z.string() }),
1932
+ output: z.object({ assetId: z.string() }),
1933
+ handler: async ({ input, traceId }) => ({
1934
+ content: [
1935
+ { type: 'image', data: await renderBase64(input.prompt), mimeType: 'image/png' },
1936
+ { type: 'text', text: `trace: ${traceId}` },
1937
+ ],
1938
+ structuredContent: { assetId: await saveAsset(identity) },
1939
+ }),
1940
+ })
1941
+ },
1942
+ })
1943
+ ```
1944
+
1945
+ The configured identity becomes the hook/lifecycle `OperationIdentity` and the
1946
+ tool `RequestEvent` (`serviceName`, `action`, `httpMethod`). A native operation
1947
+ has no HTTP route, so no fake `path` is added to that identity. If `output` is
1948
+ declared, stitchkit parses `structuredContent` with it after `afterHandle`; all
1949
+ other MCP fields and content blocks are preserved.
1950
+
1951
+ The MCP registration uses an identity carrier: the SDK advertises the compiled
1952
+ JSON Schema but forwards the raw object into Stitchkit. Input failures therefore
1953
+ use the same validation, lifecycle and hook path as contract tools.
1954
+
1955
+ ### Explicit raw SDK registration
1956
+
1957
+ `rawServer` is deliberately named as an escape hatch. A tool registered there
1958
+ does **not** receive stitchkit schema policy, lifecycle, per-call context or
1959
+ hooks. The built-in `mountViewFile` helper remains raw for callers that choose
1960
+ that boundary; it fetches media with SSRF and path-traversal defenses:
1827
1961
 
1828
1962
  ```ts
1829
1963
  import { createMcpHandler, mountViewFile } from 'stitchkit/tools'
@@ -1832,10 +1966,15 @@ const handleMcp = createMcpHandler({
1832
1966
  serverInfo: { name: 'my-app', version: '1.0.0' },
1833
1967
  auth,
1834
1968
  services: [service],
1835
- nativeTools: (server) => mountViewFile(server, { baseDir: '/srv/uploads' }),
1969
+ nativeTools: ({ rawServer }) =>
1970
+ mountViewFile(rawServer, { baseDir: '/srv/uploads' }),
1836
1971
  })
1837
1972
  ```
1838
1973
 
1974
+ Use raw registration only when opting out is intentional. For a protected
1975
+ `view_file`, define it through `registerTool` and call the exported
1976
+ `resolveMedia` core from its handler.
1977
+
1839
1978
  ## Logging tool calls — `createToolLogger`
1840
1979
 
1841
1980
  Every tool mount fires an `afterToolCall` hook. `createToolLogger` is a ready
@@ -2946,7 +3085,16 @@ either way, so your sink reads it as a column instead of re-parsing the path:
2946
3085
  > same hooks object is assignable to `ToolLifecycle`, so this recipe is the one
2947
3086
  > people apply to tools — and since ADR 0045 each tool call runs in its own
2948
3087
  > context. Read the value off the tool row (`event.toolName != null`); both rows
2949
- > carry the same `traceId`.
3088
+ > carry the same `traceId`, so recovering the enclosing request is one filter:
3089
+ >
3090
+ > ```bash
3091
+ > jq -s '[.[] | select(.dimensions.botId == "B7") | .traceId] as $t
3092
+ > | .[] | select(.traceId | IN($t[]))' audit.jsonl
3093
+ > ```
3094
+ >
3095
+ > Worth checking your **runbooks** as well as your code when this lands: a
3096
+ > dimension filter written against request rows keeps parsing and starts
3097
+ > returning nothing.
2950
3098
 
2951
3099
  ```ts
2952
3100
  // beforeHandle (success) and onError (failure) alike:
@@ -2965,6 +3113,9 @@ createHandler({ /* … */ traceId: getTraceId })
2965
3113
  falls back to its own resolver — a trusted inbound `x-request-id` / `x-trace-id`,
2966
3114
  else a fresh id — so the line never carries the string `"undefined"`.
2967
3115
 
3116
+ Inside a **tool call** the context is that call's own (→ ADR 0045), so the
3117
+ enrichment a request log picks up describes the request, not the call.
3118
+
2968
3119
  `getRequestContext()` / `getTraceId()` then return the active values from
2969
3120
  anywhere in the call — stamp `getTraceId()` onto every line your logger writes.
2970
3121
  The **request log picks the context up on its own**: with a context active, each
@@ -3073,11 +3224,11 @@ metric, a custom log line, anything that is not a full audit row.
3073
3224
  |---------|------|-------|
3074
3225
  | HTTP | `LifecycleHooks.afterHandle` / `onError` | after each HTTP request |
3075
3226
  | MCP & agent tools | `ToolCallHooks.afterToolCall` | after each tool call |
3076
- | MCP & agent tools | `ToolCallHooks.onToolError` | when a tool handler throws |
3227
+ | MCP & agent tools | `ToolCallHooks.onToolError` | when executable parsing, extension resolution, lifecycle or the handler throws |
3077
3228
 
3078
3229
  `afterHandle(ctx, result, endpoint)` runs after a handler returns;
3079
- `onError(ctx, error, endpoint)` when one throws. `afterToolCall(toolName, args,
3080
- result, durationMs, context, endpoint, error)` runs after every tool call —
3230
+ `onError(ctx, error, endpoint)` when one throws. `afterToolCall(options)` runs
3231
+ after every tool call —
3081
3232
  success and error alike — carrying the tool name, the arguments, the result, the
3082
3233
  duration, the call context, the endpoint identity, and (only when the call failed
3083
3234
  by throwing) the raw thrown value.
@@ -3086,7 +3237,7 @@ by throwing) the raw thrown value.
3086
3237
  createMcpHandler({
3087
3238
  serverInfo, auth, services,
3088
3239
  hooks: {
3089
- afterToolCall: (toolName, _args, result, durationMs, context) => {
3240
+ afterToolCall: ({ toolName, result, durationMs }) => {
3090
3241
  metrics.timing(`tool.${toolName}`, durationMs, { ok: String(result.ok) })
3091
3242
  },
3092
3243
  },
@@ -3112,7 +3263,7 @@ normalisation, stack and `cause` intact:
3112
3263
  createMcpHandler({
3113
3264
  serverInfo, auth, services,
3114
3265
  hooks: {
3115
- onToolError: (toolName, error, _context, endpoint) => {
3266
+ onToolError: ({ toolName, error, endpoint }) => {
3116
3267
  reportToolFailure({
3117
3268
  tool: toolName,
3118
3269
  action: endpoint.key,
@@ -3124,13 +3275,12 @@ createMcpHandler({
3124
3275
  })
3125
3276
  ```
3126
3277
 
3127
- It fires for a throw from `beforeHandle`, the handler or `afterHandle` — the
3128
- span where information is destroyed and runs **before** `afterToolCall`, so
3129
- whatever it records is in place when the audit hook reads it. It deliberately
3130
- does not fire for an argument-validation failure, an output-schema mismatch or a
3131
- `beforeToolCall` rejection: each of those is already described in full by the
3132
- `ToolResult`, and a second path to the same information only invites
3133
- double-logging.
3278
+ It fires for a throw from executable params/input/`ToolExtend` parsing,
3279
+ extension resolution, `beforeHandle`, the handler or `afterHandle` every span
3280
+ where information is destroyed and runs **before** `afterToolCall`. It does
3281
+ not fire for an ordinary validation result, an output-schema mismatch or a
3282
+ `beforeToolCall` rejection: each is already described in full by its
3283
+ `ToolResult`.
3134
3284
 
3135
3285
  It observes, it does not handle: the tool envelope is always the framework's, a
3136
3286
  returned value is ignored, and a throw from the hook itself is reported to
@@ -3152,11 +3302,11 @@ either — the call is simply not where that helper belongs.)
3152
3302
  ### One row that names the cause
3153
3303
 
3154
3304
  You do not need to correlate the two hooks yourself. `afterToolCall` receives the
3155
- same raw value as a **seventh parameter**, so one hook can build one record:
3305
+ same raw value as the named `error` field, so one hook can build one record:
3156
3306
 
3157
3307
  ```ts
3158
3308
  hooks: {
3159
- afterToolCall: (toolName, args, result, durationMs, context, endpoint, error) => {
3309
+ afterToolCall: ({ toolName, result, durationMs, endpoint, error }) => {
3160
3310
  void writeRow({ toolName, result, durationMs, cause: error, endpoint })
3161
3311
  },
3162
3312
  }
@@ -3164,8 +3314,8 @@ hooks: {
3164
3314
 
3165
3315
  `error` is present only when the call failed by **throwing** — a
3166
3316
  validation failure or a `beforeToolCall` rejection leaves it `undefined`, because
3167
- neither ever had a raw value to lose. The parameter is additive: a six-parameter
3168
- hook written before it keeps compiling and keeps firing.
3317
+ neither ever had a raw value to lose. Consumers destructure only the fields they
3318
+ use; future optional fields do not change callback arity.
3169
3319
 
3170
3320
  `createAuditHook` uses it already. Where the envelope was scrubbed to
3171
3321
  `INTERNAL_SERVER_ERROR`, the row's `errorMessage` becomes the real message
@@ -3183,11 +3333,13 @@ sink of your own (a tracker, a stack, an alert), `afterToolCall` for the record.
3183
3333
  `createAuditHook` already keys every event by **service** and **action**
3184
3334
  (`event.serviceName` / `event.action`, → ADR 0029) — reach for the raw hook only
3185
3335
  when you also need the handler **output**, which the audit wrapper never sees. For
3186
- that, read the endpoint identity off the `MethodDef` the hook receives —
3187
- `endpoint.serviceName` (the contract prefix) and `endpoint.key` (the endpoint key,
3336
+ that, read the endpoint identity off the `OperationIdentity` the tool hook
3337
+ receives — `endpoint.serviceName` and `endpoint.key` (the endpoint key or native
3338
+ action,
3188
3339
  e.g. `updatePartial`). They are stable and always present (→ ADR 0022); the action
3189
3340
  is not in the URL and `toolName` is absent on HTTP-only endpoints, so this is the
3190
- only reliable pair. `afterHandle` also gives you the handler `result` so it is
3341
+ only reliable pair. Contract operations are full `MethodDef` values; native MCP
3342
+ operations intentionally have no HTTP `path`. `afterHandle` also gives you the handler `result` — so it is
3191
3343
  the home for a rich mutation audit that records output:
3192
3344
 
3193
3345
  ```ts
@@ -3321,6 +3473,10 @@ serveNode({
3321
3473
 
3322
3474
  Notes for a Node host:
3323
3475
 
3476
+ - `stitchkit/node` declarations do not require `@types/bun`. Runtime-neutral
3477
+ raw routes use `RawRoute<TServer = unknown>`; supply a host server generic
3478
+ only when an embedding adapter passes one to `createHandler`.
3479
+
3324
3480
  - Add **`@types/bun`** as a dev dependency — it is an optional peer that types the
3325
3481
  shared `stitchkit/server` surface (without it `tsc` reports a missing `Bun`
3326
3482
  namespace).
@@ -3558,6 +3714,130 @@ current one *up to* your target, and apply each snippet.
3558
3714
  (`STITCH_ERROR_STATUS`, `serveFile`, `scopePrefixes`, `afterToolCall`'s
3559
3715
  `MethodDef`, `maxUploadBytes`) are available to adopt, not required.
3560
3716
 
3717
+ ## The 0.37 migration
3718
+
3719
+ Tool presentation is no longer an executable Zod parser. Replace the removed
3720
+ flatten helpers with the JSON Schema compiler, and choose the explicit
3721
+ `MountableTool` surface when using the advanced collection API:
3722
+
3723
+ ```ts
3724
+ // before
3725
+ const flat = flattenUnionsDeep(zodSchema)
3726
+ mountable.schema
3727
+
3728
+ // after
3729
+ const flat = flattenToolJsonSchema(
3730
+ z.toJSONSchema(zodSchema, { target: 'draft-07', io: 'input' }),
3731
+ )
3732
+ mountable.presentationSchema // model/MCP/manifest JSON Schema
3733
+ mountable.argumentSchema // executable CLI argument adapter only
3734
+ ```
3735
+
3736
+ Contract and native handlers keep their original Zod schemas; MCP and agent SDK
3737
+ adapters now forward raw arguments so defaults, coercions, refinements and
3738
+ transforms execute exactly once inside Stitchkit. There are no compatibility
3739
+ exports for `flattenDiscriminatedUnion`, `flattenUnionsDeep` or
3740
+ `MountableTool.schema`. → ADR 0050
3741
+
3742
+ Tool-call hooks now take one options object. Migrate all three callbacks; there
3743
+ are no positional overloads:
3744
+
3745
+ ```ts
3746
+ // before
3747
+ beforeToolCall: (toolName, args, context, endpoint) => {}
3748
+ afterToolCall: (toolName, args, result, durationMs, context, endpoint, error) => {}
3749
+ onToolError: (toolName, error, context, endpoint) => {}
3750
+
3751
+ // after
3752
+ beforeToolCall: ({ toolName, args, context, endpoint }) => {}
3753
+ afterToolCall: ({ toolName, args, result, durationMs, context, endpoint, error }) => {}
3754
+ onToolError: ({ toolName, error, context, endpoint }) => {}
3755
+ ```
3756
+
3757
+ ### MCP schema validation
3758
+
3759
+ The standalone validator now takes one object and live MCP configs carry the
3760
+ same rules under `schemaValidation`. Migrate every positional call and every
3761
+ `onIncompatibleSchema` field; there is no old-shape overload:
3762
+
3763
+ ```ts
3764
+ // before
3765
+ validateMcpSchemas(services, 'throw', logger, { requireTypedProperties: true })
3766
+ createMcpHandler({ services, onIncompatibleSchema: 'throw' })
3767
+
3768
+ // after
3769
+ validateMcpSchemas({ services, policy: 'throw', logger, requireTypedProperties: true })
3770
+ createMcpHandler({ services, schemaValidation: { policy: 'throw' } })
3771
+ ```
3772
+
3773
+ Put `extend` and `flattenUnionInput` beside `services`, not inside
3774
+ `schemaValidation`. The handler applies the profile to the exact prepared schema
3775
+ it advertises. Add `requirePortableFormats: true` when every custom JSON Schema
3776
+ format must be rejected before a client sees it.
3777
+
3778
+ Native MCP registration also changed shape in 0.37. Move protected tools to the
3779
+ framework registrar; keep an SDK-raw tool only by naming the opt-out:
3780
+
3781
+ ```ts
3782
+ // before
3783
+ nativeTools: (server, auth) => server.registerTool(name, config, handler)
3784
+
3785
+ // after — lifecycle, hooks and schema policy apply
3786
+ nativeTools: ({ registerTool }, auth) => registerTool({
3787
+ name, description, identity, input, output, handler,
3788
+ })
3789
+
3790
+ // after — intentionally raw
3791
+ nativeTools: ({ rawServer }, auth) => rawServer.registerTool(name, config, handler)
3792
+ ```
3793
+
3794
+ If a tool hook annotated `endpoint` as `MethodDef`, remove that annotation or
3795
+ use `OperationIdentity`: native operations have service/action/scope/method but
3796
+ no HTTP path.
3797
+
3798
+ ### MCP HTTP sessions
3799
+
3800
+ Finally, replace the MCP HTTP session boolean. Omission changed meaning:
3801
+
3802
+ ```ts
3803
+ // before → after
3804
+ stateless: true // → sessionMode: 'stateless'
3805
+ stateless: false // → sessionMode: 'stateful'
3806
+
3807
+ // before: omission was stateful
3808
+ // after: omission is stateless
3809
+ ```
3810
+
3811
+ If the client relies on `Mcp-Session-Id`, server push, progress across requests
3812
+ or resumable SSE, set `sessionMode: 'stateful'` explicitly. Synchronous tool
3813
+ servers should omit it and use the new restart-safe default.
3814
+
3815
+ ### Node-facing server types
3816
+
3817
+ `stitchkit/server` remains Bun-concrete: an explicitly annotated `RawRoute`
3818
+ still receives `BunServer`. `stitchkit/node` no longer drags Bun declarations
3819
+ into a Node project. Its raw routes default the host server to `unknown`, and
3820
+ its Socket.IO handle exposes only Node capabilities:
3821
+
3822
+ ```ts
3823
+ // before — Node entry still leaked Bun-only fields and ambient types
3824
+ const route: RawRoute = { handler: (_req, ctx) => ctx.server?.upgrade(...) }
3825
+ const socket = await createSocketIOServer(config)
3826
+ socket.websocket
3827
+
3828
+ // after — name a custom embedding host only when one exists
3829
+ const route: RawRoute<MyHostServer> = {
3830
+ handler: (_req, ctx) => useHost(ctx.server),
3831
+ }
3832
+ const socket = await createSocketIOServer(config)
3833
+ socket.io
3834
+ socket.attach(nodeHttpServer)
3835
+ ```
3836
+
3837
+ Inline Bun routes passed to `createServer` continue to infer `BunServer`; no
3838
+ annotation is needed. Node consumers can remove `@types/bun` unless another
3839
+ dependency independently requires it.
3840
+
3561
3841
  ## Your handlers may be returning more than the contract declares
3562
3842
 
3563
3843
  stitchkit validates every handler's return value against the endpoint's `output`
@@ -3759,23 +4039,24 @@ Also re-exports the error helpers from `stitchkit/contract`.
3759
4039
  | `zodIssues` | function | a `ZodError` → structured `{ path, code, message }[]` — the machine-readable sibling of `formatZodError` |
3760
4040
  | `ZodIssueSummary` | _type_ | one structured validation issue (`{ path, code, message }`) |
3761
4041
  | `parseBody` | function | parse + Zod-validate a JSON body → `data` or `null` (no throw) |
3762
- | `HandlerConfig` | _type_ | config for `createHandler` (runtime-agnostic) |
4042
+ | `HandlerConfig` | _type_ | config for `createHandler`, including optional `maxJsonBodyBytes`; bound to `BunServer` on this entrypoint |
3763
4043
  | `BunServerConfig` | _type_ | config for `createServer` (Bun) |
3764
4044
  | `ServiceDef` | _type_ | the result of `implement` |
3765
4045
  | `MethodDef` | _type_ | one resolved endpoint inside a service |
4046
+ | `OperationIdentity` | _type_ | path-free service/action/scope/method identity shared by contract and native tool operations |
3766
4047
  | `Handlers` | _type_ | the typed handler map `implement` expects |
3767
4048
  | `LifecycleHooks` | _type_ | `onRequest` / `beforeHandle` / `afterHandle` / `onError` |
3768
4049
  | `RouteGroup` | _type_ | a prefixed group of services with its own hooks |
3769
- | `RawRoute` | _type_ | a non-contract `Request → Response` route |
3770
- | `RawRouteContext` | _type_ | the routing context a raw handler receives |
4050
+ | `RawRoute` | _type_ | a non-contract `Request → Response` route with a concrete `BunServer` context |
4051
+ | `RawRouteContext` | _type_ | the Bun-bound routing context a raw handler receives |
3771
4052
  | `BunServer` | _type_ | the `Bun.serve` instance type |
3772
4053
  | `ServerPassthrough` | _type_ | extra `Bun.serve` options |
3773
4054
  | `StitchLogger` | _type_ | the custom-logger interface |
3774
4055
  | `LoggingConfig` | _type_ | the `logging` object — `logger` / `format` / `skip` / `enrich` |
3775
4056
  | `LogFormat` | _type_ | `'pretty'` or `'json'` — what the built-in formatter writes |
3776
4057
  | `LogOutcome` | _type_ | how a request finished, as `enrich` sees it |
3777
- | `FetchHandler` | _type_ | what `createHandler` returns |
3778
- | `FetchComposition` | _type_ | the `wrapFetch` seam shared by the servers |
4058
+ | `FetchHandler` | _type_ | what `createHandler` returns, bound to `BunServer` here |
4059
+ | `FetchComposition` | _type_ | the Bun-bound `wrapFetch` seam |
3779
4060
 
3780
4061
  ### Auth
3781
4062
 
@@ -3940,13 +4221,21 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
3940
4221
  | `createToolkit` | function | context-typed tool mounts — [guide](../guide/cli.md#typed-context) |
3941
4222
  | `mountViewFile` | function | a native multimodal "view file" MCP tool |
3942
4223
  | `resolveMedia` | function | resolve a media reference for a tool result |
3943
- | `validateMcpSchemas` | function | assert every tool schema is JSON Schema-compatible — [guide](../guide/mcp-and-agents.md#incompatible-schemas--onincompatibleschema) |
4224
+ | `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)) |
3944
4225
  | `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) |
3945
4226
  | `McpHandlerConfig` | _type_ | config for `createMcpHandler` |
4227
+ | `McpSessionMode` | _type_ | `'stateless' \| 'stateful'`; HTTP defaults to request-isolated stateless mode |
3946
4228
  | `StdioMcpServerConfig` | _type_ | config for `createStdioMcpServer` |
3947
4229
  | `McpServerBuildConfig` | _type_ | shared config for `buildMcpServer` |
3948
4230
  | `ImplementRemoteOptions` | _type_ | options for `implementRemote` |
3949
4231
  | `McpMountConfig` | _type_ | config for `mountMcp` |
4232
+ | `McpSchemaValidationConfig` | _type_ | shared `{ policy, requireTypedProperties, allowUntyped, requirePortableFormats, allowFormats }` profile |
4233
+ | `ValidateMcpSchemasConfig` | _type_ | standalone validation profile plus `services`, `extend`, flattening and logger |
4234
+ | `NativeMcpRegistrar` | _type_ | protected `registerTool` plus explicit unprotected `rawServer` access |
4235
+ | `NativeMcpToolDefinition` | _type_ | native name, operation identity, Zod schemas and MCP-result handler |
4236
+ | `NativeMcpOperationIdentity` | _type_ | `{ serviceName, action, scope?, method, meta? }` for native lifecycle/audit |
4237
+ | `NativeMcpHandlerContext` | _type_ | runtime context with the definition's parsed native input |
4238
+ | `NativeMcpResult` | _type_ | MCP content result, with typed `structuredContent` when output is declared |
3950
4239
  | `AgentMountConfig` | _type_ | config for `mountAgent` |
3951
4240
  | `AgentContext` | _type_ | the context merged into agent tool handlers |
3952
4241
  | `CliConfig` | _type_ | config for `createCli` |
@@ -3955,7 +4244,11 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
3955
4244
  | `Toolkit` | _type_ | the context-pinned tool surface from `createToolkit` |
3956
4245
  | `ToolExtend` | _type_ | extra-args extension for `mountMcp` / `mountAgent` |
3957
4246
  | `ToolLifecycle` | _type_ | `beforeHandle` / `afterHandle` gate for tool calls — [guide](../guide/mcp-and-agents.md#guarding-tools--lifecycle) |
3958
- | `ToolCallHooks` | _type_ | `beforeToolCall` / `afterToolCall` / `onToolError` observability hooks the raw thrown value reaches both of the last two (`afterToolCall`'s 7th parameter), because normalisation scrubs an unexpected error to `INTERNAL_SERVER_ERROR` ([guide](../guide/observability.md#the-cause-behind-a-failed-tool-call)) |
4247
+ | `ToolOperation` | _type_ | executable path-free operation shape shared by contract and framework-native runners |
4248
+ | `ToolCallHooks` | _type_ | object-shaped `beforeToolCall` / `afterToolCall` / `onToolError` observability hooks; the raw thrown value reaches the last two as `error` ([guide](../guide/observability.md#the-cause-behind-a-failed-tool-call)) |
4249
+ | `BeforeToolCallOptions` | _type_ | `{ toolName, args, context, endpoint }` passed before execution |
4250
+ | `AfterToolCallOptions` | _type_ | completed call options plus `{ result, durationMs, error? }` |
4251
+ | `ToolErrorOptions` | _type_ | `{ toolName, error, context, endpoint }` for a thrown handler-path value |
3959
4252
  | `ErrorHintFn` | _type_ | `(toolName, errorCode) => string \| null` — a per-tool recovery hint, shared by every mount |
3960
4253
  | `ToolResult` | _type_ | the result of one tool call |
3961
4254
  | `ToolCallContext` | _type_ | the context every tool hook receives — `{ source }` plus whatever the mount's `context` added |
@@ -3964,6 +4257,9 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
3964
4257
  | `CollectToolsConfig` | _type_ | options for `collectTools` |
3965
4258
  | `findUntypedProperties` | function | every property in a JSON Schema with no `type`/`enum`/`$ref` — what a model is shown and cannot obey ([guide](../guide/mcp-and-agents.md)) |
3966
4259
  | `UntypedProperty` | _type_ | one such property — `{ path, description? }` |
4260
+ | `findNonPortableFormats` | function | deep finder for formats outside the portable MCP/AJV baseline |
4261
+ | `NonPortableFormat` | _type_ | one `{ path, format }` portability finding |
4262
+ | `PORTABLE_JSON_SCHEMA_FORMATS` | constant | portable-format baseline used by MCP validation |
3967
4263
  | `ToolNameEntry` | _type_ | one `listToolNames` row — `{ name, service, method, transports }` |
3968
4264
  | `IncompatibleSchemaPolicy` | _type_ | `'throw' \| 'skip' \| 'warn'` |
3969
4265
  | `McpMediaContent` | _type_ | a multimodal MCP content item |
@@ -4030,9 +4326,9 @@ Advanced building blocks — the shared machinery the mounts are built on.
4030
4326
  | `TransportSummary` | _type_ | the result of `summarizeTransports` |
4031
4327
  | `TransportCounts` | _type_ | per-transport counts (`{ HTTP, MCP, AGENT, CLI }`) |
4032
4328
  | `coerceJsonArgs` | function | coerce JSON-stringified array/object tool arguments |
4033
- | `flattenDiscriminatedUnion` | function | flatten one discriminated union into a single object schema |
4034
- | `flattenUnionsDeep` | function | flatten discriminated unions at every depth union shape only; each object keeps its own key policy (`.strict()` / `.loose()` / `.catchall()`) |
4035
- | `MountableTool` | _type_ | one contract method resolved for mounting |
4329
+ | `flattenToolJsonSchema` | function | project structurally identifiable discriminated unions in a JSON Schema document into conservative object joins; never executes validation |
4330
+ | `ToolPresentationSchema` | _type_ | immutable model-facing JSON Schema document shared by tool transports |
4331
+ | `MountableTool` | _type_ | one operation with separate executable CLI argument schema and model-facing presentation schema |
4036
4332
  | `ToolManifestEntry` | _type_ | one `buildToolManifest` row |
4037
4333
 
4038
4334
  ---
@@ -4048,11 +4344,12 @@ runtime-agnostic pieces of `stitchkit/server` and the error helpers.
4048
4344
  |--------|------|---------|
4049
4345
  | `serveNode` | function | build the router and start a Node HTTP server (via `srvx`) |
4050
4346
  | `createHandler` | function | the router as a bare `(req) => Response` (same as `/server`) |
4051
- | `createSocketIOServer` | function | the typed Socket.IO server (same as `/server`) |
4347
+ | `createSocketIOServer` | function | the typed Node Socket.IO server (`io` + `attach`; no Bun engine declarations) |
4052
4348
  | `implement` / `createImplement` | function | bind a contract to typed handlers (same as `/server`) |
4053
4349
  | `NodeServerConfig` | _type_ | config for `serveNode` |
4054
4350
  | `NodeServerHandle` | _type_ | the `serveNode` handle (`{ port, stop }`) |
4055
- | `HandlerConfig` / `ServiceDef` / `RawRoute` / `RawRouteContext` / `SocketIOServerConfig` / `SocketIOServerHandle` | _type_ | re-exported from `/server` |
4351
+ | `HandlerConfig` / `ServiceDef` / `RawRoute` / `RawRouteContext` | _type_ | runtime-neutral handler types; raw routes default their host server to `unknown` |
4352
+ | `SocketIOServerConfig` / `SocketIOServerHandle` | _type_ | shared config and the Node-only `{ io, attach }` handle |
4056
4353
  | `AppError` + `appError` / `badRequest` / `unauthorized` / `forbidden` / `notFound` / `conflict` / `rateLimited` | — | error helpers (same as `/contract`) |
4057
4354
 
4058
4355
  ---