stitchkit 0.38.0 → 0.39.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 (56) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/client-url.d.ts +2 -0
  3. package/dist/browser/client-url.d.ts.map +1 -1
  4. package/dist/browser/client.d.ts +25 -1
  5. package/dist/browser/client.d.ts.map +1 -1
  6. package/dist/browser/http.d.ts +5 -6
  7. package/dist/browser/http.d.ts.map +1 -1
  8. package/dist/cli.js +2 -2
  9. package/dist/contract/define.d.ts +36 -12
  10. package/dist/contract/define.d.ts.map +1 -1
  11. package/dist/contract/index.d.ts +1 -1
  12. package/dist/contract/index.d.ts.map +1 -1
  13. package/dist/contract/index.js +1 -1
  14. package/dist/{index-czmqks7r.js → index-6jypn22c.js} +1 -1
  15. package/dist/{index-p3kwf73n.js → index-7rbzbnnf.js} +28 -15
  16. package/dist/{index-bgdd42pt.js → index-92gs1m5b.js} +1 -1
  17. package/dist/{index-mzx0an0s.js → index-fyfk537k.js} +50 -5
  18. package/dist/{index-x4wbc8sz.js → index-xax049k6.js} +73 -5
  19. package/dist/{index-n5t4gnfz.js → index-y5scd1cr.js} +1 -1
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +116 -17
  23. package/dist/internal/route-pattern.d.ts +7 -0
  24. package/dist/internal/route-pattern.d.ts.map +1 -0
  25. package/dist/node.js +3 -3
  26. package/dist/observability/index.js +1 -1
  27. package/dist/react/entity-cache.d.ts +56 -42
  28. package/dist/react/entity-cache.d.ts.map +1 -1
  29. package/dist/react.d.ts +1 -1
  30. package/dist/react.d.ts.map +1 -1
  31. package/dist/react.js +98 -46
  32. package/dist/server/create.d.ts.map +1 -1
  33. package/dist/server/index.js +10 -9
  34. package/dist/server/middleware/cors.d.ts.map +1 -1
  35. package/dist/server/openapi.d.ts.map +1 -1
  36. package/dist/server/router.d.ts +2 -0
  37. package/dist/server/router.d.ts.map +1 -1
  38. package/dist/server/socket-io.d.ts +1 -1
  39. package/dist/server/types.d.ts +4 -6
  40. package/dist/server/types.d.ts.map +1 -1
  41. package/dist/tools/agent.d.ts +3 -0
  42. package/dist/tools/agent.d.ts.map +1 -1
  43. package/dist/tools/invoker.d.ts +33 -0
  44. package/dist/tools/invoker.d.ts.map +1 -0
  45. package/dist/tools/mcp.d.ts.map +1 -1
  46. package/dist/tools/names.d.ts +1 -1
  47. package/dist/tools/names.d.ts.map +1 -1
  48. package/dist/tools/native-mcp.d.ts +6 -33
  49. package/dist/tools/native-mcp.d.ts.map +1 -1
  50. package/dist/tools/runtime-tool.d.ts +58 -0
  51. package/dist/tools/runtime-tool.d.ts.map +1 -0
  52. package/dist/tools.d.ts +3 -1
  53. package/dist/tools.d.ts.map +1 -1
  54. package/dist/tools.js +181 -82
  55. package/llms-full.txt +417 -89
  56. package/package.json +1 -1
