stitchkit 0.53.2 → 0.55.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 (125) hide show
  1. package/README.md +4 -1
  2. package/dist/browser/socket-io.d.ts +11 -1
  3. package/dist/browser/socket-io.d.ts.map +1 -1
  4. package/dist/cli.d.ts +2 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +15 -8
  7. package/dist/contract/define.d.ts +27 -0
  8. package/dist/contract/define.d.ts.map +1 -1
  9. package/dist/contract/file-ref.d.ts +13 -0
  10. package/dist/contract/file-ref.d.ts.map +1 -0
  11. package/dist/contract/index.d.ts +2 -1
  12. package/dist/contract/index.d.ts.map +1 -1
  13. package/dist/contract/index.js +21 -17
  14. package/dist/files/boundary.d.ts +52 -0
  15. package/dist/files/boundary.d.ts.map +1 -0
  16. package/dist/files.d.ts +3 -0
  17. package/dist/files.d.ts.map +1 -0
  18. package/dist/files.js +16 -0
  19. package/dist/{index-c78w2y09.js → index-0gv7k0ra.js} +515 -156
  20. package/dist/index-1bx83sw4.js +4 -0
  21. package/dist/index-1k16zv57.js +269 -0
  22. package/dist/index-6k1937bx.js +20 -0
  23. package/dist/{index-bmnhqtya.js → index-7eq9va6e.js} +1 -4
  24. package/dist/{index-yxpe3phd.js → index-cby4ar3v.js} +1 -18
  25. package/dist/index-h60df7rj.js +110 -0
  26. package/dist/{index-yez419px.js → index-js7yexng.js} +19 -6
  27. package/dist/index-k4dftwf7.js +66 -0
  28. package/dist/{index-pdv1mjjr.js → index-npye5kt7.js} +30 -59
  29. package/dist/{index-yydqk4fh.js → index-pzc32v75.js} +90 -3
  30. package/dist/{index-e1es808r.js → index-qh4xevf5.js} +1 -1
  31. package/dist/{index-gvha32jh.js → index-qx84bqzk.js} +2 -2
  32. package/dist/{index-pfqjb5xy.js → index-rgd18hx3.js} +23 -5
  33. package/dist/index-sbdmyz75.js +18 -0
  34. package/dist/{index-173tvqcp.js → index-v4bbq3p4.js} +9 -114
  35. package/dist/index.js +162 -35
  36. package/dist/node.js +30 -27
  37. package/dist/observability/event.d.ts +2 -16
  38. package/dist/observability/event.d.ts.map +1 -1
  39. package/dist/observability/index.js +28 -25
  40. package/dist/react.js +2 -2
  41. package/dist/realtime/contract.d.ts +9 -0
  42. package/dist/realtime/contract.d.ts.map +1 -1
  43. package/dist/realtime/index.d.ts +2 -1
  44. package/dist/realtime/index.d.ts.map +1 -1
  45. package/dist/realtime/request.d.ts +21 -0
  46. package/dist/realtime/request.d.ts.map +1 -0
  47. package/dist/realtime/socket.d.ts +3 -0
  48. package/dist/realtime/socket.d.ts.map +1 -1
  49. package/dist/remote.d.ts +9 -0
  50. package/dist/remote.d.ts.map +1 -0
  51. package/dist/remote.js +79 -0
  52. package/dist/server/context-contribution.d.ts +10 -0
  53. package/dist/server/context-contribution.d.ts.map +1 -0
  54. package/dist/server/context.d.ts.map +1 -1
  55. package/dist/server/index.d.ts +2 -1
  56. package/dist/server/index.d.ts.map +1 -1
  57. package/dist/server/index.js +133 -88
  58. package/dist/server/lifecycle.d.ts +4 -0
  59. package/dist/server/lifecycle.d.ts.map +1 -0
  60. package/dist/server/middleware/auth.d.ts +35 -7
  61. package/dist/server/middleware/auth.d.ts.map +1 -1
  62. package/dist/server/process-signal-common.d.ts +6 -0
  63. package/dist/server/process-signal-common.d.ts.map +1 -0
  64. package/dist/server/process-signals.d.ts.map +1 -1
  65. package/dist/server/types.d.ts +1 -1
  66. package/dist/server/types.d.ts.map +1 -1
  67. package/dist/testing/surface-conformance.d.ts +47 -0
  68. package/dist/testing/surface-conformance.d.ts.map +1 -0
  69. package/dist/testing/surface-manifest.d.ts +156 -0
  70. package/dist/testing/surface-manifest.d.ts.map +1 -0
  71. package/dist/testing.d.ts +2 -0
  72. package/dist/testing.d.ts.map +1 -1
  73. package/dist/testing.js +366 -3
  74. package/dist/tools/agent.d.ts.map +1 -1
  75. package/dist/tools/async-operation.d.ts +168 -0
  76. package/dist/tools/async-operation.d.ts.map +1 -0
  77. package/dist/tools/cli-args.d.ts +1 -1
  78. package/dist/tools/cli-args.d.ts.map +1 -1
  79. package/dist/tools/cli-command.d.ts +29 -0
  80. package/dist/tools/cli-command.d.ts.map +1 -0
  81. package/dist/tools/cli-format.d.ts +2 -2
  82. package/dist/tools/cli-format.d.ts.map +1 -1
  83. package/dist/tools/cli-wait.d.ts +11 -2
  84. package/dist/tools/cli-wait.d.ts.map +1 -1
  85. package/dist/tools/cli.d.ts +13 -6
  86. package/dist/tools/cli.d.ts.map +1 -1
  87. package/dist/tools/define-download-tool.d.ts +18 -0
  88. package/dist/tools/define-download-tool.d.ts.map +1 -0
  89. package/dist/tools/define-upload-tool.d.ts +16 -0
  90. package/dist/tools/define-upload-tool.d.ts.map +1 -0
  91. package/dist/tools/define-view-file-tool.d.ts +10 -0
  92. package/dist/tools/define-view-file-tool.d.ts.map +1 -0
  93. package/dist/tools/define-wait-tool.d.ts +22 -0
  94. package/dist/tools/define-wait-tool.d.ts.map +1 -0
  95. package/dist/tools/download-core.d.ts +20 -0
  96. package/dist/tools/download-core.d.ts.map +1 -0
  97. package/dist/tools/execute.d.ts +3 -1
  98. package/dist/tools/execute.d.ts.map +1 -1
  99. package/dist/tools/lifecycle.d.ts +4 -0
  100. package/dist/tools/lifecycle.d.ts.map +1 -0
  101. package/dist/tools/mcp-round.d.ts +0 -1
  102. package/dist/tools/mcp-round.d.ts.map +1 -1
  103. package/dist/tools/mcp-stdio-signals.d.ts +33 -0
  104. package/dist/tools/mcp-stdio-signals.d.ts.map +1 -0
  105. package/dist/tools/mount-download.d.ts +10 -4
  106. package/dist/tools/mount-download.d.ts.map +1 -1
  107. package/dist/tools/mount-upload.d.ts +4 -2
  108. package/dist/tools/mount-upload.d.ts.map +1 -1
  109. package/dist/tools/mount-wait.d.ts.map +1 -1
  110. package/dist/tools/native-definition.d.ts +15 -0
  111. package/dist/tools/native-definition.d.ts.map +1 -0
  112. package/dist/tools/runtime-tool.d.ts +5 -4
  113. package/dist/tools/runtime-tool.d.ts.map +1 -1
  114. package/dist/tools/surface.d.ts.map +1 -1
  115. package/dist/tools/upload-core.d.ts +4 -0
  116. package/dist/tools/upload-core.d.ts.map +1 -0
  117. package/dist/tools/view-file.d.ts +84 -23
  118. package/dist/tools/view-file.d.ts.map +1 -1
  119. package/dist/tools/wait-core.d.ts +16 -2
  120. package/dist/tools/wait-core.d.ts.map +1 -1
  121. package/dist/tools.d.ts +12 -3
  122. package/dist/tools.d.ts.map +1 -1
  123. package/dist/tools.js +827 -495
  124. package/llms-full.txt +720 -53
  125. package/package.json +10 -2
package/llms-full.txt CHANGED
@@ -33,7 +33,7 @@ realtime, `@tanstack/react-query` + `react-query-kit` for React). See
33
33
 
34
34
  ## Entrypoints
35
35
 
36
- stitchkit ships nine entrypoints. Each is import-safe for one environment —
36
+ stitchkit ships eleven entrypoints. Each is import-safe for one environment —
37
37
  keeping server-only code (`Bun.serve`, the MCP SDK) out of browser bundles.
38
38
 
39
39
  | Import | Use in | Holds |
@@ -44,12 +44,15 @@ keeping server-only code (`Bun.serve`, the MCP SDK) out of browser bundles.
44
44
  | `stitchkit/node` | server (Node ≥ 22) | `serveNode` + the runtime-agnostic core — the Node mirror of `/server` |
45
45
  | `stitchkit/tools` | server | `createMcpHandler`, `mountMcp`, `mountAgent`, the OAuth provider, native tools |
46
46
  | `stitchkit/cli` | server | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
47
+ | `stitchkit/remote` | browser **and** server | peer-free `implementRemote` for thin HTTP proxy processes |
48
+ | `stitchkit/files` | server (Bun or Node) | peer-free managed local-file boundary |
47
49
  | `stitchkit/observability` | server | request/tool event projections — `createObservability`, trace context, sanitisation |
48
50
  | `stitchkit/testing` | tests on Bun or Node | in-process generated clients over a real Fetch handler, without a TCP port |
49
51
  | `stitchkit/react` | browser | `createCursorQuery`, `createCacheBridge` |
50
52
 
51
53
  Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
52
- code adds `stitchkit/server` (or `stitchkit/node` on Node) and `stitchkit/tools`.
54
+ code adds `stitchkit/server` (or `stitchkit/node` on Node) and opts into
55
+ `stitchkit/tools`, `stitchkit/remote` or `stitchkit/files` by capability.
53
56
  The full export list of each is in the [API reference](../api/reference.md).
