stitchkit 0.25.0 → 0.26.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 (47) hide show
  1. package/dist/browser/client-multipart.d.ts +22 -0
  2. package/dist/browser/client-multipart.d.ts.map +1 -0
  3. package/dist/browser/client.d.ts.map +1 -1
  4. package/dist/cli.js +2 -2
  5. package/dist/contract/define.d.ts +20 -0
  6. package/dist/contract/define.d.ts.map +1 -1
  7. package/dist/contract/factory.d.ts +1 -0
  8. package/dist/contract/factory.d.ts.map +1 -1
  9. package/dist/contract/index.js +1 -1
  10. package/dist/{index-4gawbm74.js → index-0d0rb85d.js} +41 -3
  11. package/dist/{index-g8kyab85.js → index-bkccbx64.js} +10 -3
  12. package/dist/{index-x62gnfsk.js → index-h4y2wg3n.js} +1 -1
  13. package/dist/{index-a9n8m4ec.js → index-p6fge9a5.js} +1 -1
  14. package/dist/{index-h9d1fm0p.js → index-q5w3cvvp.js} +62 -22
  15. package/dist/index.js +20 -18
  16. package/dist/internal/errors.d.ts +6 -1
  17. package/dist/internal/errors.d.ts.map +1 -1
  18. package/dist/internal/write-download.d.ts +17 -0
  19. package/dist/internal/write-download.d.ts.map +1 -0
  20. package/dist/node.js +2 -2
  21. package/dist/server/create.d.ts.map +1 -1
  22. package/dist/server/error-hook.d.ts +14 -3
  23. package/dist/server/error-hook.d.ts.map +1 -1
  24. package/dist/server/implement.d.ts.map +1 -1
  25. package/dist/server/index.js +6 -6
  26. package/dist/server/types.d.ts +13 -0
  27. package/dist/server/types.d.ts.map +1 -1
  28. package/dist/tools/agent.d.ts +2 -0
  29. package/dist/tools/agent.d.ts.map +1 -1
  30. package/dist/tools/cli.d.ts.map +1 -1
  31. package/dist/tools/execute.d.ts +1 -1
  32. package/dist/tools/execute.d.ts.map +1 -1
  33. package/dist/tools/list-names.d.ts.map +1 -1
  34. package/dist/tools/mcp.d.ts +10 -3
  35. package/dist/tools/mcp.d.ts.map +1 -1
  36. package/dist/tools/mount-download.d.ts.map +1 -1
  37. package/dist/tools/mount-upload.d.ts.map +1 -1
  38. package/dist/tools/mount-wait.d.ts.map +1 -1
  39. package/dist/tools/mount.d.ts +13 -0
  40. package/dist/tools/mount.d.ts.map +1 -1
  41. package/dist/tools/names.d.ts +34 -0
  42. package/dist/tools/names.d.ts.map +1 -1
  43. package/dist/tools/remote.d.ts.map +1 -1
  44. package/dist/tools/transports.d.ts.map +1 -1
  45. package/dist/tools.js +48 -40
  46. package/llms-full.txt +97 -7
  47. package/package.json +1 -1