package/llms-full.txt CHANGED
@@ -229,8 +229,8 @@ export const users = defineContract({ prefix: 'users' }, {
229
229
 
230
230
  | Field | Required | Purpose |
231
231
  |-------|----------|---------|
232
- | `method` | yes | `GET` · `POST` · `PUT` · `PATCH` · `DELETE` |
233
- | `path` | yes | route path under the contract `prefix`; `:name` marks a path param and a terminal `/*` captures the remaining path |
232
+ | `method` | yes | `GET` · `HEAD` · `POST` · `PUT` · `PATCH` · `DELETE` |
233
+ | `path` | yes | route path under the contract `prefix`; `:name` marks a path param and a named terminal `/*filePath` captures the remaining path |
234
234
  | `desc` | yes | human description — also the MCP / agent tool description |
235
235
  | `params` | no | Zod schema for **path params** (`:id`, …) |
236
236
  | `input` | no | Zod schema for the **request body** (or query, for GET/DELETE) |
@@ -248,16 +248,24 @@ export const users = defineContract({ prefix: 'users' }, {
248
248
  | `responseMeta` | no | make a typed-data endpoint HTTP-only, optionally declare its success status and expose `ctx.response.headers` — [Typed JSON response metadata](./server.md#typed-json-response-metadata) |
249
249
  | `contentType` | no | documented response media type of a `rawResponse` endpoint (OpenAPI only) |
250
250
 
251
+ `HEAD` is an explicit HTTP-only operation, never an automatic alias for `GET`.
252
+ It must declare `rawResponse: true`; request body, `input`, multipart, `rawBody`,
253
+ typed `output` and tool exposure are rejected. The handler owns status and
254
+ headers, while Stitchkit guarantees an empty wire body even if the returned
255
+ `Response` accidentally contains one.
256
+
251
257
  ## `params` vs `input` vs `output`
252
258
 
253
259
  The three schemas are distinct on purpose:
254
260
 
255
261
  - **`params`** — values in the URL path. `path: '/:id'` ⇒
256
262
  `params: z.object({ id: z.string() })`. The client takes them from the call
257
- argument and substitutes them into the URL. A terminal wildcard is the
258
- quoted `'*'` field: `path: '/:slug/*'` with
259
- `params: z.object({ slug: z.string(), '*': z.string() })` matches both
260
- `/foo/page` and `/foo/a/b`; the handler receives `'page'` or `'a/b'`.
263
+ argument and substitutes them into the URL. A terminal wildcard is explicitly
264
+ named: `path: '/:slug/*filePath'` with
265
+ `params: z.object({ slug: z.string(), filePath: z.string() })` matches both
266
+ `/foo/page` and `/foo/a/b`; the handler reads `ctx.params.filePath` as
267
+ `'page'` or `'a/b'`. Bare `/*`, invalid/duplicate names and a wildcard before
268
+ the final segment fail at contract definition.
261
269
  - **`input`** — the request payload. For `POST` / `PUT` / `PATCH` it is the JSON
262
270
  body; for `GET` / `DELETE` it is the query string. The handler reads it as
263
271
  `ctx.input`.
@@ -272,8 +280,8 @@ the body.
272
280
  // path: '/:id', params: { id }, input: { text }
273
281
  await api.update({ id: '1', text: 'new' }) // PUT /users/1 body: { text: 'new' }
274
282
 
275
- // path: '/:slug/*', params: { slug, '*': remainder }
276
- await api.app({ slug: 'foo', '*': 'a/b' }) // GET /apps/foo/a/b
283
+ // path: '/:slug/*filePath', params: { slug, filePath }
284
+ await api.app({ slug: 'foo', filePath: 'a/b' }) // GET /apps/foo/a/b
277
285
  ```
278
286
 
279
287
  ### Input vs. output types
@@ -773,11 +781,12 @@ prefix param in the schema, use a non-strict `z.object` (extra keys are dropped
773
781
  from `ctx.params`, but `ctx.tenantId` still works), or read the param off the
774
782
  context root.
775
783
 
776
- **Trailing wildcard.** A contract path may end in `/*`. `/app/:slug/*` matches
784
+ **Trailing wildcard.** A contract path may end in a named wildcard.
785
+ `/app/:slug/*filePath` matches
777
786
  both `/app/foo` and nested paths such as `/app/foo/a/b`; the collected params are
778
- `{ slug: 'foo', '*': '' }` and `{ slug: 'foo', '*': 'a/b' }` respectively. Put
779
- the quoted `'*'` field in the endpoint's `params` schema to keep it in typed
780
- `ctx.params`. Each captured segment is URL-decoded before the remainder is
787
+ `{ slug: 'foo', filePath: '' }` and `{ slug: 'foo', filePath: 'a/b' }`
788
+ respectively. Put `filePath` in the endpoint's `params` schema to keep it in
789
+ typed `ctx.params`. Each captured segment is URL-decoded before the remainder is
781
790
  joined, so encoded spaces and reserved characters reach the handler as their
782
791
  semantic values while `/` remains the segment boundary. Static and named-param
783
792
  routes are matched before a catch-all, so a
@@ -1026,9 +1035,9 @@ createServer({
1026
1035
  })
1027
1036
  ```
1028
1037
 
1029
- A path may be exact, carry `:param` segments, or end in `/*` for a prefix
1030
- wildcard — and the two combine: `/app/:slug/*` matches `/app/x/a/b` with
1031
- `ctx.params.slug === 'x'` and the remainder in `ctx.params['*']` (a SPA
1038
+ A path may be exact, carry `:param` segments, or end in `/*filePath` for a prefix
1039
+ wildcard — and the two combine: `/app/:slug/*filePath` matches `/app/x/a/b` with
1040
+ `ctx.params.slug === 'x'` and the remainder in `ctx.params.filePath` (a SPA
1032
1041
  deep-link fallback). List more specific routes before the wildcard — the first
1033
1042
  match wins. `staticRoute()` builds a raw route that serves a directory.
1034
1043
 
@@ -1067,24 +1076,35 @@ seek and cache, use **`serveFile`** (Bun) — it streams the requested byte rang
1067
1076
  and speaks the conditional-request half of RFC 7233 / 9110:
1068
1077
 
1069
1078
  ```ts
1070
- import { serveFile } from 'stitchkit/server'
1079
+ import { defineContract } from 'stitchkit/contract'
1080
+ import { implement, serveFile } from 'stitchkit/server'
1081
+ import { z } from 'zod'
1071
1082
 
1072
- createServer({
1073
- services,
1074
- rawRoutes: [
1075
- {
1076
- // `ALL` — so a HEAD probe also reaches serveFile (raw routes match the
1077
- // method exactly, and `HEAD` is not a contract `HttpMethod`); serveFile
1078
- // itself handles GET + HEAD and answers 405 for anything else.
1079
- method: 'ALL',
1080
- path: '/media/:id',
1081
- handler: (req, ctx) =>
1082
- serveFile(req, { path: pathForId(ctx.params.id), filename: 'clip.mp4' }),
1083
- },
1084
- ],
1083
+ const MediaParams = z.object({ id: z.string() })
1084
+ const media = defineContract({ prefix: 'media' }, {
1085
+ download: {
1086
+ method: 'GET', path: '/:id', desc: 'Download media',
1087
+ params: MediaParams, rawResponse: true, contentType: 'video/mp4',
1088
+ },
1089
+ inspect: {
1090
+ method: 'HEAD', path: '/:id', desc: 'Inspect media',
1091
+ params: MediaParams, rawResponse: true, contentType: 'video/mp4',
1092
+ },
1093
+ })
1094
+
1095
+ const mediaService = implement(media, {
1096
+ download: ({ req, params }) =>
1097
+ serveFile(req, { path: pathForId(params.id), filename: 'clip.mp4' }),
1098
+ inspect: ({ req, params }) =>
1099
+ serveFile(req, { path: pathForId(params.id), filename: 'clip.mp4' }),
1085
1100
  })
1086
1101
  ```
1087
1102
 
1103
+ GET and HEAD are separate operations deliberately: declaring GET never creates
1104
+ a hidden HEAD alias. Both travel through the normal contract router, params,
1105
+ lifecycle/RBAC and request logging. A HEAD handler may inspect the raw query via
1106
+ `ctx.req.url`, but cannot declare a request input schema or body.
1107
+
1088
1108
  `serveFile` returns `206` (range, with `Content-Range` + `Content-Length`), `200`
1089
1109
  (full), `416` (unsatisfiable, `Content-Range: bytes */size`), `304`
1090
1110
  (`If-None-Match` / `If-Modified-Since`), `404` (missing) or `405` (non GET/HEAD).
@@ -1205,7 +1225,8 @@ createServer({ services: [users, orders], rawRoutes: [openApiRoute('/openapi.jso
1205
1225
  Only HTTP-exposed methods appear (an MCP/agent-only tool is skipped).
1206
1226
 
1207
1227
  OpenAPI 3.1 has no standard multi-segment path parameter. For a contract path
1208
- ending in `/*`, Stitchkit keeps the literal runtime path, omits `*` from the
1228
+ ending in a named wildcard such as `/*filePath`, Stitchkit keeps the literal
1229
+ runtime path, omits `filePath` from the
1209
1230
  standard `in: path` parameter list, and emits
1210
1231
  `x-stitchkit-trailing-wildcard` on the operation with its parameter name,
1211
1232
  schema and semantics. A generic OpenAPI client therefore cannot invent
@@ -1266,9 +1287,13 @@ and adds cookie auth, SSR cookie forwarding, error parsing into `ApiError`, a
1266
1287
  `401 → unauthorized` event stream and safe transport retry.
1267
1288
 
1268
1289
  ```ts
1269
- import { createHttpClient } from 'stitchkit'
1290
+ import { contractEndpointMatchers, createHttpClient } from 'stitchkit'
1291
+ import { publicAuth } from '../shared/contracts'
1270
1292
 
1271
- const http = createHttpClient({ baseUrl: '/api' })
1293
+ const http = createHttpClient({
1294
+ baseUrl: '/api',
1295
+ suppressUnauthorizedFor: contractEndpointMatchers(publicAuth, ['complete', 'verify']),
1296
+ })
1272
1297
  ```
1273
1298
 
1274
1299
  The returned `ConfiguredHttpClient` keeps that `baseUrl` as a readonly public
@@ -1284,13 +1309,22 @@ without repeating transport configuration.
1284
1309
  | `credentials` | `'include'` | fetch credentials mode |
1285
1310
  | `retry` | 2× GET, network errors only | transport retry policy |
1286
1311
  | `headers` | — | extra headers — an object, or a function re-run per request |
1287
- | `authEndpoints` | `['auth/']` | paths that should **not** emit `unauthorized` on 401 |
1312
+ | `suppressUnauthorizedFor` | `[]` | exact contract-derived operation matchers whose expected 401 does not emit `unauthorized` |
1288
1313
  | `parseError` | built-in | map an error body to `{ code, message, details, hint }` |
1289
1314
  | `trace` | `false` | emit a W3C `traceparent` header on every request |
1290
1315
 
1291
1316
  `headers` as a function is the hook for runtime tokens — a bearer token or any
1292
1317
  short-lived credential — re-evaluated on every request.
1293
1318
 
1319
+ Expected 401 policy is explicit and contract-driven. Select individual
1320
+ operations with `contractEndpointMatchers(contract, ['login'])`, or omit the
1321
+ second argument to select every HTTP operation in that contract. Pass the same
1322
+ `ContractClientConfig` as the typed client when routes use a static or dynamic
1323
+ `pathPrefix`; dynamic matchers require `stripPrefixKeys`, so the helper can
1324
+ compile the prefix structure without a concrete tenant id. Matching is exact by
1325
+ path segments, including params and trailing wildcards — a shared prefix never
1326
+ suppresses a neighbouring protected endpoint.
1327
+
1294
1328
  `trace: true` mints a fresh root trace per request. The stitchkit server
1295
1329
  [continues an inbound `traceparent`](./observability.md#trace-context), so the
1296
1330
  browser call, the HTTP handler and every nested tool call share one trace id
@@ -1316,12 +1350,22 @@ await api.update({ id: '1', name: 'M' }) // PUT /users/1 body: { name }
1316
1350
  await api.delete({ id: '1' }) // DELETE /users/1
1317
1351
  ```
1318
1352
 
1353
+ An explicit contract `HEAD` operation is exposed like any other typed method.
1354
+ Because HEAD endpoints are `rawResponse`, it resolves to the untouched
1355
+ `Response`, giving the caller direct access to status and headers without JSON
1356
+ parsing:
1357
+
1358
+ ```ts
1359
+ const response = await assets.head({ name: 'clip.mp4' })
1360
+ console.log(response.headers.get('content-length'))
1361
+ ```
1362
+
1319
1363
  Each call takes one argument object. The client routes each field by the
1320
1364
  contract:
1321
1365
 
1322
1366
  - a **path param** (`:id`) is substituted into the URL,
1323
- - a terminal wildcard (`/*`) consumes the `'*'` field and preserves its path
1324
- segments (`{ '*': 'a/b' }` → `/a/b`, not `/%2Fa%2Fb` or a query field),
1367
+ - a named terminal wildcard (`/*filePath`) consumes that field and preserves its
1368
+ path segments (`{ filePath: 'a/b' }` → `/a/b`, not `/%2Fa%2Fb` or a query field),
1325
1369
  - for `GET` / `DELETE`, the remaining fields become the **query string**
1326
1370
  (arrays become repeated keys),
1327
1371
  - for `POST` / `PUT` / `PATCH`, they become the **JSON body**,
@@ -1340,6 +1384,32 @@ await api.users.list()
1340
1384
  await api.posts.create({ title: 'Hi' })
1341
1385
  ```
1342
1386
 
1387
+ When contracts use different path-prefix rules, route the same registry by the
1388
+ scope already declared in each contract. An array composes contracts with
1389
+ different scopes into one logical namespace:
1390
+
1391
+ ```ts
1392
+ const api = createScopedClients(
1393
+ { auth: [publicAuth, authenticatedAuth], widgets },
1394
+ http,
1395
+ {
1396
+ public: {},
1397
+ user: {},
1398
+ tenant: {
1399
+ stripPrefixKeys: ['tenantId'],
1400
+ pathPrefix: ({ tenantId }) => `tenants/${tenantId}`,
1401
+ },
1402
+ },
1403
+ )
1404
+
1405
+ await api.auth.login()
1406
+ await api.auth.me()
1407
+ await api.widgets.list({ tenantId: 't1' })
1408
+ ```
1409
+
1410
+ Every scope present in the registry needs a config. Unknown/missing scopes and
1411
+ duplicate method names inside a composed namespace fail before a request runs.
1412
+
1343
1413
  `createClients` builds one typed client per contract from a registry — list the
1344
1414
  contracts once, get the whole API typed. It accepts the same optional scoped
1345
1415
  config as `createClient`, so a whole registry can share one resource prefix:
@@ -1375,13 +1445,20 @@ const src = mediaUrls.file({
1375
1445
  thumbnail: true,
1376
1446
  })
1377
1447
 
1448
+ // Body and multipart fields are intentionally absent: only the URL-bound
1449
+ // params are accepted by URL functions.
1450
+ const formAction = mediaUrls.replace({ tenantId: 't_123', fileId: 'f_456' })
1451
+ const beaconUrl = mediaUrls.track({ tenantId: 't_123' })
1452
+
1378
1453
  const urls = createUrlBuilders({ media, exports }, http)
1379
1454
  ```
1380
1455
 
1381
- Only HTTP-exposed, non-multipart `GET` endpoints appear on a URL builder. Raw
1382
- response GET endpoints are included, so downloads and streams stay
1383
- contract-driven. Path and scoped-prefix keys are consumed by the path; remaining
1384
- GET input becomes the query string, including repeated keys for arrays.
1456
+ Every HTTP-exposed endpoint appears on a URL builder, including body, multipart
1457
+ and raw-response operations. Path and scoped-prefix keys are consumed by the
1458
+ path. `GET` and `DELETE` input becomes the query string, including repeated keys
1459
+ for arrays; body-method input and multipart files are not URL arguments and are
1460
+ never serialized into the URL. Passing such a field through an untyped boundary
1461
+ fails before a URL is returned.
1385
1462
 
1386
1463
  Building a URL is synchronous and performs no request, auth event, header
1387
1464
  resolution or output validation. A `ConfiguredHttpClient` created by
@@ -1417,7 +1494,7 @@ The HTTP client emits events your app can react to globally:
1417
1494
 
1418
1495
  ```ts
1419
1496
  const unsubscribe = http.subscribe((event) => {
1420
- if (event.type === 'unauthorized') redirectToLogin() // a 401 outside authEndpoints
1497
+ if (event.type === 'unauthorized') redirectToLogin() // a non-suppressed 401
1421
1498
  if (event.type === 'network_error') showOfflineBanner()
1422
1499
  })
1423
1500
 
@@ -1544,7 +1621,8 @@ The same contract that drives the HTTP API also drives AI tooling. An endpoint
1544
1621
  exposed on `MCP` becomes a [Model Context Protocol](https://modelcontextprotocol.io)
1545
1622
  tool — callable from Claude, Cursor and other MCP clients. An endpoint exposed on
1546
1623
  `AGENT` becomes a [Vercel AI SDK](https://sdk.vercel.ai) tool — callable from an
1547
- agent loop. No tool is hand-written; both come from the contract.
1624
+ agent loop. Contract operations are never re-described by hand; pathless
1625
+ runtime operations use one framework definition shared by both tool transports.
1548
1626
 
1549
1627
  ## Which endpoints become tools
1550
1628
 
@@ -1604,6 +1682,38 @@ and after, compare.
1604
1682
  > HTTP-only shows up in the list, which is the one check that catches it however
1605
1683
  > many places the line was forgotten.
1606
1684
 
1685
+ ## In-process calls — `createToolInvoker`
1686
+
1687
+ When one application operation dispatches to a contract tool, do not mount an
1688
+ AI SDK `ToolSet` and call its transport adapter manually. Compile an in-process
1689
+ invoker once and call the shared framework runner directly:
1690
+
1691
+ ```ts
1692
+ import { createToolInvoker } from 'stitchkit/tools'
1693
+
1694
+ const invoker = createToolInvoker(services, {
1695
+ transport: 'AGENT', // required exposure policy
1696
+ source: 'internal', // default; audit names the real call source
1697
+ context: { identity },
1698
+ lifecycle,
1699
+ hooks,
1700
+ })
1701
+
1702
+ const result = await invoker.invoke('update_entity', args)
1703
+ if (!result.ok) throw new AppError(result.code, 'Nested tool call failed')
1704
+ ```
1705
+
1706
+ `transport` is required and uses the exact existing `MCP`, `AGENT` or `CLI`
1707
+ exposure rules; there is no internal bypass mode. The immutable name lookup is
1708
+ compiled once. Every invocation—including parallel and recursive calls—gets a
1709
+ fresh tool-call context and runs the same extension resolution, input/output
1710
+ validation, lifecycle, hooks and output-strip reporter as mounted tools.
1711
+
1712
+ `invoke` returns the canonical discriminated `ToolResult`, not an AI SDK or MCP
1713
+ presentation envelope. An unknown name throws `AppError('NOT_FOUND')` before
1714
+ the runner because there is no operation identity against which hooks could run.
1715
+ Duplicate and provider-invalid names fail when the invoker is created.
1716
+
1607
1717
  ## MCP — `createMcpHandler`
1608
1718
 
1609
1719
  `createMcpHandler` builds a complete Streamable-HTTP MCP server as a single
@@ -1981,6 +2091,7 @@ the merged `params` + `input`. `context` is merged into every tool handler's
1981
2091
  | `lifecycle` | `beforeHandle` / `afterHandle` — the tool-side auth gate (see [Guarding tools](#guarding-tools--lifecycle)) |
1982
2092
  | `hooks` | tool-call observability hooks — `afterToolCall` fires on every result |
1983
2093
  | `extend` | add extra args resolved before the handler runs (see below) |
2094
+ | `runtimeTools` | framework-managed pathless operations from `defineRuntimeTool` |
1984
2095
 
1985
2096
  `lifecycle` works the same as on the MCP server — without it an agent tool call
1986
2097
  bypasses the HTTP `beforeHandle` auth gate. Pass your `createAuthHook` result.
@@ -2031,51 +2142,78 @@ as the HTTP prefix param — see
2031
2142
  so one handler serves both surfaces. Pair `extend` with `lifecycle` (your
2032
2143
  `createAuthHook`) so the tool call is still scope-gated.
2033
2144
 
2034
- ## Native multimodal tools
2145
+ ## Pathless runtime tools and multimodal results
2035
2146
 
2036
- Contract tools return JSON. A native tool can return MCP text/image/audio/
2037
- resource content directly while still using stitchkit's input/output validation,
2038
- isolated per-call context, lifecycle/RBAC and tool hooks:
2147
+ Use `defineRuntimeTool` for an operation that has no HTTP path but still needs
2148
+ the same validation, isolated per-call context, lifecycle/RBAC and hooks as a
2149
+ contract tool. The handler returns one transport-neutral, schema-validated
2150
+ result. Optional presentation callbacks map that result to MCP content and AI
2151
+ SDK model output without coupling the handler to either SDK:
2039
2152
 
2040
2153
  ```ts
2041
- import { createMcpHandler } from 'stitchkit/tools'
2154
+ import { createMcpHandler, defineRuntimeTool, mountAgent } from 'stitchkit/tools'
2042
2155
  import { z } from 'zod'
2043
2156
 
2157
+ const renderPreview = defineRuntimeTool({
2158
+ name: 'render_preview',
2159
+ description: 'Render and inspect a preview',
2160
+ identity: {
2161
+ serviceName: 'mediaTools',
2162
+ action: 'renderPreview',
2163
+ scope: 'admin',
2164
+ method: 'POST',
2165
+ },
2166
+ input: z.object({ prompt: z.string() }),
2167
+ output: z.object({ assetId: z.string(), imageBase64: z.string() }),
2168
+ handler: async ({ input }) => renderAndSave(input.prompt),
2169
+ present: {
2170
+ mcp: (output) => ({
2171
+ content: [
2172
+ { type: 'image', data: output.imageBase64, mimeType: 'image/png' },
2173
+ { type: 'text', text: output.assetId },
2174
+ ],
2175
+ }),
2176
+ agent: (output) => ({
2177
+ type: 'content',
2178
+ value: [{
2179
+ type: 'file',
2180
+ data: { type: 'data', data: output.imageBase64 },
2181
+ mediaType: 'image/png',
2182
+ filename: `${output.assetId}.png`,
2183
+ }],
2184
+ }),
2185
+ },
2186
+ })
2187
+
2044
2188
  const handleMcp = createMcpHandler({
2045
2189
  serverInfo: { name: 'my-app', version: '1.0.0' },
2046
2190
  auth,
2047
2191
  services: [service],
2048
2192
  lifecycle: { beforeHandle: authHook },
2049
2193
  hooks: audit.toolCall,
2050
- nativeTools: ({ registerTool }, identity) => {
2051
- registerTool({
2052
- name: 'render_preview',
2053
- description: 'Render and inspect a preview',
2054
- identity: {
2055
- serviceName: 'mediaTools',
2056
- action: 'renderPreview',
2057
- scope: 'admin',
2058
- method: 'POST',
2059
- },
2060
- input: z.object({ prompt: z.string() }),
2061
- output: z.object({ assetId: z.string() }),
2062
- handler: async ({ input, traceId }) => ({
2063
- content: [
2064
- { type: 'image', data: await renderBase64(input.prompt), mimeType: 'image/png' },
2065
- { type: 'text', text: `trace: ${traceId}` },
2066
- ],
2067
- structuredContent: { assetId: await saveAsset(identity) },
2068
- }),
2069
- })
2070
- },
2194
+ nativeTools: ({ registerTool }) => registerTool(renderPreview),
2195
+ })
2196
+
2197
+ const agentTools = mountAgent([service], {
2198
+ runtimeTools: [renderPreview],
2199
+ lifecycle: { beforeHandle: authHook },
2200
+ hooks: audit.toolCall,
2071
2201
  })
2072
2202
  ```
2073
2203
 
2074
- The configured identity becomes the hook/lifecycle `OperationIdentity` and the
2075
- tool `RequestEvent` (`serviceName`, `action`, `httpMethod`). A native operation
2076
- has no HTTP route, so no fake `path` is added to that identity. If `output` is
2077
- declared, stitchkit parses `structuredContent` with it after `afterHandle`; all
2078
- other MCP fields and content blocks are preserved.
2204
+ `transports` defaults to `['MCP', 'AGENT']`; set an explicit subset when an
2205
+ operation belongs on only one surface. The configured identity becomes the
2206
+ hook/lifecycle `OperationIdentity` and the tool `RequestEvent`
2207
+ (`serviceName`, `action`, `httpMethod`). A runtime operation has no HTTP route,
2208
+ so no fake `path` is added.
2209
+
2210
+ When `output` is declared, Stitchkit validates the neutral handler result after
2211
+ `afterHandle`. MCP owns `structuredContent` and `isError`: a presenter supplies
2212
+ only rich `content`/metadata, while the framework inserts the validated
2213
+ structured result and normalises failures. The Agent adapter uses the AI SDK's
2214
+ official `toModelOutput` callback, so `execute` and application UI keep the
2215
+ neutral output while the model receives text/file content. Presentation
2216
+ callbacks require an output schema.
2079
2217
 
2080
2218
  The MCP registration uses an identity carrier: the SDK advertises the compiled
2081
2219
  JSON Schema but forwards the raw object into Stitchkit. Input failures therefore
@@ -2369,7 +2507,7 @@ It returns a handle with three pieces, all wired into `createServer`:
2369
2507
  createServer({
2370
2508
  services,
2371
2509
  websocket: socket.websocket, // → Bun.serve websocket handlers
2372
- rawRoutes: [socket.route], // ready-made /socket.io/* route
2510
+ rawRoutes: [socket.route], // ready-made /socket.io/*socketPath route
2373
2511
  })
2374
2512
 
2375
2513
  // elsewhere — broadcast:
@@ -2380,7 +2518,7 @@ socket.io.emit('note:created', note)
2380
2518
  |--------------|---------|
2381
2519
  | `io` | the typed Socket.IO server — attach `connection` handlers, broadcast |
2382
2520
  | `websocket` | Bun WebSocket handlers — pass to `createServer({ websocket })` |
2383
- | `route` | the `/socket.io/*` raw route — pass to `createServer({ rawRoutes })` |
2521
+ | `route` | the `/socket.io/*socketPath` raw route — pass to `createServer({ rawRoutes })` |
2384
2522
 
2385
2523
  `SocketIOServerConfig` also takes `path`, `transports`, `pingTimeout` and
2386
2524
  `pingInterval`. For anything else socket.io's `ServerOptions` exposes, use the
@@ -2556,9 +2694,16 @@ updater per entity:
2556
2694
  import { createEntityCacheHandlers } from 'stitchkit/react'
2557
2695
 
2558
2696
  const widgetCache = createEntityCacheHandlers<Widget>({
2559
- getId: (w) => w.id,
2560
- listKey: ['widgets'],
2561
- detailKey: (id) => ['widgets', id],
2697
+ getId: (widget) => widget.id,
2698
+ getListItemId: (widget) => widget.id,
2699
+ toListItem: (widget) => widget,
2700
+ list: {
2701
+ key: ['widgets'],
2702
+ shape: 'paginated',
2703
+ createAt: 'start',
2704
+ updateMissing: 'skip',
2705
+ },
2706
+ detailKey: (event) => ['widgets', event.id],
2562
2707
  })
2563
2708
 
2564
2709
  createCacheBridge({ socket, queryClient, handlers: {
@@ -2568,10 +2713,52 @@ createCacheBridge({ socket, queryClient, handlers: {
2568
2713
  }})
2569
2714
  ```
2570
2715
 
2571
- It patches stitchkit's `Paginated<T>` list envelope (plain or an infinite list
2572
- of pages) and honours the same `isFresh` echo guard. It deliberately does **not**
2573
- flatten pages or add a `useAllX` surface — flattening stays in the component;
2574
- this only keeps the cache correct.
2716
+ The `list.shape` discriminant supports `array`, `paginated`, `infinite-array`
2717
+ and `infinite-paginated`. Every mutation preserves the surrounding envelope,
2718
+ page metadata and `pageParams`; an infinite create changes only the selected
2719
+ edge page (`createAt: 'start' | 'end'`). Creates are deduplicated across every
2720
+ cached page. `updateMissing` makes an absent update explicitly skip or insert.
2721
+
2722
+ The event entity may be richer than a list row. Keep the full value in detail
2723
+ cache, project it for lists, and provide the same comparator the backend uses:
2724
+
2725
+ ```ts
2726
+ const memberCache = createEntityCacheHandlers<Member, MemberListItem>({
2727
+ getId: (member) => member.id,
2728
+ getListItemId: (item) => item.id,
2729
+ toListItem: (member) => ({
2730
+ id: member.id,
2731
+ name: member.name,
2732
+ joinedAt: member.joinedAt,
2733
+ }),
2734
+ list: {
2735
+ key: (event) => {
2736
+ if (event.type !== 'deleted') {
2737
+ return ['workspaces', event.entity.workspaceId, 'members']
2738
+ }
2739
+ if ('workspaceId' in event.payload) {
2740
+ return ['workspaces', event.payload.workspaceId, 'members']
2741
+ }
2742
+ throw new Error('A scoped delete must carry its entity')
2743
+ },
2744
+ shape: 'array',
2745
+ createAt: 'start',
2746
+ updateMissing: 'skip',
2747
+ compare: (left, right) => left.joinedAt.localeCompare(right.joinedAt),
2748
+ },
2749
+ detailKey: (event) => ['members', event.id],
2750
+ })
2751
+ ```
2752
+
2753
+ Static `QueryKey` values remain the short path. A key factory receives a typed
2754
+ `created | updated | deleted` event, so scoped keys can use the full entity or
2755
+ deleted payload without guessing. The same resolved detail key drives the
2756
+ `isFresh` echo guard. Shape checks also leave neighbouring detail caches alone
2757
+ when a list key is intentionally used as a partial query-key prefix.
2758
+
2759
+ The helper deliberately does **not** flatten pages, update totals, derive a
2760
+ sort order or replace arbitrary `setQueryData` logic. Those are application
2761
+ policies; this helper only applies declared CRUD semantics.
2575
2762
 
2576
2763
  ## Raw binary lane (Bun)
2577
2764
 
@@ -2645,7 +2832,7 @@ Notes:
2645
2832
  - **Bun-only.** On Node, Socket.IO attaches to the `node:http.Server` `upgrade`
2646
2833
  event (`serveNode({ socket })`); a raw lane there is a separate upgrade
2647
2834
  handler, not this composition. See [ADR 0020](../decisions/0020-raw-websocket-lane.md).
2648
- - The upgrade path must not collide with `/socket.io/*`.
2835
+ - The upgrade path must not collide with `/socket.io/*socketPath`.
2649
2836
  - The tuning (`maxPayloadLength`, `idleTimeout`, `backpressureLimit`, …) is
2650
2837
  global — keep `idleTimeout` ≥ Socket.IO needs (> 2 × `pingInterval`).
2651
2838
  - For high throughput, handle backpressure in the raw lane: `ws.send()` returns
@@ -2705,6 +2892,19 @@ gives you the contract and the metadata (`idempotent`, the open `source` tag,
2705
2892
 
2706
2893
  # Auth & errors
2707
2894
 
2895
+ Client-side login/session operations may legitimately answer `401`. Declare
2896
+ that policy with contract-owned matchers instead of path strings:
2897
+
2898
+ ```ts
2899
+ const http = createHttpClient({
2900
+ baseUrl,
2901
+ suppressUnauthorizedFor: contractEndpointMatchers(publicAuth, ['login', 'verify']),
2902
+ })
2903
+ ```
2904
+
2905
+ Only the selected operations suppress the global `unauthorized` event; a 401
2906
+ from any neighbouring protected route still signals session expiry.
2907
+
2708
2908
  stitchkit carries no domain model — it does not know what a user is. What it
2709
2909
  provides is the *control flow*: a scope on every endpoint, one hook that
2710
2910
  enforces it, and one error model shared by every transport. The identity and
@@ -3857,6 +4057,109 @@ current one *up to* your target, and apply each snippet.
3857
4057
  (`STITCH_ERROR_STATUS`, `serveFile`, `scopePrefixes`, `afterToolCall`'s
3858
4058
  `MethodDef`, `maxUploadBytes`) are available to adopt, not required.
3859
4059
 
4060
+ ## Unreleased breaking migrations
4061
+
4062
+ Entity cache handlers now require the cached list shape and CRUD policies. Move
4063
+ `listKey` under `list`, make detail keys event-aware, and state the list-item
4064
+ identity/projection explicitly:
4065
+
4066
+ ```ts
4067
+ // before
4068
+ createEntityCacheHandlers<Entity>({
4069
+ getId,
4070
+ listKey: ['entities'],
4071
+ detailKey: (id) => ['entities', id],
4072
+ })
4073
+
4074
+ // after
4075
+ createEntityCacheHandlers<Entity, EntityListItem>({
4076
+ getId,
4077
+ getListItemId: (item) => item.id,
4078
+ toListItem: (entity) => ({ id: entity.id, name: entity.name }),
4079
+ list: {
4080
+ key: ['entities'],
4081
+ shape: 'paginated',
4082
+ createAt: 'start',
4083
+ updateMissing: 'skip',
4084
+ },
4085
+ detailKey: (event) => ['entities', event.id],
4086
+ })
4087
+ ```
4088
+
4089
+ Choose `array`, `paginated`, `infinite-array` or `infinite-paginated` to match
4090
+ the actual cached data. A dynamic `list.key` / `detailKey` receives a
4091
+ discriminated event and can derive scoped keys from the created/updated entity
4092
+ or deleted payload. Add `compare` only when the backend has a canonical order;
4093
+ the framework does not guess it or mutate pagination metadata.
4094
+
4095
+ Protected native MCP operations now use the transport-neutral runtime tool
4096
+ definition. Return the schema-owned value from the handler and move MCP content
4097
+ or metadata into `present.mcp`; `structuredContent` and `isError` are
4098
+ framework-owned:
4099
+
4100
+ ```ts
4101
+ // before
4102
+ registerTool({ input, output, handler: async () => ({
4103
+ content: [{ type: 'image', data, mimeType: 'image/png' }],
4104
+ structuredContent: { assetId },
4105
+ }) })
4106
+
4107
+ // after
4108
+ const preview = defineRuntimeTool({
4109
+ name: 'render_preview', description, identity, input, output,
4110
+ handler: async () => ({ assetId, data }),
4111
+ present: {
4112
+ mcp: (result) => ({
4113
+ content: [{ type: 'image', data: result.data, mimeType: 'image/png' }],
4114
+ }),
4115
+ },
4116
+ })
4117
+ nativeTools: ({ registerTool }) => registerTool(preview)
4118
+ ```
4119
+
4120
+ The removed `NativeMcp*` types have no aliases. Use `RuntimeToolDefinition`,
4121
+ `RuntimeToolIdentity`, `RuntimeToolHandlerContext` and
4122
+ `RuntimeMcpPresentation`. The same definition can now be passed to
4123
+ `mountAgent(services, { runtimeTools: [preview] })`; add `present.agent` only
4124
+ when the model needs rich text/file content instead of the neutral JSON result.
4125
+
4126
+ Trailing wildcards must be named consistently across the path and params schema:
4127
+
4128
+ ```ts
4129
+ // before
4130
+ path: '/app/:slug/*'
4131
+ params: z.object({ slug: z.string(), '*': z.string() })
4132
+ ctx.params['*']
4133
+ api.app({ slug: 'foo', '*': 'a/b' })
4134
+
4135
+ // after
4136
+ path: '/app/:slug/*filePath'
4137
+ params: z.object({ slug: z.string(), filePath: z.string() })
4138
+ ctx.params.filePath
4139
+ api.app({ slug: 'foo', filePath: 'a/b' })
4140
+ ```
4141
+
4142
+ Bare wildcards have no compatibility alias; raw routes use the same named form.
4143
+
4144
+ ### Expected-401 matchers
4145
+
4146
+ `HttpClientConfig.authEndpoints` is removed. Replace manual path prefixes with
4147
+ the operations whose 401 response is expected:
4148
+
4149
+ ```ts
4150
+ // before
4151
+ createHttpClient({ baseUrl, authEndpoints: ['/api/auth/'] })
4152
+
4153
+ // after
4154
+ createHttpClient({
4155
+ baseUrl,
4156
+ suppressUnauthorizedFor: contractEndpointMatchers(authContract, ['login', 'verify']),
4157
+ })
4158
+ ```
4159
+
4160
+ There is no implicit `/auth/` suppression. Omit `suppressUnauthorizedFor` when
4161
+ every 401 should emit the global `unauthorized` event.
4162
+
3860
4163
  ## The 0.37 migration
3861
4164
 
3862
4165
  Tool presentation is no longer an executable Zod parser. Replace the removed
@@ -4052,17 +4355,25 @@ The browser-and-server entrypoint. Re-exports everything from
4052
4355
  |--------|------|---------|
4053
4356
  | `createClient` | function | build a typed client from a contract — [guide](../guide/client.md#createclient) |
4054
4357
  | `createClients` | function | build one exact typed client per contract from a registry; accepts the same scoped config and transports as `createClient` |
4055
- | `createUrlBuilder` | function | build synchronous browser-native URLs for one contract's HTTP GET endpoints — [guide](../guide/client.md#contract-url-builders) |
4358
+ | `createScopedClients` | function | build one registry routed by contract scope; arrays compose contracts into one namespace |
4359
+ | `ScopeClientConfigs` | _type_ | per-scope client routing configuration consumed by `createScopedClients` |
4360
+ | `ScopedClientRegistry` | _type_ | exact composed registry returned by `createScopedClients` |
4361
+ | `ClientRegistryValue` | _type_ | one contract or a contract array composing one client namespace |
4362
+ | `ClientContract` | _type_ | HTTP-client-compatible contract value used by scoped registries |
4363
+ | `RegistryScope` | _type_ | contract-scope union inferred from a scoped client registry value |
4364
+ | `createUrlBuilder` | function | build synchronous browser-native URLs for all HTTP endpoints; body methods accept URL-bound args only — [guide](../guide/client.md#contract-url-builders) |
4056
4365
  | `createUrlBuilders` | function | build one exact URL builder per contract in a registry |
4057
4366
  | `UrlBuilderConfig` | _type_ | explicit `{ baseUrl }` source for a URL builder |
4058
4367
  | `ClientConfig` | _type_ | config for `createClient`'s bare-fetch mode (2nd arg, no `HttpClient`) |
4059
4368
  | `ContractClientConfig` | _type_ | per-tenant / resource-scoped client config — dynamic `pathPrefix` + `stripPrefixKeys` ([guide](../guide/client.md#contractclientconfig--per-tenant--resource-scoped-clients)) |
4369
+ | `contractEndpointMatchers` | function | compile exact pathname matchers for selected HTTP contract operations and expected-401 policy |
4060
4370
  | `PathPrefixArgs` | _type_ | required string-valued keys exposed to a typed dynamic `pathPrefix` callback |
4061
4371
  | `createHttpClient` | function | the Ky-based HTTP transport — [guide](../guide/client.md#createhttpclient) |
4062
4372
  | `ApiError` | class | a non-2xx response, with `code` / `status` / `details` / `hint` |
4063
4373
  | `HttpClient` | _type_ | the transport interface `createClient` builds on |
4064
4374
  | `ConfiguredHttpClient` | _type_ | a framework-created `HttpClient` carrying its readonly `baseUrl` for URL builders |
4065
4375
  | `HttpClientConfig` | _type_ | config for `createHttpClient` |
4376
+ | `UnauthorizedMatcher` | _type_ | exact `(pathname) => boolean` policy accepted by `suppressUnauthorizedFor` |
4066
4377
  | `RequestOptions` | _type_ | per-call options — params, timeout, response type |
4067
4378
  | `HeaderProvider` | _type_ | static or per-request headers |
4068
4379
  | `ApiEvent` | _type_ | a client event — `unauthorized` / `network_error` / `logout` |
@@ -4113,11 +4424,12 @@ from the root `stitchkit`.
4113
4424
  | `ContractDef` | _type_ | a defined contract |
4114
4425
  | `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` and `meta` (a default every endpoint shallow-merges over) |
4115
4426
  | `EndpointDef` | _type_ | a single endpoint definition |
4427
+ | `HeadEndpointDef` | _type_ | explicit HTTP-only, bodyless `HEAD` endpoint definition |
4116
4428
  | `EndpointResponseMeta` | _type_ | static success metadata declared by an HTTP-only typed-data endpoint |
4117
4429
  | `ResponseMetadata` | _type_ | per-request outbound collector exposed as `ctx.response` only for a `responseMeta` endpoint |
4118
4430
  | `HttpSuccessStatus` | _type_ | supported declared 2xx success statuses |
4119
4431
  | `BodyHttpSuccessStatus` | _type_ | supported 2xx statuses excluding bodyless 204/205 |
4120
- | `HttpMethod` | _type_ | `GET \| POST \| PUT \| PATCH \| DELETE` |
4432
+ | `HttpMethod` | _type_ | `GET \| HEAD \| POST \| PUT \| PATCH \| DELETE` |
4121
4433
  | `Transport` | _type_ | `HTTP \| MCP \| AGENT \| CLI` |
4122
4434
  | `TransportSource` | _type_ | `http \| mcp \| agent \| cli` — the value of `ctx.source` |
4123
4435
  | `RuntimeContext` | _type_ | the loose context seen by transport and hooks |
@@ -4127,7 +4439,7 @@ from the root `stitchkit`.
4127
4439
  | `TypedHttpClient` | _type_ | the typed client, HTTP endpoints only (`= ScopedHttpClient<C, unknown>`) |
4128
4440
  | `ScopedHttpClient` | _type_ | a client whose `stripPrefixKeys` become required args ([guide](../guide/multi-tenant.md)) |
4129
4441
  | `ScopedEndpointFn` | _type_ | one method's signature with the consumed keys folded in |
4130
- | `TypedUrlBuilder` | _type_ | one contract's HTTP, non-multipart GET endpoints as synchronous URL functions |
4442
+ | `TypedUrlBuilder` | _type_ | one contract's HTTP endpoints as synchronous, method-aware URL functions |
4131
4443
  | `ScopedUrlBuilder` | _type_ | a URL builder whose scoped-prefix keys are required method arguments |
4132
4444
  | `ScopedUrlFn` | _type_ | one URL method's signature with scoped-prefix keys folded in |
4133
4445
  | `MultipartFile` | _type_ | a `multipart` file field — `Blob \| FileDescriptor` |
@@ -4372,6 +4684,8 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
4372
4684
  | `mountMcp` | function | add contract tools to an existing `McpServer` — [guide](../guide/mcp-and-agents.md#mountmcp) |
4373
4685
  | `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
4374
4686
  | `mountAgent` | function | a Vercel AI SDK `ToolSet` from a service — [guide](../guide/mcp-and-agents.md#ai-agents--mountagent) |
4687
+ | `defineRuntimeTool` | function | define one validated pathless operation for MCP, Agent or both — [guide](../guide/mcp-and-agents.md#pathless-runtime-tools-and-multimodal-results) |
4688
+ | `createToolInvoker` | function | compile an exposure-aware in-process dispatcher over the canonical tool runner — [guide](../guide/mcp-and-agents.md#in-process-calls--createtoolinvoker) |
4375
4689
  | `createCli` | function | a command-line program from contracts — [guide](../guide/cli.md) (also on `stitchkit/cli`) |
4376
4690
  | `createToolkit` | function | context-typed tool mounts — [guide](../guide/cli.md#typed-context) |
4377
4691
  | `mountViewFile` | function | a native multimodal "view file" MCP tool |
@@ -4387,10 +4701,17 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
4387
4701
  | `McpSchemaValidationConfig` | _type_ | shared `{ policy, requireTypedProperties, allowUntyped, requirePortableFormats, allowFormats }` profile |
4388
4702
  | `ValidateMcpSchemasConfig` | _type_ | standalone validation profile plus `services`, `extend`, flattening and logger |
4389
4703
  | `NativeMcpRegistrar` | _type_ | protected `registerTool` plus explicit unprotected `rawServer` access |
4390
- | `NativeMcpToolDefinition` | _type_ | native name, operation identity, Zod schemas and MCP-result handler |
4391
- | `NativeMcpOperationIdentity` | _type_ | `{ serviceName, action, scope?, method, meta? }` for native lifecycle/audit |
4392
- | `NativeMcpHandlerContext` | _type_ | runtime context with the definition's parsed native input |
4393
- | `NativeMcpResult` | _type_ | MCP content result, with typed `structuredContent` when output is declared |
4704
+ | `RuntimeToolDefinition` | _type_ | transport-neutral pathless operation with identity, schemas, handler and optional presenters |
4705
+ | `RuntimeToolDefinitionBase` | _type_ | common name, identity, input, exposure and MCP metadata fields |
4706
+ | `RuntimeToolDefinitionWithOutput` | _type_ | runtime definition whose handler and presenters share a validated output type |
4707
+ | `RuntimeToolDefinitionWithoutOutput` | _type_ | runtime definition without output validation or presentation callbacks |
4708
+ | `RuntimeToolIdentity` | _type_ | `{ serviceName, action, scope?, method, meta? }` for runtime lifecycle/audit |
4709
+ | `RuntimeToolHandlerContext` | _type_ | runtime context with the definition's parsed input |
4710
+ | `RuntimeToolOutput` | _type_ | output inferred from a runtime tool's optional Zod schema |
4711
+ | `RuntimeToolPresenters` | _type_ | optional MCP and AI SDK `toModelOutput` presentation callbacks |
4712
+ | `RuntimeMcpPresentation` | _type_ | MCP content/metadata result without framework-owned `structuredContent` or `isError` |
4713
+ | `RuntimeAgentModelOutput` | _type_ | AI SDK model-facing text/JSON/content output returned by `present.agent` |
4714
+ | `RuntimeToolTransport` | _type_ | runtime exposure: `'MCP' \| 'AGENT'` |
4394
4715
  | `AgentMountConfig` | _type_ | config for `mountAgent` |
4395
4716
  | `AgentContext` | _type_ | the context merged into agent tool handlers |
4396
4717
  | `CliConfig` | _type_ | config for `createCli` |
@@ -4406,6 +4727,9 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
4406
4727
  | `ToolErrorOptions` | _type_ | `{ toolName, error, context, endpoint }` for a thrown handler-path value |
4407
4728
  | `ErrorHintFn` | _type_ | `(toolName, errorCode) => string \| null` — a per-tool recovery hint, shared by every mount |
4408
4729
  | `ToolResult` | _type_ | the result of one tool call |
4730
+ | `ToolInvoker` | _type_ | immutable compiled dispatcher (`names` + `invoke`) |
4731
+ | `ToolInvokerConfig` | _type_ | exposure policy, context, lifecycle, hooks and runner options |
4732
+ | `ToolInvokerTransport` | _type_ | invoker exposure policy: `MCP \| AGENT \| CLI` |
4409
4733
  | `ToolCallContext` | _type_ | the context every tool hook receives — `{ source }` plus whatever the mount's `context` added |
4410
4734
  | `ViewFileOptions` | _type_ | options for `mountViewFile` |
4411
4735
  | `McpAnnotations` | _type_ | MCP annotations on a media result |
@@ -4544,6 +4868,10 @@ and `react-query-kit` peers.
4544
4868
  | `createEntityCacheHandlers` | function | created/updated/deleted cache handlers for one entity — [guide](../guide/realtime.md#entity-cache-handlers) |
4545
4869
  | `EntityCacheConfig` | _type_ | config for `createEntityCacheHandlers` |
4546
4870
  | `EntityCacheHandlers` | _type_ | the `{ created, updated, deleted }` handlers it returns |
4871
+ | `EntityCacheEvent` | _type_ | discriminated created/updated/deleted input for dynamic cache keys |
4872
+ | `EntityCacheKey` | _type_ | static `QueryKey` or event-aware key factory |
4873
+ | `EntityCacheListConfig` | _type_ | list shape, scoped key, insertion/missing-update policy and comparator |
4874
+ | `EntityCacheListShape` | _type_ | `array \| paginated \| infinite-array \| infinite-paginated` |
4547
4875
  | `DeletedPayload` | _type_ | a `deleted` event payload — the entity or a bare `{ id }` |
4548
4876
  | `CursorQueryConfig` | _type_ | config for `createCursorQuery` |
4549
4877
  | `CacheBridge` | _type_ | the `createCacheBridge` handle |