stitchkit 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/client-multipart.d.ts +22 -0
- package/dist/browser/client-multipart.d.ts.map +1 -0
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +7 -1
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/contract/define.d.ts +79 -2
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/factory.d.ts +1 -0
- package/dist/contract/factory.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/{index-h9d1fm0p.js → index-4whcb3c3.js} +64 -22
- package/dist/{index-a9n8m4ec.js → index-jvescqgr.js} +1 -1
- package/dist/{index-x62gnfsk.js → index-pmftwk2a.js} +19 -1
- package/dist/{index-g8kyab85.js → index-y4132gqh.js} +81 -12
- package/dist/{index-tje0q6gp.js → index-zfscdskj.js} +6 -1
- package/dist/{index-4gawbm74.js → index-zza375qp.js} +41 -3
- package/dist/index.js +39 -25
- package/dist/internal/errors.d.ts +6 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/write-download.d.ts +17 -0
- package/dist/internal/write-download.d.ts.map +1 -0
- package/dist/node.js +3 -3
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/error-hook.d.ts +14 -3
- package/dist/server/error-hook.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +21 -8
- package/dist/server/middleware/cors.d.ts +19 -0
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/router.d.ts +27 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/types.d.ts +44 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/agent.d.ts +2 -0
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/execute.d.ts +1 -1
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/list-names.d.ts +2 -1
- package/dist/tools/list-names.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +10 -3
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/mount-upload.d.ts.map +1 -1
- package/dist/tools/mount-wait.d.ts.map +1 -1
- package/dist/tools/mount.d.ts +13 -0
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/names.d.ts +34 -0
- package/dist/tools/names.d.ts.map +1 -1
- package/dist/tools/remote.d.ts.map +1 -1
- package/dist/tools/transports.d.ts.map +1 -1
- package/dist/tools.js +69 -49
- package/llms-full.txt +223 -12
- package/llms.txt +1 -1
- package/package.json +1 -1
package/llms-full.txt
CHANGED
|
@@ -237,11 +237,13 @@ 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 (
|
|
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) |
|
|
244
244
|
| `meta` | no | opaque app metadata — read in hooks / on tool mounts, never in OpenAPI ([below](#endpoint-metadata-meta)) |
|
|
245
|
+
| `rawResponse` | no | the handler returns the `Response` itself — a download, a file, an SSE stream. HTTP-only, never a tool, no `output`. See [Raw-response endpoints](./server.md#raw-response-endpoints) |
|
|
246
|
+
| `contentType` | no | documented response media type of a `rawResponse` endpoint (OpenAPI only) |
|
|
245
247
|
|
|
246
248
|
## `params` vs `input` vs `output`
|
|
247
249
|
|
|
@@ -313,8 +315,10 @@ tools. Narrow it with `expose`:
|
|
|
313
315
|
- `expose: ['MCP', 'AGENT']` — a tool only; no HTTP route.
|
|
314
316
|
- omit `expose` — all transports.
|
|
315
317
|
|
|
316
|
-
Tool transports (`MCP`, `AGENT`) skip
|
|
317
|
-
file upload is not a tool call
|
|
318
|
+
Tool transports (`MCP`, `AGENT`) skip two kinds of endpoint automatically:
|
|
319
|
+
`multipart` (a file upload is not a tool call) and
|
|
320
|
+
[`rawResponse`](./server.md#raw-response-endpoints) (its answer is bytes, which
|
|
321
|
+
a tool result cannot carry — it would serialize to `{}`).
|
|
318
322
|
|
|
319
323
|
## `toolName`
|
|
320
324
|
|
|
@@ -326,8 +330,53 @@ derivation from the method key + prefix (`users` + `create` ⇒ `create_user`,
|
|
|
326
330
|
{ method: 'POST', path: '/', desc: 'Create a user', toolName: 'create_user', /* … */ }
|
|
327
331
|
```
|
|
328
332
|
|
|
333
|
+
**Every tool name — derived or explicit — must match `[a-zA-Z0-9_-]` and be at
|
|
334
|
+
most 64 characters**, the character class every major provider accepts.
|
|
335
|
+
|
|
336
|
+
Note the two classes differ. What is *accepted* includes the hyphen; what
|
|
337
|
+
derivation *keeps* does not — a derived name normalises everything outside
|
|
338
|
+
`[a-zA-Z0-9_]` to `_`, the hyphen included, so `bot-status` + `get` ⇒
|
|
339
|
+
`get_bot_status` and `admin/analytics` + `get` ⇒ `get_admin_analytics`. A hyphen
|
|
340
|
+
survives only in an explicit `toolName`, which is taken verbatim.
|
|
341
|
+
|
|
342
|
+
A prefix with *no* usable character at all (`'///'`, `'_'`, a fully non-ASCII
|
|
343
|
+
prefix) and any explicit `toolName` outside the accepted class **throw at
|
|
344
|
+
mount**. An unusable prefix is rescued by setting an explicit `toolName` — the
|
|
345
|
+
prefix then never enters the name. An over-long name is fixed by a shorter
|
|
346
|
+
explicit `toolName` (or a shorter prefix / method key). Nothing downstream checks this: the provider rejects the whole
|
|
347
|
+
request, so one bad name takes every tool of that mount down with it.
|
|
348
|
+
→ [ADR 0035](../decisions/0035-tool-name-derivation-and-validation.md).
|
|
349
|
+
|
|
329
350
|
## Endpoint metadata (`meta`)
|
|
330
351
|
|
|
352
|
+
A contract can declare a **default** `meta` that every endpoint inherits, and an
|
|
353
|
+
endpoint's own keys are **shallow-merged over** it:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
defineContract({ prefix: 'admin', meta: { public: true } }, {
|
|
357
|
+
list: { method: 'GET', path: '/', desc: 'List', /* meta → { public: true } */ },
|
|
358
|
+
purge: { method: 'POST', path: '/purge', desc: 'Purge', meta: { rateTier: 2 } },
|
|
359
|
+
// meta → { public: true, rateTier: 2 }
|
|
360
|
+
})
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
One level deep — no deep merge. To **opt an endpoint out** of an inherited key,
|
|
364
|
+
declare it with an explicit `undefined`:
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
defineContract({ prefix: 'leads', meta: { page: 'LEADS' } }, {
|
|
368
|
+
// Public website form inside an otherwise admin-gated contract:
|
|
369
|
+
submit: { method: 'POST', path: '/', desc: 'Submit', meta: { page: undefined } },
|
|
370
|
+
// meta → { page: undefined } — gate off
|
|
371
|
+
list: { method: 'GET', path: '/list', desc: 'List' }, // meta → { page: 'LEADS' }
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
The key stays *present* with value `undefined`, so read `meta` by **value**
|
|
376
|
+
(`method.meta?.page`), never by key membership. `expose` deliberately has **no**
|
|
377
|
+
contract-level equivalent
|
|
378
|
+
(→ [ADR 0036](../decisions/0036-contract-level-meta.md)).
|
|
379
|
+
|
|
331
380
|
`meta` is an **opaque, app-defined** bag the core attaches no meaning to — the
|
|
332
381
|
same escape-hatch spirit as `scope` being a free string ([ADR 0002](../decisions/0002-generic-core.md) /
|
|
333
382
|
[ADR 0021](../decisions/0021-endpoint-meta-passthrough.md)). Declare app concerns
|
|
@@ -569,7 +618,7 @@ server. See [Testing & deployment](./testing-and-deployment.md).
|
|
|
569
618
|
| `rawRoutes` | non-contract routes (see below) |
|
|
570
619
|
| `maxUploadBytes` | default multipart upload cap (bytes); per-route `EndpointDef.maxUploadBytes` overrides |
|
|
571
620
|
| `port` / `hostname` | listen address — port defaults to `3000` |
|
|
572
|
-
| `cors` | CORS policy — `{ origin,
|
|
621
|
+
| `cors` | CORS policy — `{ origin, credentials, methods, headers, exposeHeaders }` |
|
|
573
622
|
| `hooks` | lifecycle hooks (see below) |
|
|
574
623
|
| `logging` | `true` for built-in request logs, or a custom `StitchLogger` |
|
|
575
624
|
| `traceId` | override per-request trace-id resolution |
|
|
@@ -689,6 +738,87 @@ createServer({
|
|
|
689
738
|
Hooks see `RuntimeContext` (loose types); handlers see `HandlerContext` (typed).
|
|
690
739
|
That split is deliberate — see [ADR 0003](../decisions/0003-two-context-types.md).
|
|
691
740
|
|
|
741
|
+
## Raw-response endpoints
|
|
742
|
+
|
|
743
|
+
An endpoint that answers with **bytes rather than data** — a PDF download, a
|
|
744
|
+
file, an SSE stream — declares `rawResponse: true` and returns the `Response` itself:
|
|
745
|
+
|
|
746
|
+
```ts
|
|
747
|
+
// contract
|
|
748
|
+
export const documents = defineContract(
|
|
749
|
+
{ prefix: 'documents', scope: 'admin' },
|
|
750
|
+
{
|
|
751
|
+
download: {
|
|
752
|
+
method: 'GET', path: '/:id/pdf', desc: 'Download a document as a PDF',
|
|
753
|
+
params: z.object({ id: z.uuid() }),
|
|
754
|
+
rawResponse: true, contentType: 'application/pdf',
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
// handler — no guard on the first line; `beforeHandle` already ran
|
|
760
|
+
download: (ctx) => serveFile(ctx.req, { path: pathFor(ctx.params.id),
|
|
761
|
+
filename: 'offer.pdf' }),
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
The request half is untouched: `params`, `input` and `multipart` parse and
|
|
765
|
+
validate exactly as elsewhere, and the endpoint goes through `beforeHandle` — so
|
|
766
|
+
the **auth gate applies without a guard in the handler**. Only the response is
|
|
767
|
+
handed over, so there is no `output` schema, `afterHandle` is skipped (it
|
|
768
|
+
transforms data; there is none) and the endpoint is HTTP-only: never an MCP
|
|
769
|
+
tool, an agent tool or a CLI command. Declaring `output`, `toolName`, `ui`,
|
|
770
|
+
`annotations` or a non-HTTP `expose` alongside `rawResponse` is a type error, and throws
|
|
771
|
+
at definition time for a contract assembled at runtime.
|
|
772
|
+
|
|
773
|
+
On the typed client the method resolves to the untouched `Response` — the
|
|
774
|
+
filename lives in `Content-Disposition`, so a `Blob` alone would lose it:
|
|
775
|
+
|
|
776
|
+
```ts
|
|
777
|
+
const client = createClient(documents, http)
|
|
778
|
+
const res = await client.download({ id }) // Response
|
|
779
|
+
const name = res.headers.get('Content-Disposition')
|
|
780
|
+
const blob = await res.blob()
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
Cross-origin, remember that those headers are readable only because CORS exposes
|
|
784
|
+
them — see [`cors.exposeHeaders`](#serving-files--range-requests).
|
|
785
|
+
|
|
786
|
+
**Raw response or [raw route](#raw-routes)?** Both hand the `Response` to your
|
|
787
|
+
code. A raw-response *endpoint* stays in the contract: only its response is
|
|
788
|
+
raw — it is still routed, gated, typed and documented like every other endpoint.
|
|
789
|
+
A raw *route* is outside the contract entirely — no schemas, no auth gate, no
|
|
790
|
+
client — which is what you want for an OAuth redirect or a webhook, and what you
|
|
791
|
+
do not want for a download.
|
|
792
|
+
|
|
793
|
+
⚠️ **Delete the old raw route when you move an endpoint into the contract.** Raw
|
|
794
|
+
routes are matched **first**, so a leftover one keeps serving the bytes and the
|
|
795
|
+
contract endpoint — with its auth gate — never runs. stitchkit warns at startup
|
|
796
|
+
when a raw route shadows a contract route, naming both and the scope being
|
|
797
|
+
bypassed; treat that warning as a bug.
|
|
798
|
+
|
|
799
|
+
⚠️ **A path built from user input needs a containment check.** `staticRoute`
|
|
800
|
+
enforces it; `serveFile` deliberately leaves it to the caller, so an endpoint
|
|
801
|
+
serving `/:filename` must not pass it through:
|
|
802
|
+
|
|
803
|
+
```ts
|
|
804
|
+
import { isWithinDir, serveFile } from 'stitchkit/server'
|
|
805
|
+
import { resolve } from 'node:path'
|
|
806
|
+
|
|
807
|
+
const ROOT = resolve('./uploads')
|
|
808
|
+
|
|
809
|
+
file: (ctx) => {
|
|
810
|
+
const target = resolve(ROOT, ctx.params.filename)
|
|
811
|
+
// `../../etc/passwd` resolves outside ROOT — reject before touching disk.
|
|
812
|
+
if (!isWithinDir(ROOT, target)) throw notFound('File not found')
|
|
813
|
+
return serveFile(ctx.req, { path: target })
|
|
814
|
+
},
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
`implementRemote` proxies a raw-response endpoint like any other — the remote
|
|
818
|
+
`Response` is forwarded verbatim. Request headers are not relayed, so a `Range`
|
|
819
|
+
sent to the proxy does not reach the origin and the full body comes back.
|
|
820
|
+
→ ADR 0038.
|
|
821
|
+
|
|
692
822
|
## Raw routes
|
|
693
823
|
|
|
694
824
|
Some routes cannot be a clean JSON contract — an OAuth redirect, a webhook with
|
|
@@ -782,6 +912,13 @@ It always sets `Accept-Ranges: bytes`, a weak `ETag` and `Last-Modified` (so
|
|
|
782
912
|
and `nosniff`. `Content-Type` is auto-detected from the path — override it, or
|
|
783
913
|
pass `disposition` / `cacheControl` / `etag: false`, via the options.
|
|
784
914
|
|
|
915
|
+
Cross-origin, the browser lets JavaScript read only the CORS-safelisted response
|
|
916
|
+
headers. stitchkit therefore exposes the download-relevant ones by default
|
|
917
|
+
(`Content-Disposition`, `Content-Range`, `ETag`, …) — without that a `fetch`-based
|
|
918
|
+
download cannot recover the file's name. Override with `cors.exposeHeaders`
|
|
919
|
+
(extend `DEFAULT_CORS_EXPOSE_HEADERS` rather than replacing it), or pass `[]` to
|
|
920
|
+
emit none.
|
|
921
|
+
|
|
785
922
|
`serveFile` takes an explicit `path` and trusts it — **the caller owns
|
|
786
923
|
containment**. For a URL-derived path use `staticRoute` (which enforces it) or
|
|
787
924
|
`isWithinDir` first. The byte-range parser is exported on its own as
|
|
@@ -803,11 +940,20 @@ focused helper — not a sub-framework.
|
|
|
803
940
|
|
|
804
941
|
### SSE streaming
|
|
805
942
|
|
|
943
|
+
`streamSSE` returns a `Response`, so its endpoint declares
|
|
944
|
+
[`rawResponse: true`](#raw-response-endpoints) — in a plain contract handler the response
|
|
945
|
+
would be serialized into `{}` (that now fails loudly instead of shipping silently).
|
|
946
|
+
|
|
806
947
|
```ts
|
|
807
948
|
import { streamSSE } from 'stitchkit/server'
|
|
808
949
|
|
|
950
|
+
// contract
|
|
951
|
+
stream: { method: 'GET', path: '/stream', desc: 'Stream tokens',
|
|
952
|
+
rawResponse: true, contentType: 'text/event-stream' },
|
|
953
|
+
|
|
954
|
+
// handler
|
|
809
955
|
async function* tokens() { yield 'a'; yield 'b' }
|
|
810
|
-
|
|
956
|
+
stream: () => streamSSE(tokens()), // → a text/event-stream Response
|
|
811
957
|
```
|
|
812
958
|
|
|
813
959
|
The client side is [`parseSSE`](./client.md#sse).
|
|
@@ -1169,10 +1315,23 @@ By default every endpoint is a tool on every transport. `expose` narrows it:
|
|
|
1169
1315
|
{ method: 'GET', path: '/lookup', desc: 'Look up a price', expose: ['MCP'] } // MCP tool only
|
|
1170
1316
|
```
|
|
1171
1317
|
|
|
1318
|
+
Two kinds of endpoint are **never** tools, whatever `expose` says: a `multipart`
|
|
1319
|
+
upload (not a tool call), and a
|
|
1320
|
+
[`rawResponse`](./server.md#raw-response-endpoints) endpoint (its answer is
|
|
1321
|
+
bytes — a tool result cannot carry them, and it would reach the model as `{}`).
|
|
1322
|
+
Pin the full list with `listToolNames` in a snapshot test.
|
|
1323
|
+
|
|
1172
1324
|
`desc` is the tool description the model reads — write it for the model, not
|
|
1173
|
-
just for a human.
|
|
1325
|
+
just for a human. The tool name defaults
|
|
1174
1326
|
to a verb-aware name from the method + prefix (`list` → `list_widgets`, `get` →
|
|
1175
|
-
`get_widget`); set `toolName` for an explicit one.
|
|
1327
|
+
`get_widget`); set `toolName` for an explicit one. Derivation normalises every
|
|
1328
|
+
character outside `[a-zA-Z0-9_]` to `_` — the hyphen included, so `bot-status`
|
|
1329
|
+
derives `get_bot_status` — while a name is *accepted* if it matches
|
|
1330
|
+
`[a-zA-Z0-9_-]`, so a hyphen survives in an explicit `toolName`. A name that
|
|
1331
|
+
still cannot be delivered (illegal explicit `toolName`, over 64 characters, or a
|
|
1332
|
+
prefix with no usable character) throws at mount rather than at the first model
|
|
1333
|
+
call —
|
|
1334
|
+
→ [ADR 0035](../decisions/0035-tool-name-derivation-and-validation.md). See
|
|
1176
1335
|
[Contracts → transports](./contracts.md#transports).
|
|
1177
1336
|
|
|
1178
1337
|
### Pinning tool names — `listToolNames`
|
|
@@ -1196,6 +1355,14 @@ now fails this test instead of silently breaking the clients that call the
|
|
|
1196
1355
|
tool. It is also the mechanical diff when migrating a service: run it before
|
|
1197
1356
|
and after, compare.
|
|
1198
1357
|
|
|
1358
|
+
> **This is also the guard against a forgotten `expose`.** An endpoint that
|
|
1359
|
+
> declares none is a tool on MCP **and** AGENT — the default is fail-open, and
|
|
1360
|
+
> there is no contract-level `expose` to set once (→
|
|
1361
|
+
> [ADR 0036](../decisions/0036-contract-level-meta.md)). A snapshot of
|
|
1362
|
+
> `listToolNames` fails the build the moment an endpoint you meant to keep
|
|
1363
|
+
> HTTP-only shows up in the list, which is the one check that catches it however
|
|
1364
|
+
> many places the line was forgotten.
|
|
1365
|
+
|
|
1199
1366
|
## MCP — `createMcpHandler`
|
|
1200
1367
|
|
|
1201
1368
|
`createMcpHandler` builds a complete Streamable-HTTP MCP server as a single
|
|
@@ -1234,11 +1401,11 @@ createServer({
|
|
|
1234
1401
|
| `hooks` | tool-call observability hooks — `afterToolCall` fires on every result |
|
|
1235
1402
|
| `onIncompatibleSchema` | `'throw'` (default) · `'skip'` · `'warn'` — see below |
|
|
1236
1403
|
| `logger` | a `StitchLogger` for the `'warn'` policy |
|
|
1237
|
-
| `nativeTools` | register non-contract tools directly on the `McpServer` |
|
|
1404
|
+
| `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
1405
|
| `instructions` | a short host-facing usage hint, surfaced to MCP tool-search |
|
|
1239
1406
|
|
|
1240
|
-
`services` and `
|
|
1241
|
-
shown only its own tools and every handler can read `ctx.tenantId`.
|
|
1407
|
+
`services`, `context` and `nativeTools` all receive the resolved identity, so a
|
|
1408
|
+
tenant can be shown only its own tools and every handler can read `ctx.tenantId`.
|
|
1242
1409
|
|
|
1243
1410
|
### Guarding tools — `lifecycle`
|
|
1244
1411
|
|
|
@@ -2479,7 +2646,13 @@ const onError = createErrorHook({
|
|
|
2479
2646
|
CONFLICT: 'conflict', RATE_LIMITED: 'rate_limited',
|
|
2480
2647
|
INTERNAL_SERVER_ERROR: 'internal',
|
|
2481
2648
|
} satisfies Record<StitchErrorCode, string>,
|
|
2482
|
-
|
|
2649
|
+
// `ctx` is the request's RuntimeContext — read `ctx.traceId` for a
|
|
2650
|
+
// correlation id in the envelope. Declaring it is optional.
|
|
2651
|
+
render: (info, ctx) => ({
|
|
2652
|
+
ok: false,
|
|
2653
|
+
error: { code: info.code, message: info.message },
|
|
2654
|
+
traceId: ctx.traceId,
|
|
2655
|
+
}),
|
|
2483
2656
|
})
|
|
2484
2657
|
|
|
2485
2658
|
createServer({ services, hooks: { onError } })
|
|
@@ -3123,6 +3296,42 @@ current one *up to* your target, and apply each snippet.
|
|
|
3123
3296
|
(`STITCH_ERROR_STATUS`, `serveFile`, `scopePrefixes`, `afterToolCall`'s
|
|
3124
3297
|
`MethodDef`, `maxUploadBytes`) are available to adopt, not required.
|
|
3125
3298
|
|
|
3299
|
+
## Your handlers may be returning more than the contract declares
|
|
3300
|
+
|
|
3301
|
+
stitchkit validates every handler's return value against the endpoint's `output`
|
|
3302
|
+
schema and **passes on the parsed result** — so any field the schema does not
|
|
3303
|
+
declare is silently removed. That is deliberate (the contract is the published
|
|
3304
|
+
shape of the response), but when you are moving a *live* API onto stitchkit it is
|
|
3305
|
+
invisible: TypeScript does not reject excess properties, nothing logs it, and the
|
|
3306
|
+
client just receives fewer fields.
|
|
3307
|
+
|
|
3308
|
+
While migrating, turn the diagnostic on:
|
|
3309
|
+
|
|
3310
|
+
```ts
|
|
3311
|
+
createServer({ services, warnOnOutputStrip: true }) // off by default
|
|
3312
|
+
```
|
|
3313
|
+
|
|
3314
|
+
Every removed key is logged as a dot-path with the endpoint that produced it
|
|
3315
|
+
(`notes.get: secret, nested.alsoSecret`). Tool transports strip identically —
|
|
3316
|
+
`mountMcp` / `mountAgent` take `onOutputStrip: (toolName, paths) => …`. Read the
|
|
3317
|
+
list, then either widen the contract or stop returning the field, and turn the
|
|
3318
|
+
flag back off: it is for the migration window, not for production.
|
|
3319
|
+
|
|
3320
|
+
## Tool names may shift between versions
|
|
3321
|
+
|
|
3322
|
+
Derived tool names are part of your public surface — an MCP client config or an
|
|
3323
|
+
agent prompt refers to them by string. Before and after any upgrade that touches
|
|
3324
|
+
name derivation, diff them mechanically:
|
|
3325
|
+
|
|
3326
|
+
```ts
|
|
3327
|
+
import { listToolNames } from 'stitchkit/tools'
|
|
3328
|
+
console.log(JSON.stringify(listToolNames(services), null, 2))
|
|
3329
|
+
```
|
|
3330
|
+
|
|
3331
|
+
`listToolNames` never throws on an illegal name — that is deliberate, so it can
|
|
3332
|
+
show you the offending row when a mount would refuse it. Pin it in a snapshot
|
|
3333
|
+
test and a shift fails your build instead of your clients.
|
|
3334
|
+
|
|
3126
3335
|
## When you author a breaking change in stitchkit
|
|
3127
3336
|
|
|
3128
3337
|
You are on the other side of this flow — see
|
|
@@ -3212,7 +3421,7 @@ from the root `stitchkit`.
|
|
|
3212
3421
|
| `ScopedDefineContract` | _type_ | the `defineContract` `createContractFactory` returns |
|
|
3213
3422
|
| `ALL_TRANSPORTS` | constant | `['HTTP', 'MCP', 'AGENT', 'CLI']` |
|
|
3214
3423
|
| `ContractDef` | _type_ | a defined contract |
|
|
3215
|
-
| `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` |
|
|
3424
|
+
| `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` and `meta` (a default every endpoint shallow-merges over) |
|
|
3216
3425
|
| `EndpointDef` | _type_ | a single endpoint definition |
|
|
3217
3426
|
| `HttpMethod` | _type_ | `GET \| POST \| PUT \| PATCH \| DELETE` |
|
|
3218
3427
|
| `Transport` | _type_ | `HTTP \| MCP \| AGENT \| CLI` |
|
|
@@ -3276,6 +3485,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
3276
3485
|
| `staticRoute` | function | a raw route that serves a directory |
|
|
3277
3486
|
| `serveFile` | function | serve a file with `Range` / `304` / `HEAD` — [guide](../guide/server.md#serving-files--range-requests) |
|
|
3278
3487
|
| `parseByteRange` | function | parse a single `Range` header → range / `unsatisfiable` / `null` |
|
|
3488
|
+
| `isWithinDir` | function | path containment — `(root, resolvedTarget) => boolean`; call it before `serveFile` on any URL-derived path |
|
|
3279
3489
|
| `weakETag` | function | a weak `ETag` from size + mtime |
|
|
3280
3490
|
| `ServeFileOptions` | _type_ | options for `serveFile` |
|
|
3281
3491
|
| `ByteRange` | _type_ | an inclusive `{ start, end }` byte range |
|
|
@@ -3332,6 +3542,7 @@ Also re-exports the error helpers from `stitchkit/contract`.
|
|
|
3332
3542
|
| `corsHeaders` | function | compute CORS response headers |
|
|
3333
3543
|
| `corsPreflightResponse` | function | build a preflight `Response` |
|
|
3334
3544
|
| `DEFAULT_CORS_ALLOW_HEADERS` | const | the default `Access-Control-Allow-Headers` (incl. `traceparent`) — extend it when overriding `cors.headers` |
|
|
3545
|
+
| `DEFAULT_CORS_EXPOSE_HEADERS` | const | the default `Access-Control-Expose-Headers` (incl. `Content-Disposition`, `ETag`, `Content-Range`) — extend it when overriding `cors.exposeHeaders` |
|
|
3335
3546
|
| `CookieDef` | _type_ | the `defineCookie` handle |
|
|
3336
3547
|
| `CookieOptions` | _type_ | cookie attributes |
|
|
3337
3548
|
| `CorsConfig` | _type_ | CORS policy |
|
package/llms.txt
CHANGED
|
@@ -7,7 +7,7 @@ Build with stitchkit: define a contract once, then `implement` it and serve it (
|
|
|
7
7
|
## Guide
|
|
8
8
|
- [Getting started](https://github.com/max-listov/stitchkit/blob/master/docs/guide/getting-started.md): install, entrypoints, and a first contract → server → client app
|
|
9
9
|
- [Contracts](https://github.com/max-listov/stitchkit/blob/master/docs/guide/contracts.md): every endpoint field — method, path, params/input/output, scope, expose, meta, multipart
|
|
10
|
-
- [HTTP server](https://github.com/max-listov/stitchkit/blob/master/docs/guide/server.md): createServer/createHandler, implement, lifecycle hooks, raw routes + helpers, scopePrefixes, serveFile, primitives
|
|
10
|
+
- [HTTP server](https://github.com/max-listov/stitchkit/blob/master/docs/guide/server.md): createServer/createHandler, implement, lifecycle hooks, raw routes + raw-response endpoints + helpers, scopePrefixes, serveFile, primitives
|
|
11
11
|
- [Typed client](https://github.com/max-listov/stitchkit/blob/master/docs/guide/client.md): createClient/createHttpClient, the typed call surface, scoped clients, SSE
|
|
12
12
|
- [MCP & agents](https://github.com/max-listov/stitchkit/blob/master/docs/guide/mcp-and-agents.md): contracts as MCP tools (createMcpHandler) and AI-agent tools (mountAgent); tool lifecycle, extend, identity
|
|
13
13
|
- [CLI](https://github.com/max-listov/stitchkit/blob/master/docs/guide/cli.md): contracts as a command-line program
|
package/package.json
CHANGED