54
57
 
55
58
  ## Project layout
@@ -2364,8 +2367,9 @@ must not be used as authorization input.
2364
2367
  ### Modern discovery, routing and cache hints
2365
2368
 
2366
2369
  Stitchkit forwards the negotiated protocol era and validated MCP request data
2367
- to `RequestEvent.mcp`; untrusted routing headers never become application
2368
- identity. The SDK validates `MCP-Protocol-Version`, JSON-RPC shape and routing
2370
+ to both `RequestEvent.mcp` and the current managed call's typed `context.mcp`;
2371
+ untrusted routing headers never become application identity. The SDK validates
2372
+ `MCP-Protocol-Version`, JSON-RPC shape and routing
2369
2373
  metadata before lifecycle or tool handlers run. Framework-owned tool and
2370
2374
  resource registration preserves declaration order across HTTP and stdio. Tool
2371
2375
  manifests preserve mount order; `listToolNames` is a sorted diagnostics view.
@@ -2380,6 +2384,30 @@ finite `surfaces` registry when identities share a provably immutable surface.
2380
2384
  Stitchkit never advertises list-change or subscription capabilities
2381
2385
  without an implementation.
2382
2386
 
2387
+ ### Typed MCP call metadata
2388
+
2389
+ Contract handlers, runtime-tool handlers/factories, lifecycle and tool hooks all
2390
+ see the same optional `McpCallContext` for the current invocation:
2391
+
2392
+ ```ts
2393
+ const sourceLabel = context.mcp?.clientInfo?.name ?? context.source
2394
+
2395
+ context.mcp?.era // 'modern' | 'legacy'
2396
+ context.mcp?.method // validated MCP method
2397
+ context.mcp?.toolName // resolved framework tool name
2398
+ context.mcp?.protocolVersion // when supplied by a modern host
2399
+ context.mcp?.clientInfo // { name, version } when validated
2400
+ context.mcp?.outcome // multi-round attempt outcome
2401
+ context.mcp?.round // multi-round attempt number
2402
+ ```
2403
+
2404
+ The field is absent on HTTP, Agent, CLI and bring-your-own transports. A legacy
2405
+ MCP call exposes `era: 'legacy'` but does not invent modern client information.
2406
+ `clientInfo` is the host's self-description: use it for labels or analytics,
2407
+ never for authentication, RBAC, tenant selection or rate limiting. Verified
2408
+ identity still comes from `auth`; the `context(auth)` callback remains the
2409
+ application-context factory and receives no protocol metadata.
2410
+
2383
2411
  ### Multi-round tool input (`input_required`)
2384
2412
 
2385
2413
  An MCP-only operation can require typed user input before its final side effect:
@@ -2598,7 +2626,7 @@ client (Claude Desktop, Claude Code, Cursor, Codex), on the user's machine, so
2598
2626
  it can reach the local filesystem.
2599
2627
 
2600
2628
  ```ts
2601
- import { createStdioMcpServer } from 'stitchkit/tools'
2629
+ import { bindStdioProcessSignals, createStdioMcpServer } from 'stitchkit/tools'
2602
2630
 
2603
2631
  const stdio = await createStdioMcpServer({
2604
2632
  serverInfo: { name: 'my-app', version: '1.0.0' },
@@ -2606,8 +2634,16 @@ const stdio = await createStdioMcpServer({
2606
2634
  services: [usersService],
2607
2635
  })
2608
2636
 
2609
- // During graceful shutdown:
2610
- await stdio.close()
2637
+ const signals = bindStdioProcessSignals(stdio, {
2638
+ onClose: () => stopWorkers(),
2639
+ onComplete: () => { process.exitCode = 0 },
2640
+ onError: (_phase, error) => {
2641
+ console.error(error) // stderr only — stdout is JSON-RPC
2642
+ process.exitCode = 1
2643
+ },
2644
+ })
2645
+
2646
+ await signals.promise
2611
2647
  ```
2612
2648
 