package/llms-full.txt CHANGED
@@ -237,7 +237,7 @@ export const users = defineContract({ prefix: 'users' }, {
237
237
  | `output` | no | Zod schema for the **response body** |
238
238
  | `scope` | no | access scope for this endpoint — see [Auth & errors](./auth-and-errors.md) |
239
239
  | `expose` | no | which transports carry this endpoint — see [below](#transports) |
240
- | `toolName` | no | explicit MCP / agent tool name (defaults to `prefix_key`) |
240
+ | `toolName` | no | explicit MCP / agent tool name (default: a verb-aware derivation, see below — not a literal `prefix_key`) |
241
241
  | `multipart` | no | field name of a file upload — see [below](#file-uploads) |
242
242
  | `timeout` | no | per-endpoint client timeout in ms, for slow endpoints |
243
243
  | `idempotent` | no | safe to call twice with the same input (like `PUT`/`DELETE`); a retrying transport reads it — see [Realtime](./realtime.md#bring-your-own-transport) |
@@ -326,8 +326,41 @@ derivation from the method key + prefix (`users` + `create` ⇒ `create_user`,
326
326
  { method: 'POST', path: '/', desc: 'Create a user', toolName: 'create_user', /* … */ }
327
327
  ```
328
328
 
329
+ **Every tool name — derived or explicit — must match `[a-zA-Z0-9_-]` and be at
330
+ most 64 characters**, the character class every major provider accepts.
331
+
332
+ Note the two classes differ. What is *accepted* includes the hyphen; what
333
+ derivation *keeps* does not — a derived name normalises everything outside
334
+ `[a-zA-Z0-9_]` to `_`, the hyphen included, so `bot-status` + `get` ⇒
335
+ `get_bot_status` and `admin/analytics` + `get` ⇒ `get_admin_analytics`. A hyphen
336
+ survives only in an explicit `toolName`, which is taken verbatim.
337
+
338
+ A prefix with *no* usable character at all (`'///'`, `'_'`, a fully non-ASCII
339
+ prefix) and any explicit `toolName` outside the accepted class **throw at
340
+ mount**. An unusable prefix is rescued by setting an explicit `toolName` — the
341
+ prefix then never enters the name. An over-long name is fixed by a shorter
342
+ explicit `toolName` (or a shorter prefix / method key). Nothing downstream checks this: the provider rejects the whole
343
+ request, so one bad name takes every tool of that mount down with it.
344
+ → [ADR 0035](../decisions/0035-tool-name-derivation-and-validation.md).
345
+
329
346
  ## Endpoint metadata (`meta`)
330
347
 
348
+ A contract can declare a **default** `meta` that every endpoint inherits, and an
349
+ endpoint's own keys are **shallow-merged over** it:
350
+
351
+ ```ts
352
+ defineContract({ prefix: 'admin', meta: { public: true } }, {
353
+ list: { method: 'GET', path: '/', desc: 'List', /* meta → { public: true } */ },
354
+ purge: { method: 'POST', path: '/purge', desc: 'Purge', meta: { rateTier: 2 } },
355
+ // meta → { public: true, rateTier: 2 }
356
+ })
357
+ ```
358
+
359
+ One level deep — no deep merge, no way to unset an inherited key; declare it on
360
+ the endpoint instead. `expose` deliberately has **no** contract-level equivalent
361
+ (→ [ADR 0036](../decisions/0036-contract-level-meta.md)).
362
+
363
+
331
364
  `meta` is an **opaque, app-defined** bag the core attaches no meaning to — the
332
365
  same escape-hatch spirit as `scope` being a free string ([ADR 0002](../decisions/0002-generic-core.md) /
333
366
  [ADR 0021](../decisions/0021-endpoint-meta-passthrough.md)). Declare app concerns
@@ -1172,7 +1205,14 @@ By default every endpoint is a tool on every transport. `expose` narrows it:
1172
1205
  `desc` is the tool description the model reads — write it for the model, not
1173
1206
  just for a human. A `multipart` endpoint is never a tool. The tool name defaults
1174
1207
  to a verb-aware name from the method + prefix (`list` → `list_widgets`, `get` →
1175
- `get_widget`); set `toolName` for an explicit one. See
1208
+ `get_widget`); set `toolName` for an explicit one. Derivation normalises every
1209
+ character outside `[a-zA-Z0-9_]` to `_` — the hyphen included, so `bot-status`
1210
+ derives `get_bot_status` — while a name is *accepted* if it matches
1211
+ `[a-zA-Z0-9_-]`, so a hyphen survives in an explicit `toolName`. A name that
1212
+ still cannot be delivered (illegal explicit `toolName`, over 64 characters, or a
1213
+ prefix with no usable character) throws at mount rather than at the first model
1214
+ call —
1215
+ → [ADR 0035](../decisions/0035-tool-name-derivation-and-validation.md). See
1176
1216
  [Contracts → transports](./contracts.md#transports).
1177
1217
 
1178
1218
  ### Pinning tool names — `listToolNames`
@@ -1196,6 +1236,14 @@ now fails this test instead of silently breaking the clients that call the
1196
1236
  tool. It is also the mechanical diff when migrating a service: run it before
1197
1237
  and after, compare.
1198
1238
 
1239
+ > **This is also the guard against a forgotten `expose`.** An endpoint that
1240
+ > declares none is a tool on MCP **and** AGENT — the default is fail-open, and
1241
+ > there is no contract-level `expose` to set once (→
1242
+ > [ADR 0036](../decisions/0036-contract-level-meta.md)). A snapshot of
1243
+ > `listToolNames` fails the build the moment an endpoint you meant to keep
1244
+ > HTTP-only shows up in the list, which is the one check that catches it however
1245
+ > many places the line was forgotten.
1246
+
1199
1247
  ## MCP — `createMcpHandler`
1200
1248
 
1201
1249
  `createMcpHandler` builds a complete Streamable-HTTP MCP server as a single
@@ -1234,11 +1282,11 @@ createServer({
1234
1282
  | `hooks` | tool-call observability hooks — `afterToolCall` fires on every result |
1235
1283
  | `onIncompatibleSchema` | `'throw'` (default) · `'skip'` · `'warn'` — see below |
1236
1284
  | `logger` | a `StitchLogger` for the `'warn'` policy |
1237
- | `nativeTools` | register non-contract tools directly on the `McpServer` |
1285
+ | `nativeTools` | `(server, auth) => …` — register non-contract tools directly on the `McpServer`; receives the resolved identity, but is **not** a scope gate (`lifecycle` does not run for native tools) |
1238
1286
  | `instructions` | a short host-facing usage hint, surfaced to MCP tool-search |
1239
1287
 
1240
- `services` and `context` receive the resolved identity, so a tenant can be
1241
- shown only its own tools and every handler can read `ctx.tenantId`.
1288
+ `services`, `context` and `nativeTools` all receive the resolved identity, so a
1289
+ tenant can be shown only its own tools and every handler can read `ctx.tenantId`.
1242
1290
 
1243
1291
  ### Guarding tools — `lifecycle`
1244
1292
 
@@ -2479,7 +2527,13 @@ const onError = createErrorHook({
2479
2527
  CONFLICT: 'conflict', RATE_LIMITED: 'rate_limited',
2480
2528
  INTERNAL_SERVER_ERROR: 'internal',
2481
2529
  } satisfies Record<StitchErrorCode, string>,
2482
- render: (info) => ({ ok: false, error: { code: info.code, message: info.message } }),
2530
+ // `ctx` is the request's RuntimeContext — read `ctx.traceId` for a
2531
+ // correlation id in the envelope. Declaring it is optional.
2532
+ render: (info, ctx) => ({
2533
+ ok: false,
2534
+ error: { code: info.code, message: info.message },
2535
+ traceId: ctx.traceId,
2536
+ }),
2483
2537
  })
2484
2538
 
2485
2539
  createServer({ services, hooks: { onError } })
@@ -3123,6 +3177,42 @@ current one *up to* your target, and apply each snippet.
3123
3177
  (`STITCH_ERROR_STATUS`, `serveFile`, `scopePrefixes`, `afterToolCall`'s
3124
3178
  `MethodDef`, `maxUploadBytes`) are available to adopt, not required.
3125
3179
 
3180
+ ## Your handlers may be returning more than the contract declares
3181
+
3182
+ stitchkit validates every handler's return value against the endpoint's `output`
3183
+ schema and **passes on the parsed result** — so any field the schema does not
3184
+ declare is silently removed. That is deliberate (the contract is the published
3185
+ shape of the response), but when you are moving a *live* API onto stitchkit it is
3186
+ invisible: TypeScript does not reject excess properties, nothing logs it, and the
3187
+ client just receives fewer fields.
3188
+
3189
+ While migrating, turn the diagnostic on:
3190
+
3191
+ ```ts
3192
+ createServer({ services, warnOnOutputStrip: true }) // off by default
3193
+ ```
3194
+
3195
+ Every removed key is logged as a dot-path with the endpoint that produced it
3196
+ (`notes.get: secret, nested.alsoSecret`). Tool transports strip identically —
3197
+ `mountMcp` / `mountAgent` take `onOutputStrip: (toolName, paths) => …`. Read the
3198
+ list, then either widen the contract or stop returning the field, and turn the
3199
+ flag back off: it is for the migration window, not for production.
3200
+
3201
+ ## Tool names may shift between versions
3202
+
3203
+ Derived tool names are part of your public surface — an MCP client config or an
3204
+ agent prompt refers to them by string. Before and after any upgrade that touches
3205
+ name derivation, diff them mechanically:
3206
+
3207
+ ```ts
3208
+ import { listToolNames } from 'stitchkit/tools'
3209
+ console.log(JSON.stringify(listToolNames(services), null, 2))
3210
+ ```
3211
+
3212
+ `listToolNames` never throws on an illegal name — that is deliberate, so it can
3213
+ show you the offending row when a mount would refuse it. Pin it in a snapshot
3214
+ test and a shift fails your build instead of your clients.
3215
+
3126
3216
  ## When you author a breaking change in stitchkit
3127
3217
 
3128
3218
  You are on the other side of this flow — see
@@ -3212,7 +3302,7 @@ from the root `stitchkit`.
3212
3302
  | `ScopedDefineContract` | _type_ | the `defineContract` `createContractFactory` returns |
3213
3303
  | `ALL_TRANSPORTS` | constant | `['HTTP', 'MCP', 'AGENT', 'CLI']` |
3214
3304
  | `ContractDef` | _type_ | a defined contract |
3215
- | `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` |
3305
+ | `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` and `meta` (a default every endpoint shallow-merges over) |
3216
3306
  | `EndpointDef` | _type_ | a single endpoint definition |
3217
3307
  | `HttpMethod` | _type_ | `GET \| POST \| PUT \| PATCH \| DELETE` |
3218
3308
  | `Transport` | _type_ | `HTTP \| MCP \| AGENT \| CLI` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",