2613
2649
  A stdio server is a single process serving one client, so `auth` is a value (or
@@ -2620,6 +2656,14 @@ through the official stdio adapter. Use `legacy: 'reject'` for a modern-only
2620
2656
  binary. Each invocation builds a fresh server; the returned handle owns
2621
2657
  transport shutdown.
2622
2658
 
2659
+ Signal binding is explicit: `createStdioMcpServer` installs no global process
2660
+ listeners by itself. The first `SIGINT`/`SIGTERM` starts exactly one official
2661
+ `close()` chain. Stdio has no force primitive, so a later signal restores the
2662
+ OS default disposition instead of reporting a fictional forced result.
2663
+ `signals.close()` removes an idle binding; the framework never calls
2664
+ `process.exit()` or chooses the exit code. See
2665
+ [deployment lifecycle](./testing-and-deployment.md#stdio-process-signals).
2666
+
2623
2667
  Both transports build the server through the shared `buildMcpServer` — same
2624
2668
  contract/runtime pipeline, same surface selection, context, hooks, raw escape
2625
2669
  hatch and instructions.
@@ -2736,7 +2780,8 @@ through the contract's typed client:
2736
2780
 
2737
2781
  ```ts
2738
2782
  import { createHttpClient } from 'stitchkit'
2739
- import { createStdioMcpServer, implementRemote } from 'stitchkit/tools'
2783
+ import { implementRemote } from 'stitchkit/remote'
2784
+ import { createStdioMcpServer } from 'stitchkit/tools'
2740
2785
 
2741
2786
  const http = createHttpClient({
2742
2787
  baseUrl: 'https://api.example.com',
@@ -2943,7 +2988,9 @@ const countRecords = knowledgeTools.define({
2943
2988
  `defineRuntimeTool` when tools do not share a context schema or identity.
2944
2989
 
2945
2990
  `transports` defaults to `['MCP', 'AGENT']`; set an explicit subset when an
2946
- operation belongs on only one surface. The configured identity becomes the
2991
+ operation belongs on only one surface. Include `'CLI'` explicitly to reuse the
2992
+ definition through `createCli({ runtimeTools })`; adding CLI support never
2993
+ widens existing definitions silently. The configured identity becomes the
2947
2994
  hook/lifecycle `OperationIdentity` and the tool `RequestEvent`
2948
2995
  (`serviceName`, `action`, `httpMethod`). A runtime operation has no HTTP route,
2949
2996
  so no fake `path` is added.
@@ -2960,12 +3007,186 @@ The MCP registration uses an identity carrier: the SDK advertises the compiled
2960
3007
  JSON Schema but forwards the raw object into Stitchkit. Input failures therefore
2961
3008
  use the same validation, lifecycle and hook path as contract tools.
2962
3009
 
3010
+ ### Managed wait, download, upload and view-file
3011
+
3012
+ The common imperative shapes have typed definition factories. They produce
3013
+ ordinary runtime-tool definitions, so one value can enter `runtimeTools` on MCP
3014
+ and Agent and receives the same lifecycle, hooks, context, cancellation,
3015
+ collision checks and introspection as any `defineRuntimeTool` operation:
3016
+
3017
+ ```ts
3018
+ import {
3019
+ defineDownloadTool,
3020
+ defineUploadTool,
3021
+ defineViewFileTool,
3022
+ defineWaitTool,
3023
+ } from 'stitchkit/tools'
3024
+ import { createManagedFileBoundary } from 'stitchkit/files'
3025
+ import { z } from 'zod'
3026
+
3027
+ const files = await createManagedFileBoundary({ root: '/srv/job-media' })
3028
+
3029
+ const waitForJob = defineWaitTool({
3030
+ name: 'wait_for_job',
3031
+ description: 'Wait for a job to finish',
3032
+ identity: { serviceName: 'jobs', action: 'waitForJob', scope: 'user' },
3033
+ input: z.object({ id: z.string() }),
3034
+ state: z.object({ id: z.string(), status: z.enum(['RUNNING', 'DONE']) }),
3035
+ poll: ({ id }, context) => getJob(id, context.signal),
3036
+ done: (state) => state.status === 'DONE',
3037
+ backoff: [2, 3, 5],
3038
+ defaultTimeout: 120,
3039
+ })
3040
+
3041
+ const downloadResult = defineDownloadTool({
3042
+ name: 'download_result',
3043
+ description: 'Download a completed result',
3044
+ identity: { serviceName: 'jobs', action: 'downloadResult', scope: 'user' },
3045
+ input: z.object({ id: z.string() }),
3046
+ resolveUrl: ({ id }) => resolveResultUrl(id),
3047
+ files,
3048
+ pathFromInput: ({ id }) => `results/${id}.bin`,
3049
+ })
3050
+
3051
+ const uploadInput = defineUploadTool({
3052
+ name: 'upload_input',
3053
+ description: 'Upload a local input file',
3054
+ identity: { serviceName: 'jobs', action: 'uploadInput', scope: 'user' },
3055
+ output: z.object({ url: z.url() }),
3056
+ files,
3057
+ upload: (source, context) => uploadBytes(source.bytes, context.signal),
3058
+ })
3059
+
3060
+ const viewResult = defineViewFileTool({
3061
+ name: 'view_result',
3062
+ description: 'Inspect generated media',
3063
+ identity: { serviceName: 'jobs', action: 'viewResult', scope: 'user' },
3064
+ // Local files are disabled when files is omitted. URL fetches retain SSRF
3065
+ // protection; use allowPrivateHosts only for an explicitly trusted network.
3066
+ files,
3067
+ })
3068
+
3069
+ const runtimeTools = [waitForJob, downloadResult, uploadInput, viewResult]
3070
+ ```
3071
+
3072
+ Managed factories return neutral validated data. Expected failures go through
3073
+ the standard failed `ToolResult` path rather than masquerading as a successful
3074
+ payload with `isError`. Wait sleep is abort-aware, guarded downloads receive the
3075
+ active signal, and upload implementations receive it on their typed context.
3076
+ Managed view-file accepts one or several paths/URLs, retains successful
3077
+ multimodal content beside structured per-item `errors`, and charges every item
3078
+ to one total 20 MB batch budget. Its MCP and Agent presenters are built in;
3079
+ remote fetch cancellation follows the active call signal.
3080
+
3081
+ The older `mountWait`, `mountDownload`, `mountUpload` and `mountViewFile`
3082
+ functions remain
3083
+ intentional raw MCP adapters with their text-envelope behavior. Use them under
3084
+ `rawTools` only when opting out of managed policy is deliberate; they share the
3085
+ same neutral mechanics but do not gain lifecycle or hooks. The raw view-file
3086
+ adapter preserves its content-only MCP envelope while using the same bounded
3087
+ batch operation.
3088
+
3089
+ ### Async-operation protocol
3090
+
3091
+ For a complete long-running operation, define the linked runtime-only surface
3092
+ once. Stitchkit owns transport names, validation and wait mechanics; the
3093
+ application still owns storage, execution, retries and domain states:
3094
+
3095
+ ```ts
3096
+ import {
3097
+ createAsyncOperationSnapshotSchema,
3098
+ defineAsyncOperation,
3099
+ } from 'stitchkit/tools'
3100
+
3101
+ const id = z.object({ id: z.string() })
3102
+ const state = z.object({
3103
+ phase: z.enum(['pending', 'running', 'succeeded']),
3104
+ value: z.string().optional(),
3105
+ })
3106
+ const snapshot = createAsyncOperationSnapshotSchema({
3107
+ failure: z.object({ code: z.string() }),
3108
+ progress: z.object({ current: z.number(), total: z.number() }),
3109
+ })
3110
+
3111
+ const exportOperation = defineAsyncOperation({
3112
+ mode: 'runtime-only',
3113
+ name: 'export',
3114
+ description: 'Export project data',
3115
+ identity: { serviceName: 'exports', action: 'export', scope: 'user' },
3116
+ startInput: z.object({ projectId: z.string() }),
3117
+ id,
3118
+ state,
3119
+ snapshot,
3120
+ start: ({ projectId }) => enqueueExport(projectId),
3121
+ authorize: ({ id }, capability, ctx) => authorizeExport(id, capability, ctx),
3122
+ inspect: ({ id }, ctx) => readExport(id, ctx.signal),
3123
+ classify: (job) => ({ phase: job.phase }),
3124
+ result: {
3125
+ output: z.object({ value: z.string() }),
3126
+ handler: (job) => ({ value: job.value ?? '' }),
3127
+ },
3128
+ })
3129
+
3130
+ const runtimeTools = exportOperation.runtimeTools
3131
+ ```
3132
+
3133
+ Every follow-up repeats `authorize`; an opaque id is never authority. Aborting
3134
+ `wait` only stops waiting and never calls optional domain `cancel`. A
3135
+ contract-backed operation uses `bindContractAsyncOperation` with literal methods
3136
+ from a dedicated existing contract, so it creates no second HTTP router or
3137
+ duplicate schemas. Capability keys are narrowed by schema-compatible TypeScript
3138
+ input/output types. At runtime, the binder additionally requires the exact same
3139
+ id and snapshot Zod instances; declare each once and reuse it:
3140
+
3141
+ ```ts
3142
+ import { defineContract } from 'stitchkit/contract'
3143
+ import { bindContractAsyncOperation } from 'stitchkit/tools'
3144
+ import { z } from 'zod'
3145
+
3146
+ const operationId = z.object({ id: z.string() })
3147
+ const operationSnapshot = z.object({
3148
+ phase: z.enum(['pending', 'running', 'succeeded', 'failed', 'cancelled']),
3149
+ })
3150
+
3151
+ const operations = defineContract(
3152
+ { prefix: 'exports' },
3153
+ {
3154
+ start: {
3155
+ method: 'POST', path: '/', desc: 'Start export',
3156
+ input: z.object({ projectId: z.string() }), output: operationId,
3157
+ },
3158
+ status: {
3159
+ method: 'POST', path: '/status', desc: 'Read export status',
3160
+ input: operationId, output: operationSnapshot,
3161
+ },
3162
+ wait: {
3163
+ method: 'POST', path: '/wait', desc: 'Wait for export',
3164
+ input: operationId, output: operationSnapshot,
3165
+ },
3166
+ },
3167
+ )
3168
+
3169
+ const operationHandlers = {
3170
+ start: () => ({ id: 'example' }),
3171
+ status: (): z.output<typeof operationSnapshot> => ({ phase: 'pending' }),
3172
+ wait: (): z.output<typeof operationSnapshot> => ({ phase: 'succeeded' }),
3173
+ }
3174
+
3175
+ const bound = bindContractAsyncOperation({
3176
+ mode: 'contract-backed',
3177
+ contract: operations,
3178
+ capabilities: { start: 'start', status: 'status', wait: 'wait' },
3179
+ handlers: operationHandlers,
3180
+ })
3181
+ ```
3182
+
2963
3183
  ### Explicit raw SDK registration
2964
3184
 
2965
3185
  `rawTools` is deliberately named as an escape hatch. A tool registered there
2966
3186
  does **not** receive stitchkit schema policy, lifecycle, per-call context or
2967
- hooks. The built-in `mountViewFile` helper remains raw for callers that choose
2968
- that boundary; it fetches media with SSRF and path-traversal defenses:
3187
+ hooks. The built-in `mountViewFile`, `mountWait`, `mountDownload` and
3188
+ `mountUpload` helpers remain raw for callers that choose that boundary; the
3189
+ media helper fetches with SSRF and path-traversal defenses:
2969
3190
 
2970
3191
  ```ts
2971
3192
  import { createMcpHandler, mountViewFile } from 'stitchkit/tools'
@@ -2974,13 +3195,15 @@ const handleMcp = createMcpHandler({
2974
3195
  serverInfo: { name: 'my-app', version: '1.0.0' },
2975
3196
  auth,
2976
3197
  services: [service],
2977
- rawTools: (server) => mountViewFile(server, { baseDir: '/srv/uploads' }),
3198
+ rawTools: (server) => mountViewFile(server, { files }),
2978
3199
  })
2979
3200
  ```
2980
3201
 
2981
3202
  Use raw registration only when opting out is intentional. For a protected
2982
- `view_file`, define it with `defineRuntimeTool`, include it in `runtimeTools`,
2983
- and call the exported `resolveMedia` core from its handler.
3203
+ `view_file`, use `defineViewFileTool` in `runtimeTools`; it supplies the
3204
+ canonical schemas, shared batch budget, structured partial failures and both
3205
+ multimodal presenters. `resolveMedia` remains the single-item core for custom
3206
+ operations whose contract intentionally differs.
2984
3207
 
2985
3208
  ## Introspecting the complete tool surface
2986
3209
 
@@ -3061,11 +3284,13 @@ identical. Write the handler once; it serves every surface.
3061
3284
 
3062
3285
  # CLI
3063
3286
 
3064
- The same contract that drives the HTTP API, MCP tools and agent tools also
3065
- drives a command-line program. `createCli` turns contract methods into commands
3066
- `myapp generate "a fox" --wait`, `myapp models list --json | jq …` — run
3067
- through the very same validation, auth gate and error model as every other
3068
- surface (HTTP MCP agent CLI, [ADR 0014](../decisions/0014-tool-http-parity.md)).
3287
+ The same contract and managed runtime tools that drive HTTP, MCP and agent
3288
+ surfaces can also drive a command-line program. `createCli` composes their
3289
+ commands with explicitly local binary commands in one router and help tree.
3290
+ Managed commands `myapp generate "a fox" --wait`,
3291
+ `myapp models list --json | jq …` — run through the same validation, auth gate
3292
+ and error model as every other managed surface (HTTP ≡ MCP ≡ agent ≡ CLI,
3293
+ [ADR 0014](../decisions/0014-tool-http-parity.md)).
3069
3294
 
3070
3295
  It exists for what the other three surfaces cannot do: a generation kicked off
3071
3296
  with `Bash(run_in_background)` that notifies on exit, a `SKILL.md` that shells
@@ -3086,8 +3311,10 @@ the CLI never silently turns your existing API tools into shell commands.
3086
3311
  ```
3087
3312
 
3088
3313
  A fresh contract shows **zero** CLI commands until methods opt in — that is the
3089
- design, not a bug. The command name is the tool name `toolName` if set, else a
3090
- verb-aware name from the method + prefix (`list` `list_widgets`, `get`
3314
+ design, not a bug. A pathless runtime definition follows the same rule: it must
3315
+ explicitly include `'CLI'` in `transports`; the undefined default remains
3316
+ `['MCP', 'AGENT']`. The command name is the tool name — `toolName` if set, else
3317
+ a verb-aware name from the method + prefix (`list` → `list_widgets`, `get` →
3091
3318
  `get_widget`), not a literal `prefix_key`.
3092
3319
 
3093
3320
  ## A minimal CLI
@@ -3105,10 +3332,16 @@ await createCli({
3105
3332
  name: 'myapp',
3106
3333
  version: '1.0.0',
3107
3334
  services: [catalogService, generateService],
3108
- auth: process.env.MYAPP_TOKEN, // resolved ONCE, like a stdio MCP server
3335
+ resolveAuth: () => resolveToken(process.env.MYAPP_TOKEN), // lazy, at most once
3109
3336
  })
3110
3337
  ```
3111
3338
 
3339
+ `createCli({ signal })` and its wait loop honor an explicit caller
3340
+ `AbortSignal`. Stitchkit does not install a process-global SIGINT handler for
3341
+ ordinary CLI programs: if desired, the application binds SIGINT to an
3342
+ `AbortController` and passes its signal. Aborting wait stops polling only; it
3343
+ does not cancel the underlying job.
3344
+
3112
3345
  ```json
3113
3346
  // package.json
3114
3347
  { "bin": { "myapp": "./dist/cli.js" } }
@@ -3117,6 +3350,81 @@ await createCli({
3117
3350
  `stitchkit/cli` pulls in neither the MCP SDK nor `ai`, so a CLI binary needs no
3118
3351
  MCP/agent peer dependencies.
3119
3352
 
3353
+ ## Pathless managed commands
3354
+
3355
+ Use `runtimeTools` when an application operation has no HTTP path but must keep
3356
+ the canonical operation identity, context, lifecycle/RBAC, hooks and
3357
+ introspection. It can share one definition with MCP and Agent while opting into
3358
+ CLI explicitly:
3359
+
3360
+ ```ts
3361
+ import { createCli } from 'stitchkit/cli'
3362
+ import { defineUploadTool } from 'stitchkit/tools'
3363
+ import { z } from 'zod'
3364
+
3365
+ const uploadInput = defineUploadTool({
3366
+ name: 'upload_input',
3367
+ description: 'Upload one local input file',
3368
+ identity: { serviceName: 'jobs', action: 'uploadInput', scope: 'user' },
3369
+ output: z.object({ url: z.url() }),
3370
+ transports: ['MCP', 'AGENT', 'CLI'],
3371
+ upload: (path, context) => uploadFile(path, context.signal),
3372
+ })
3373
+
3374
+ await createCli({
3375
+ name: 'myapp',
3376
+ version: '1.0.0',
3377
+ runtimeTools: [uploadInput],
3378
+ })
3379
+ ```
3380
+
3381
+ `services` and `runtimeTools` may each be static arrays or factories receiving
3382
+ the resolved identity. A runtime-only CLI is valid; `stitchkit/cli` still pulls
3383
+ in neither MCP nor AI peers. Contract/runtime name collisions and reserved
3384
+ option fields fail through the same checks before managed dispatch.
3385
+
3386
+ ## Native binary commands
3387
+
3388
+ Login, self-update, diagnostics, integration setup and shell completion belong
3389
+ to the executable, not to HTTP/MCP/Agent. Define them with `defineCliCommand`:
3390
+
3391
+ ```ts
3392
+ import { createCli, defineCliCommand } from 'stitchkit/cli'
3393
+ import { z } from 'zod'
3394
+
3395
+ const login = defineCliCommand({
3396
+ name: 'login',
3397
+ description: 'Store credentials for later managed commands',
3398
+ input: z.object({ token: z.string() }),
3399
+ output: z.object({ configured: z.boolean() }),
3400
+ handler: async ({ input }) => {
3401
+ await saveToken(input.token)
3402
+ return { configured: true }
3403
+ },
3404
+ })
3405
+
3406
+ await createCli({
3407
+ name: 'myapp',
3408
+ version: '1.0.0',
3409
+ commands: [login],
3410
+ resolveAuth: loadStoredIdentity,
3411
+ services: (identity) => createRemoteServices(identity),
3412
+ })
3413
+ ```
3414
+
3415
+ Native commands receive only typed `input`, parsed global `options` and the
3416
+ configured stdout/stderr writers. They reuse help, argv/stdin parsing,
3417
+ validation, dry-run, error envelopes and exit mapping, but deliberately have no
3418
+ fake service/action/scope/method identity, lifecycle or tool hooks and never
3419
+ appear in MCP/Agent manifests.
3420
+
3421
+ `--version`, a selected native command and its command help run before
3422
+ `resolveAuth`, services, context or runtime-tool factories. Top-level help is
3423
+ also credential-free when managed surfaces are static. A dynamic factory must
3424
+ resolve identity to discover its command names; its collisions are checked at
3425
+ that resolution boundary. If eager global collision proof matters, keep the
3426
+ surface static.
3427
+
3120
3428
  ## Calling commands
3121
3429
 
3122
3430
  ```
@@ -3147,7 +3455,7 @@ same Zod schema an HTTP or MCP call does.
3147
3455
 
3148
3456
  | Flag | Effect |
3149
3457
  | --------------------- | ---------------------------------------------------------- |
3150
- | `--json` | Raw JSON on stdout for piping / scripts |
3458
+ | `--json` | Compact success/error JSON records for scripts |
3151
3459
  | `--wait` | Block-poll an async result to a terminal state |
3152
3460
  | `--wait-timeout <s>` | Override the `--wait` timeout |
3153
3461
  | `--output-dir <dir>` | Download result media into a directory |
@@ -3155,10 +3463,18 @@ same Zod schema an HTTP or MCP call does.
3155
3463
  | `--dry-run` | Print the resolved call without executing |
3156
3464
  | `--help`, `-h` | Usage — top-level or per-command flag table |
3157
3465
 
3158
- stdout carries the result; errors and progress go to stderr, so `--json` stays
3159
- pipeable and `2>/dev/null` stays clean. The process exit code carries the error
3160
- class (`0` ok, `VALIDATION_ERROR 1`, `UNAUTHORIZED 2`, `FORBIDDEN → 3`,
3161
- `NOT_FOUND 4`, …) override per app with `exitCodes`.
3466
+ stdout carries the result; structured errors and progress go to stderr. With
3467
+ `--json`, a success or structured failure is exactly one compact,
3468
+ newline-terminated JSON record on its respective stream; progress and CLI usage
3469
+ diagnostics remain ordinary stderr text. This keeps stdout pipeable and
3470
+ `2>/dev/null` clean. The process exit code carries the error class (`0` ok,
3471
+ `VALIDATION_ERROR → 1`, `UNAUTHORIZED → 2`, `FORBIDDEN → 3`, `NOT_FOUND → 4`,
3472
+ …) — override per app with `exitCodes`.
3473
+
3474
+ Per-command help derives the positional form from the same schema order as the
3475
+ argv parser. For example, a required `action` and optional `profile` render as
3476
+ `Usage: myapp skill <action> [profile] [--flags]`; the argument table also shows
3477
+ `<action> | --action` and `[profile] | --profile`. Boolean fields remain flags.
3162
3478
 
3163
3479
  ## `--wait` — background-friendly generation
3164
3480
 
@@ -3175,12 +3491,19 @@ await createCli({
3175
3491
  generate: {
3176
3492
  tool: 'get_generation',
3177
3493
  poll: (r) => (isRecord(r) && typeof r.id === 'string' ? { id: r.id } : null),
3178
- done: (r) => isRecord(r) && r.status === 'COMPLETED',
3494
+ done: (r) => isRecord(r) && ['COMPLETED', 'FAILED'].includes(String(r.status)),
3495
+ failed: (r) => isRecord(r) && r.status === 'FAILED',
3179
3496
  },
3180
3497
  },
3181
3498
  })
3182
3499
  ```
3183
3500
 
3501
+ `failed` is optional. When it matches either the initial result or a later poll,
3502
+ polling stops and the CLI emits `WAIT_FAILED` on stderr with a non-zero exit;
3503
+ the terminal payload is retained under `details.result`. `failed` is checked
3504
+ before `done`, so overlapping predicates fail closed. A failed poll tool call
3505
+ keeps its own error code, and an elapsed deadline remains `TIMEOUT`.
3506
+
3184
3507
  ```bash
3185
3508
  # foreground
3186
3509
  myapp generate "a fox" --wait --output-dir ./out
@@ -3201,7 +3524,7 @@ const authHook = createAuthHook({ /* resolve, resolveFromContext, rules */ })
3201
3524
  await createCli({
3202
3525
  name: 'myapp',
3203
3526
  version: '1.0.0',
3204
- auth: await resolveIdentityFromToken(process.env.MYAPP_TOKEN),
3527
+ resolveAuth: () => resolveIdentityFromToken(process.env.MYAPP_TOKEN),
3205
3528
  context: (identity) => ({ user: identity }), // resolveFromContext reads this
3206
3529
  lifecycle: { beforeHandle: authHook }, // same policy; HTTP wires it as authorize
3207
3530
  services,
@@ -3233,11 +3556,12 @@ await tools.createCli({
3233
3556
  })
3234
3557
  ```
3235
3558
 
3236
- ## Not in v1
3559
+ ## Remaining boundary
3237
3560
 
3238
- File-upload (`multipart`) endpoints are CLI-invisible, the same as on MCP /
3239
- agent a dedicated upload command (auto-uploading local paths) is future work.
3240
- Streaming (SSE) output is not yet piped to stdout.
3561
+ File-upload (`multipart`) contract endpoints remain CLI-invisible, the same as
3562
+ on MCP/Agent: their wire body is not a JSON tool form. Model file-oriented
3563
+ application behavior as a managed pathless command, or binary-only behavior as
3564
+ a native command. Streaming (SSE) output is not yet piped to stdout.
3241
3565
 
3242
3566
 
3243
3567
  ==============================================================================
@@ -3285,6 +3609,57 @@ runtime. A malformed inbound tuple never reaches the application handler;
3285
3609
  invalid outbound data throws before Socket.IO publishes it. Rejections call the
3286
3610
  optional `onRejected` hook with event, direction, phase and the Zod error.
3287
3611
 
3612
+ ### Protocol generations
3613
+
3614
+ For a distributed producer/consumer pair, put a literal generation first in the
3615
+ first payload object. Zod validates object fields in declaration order, so an
3616
+ incompatible peer is identified before the rest of its payload is interpreted:
3617
+
3618
+ ```ts
3619
+ const ReplicationMessage = z.object({
3620
+ v: z.literal(2),
3621
+ item: ReplicatedItem,
3622
+ })
3623
+
3624
+ const realtimeContract = defineRealtimeContract({
3625
+ serverToClient: {
3626
+ replicated: { args: z.tuple([ReplicationMessage]) },
3627
+ },
3628
+ clientToServer: {},
3629
+ })
3630
+ ```
3631
+
3632
+ The original `z.ZodError` is retained as `RealtimeRejectedEvent.error.cause`.
3633
+ Because event arguments are a tuple, the first payload's `v` path is
3634
+ `[0, 'v']` (not `['v']`):
3635
+
3636
+ ```ts
3637
+ import type { RealtimeRejectedEvent } from 'stitchkit'
3638
+ import { z } from 'zod'
3639
+
3640
+ function isProtocolGenerationMismatch(rejected: RealtimeRejectedEvent): boolean {
3641
+ const cause = rejected.error.cause
3642
+ if (!(cause instanceof z.ZodError)) return false
3643
+ const first = cause.issues[0]
3644
+ return first?.code === 'invalid_value'
3645
+ && first.path.length === 2
3646
+ && first.path[0] === 0
3647
+ && first.path[1] === 'v'
3648
+ }
3649
+
3650
+ const socket = createRealtimeClient(realtimeContract, {
3651
+ url,
3652
+ onRejected: (rejected) => {
3653
+ if (isProtocolGenerationMismatch(rejected)) schedulePeerUpgrade()
3654
+ else reportMalformedRealtimePayload(rejected)
3655
+ },
3656
+ })
3657
+ ```
3658
+
3659
+ A generation mismatch means “upgrade the peer”; another schema rejection means
3660
+ “fix the producer or payload”. This stays an application convention, not a core
3661
+ API: only the application knows which field denotes protocol compatibility.
3662
+
3288
3663
  ## Server — `createSocketIOServer`
3289
3664
 
3290
3665
  ```ts
@@ -3383,6 +3758,35 @@ socket.on('note:created', (note) => { /* typed note */ })
3383
3758
  socket.emit('room:join', 'general', ({ joined }) => { /* typed + validated */ })
3384
3759
  ```
3385
3760
 
3761
+ ### Request-response over realtime
3762
+
3763
+ For an event with an `ack` schema, `request()` is the Promise form of the same
3764
+ native Socket.IO acknowledgement. Arguments and the resolved value are inferred
3765
+ from the contract and validated on both sides:
3766
+
3767
+ ```ts
3768
+ const result = await socket.request(
3769
+ 'room:join',
3770
+ 'general',
3771
+ { timeoutMs: 5_000 },
3772
+ )
3773
+ // result: { joined: boolean }
3774
+ ```
3775
+
3776
+ An event without `ack` cannot be passed to `request()` by type. A disconnected
3777
+ client rejects immediately with `RealtimeRequestDisconnectedError`; an in-flight
3778
+ disconnect uses the same error; an elapsed native acknowledgement timeout uses
3779
+ `RealtimeRequestTimeoutError`. These are distinct stable classes and codes, so
3780
+ never parse Socket.IO error text. An invalid acknowledgement still fires the
3781
+ existing `onRejected` hook with phase `acknowledgement`, then rejects with
3782
+ `RealtimeRequestInvalidAcknowledgementError`.
3783
+
3784
+ `timeoutMs` must be finite and greater than zero. Use acknowledgements only for
3785
+ bounded request-response work. A job that runs for minutes, progress streaming,
3786
+ or resumable delivery should use separate correlated events or the async
3787
+ operation protocol; keeping one acknowledgement open is not a durable RPC/job
3788
+ transport.
3789
+
3386
3790
  ## Low-level transport
3387
3791
 
3388
3792
  `createSocketIOClient` remains the low-level Socket.IO transport wrapper for
@@ -3867,10 +4271,11 @@ The value of each `rules` entry, keyed by scope:
3867
4271
 
3868
4272
  - **`'public'`** — always passes; the identity is attached if present.
3869
4273
  - **`'authenticated'`** — any resolved identity passes; no identity ⇒ 401.
3870
- - **a function** `(identity, ctx) => boolean | Promise<boolean>` — a custom
4274
+ - **a function** `(identity, ctx) => boolean | contribution`, sync or async — a custom
3871
4275
  check. It receives request metadata and validated path params, so a
3872
4276
  resource-scoped rule can do a DB lookup. It cannot read `input` or files: the
3873
- body has deliberately not been consumed yet. May be async.
4277
+ body has deliberately not been consumed yet. `false` denies, `true` passes,
4278
+ and a returned plain object passes and contributes inferred handler fields.
3874
4279
 
3875
4280
  #### Resource-scoped rule — reading a path/prefix param
3876
4281
 
@@ -3900,6 +4305,30 @@ identity *and* any derived values (role, parent-id) on `ctx` for handlers. Read
3900
4305
  them back as `ctx.tenantRole` (typed `unknown` — narrow at the read site). The
3901
4306
  endpoints under the group declare `scope: 'tenant'`.
3902
4307
 
4308
+ When the async authorization lookup already produced handler data, return it
4309
+ instead of repeating the lookup in `inject` and a handwritten scope map:
4310
+
4311
+ ```ts
4312
+ const auth = createAuthHook({
4313
+ resolve: sessionResolver,
4314
+ rules: {
4315
+ project: async (user, ctx) => {
4316
+ const membership = await findMembership(user.id, String(ctx.projectId))
4317
+ return membership
4318
+ ? { userId: user.id, projectId: membership.projectId, role: membership.role }
4319
+ : false
4320
+ },
4321
+ },
4322
+ })
4323
+
4324
+ const implementFor = createScopedImplement<AuthScopes<typeof auth>>()
4325
+ ```
4326
+
4327
+ Contributions are merged only after full validation. Runtime-owned context keys,
4328
+ arrays, class instances, accessors, symbols, unsafe prototypes and partial proxy
4329
+ reads fail without mutating context. `false | object` contributes required
4330
+ fields; `true | object` correctly makes them optional.
4331
+
3903
4332
  ### `AuthHookConfig`
3904
4333
 
3905
4334
  | Field | Purpose |
@@ -4767,6 +5196,25 @@ observe. (This is also why it lives on `ToolCallHooks` rather than being an
4767
5196
  `Response`, which a tool call has no use for, and a whole `createServer` hooks
4768
5197
  object must stay assignable to `ToolLifecycle`.)
4769
5198
 
5199
+ ### Compose lifecycle policy explicitly
5200
+
5201
+ When auth, a feature policy and audit identity are independent modules, compose
5202
+ their existing hooks instead of forwarding each phase by hand:
5203
+
5204
+ ```ts
5205
+ import { composeLifecycleHooks } from 'stitchkit/server'
5206
+ import { composeToolLifecycle } from 'stitchkit/tools'
5207
+
5208
+ const httpLifecycle = composeLifecycleHooks(authLifecycle, featurePolicy, auditIdentity)
5209
+ const toolLifecycle = composeToolLifecycle(authHook, featureToolPolicy, auditToolIdentity)
5210
+ ```
5211
+
5212
+ Phases run left to right. `onRequest` stops on the first `Response`; `onError`
5213
+ falls through until a hook handles it; after transforms feed the current value
5214
+ forward and preserve it when a hook returns `undefined`. Throws remain owned by
5215
+ the existing outer error boundary, and the original context/signal identity is
5216
+ never replaced.
5217
+
4770
5218
  **Do not reach for `setRequestError` here.** It writes to the *request* context,
4771
5219
  which the built-in **tool** row does not read: a tool event takes
4772
5220
  `errorCode` / `errorMessage` / `errorDetail` from the `ToolResult`, and only
@@ -4882,6 +5330,38 @@ contract registry. Both helpers are Fetch-only: they construct ordinary
4882
5330
  absolute `Request` objects and call `createHandler` directly, so Bun and Node
4883
5331
  exercise the same framework pipeline.
4884
5332
 
5333
+ ### Transport conformance
5334
+
5335
+ `buildSurfaceManifest` snapshots actual HTTP topology, MCP/Agent/CLI names,
5336
+ CLI-only commands, extensions and canonical schema digests. Compare real runner
5337
+ discovery with `assertSurfaceDiscovery`, then use `runSurfaceProbes` only for
5338
+ the transports you explicitly provide:
5339
+
5340
+ ```ts
5341
+ import {
5342
+ assertSurfaceDiscovery,
5343
+ buildSurfaceManifest,
5344
+ runSurfaceProbes,
5345
+ } from 'stitchkit/testing'
5346
+
5347
+ const manifest = buildSurfaceManifest({ services, runtimeTools, cliCommands })
5348
+ assertSurfaceDiscovery(manifest, {
5349
+ openApi,
5350
+ MCP: (await mcpClient.listTools()).tools.map((tool) => tool.name),
5351
+ AGENT: Object.keys(agentTools),
5352
+ CLI: cliHelpNames,
5353
+ })
5354
+
5355
+ await runSurfaceProbes({
5356
+ probes,
5357
+ drivers: { HTTP: httpDriver, MCP: mcpDriver, AGENT: agentDriver, CLI: cliDriver },
5358
+ })
5359
+ ```
5360
+
5361
+ The kit never starts a server or invents credentials. Each probe declares its
5362
+ expected normalized outcome and has bounded timeout, setup/teardown and an
5363
+ AbortSignal. A missing driver is unsupported, not silently marked conformant.
5364
+
4885
5365
  ### Test handlers with raw Requests
4886
5366
 
4887
5367
  `createHandler` is the router as a plain `(req) => Promise<Response>` function —
@@ -5079,6 +5559,38 @@ domain run-state remain application resources and close explicitly after server
5079
5559
  drain. Do not call `runtime.stop()` or `socket.io.close()` in parallel with
5080
5560
  `shutdown()`.
5081
5561
 
5562
+ ### Stdio process signals
5563
+
5564
+ An MCP stdio handle has `close()`, not managed HTTP `shutdown()` with a force
5565
+ signal and deadline. Bind its process lifecycle with the truthful close-only
5566
+ sibling from `stitchkit/tools`:
5567
+
5568
+ ```ts
5569
+ import {
5570
+ bindStdioProcessSignals,
5571
+ createStdioMcpServer,
5572
+ } from 'stitchkit/tools'
5573
+
5574
+ const stdio = await createStdioMcpServer(config)
5575
+ const binding = bindStdioProcessSignals(stdio, {
5576
+ onClose: () => stopWorkers(),
5577
+ onComplete: () => { process.exitCode = 0 },
5578
+ onError: (phase, error) => {
5579
+ console.error(phase, error) // stderr; stdout stays JSON-RPC-only
5580
+ process.exitCode = 1
5581
+ },
5582
+ })
5583
+
5584
+ await binding.promise
5585
+ ```
5586
+
5587
+ The first signal starts exactly one close chain. A same-turn duplicate is
5588
+ ignored; a later signal restores the default OS disposition because the
5589
+ official stdio close is not abortable and Stitchkit will not pretend otherwise.
5590
+ `binding.close()` removes idle listeners and resolves the promise with
5591
+ `undefined`. No listener is installed until the binder is called, and the
5592
+ framework never calls `process.exit()`.
5593
+
5082
5594
  ### Deploy on Node
5083
5595
 
5084
5596
  The contract, `implement`, hooks, auth and the client are runtime-agnostic. Only
@@ -5409,6 +5921,45 @@ current one *up to* your target, and apply each snippet.
5409
5921
  runtime): bootstrap the server, one HTTP request, and any feature you rely on
5410
5922
  (Socket.IO connect, an MCP tool call, a multipart upload, …).
5411
5923
 
5924
+ ## Unreleased migration: peer-free `implementRemote`
5925
+
5926
+ `implementRemote` now has one canonical, optional-peer-free owner. This keeps
5927
+ MCP SDK and AI SDK modules out of CLI bundles that only proxy HTTP calls:
5928
+
5929
+ ```ts
5930
+ // before
5931
+ import { implementRemote } from 'stitchkit/tools'
5932
+ // after
5933
+ import { implementRemote } from 'stitchkit/remote'
5934
+ ```
5935
+
5936
+ ## Unreleased migration: managed file boundary and strict auth returns
5937
+
5938
+ Create one boundary during application bootstrap and pass the capability, never
5939
+ a per-call directory or host path:
5940
+
5941
+ ```ts
5942
+ import { createManagedFileBoundary } from 'stitchkit/files'
5943
+
5944
+ const files = await createManagedFileBoundary({ root: '/srv/app-files' })
5945
+
5946
+ // before
5947
+ defineDownloadTool({ defaultDir: '/srv/app-files', resolveUrl, ...common })
5948
+ defineUploadTool({ upload: (path) => provider.uploadFile(path), ...common })
5949
+ defineViewFileTool({ baseDir: '/srv/app-files', ...common })
5950
+
5951
+ // after
5952
+ defineDownloadTool({ files, resolveUrl, ...common })
5953
+ defineUploadTool({ files, upload: ({ bytes }) => provider.upload(bytes), ...common })
5954
+ defineViewFileTool({ files, ...common })
5955
+ ```
5956
+
5957
+ Downloaded `path` is now relative to the boundary and MIME metadata is named
5958
+ `mediaType`. Update raw `mountDownload`/`mountUpload`/`mountViewFile` configs the
5959
+ same way. Auth predicates must explicitly return `true`, `false`, or a plain
5960
+ object of context fields; replace accidental `undefined` fallthroughs with the
5961
+ intended boolean.
5962
+
5412
5963
  ## Released migration: 0.53.0
5413
5964
 
5414
5965
  ### Realtime `emit` returns `boolean` instead of `void`
@@ -6378,11 +6929,18 @@ The browser-and-server entrypoint. Re-exports everything from
6378
6929
  | `createRealtimeClient` | function | inferred, runtime-validated Socket.IO client — [guide](../guide/realtime.md#client--createrealtimeclient) |
6379
6930
  | `createRetainedTopics` | function | retained last-value store for sticky events — [guide](../guide/realtime.md#sticky-events) |
6380
6931
  | `parseSSE` | function | parse an SSE `Response` into an async generator — [guide](../guide/client.md#sse) |
6381
- | `SocketIOClient` | _type_ | the client handle; `emit` returns `false` for an emit dropped while disconnected |
6932
+ | `SocketIOClient` | _type_ | low-level client handle; `emit` reports disconnected drops and `emitWithAck` exposes the native Promise primitive used by validated `request()` |
6382
6933
  | `SocketIOClientConfig` | _type_ | config for `createSocketIOClient` (incl. `retain`, `onConnectError`, `onDroppedEmit`) |
6383
6934
  | `SocketEventMap` | _type_ | the shape of an event map |
6384
6935
  | `RealtimeClient` | _type_ | validated client inferred from a realtime contract |
6385
6936
  | `RealtimeClientOptions` | _type_ | transport options and the rejected-event hook for `createRealtimeClient` |
6937
+ | `RealtimeAcknowledgedEvent` | _type_ | event-name union restricted to definitions with an `ack` schema |
6938
+ | `RealtimeAcknowledgement` | _type_ | validated acknowledgement output inferred from an event definition |
6939
+ | `RealtimeRequestArguments` | _type_ | request arguments inferred from an acknowledged event tuple |
6940
+ | `RealtimeRequestOptions` | _type_ | finite positive native acknowledgement `timeoutMs` |
6941
+ | `RealtimeRequestTimeoutError` | class | stable `REALTIME_REQUEST_TIMEOUT` rejection |
6942
+ | `RealtimeRequestDisconnectedError` | class | stable `REALTIME_REQUEST_DISCONNECTED` rejection, including an immediate disconnected call |
6943
+ | `RealtimeRequestInvalidAcknowledgementError` | class | invalid ack was reported through `onRejected` and the request rejected |
6386
6944
  | `RealtimeContract` | _type_ | shared server-to-client and client-to-server event registries |
6387
6945
  | `RealtimeEventRegistry` | _type_ | string-keyed registry of event definitions |
6388
6946
  | `RealtimeEventDefinition` | _type_ | one tuple-shaped event and optional acknowledgement schema |
@@ -6443,8 +7001,11 @@ from the root `stitchkit`.
6443
7001
  | `HttpMethod` | _type_ | `GET \| HEAD \| POST \| PUT \| PATCH \| DELETE` |
6444
7002
  | `Transport` | _type_ | `HTTP \| MCP \| AGENT \| CLI` |
6445
7003
  | `TransportSource` | _type_ | `http \| mcp \| agent \| cli` — the value of `ctx.source` |
6446
- | `RuntimeContext` | _type_ | the loose context seen by transport and hooks |
6447
- | `HandlerContext` | _type_ | the typed context seen by a handler |
7004
+ | `RuntimeContext` | _type_ | the loose context seen by transport and hooks, including optional typed `mcp` metadata |
7005
+ | `HandlerContext` | _type_ | the typed context seen by a handler, including optional typed `mcp` metadata |
7006
+ | `McpCallContext` | _type_ | validated metadata for the active managed MCP call (`era`, method, tool, client and multi-round fields) |
7007
+ | `McpClientInfo` | _type_ | self-reported MCP host name/version; attribution only, never application identity |
7008
+ | `McpRoundOutcome` | _type_ | managed multi-round attempt outcome |
6448
7009
  | `EndpointHandlerContext` | _type_ | one endpoint handler's fully inferred params, input, files and runtime context |
6449
7010
  | `EndpointFn` | _type_ | the call signature of one client method |
6450
7011
  | `TypedClient` | _type_ | the full typed client for a contract |
@@ -6560,6 +7121,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
6560
7121
  | `OperationIdentity` | _type_ | path-free service/action/scope/method identity shared by contract and native tool operations |
6561
7122
  | `Handlers` | _type_ | the typed handler map `implement` expects |
6562
7123
  | `LifecycleHooks` | _type_ | `onRequest` / pre-body `authorize` / `beforeHandle` / `afterHandle` / `onError` |
7124
+ | `composeLifecycleHooks` | function | compose HTTP lifecycle phases in declaration order with short-circuit/fallthrough semantics |
6563
7125
  | `AuthorizationContext` | _type_ | HTTP pre-body context with validated params, `input: undefined` and no files |
6564
7126
  | `RouteGroup` | _type_ | a prefixed group of services with its own hooks |
6565
7127
  | `RawRoute` | _type_ | a non-contract `Request → Response` route with a concrete `BunServer` context |
@@ -6591,6 +7153,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
6591
7153
  | `AuthHook` | _type_ | the hook `createAuthHook` returns |
6592
7154
  | `AuthHookConfig` | _type_ | config for `createAuthHook` |
6593
7155
  | `AuthRule` | _type_ | `'public' \| 'authenticated' \| predicate` |
7156
+ | `AuthRuleContribution` | _type_ | plain context fields a sync/async rule may contribute after authorizing |
6594
7157
  | `ScopedAuthRule` | _type_ | a rule plus its typed context contribution — `{ rule, inject? }` |
6595
7158
  | `AuthRules` | _type_ | the `rules` map: bare rules or scoped rules |
6596
7159
  | `RuleScopes` | _type_ | scope→context map derived from a `rules` object; `'public'` fields become optional |
@@ -6759,6 +7322,18 @@ audit event. See the [Observability guide](../guide/observability.md).
6759
7322
 
6760
7323
  ---
6761
7324
 
7325
+ ## `stitchkit/remote`
7326
+
7327
+ Peer-free remote implementation boundary. Importing it does not load the MCP
7328
+ SDK, `ai`, or other optional tool peers, so it is safe in a thin CLI bundle.
7329
+
7330
+ | Export | Kind | Summary |
7331
+ |--------|------|---------|
7332
+ | `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
7333
+ | `ImplementRemoteOptions` | _type_ | optional argument-rewrite hook for `implementRemote` |
7334
+
7335
+ ---
7336
+
6762
7337
  ## `stitchkit/tools`
6763
7338
 
6764
7339
  Server-only. Turns contracts into MCP and AI-agent tools. MCP server surfaces
@@ -6775,14 +7350,15 @@ payload.
6775
7350
  | `createMcpHandler` | function | a stateless dual-era Streamable-HTTP MCP handler — [guide](../guide/mcp-and-agents.md#mcp--createmcphandler) |
6776
7351
  | `createMcpHttpRoute` | function | framework-owned `RawRoute` adapter for an MCP HTTP handler |
6777
7352
  | `createStdioMcpServer` | function | a complete stdio MCP server — [guide](../guide/mcp-and-agents.md#mcp-over-stdio--createstdiomcpserver) |
7353
+ | `bindStdioProcessSignals` | function | explicitly bind OS signals to one close-only stdio handle — [guide](../guide/testing-and-deployment.md#stdio-process-signals) |
6778
7354
  | `buildMcpServer` | function | build an `McpServer` from contract/runtime surfaces; no-auth configs omit the second argument |
6779
7355
  | `mountMcp` | function | add contract tools to an existing `McpServer` — [guide](../guide/mcp-and-agents.md#mountmcp) |
6780
- | `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
6781
7356
  | `mountAgent` | function | a Vercel AI SDK `ToolSet` from a service — [guide](../guide/mcp-and-agents.md#ai-agents--mountagent) |
6782
- | `defineRuntimeTool` | function | define one validated pathless operation for MCP, Agent or both — [guide](../guide/mcp-and-agents.md#pathless-runtime-tools-and-multimodal-results) |
7357
+ | `defineRuntimeTool` | function | define one validated pathless operation for explicit MCP, Agent and/or CLI surfaces — [guide](../guide/mcp-and-agents.md#pathless-runtime-tools-and-multimodal-results) |
6783
7358
  | `createRuntimeToolFactory` | function | bind shared identity and Zod-validated per-call context for runtime tools — [guide](../guide/mcp-and-agents.md#pathless-runtime-tools-and-multimodal-results) |
6784
7359
  | `createToolInvoker` | function | compile an exposure-aware in-process dispatcher over the canonical tool runner — [guide](../guide/mcp-and-agents.md#in-process-calls--createtoolinvoker) |
6785
7360
  | `createCli` | function | a command-line program from contracts — [guide](../guide/cli.md) (also on `stitchkit/cli`) |
7361
+ | `defineCliCommand` | function | define one typed CLI-only command with no fake managed-tool identity — [guide](../guide/cli.md#native-binary-commands) (also on `stitchkit/cli`) |
6786
7362
  | `createToolkit` | function | context-typed tool mounts — [guide](../guide/cli.md#typed-context) |
6787
7363
  | `mountViewFile` | function | a native multimodal "view file" MCP tool |
6788
7364
  | `resolveMedia` | function | resolve a media reference for a tool result |
@@ -6796,6 +7372,10 @@ payload.
6796
7372
  | `McpLegacyPolicy` | _type_ | `'serve' \| 'reject'` protocol-era compatibility policy |
6797
7373
  | `McpStdioHandle` | _type_ | closeable official stdio transport handle |
6798
7374
  | `StdioMcpServerConfig` | _type_ | config for `createStdioMcpServer` |
7375
+ | `StdioCloseTarget` | _type_ | minimal close-only target accepted by `bindStdioProcessSignals` |
7376
+ | `StdioProcessSignalsOptions` | _type_ | signal source, close callbacks and phased error reporting |
7377
+ | `StdioProcessSignalsBinding` | _type_ | close-only signal binding with observed `promise` and listener `close()` |
7378
+ | `StdioProcessSignalsErrorPhase` | _type_ | `'prepare' \| 'close' \| 'complete'` reporting phase |
6799
7379
  | `McpServerBuildConfig` | _type_ | shared config for `buildMcpServer` |
6800
7380
  | `McpServerSharedConfig` | _type_ | transport-neutral options shared by direct and finite surface configs |
6801
7381
  | `McpServer` | _type_ | official split-SDK server instance accepted by raw extension points |
@@ -6804,7 +7384,6 @@ payload.
6804
7384
  | `McpSurfaceDefinition` | _type_ | one immutable `{ services, runtimeTools }` MCP surface |
6805
7385
  | `McpSurfaceRegistry` | _type_ | finite keyed surface registry for eager preparation |
6806
7386
  | `StdioAuthConfig` | _type_ | startup identity composed into `StdioMcpServerConfig` |
6807
- | `ImplementRemoteOptions` | _type_ | options for `implementRemote` |
6808
7387
  | `McpMountConfig` | _type_ | config for `mountMcp` |
6809
7388
  | `McpSchemaValidationConfig` | _type_ | shared `{ policy, requireTypedProperties, allowUntyped, requirePortableFormats, allowFormats }` profile |
6810
7389
  | `ValidateMcpSchemasConfig` | _type_ | standalone validation profile plus `services`, `extend`, flattening and logger |
@@ -6833,10 +7412,16 @@ payload.
6833
7412
  | `CimdCacheEvent` | _type_ | observable CIMD cache hit, miss, revalidation and eviction event |
6834
7413
  | `createSecureClientMetadataFetcher` | function | production HTTPS, DNS/IP-pinned CIMD fetcher |
6835
7414
  | `RuntimeAgentModelOutput` | _type_ | AI SDK model-facing text/JSON/content output returned by `present.agent` |
6836
- | `RuntimeToolTransport` | _type_ | runtime exposure: `'MCP' \| 'AGENT'` |
7415
+ | `RuntimeToolTransport` | _type_ | runtime exposure: `'MCP' \| 'AGENT' \| 'CLI'`; omission still means MCP+Agent only |
6837
7416
  | `AgentMountConfig` | _type_ | config for `mountAgent` |
6838
7417
  | `AgentContext` | _type_ | the context merged into agent tool handlers |
6839
7418
  | `CliConfig` | _type_ | config for `createCli` |
7419
+ | `CliSurfaceSource` | _type_ | static managed surface or identity-dependent surface factory for `createCli` |
7420
+ | `CliCommandDefinition` | _type_ | Zod-first CLI-only command union |
7421
+ | `CliCommandDefinitionBase` | _type_ | native command name, description and input schema |
7422
+ | `CliCommandDefinitionWithOutput` | _type_ | native command with declared output schema and validated handler result |
7423
+ | `CliCommandDefinitionWithoutOutput` | _type_ | void native command with no output schema |
7424
+ | `CliCommandContext` | _type_ | parsed native command input, global options and injected writers |
6840
7425
  | `CliWaitConfig` | _type_ | `--wait` polling config |
6841
7426
  | `ExitCodeMap` | _type_ | `ToolResult.code` → process exit code |
6842
7427
  | `Toolkit` | _type_ | the context-pinned tool surface from `createToolkit` |
@@ -6853,9 +7438,15 @@ payload.
6853
7438
  | `ToolInvokerConfig` | _type_ | compile-time exposure, extension and presentation options |
6854
7439
  | `ToolInvocationOptions` | _type_ | per-call source, context, lifecycle, hooks and output-strip reporter |
6855
7440
  | `ToolInvokerTransport` | _type_ | invoker exposure policy: `MCP \| AGENT \| CLI` |
6856
- | `ToolCallContext` | _type_ | the context every tool hook receives — `{ source }` plus whatever the mount's `context` added |
6857
- | `ViewFileOptions` | _type_ | options for `mountViewFile` |
7441
+ | `ToolCallContext` | _type_ | the context every tool hook receives — `{ source, mcp? }` plus whatever the mount's `context` added |
7442
+ | `ViewFileOptions` | _type_ | shared URL/managed-file-boundary policy for `defineViewFileTool`, `mountViewFile` and `resolveMedia` |
7443
+ | `ViewFileOutput` | _type_ | neutral managed batch result with multimodal `content` and per-item `errors` |
7444
+ | `ViewFileInputSchema` | constant | fixed one-or-many media path/URL input schema |
7445
+ | `ViewFileOutputSchema` | constant | Zod schema for the neutral managed view-file batch result |
7446
+ | `ViewFileErrorSchema` | constant | Zod schema for one structured per-item view failure |
6858
7447
  | `McpAnnotations` | _type_ | MCP annotations on a media result |
7448
+ | `McpAnnotationsSchema` | constant | Zod schema for MCP media annotations |
7449
+ | `McpMediaContentSchema` | constant | Zod schema for text/image/audio media content |
6859
7450
  | `CollectToolsConfig` | _type_ | options for `collectTools` |
6860
7451
  | `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)) |
6861
7452
  | `UntypedProperty` | _type_ | one such property — `{ path, description? }` |
@@ -6868,13 +7459,44 @@ payload.
6868
7459
 
6869
7460
  ### Native tools
6870
7461
 
6871
- Generic host-supplied tools mounted onto a server not derived from a contract.
7462
+ Generic host-supplied operationsmanaged definitions for the canonical
7463
+ runtime-tool runner, plus deliberate raw MCP adapters over the same mechanics.
6872
7464
 
6873
7465
  | Export | Kind | Summary |
6874
7466
  |--------|------|---------|
6875
- | `mountDownload` | function | a "download a URL to disk" tool (SSRF-guarded, size-capped) |
6876
- | `mountUpload` | function | an "upload a local file" tool |
6877
- | `mountWait` | function | a generic `--wait`-style polling tool |
7467
+ | `defineDownloadTool` | function | define a guarded, size-capped managed download for MCP/Agent `runtimeTools` |
7468
+ | `defineUploadTool` | function | define a typed managed local-file upload for MCP/Agent `runtimeTools` |
7469
+ | `defineWaitTool` | function | define a typed, cancellable managed polling operation for MCP/Agent `runtimeTools` |
7470
+ | `defineViewFileTool` | function | define a guarded, batch-capped managed multimodal operation for MCP/Agent `runtimeTools` |
7471
+ | `DefineDownloadToolConfig` | _type_ | Zod input, stable identity, URL resolver, storage policy and presenters for `defineDownloadTool` |
7472
+ | `DefineUploadToolConfig` | _type_ | stable identity, typed output, upload callback and presenters for `defineUploadTool` |
7473
+ | `DefineWaitToolConfig` | _type_ | Zod input/state, identity, poll/done/timeout policy and presenters for `defineWaitTool` |
7474
+ | `DefineViewFileToolConfig` | _type_ | identity, media security policy and optional presenters for `defineViewFileTool` |
7475
+ | `ManagedNativeToolConfig` | _type_ | shared name, description, identity, exposure and annotations for managed native factories |
7476
+ | `NativeToolIdentity` | _type_ | pathless service/action/scope/meta identity; semantic method is factory-owned |
7477
+ | `ManagedWaitRender` | _type_ | optional managed wait terminal text and failure classification |
7478
+ | `UploadToolInputSchema` | constant | fixed `{ path: string }` input schema for `defineUploadTool` |
7479
+ | `defineAsyncOperation` | function | runtime-only start/status/wait plus configured cancel/result/artifacts definitions |
7480
+ | `bindContractAsyncOperation` | function | bind literal methods from an existing contract without creating another HTTP surface |
7481
+ | `createAsyncOperationSnapshotSchema` | function | canonical pending/running/succeeded/failed/cancelled Zod snapshot |
7482
+ | `AsyncOperationCancelResultSchema` | constant | validated accepted/already_terminal/rejected cancellation result |
7483
+ | `AsyncOperationCancelResult` | _type_ | validated cancel capability result |
7484
+ | `AsyncOperationCancelCapability` | _type_ | optional typed domain cancellation callback |
7485
+ | `AsyncOperationCapability` | _type_ | generated capability-name union |
7486
+ | `AsyncOperationStartDefinition` / `AsyncOperationFollowDefinition` | _type_ | generated runtime definition types |
7487
+ | `AsyncOperationIdentity` | _type_ | shared service/action/scope/meta identity for an operation |
7488
+ | `AsyncOperationOutputCapability` | _type_ | optional result/artifact schema plus handler |
7489
+ | `RuntimeAsyncOperationConfig` | _type_ | runtime-only descriptor configuration |
7490
+ | `RuntimeAsyncOperation` | _type_ | inferred generated definitions and schemas |
7491
+ | `ContractAsyncOperationConfig` | _type_ | literal contract method binding and handlers |
7492
+ | `ContractAsyncOperationKeys` | _type_ | literal method-key union of a bound contract |
7493
+ | `ContractAsyncOperationStartKey` | _type_ | contract keys with a declared output schema, valid for `start` |
7494
+ | `ContractAsyncOperationFollowKey` | _type_ | contract keys whose input schema type matches the selected start output |
7495
+ | `ContractAsyncOperationWaitKey` | _type_ | follow-up keys whose output schema type also matches the selected status output |
7496
+ | `composeToolLifecycle` | function | ordered composition of tool before/after phases |
7497
+ | `mountDownload` | function | raw MCP "download a URL to disk" adapter (SSRF-guarded, size-capped) |
7498
+ | `mountUpload` | function | raw MCP "upload a local file" adapter |
7499
+ | `mountWait` | function | raw MCP generic `--wait`-style polling adapter |
6878
7500
  | `DownloadToolConfig` | _type_ | config for `mountDownload` |
6879
7501
  | `UploadToolConfig` | _type_ | config for `mountUpload` |
6880
7502
  | `WaitToolConfig` | _type_ | config for `mountWait` |
@@ -6951,6 +7573,43 @@ handler pipeline without opening a TCP port.
6951
7573
  | `HandlerTestClientConfig` | _type_ | handler, contract, path prefix, scoped config and client request defaults |
6952
7574
  | `HandlerTestClientsConfig` | _type_ | batch helper configuration |
6953
7575
  | `HandlerTestTransportConfig` | _type_ | shared in-process handler, origin, prefix, client defaults and optional server handle |
7576
+ | `buildSurfaceManifest` | function | deterministic actual HTTP/tool/CLI/extension topology with versioned schema digests |
7577
+ | `assertSurfaceManifestSnapshot` | function | bounded deterministic manifest drift assertion |
7578
+ | `assertSurfaceDiscovery` | function | compare real OpenAPI/MCP/Agent/CLI discovery to a manifest |
7579
+ | `runSurfaceProbes` | function | execute explicit transport drivers with per-probe setup, teardown, timeout and cancellation |
7580
+ | `TransportObservationSchema` | constant | normalized success/validation/domain-error/aborted probe outcome |
7581
+ | `ConformanceTransport` | _type_ | explicit probe transport union |
7582
+ | `RunSurfaceProbesConfig` | _type_ | probes, drivers and diagnostic byte cap |
7583
+ | `SurfaceDiscoveryObservation` | _type_ | real OpenAPI/tool/CLI/extension discovery values |
7584
+ | `SurfaceProbe` / `SurfaceProbeDriver` | _type_ | one bounded scenario and its consumer-supplied runner |
7585
+ | `TransportObservation` | _type_ | validated normalized driver result |
7586
+ | `SurfaceManifest` / `SurfaceManifestConfig` | _type_ | deterministic surface snapshot and its inputs |
7587
+ | `SurfaceManifestOperation` / `SurfaceManifestOperationSchema` | _type_ / schema | one contract or runtime operation row |
7588
+ | `SurfaceManifestExtension` / `SurfaceManifestExtensionSchema` | _type_ / schema | declared transport extension row |
7589
+ | `SurfaceManifestSchema` | schema | complete deterministic manifest schema |
7590
+ | `SurfaceRuntimeToolDefinition` | _type_ | peer-free structural runtime-operation subset accepted by manifests |
7591
+ | `SurfaceSchemaDigestsSchema` | schema | params/input/output/multipart digest object |
7592
+ | `serializeSurfaceValue` | function | canonical versioned serialization used for deterministic digests |
7593
+
7594
+ ---
7595
+
7596
+ ## `stitchkit/files`
7597
+
7598
+ Peer-free Bun/Node filesystem capability. Browser/contract-safe refs are also
7599
+ available from `stitchkit/contract`.
7600
+
7601
+ | Export | Kind | Summary |
7602
+ |--------|------|---------|
7603
+ | `createManagedFileBoundary` | function | bind one existing application-owned root for capped reads and atomic writes |
7604
+ | `ManagedFileBoundary` | _type_ | non-reopenable `read`/`write` capability over canonical relative paths |
7605
+ | `ManagedFileBoundaryConfig` | _type_ | bound root, finite limits, inspector and cleanup observer |
7606
+ | `ManagedFileRefSchema` / `ManagedFileRef` | schema / _type_ | transport-safe relative path, measured size and optional media metadata |
7607
+ | `ManagedFilePathSchema` / `ManagedFilePath` | schema / _type_ | canonical POSIX relative managed-file path |
7608
+ | `ManagedFileSource` | _type_ | bounded immutable bytes plus validated ref passed to upload callbacks |
7609
+ | `ManagedFileReadOptions` / `ManagedFileWriteOptions` | _type_ | per-operation byte cap, signal and atomic write policy |
7610
+ | `ManagedFileError` / `ManagedFileErrorCode` | class / _type_ | stable caller-safe boundary failures |
7611
+ | `ManagedFileInspector` | _type_ | bounded-prefix content inspection callback that cannot own path or size |
7612
+ | `ManagedFileInspectionInput` / `ManagedFileInspection` | _type_ | inspector input and normalized metadata-only result |
6954
7613
 
6955
7614
  ---
6956
7615
 
@@ -6979,20 +7638,28 @@ runtime-agnostic pieces of `stitchkit/server` and the error helpers.
6979
7638
 
6980
7639
  ## `stitchkit/cli`
6981
7640
 
6982
- Server-only. Turns contracts into a command-line program — the fourth transport.
6983
- Light by design: needs neither the MCP SDK nor the `ai` peer.
7641
+ Server-only. Composes contract/runtime managed operations and CLI-only native
7642
+ commands into one command-line program. Light by design: needs neither the MCP
7643
+ SDK nor the `ai` peer.
6984
7644
 
6985
7645
  | Export | Kind | Summary |
6986
7646
  |--------|------|---------|
6987
7647
  | `createCli` | function | build and run a CLI from contracts — [guide](../guide/cli.md) |
7648
+ | `defineCliCommand` | function | define one Zod-typed CLI-only executable command |
6988
7649
  | `parseCliArgs` | function | argv → typed tool args against a schema (advanced) |
6989
7650
  | `pollUntilDone` | function | the generic `--wait` poller (advanced) |
6990
- | `emitResult` | function | write a `ToolResult` to stdout/stderr + exit code (advanced) |
7651
+ | `emitResult` | function | write a pretty or compact `ToolResult` record to stdout/stderr + exit code (advanced) |
6991
7652
  | `DEFAULT_EXIT_CODES` | const | the default `ToolResult.code` → exit-code map |
6992
7653
  | `CliConfig` | _type_ | config for `createCli` |
6993
- | `CliRunOptions` | _type_ | parsed global flags (`--json`, `--wait`, …) |
7654
+ | `CliSurfaceSource` | _type_ | static service/runtime array or identity-dependent factory |
7655
+ | `CliCommandDefinition` | _type_ | native command definition union |
7656
+ | `CliCommandDefinitionBase` | _type_ | native command name, description and input schema |
7657
+ | `CliCommandDefinitionWithOutput` | _type_ | native command with validated declared output |
7658
+ | `CliCommandDefinitionWithoutOutput` | _type_ | native void command without an output contract |
7659
+ | `CliCommandContext` | _type_ | parsed input, global options and stdout/stderr writers |
7660
+ | `CliRunOptions` | _type_ | parsed global flags (`--json` compacts success/error records, `--wait`, …) |
6994
7661
  | `ParsedCliArgs` | _type_ | result of `parseCliArgs` |
6995
- | `CliWaitConfig` | _type_ | per-command `--wait` polling config |
7662
+ | `CliWaitConfig` | _type_ | per-command `--wait` polling config; optional `failed(result)` maps a terminal domain failure to `WAIT_FAILED` and a non-zero exit |
6996
7663
  | `ExitCodeMap` | _type_ | `ToolResult.code` → process exit code |
6997
7664
  | `PollParams` | _type_ | params for `pollUntilDone` |
6998
7665
  | `CliWriters` | _type_ | stdout/stderr sinks for `emitResult` |