stitchkit 0.38.0 → 0.40.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/README.md +1 -1
- package/dist/browser/client-url.d.ts +2 -0
- package/dist/browser/client-url.d.ts.map +1 -1
- package/dist/browser/client.d.ts +32 -1
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +5 -6
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/contract/define.d.ts +36 -12
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/factory.d.ts +3 -3
- package/dist/contract/factory.d.ts.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/{index-czmqks7r.js → index-6jypn22c.js} +1 -1
- package/dist/{index-p3kwf73n.js → index-7rbzbnnf.js} +28 -15
- package/dist/{index-bgdd42pt.js → index-92gs1m5b.js} +1 -1
- package/dist/{index-mzx0an0s.js → index-fyfk537k.js} +50 -5
- package/dist/{index-n5t4gnfz.js → index-p1x65gk7.js} +18 -3
- package/dist/{index-x4wbc8sz.js → index-xax049k6.js} +73 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +122 -17
- package/dist/internal/route-pattern.d.ts +7 -0
- package/dist/internal/route-pattern.d.ts.map +1 -0
- package/dist/node.js +3 -3
- package/dist/observability/index.js +1 -1
- package/dist/react/entity-cache.d.ts +56 -42
- package/dist/react/entity-cache.d.ts.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +98 -46
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/index.js +10 -9
- 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 +2 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/socket-io.d.ts +1 -1
- package/dist/server/types.d.ts +4 -6
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/agent.d.ts +3 -0
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/execute.d.ts +8 -4
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/invoker.d.ts +37 -0
- package/dist/tools/invoker.d.ts.map +1 -0
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/names.d.ts +1 -1
- package/dist/tools/names.d.ts.map +1 -1
- package/dist/tools/native-mcp.d.ts +6 -33
- package/dist/tools/native-mcp.d.ts.map +1 -1
- package/dist/tools/runtime-tool.d.ts +58 -0
- package/dist/tools/runtime-tool.d.ts.map +1 -0
- package/dist/tools.d.ts +3 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +193 -82
- package/llms-full.txt +492 -93
- package/package.json +1 -1
package/llms-full.txt
CHANGED
|
@@ -229,8 +229,8 @@ export const users = defineContract({ prefix: 'users' }, {
|
|
|
229
229
|
|
|
230
230
|
| Field | Required | Purpose |
|
|
231
231
|
|-------|----------|---------|
|
|
232
|
-
| `method` | yes | `GET` · `POST` · `PUT` · `PATCH` · `DELETE` |
|
|
233
|
-
| `path` | yes | route path under the contract `prefix`; `:name` marks a path param and a terminal
|
|
232
|
+
| `method` | yes | `GET` · `HEAD` · `POST` · `PUT` · `PATCH` · `DELETE` |
|
|
233
|
+
| `path` | yes | route path under the contract `prefix`; `:name` marks a path param and a named terminal `/*filePath` captures the remaining path |
|
|
234
234
|
| `desc` | yes | human description — also the MCP / agent tool description |
|
|
235
235
|
| `params` | no | Zod schema for **path params** (`:id`, …) |
|
|
236
236
|
| `input` | no | Zod schema for the **request body** (or query, for GET/DELETE) |
|
|
@@ -248,16 +248,24 @@ export const users = defineContract({ prefix: 'users' }, {
|
|
|
248
248
|
| `responseMeta` | no | make a typed-data endpoint HTTP-only, optionally declare its success status and expose `ctx.response.headers` — [Typed JSON response metadata](./server.md#typed-json-response-metadata) |
|
|
249
249
|
| `contentType` | no | documented response media type of a `rawResponse` endpoint (OpenAPI only) |
|
|
250
250
|
|
|
251
|
+
`HEAD` is an explicit HTTP-only operation, never an automatic alias for `GET`.
|
|
252
|
+
It must declare `rawResponse: true`; request body, `input`, multipart, `rawBody`,
|
|
253
|
+
typed `output` and tool exposure are rejected. The handler owns status and
|
|
254
|
+
headers, while Stitchkit guarantees an empty wire body even if the returned
|
|
255
|
+
`Response` accidentally contains one.
|
|
256
|
+
|
|
251
257
|
## `params` vs `input` vs `output`
|
|
252
258
|
|
|
253
259
|
The three schemas are distinct on purpose:
|
|
254
260
|
|
|
255
261
|
- **`params`** — values in the URL path. `path: '/:id'` ⇒
|
|
256
262
|
`params: z.object({ id: z.string() })`. The client takes them from the call
|
|
257
|
-
argument and substitutes them into the URL. A terminal wildcard is
|
|
258
|
-
|
|
259
|
-
`params: z.object({ slug: z.string(),
|
|
260
|
-
`/foo/page` and `/foo/a/b`; the handler
|
|
263
|
+
argument and substitutes them into the URL. A terminal wildcard is explicitly
|
|
264
|
+
named: `path: '/:slug/*filePath'` with
|
|
265
|
+
`params: z.object({ slug: z.string(), filePath: z.string() })` matches both
|
|
266
|
+
`/foo/page` and `/foo/a/b`; the handler reads `ctx.params.filePath` as
|
|
267
|
+
`'page'` or `'a/b'`. Bare `/*`, invalid/duplicate names and a wildcard before
|
|
268
|
+
the final segment fail at contract definition.
|
|
261
269
|
- **`input`** — the request payload. For `POST` / `PUT` / `PATCH` it is the JSON
|
|
262
270
|
body; for `GET` / `DELETE` it is the query string. The handler reads it as
|
|
263
271
|
`ctx.input`.
|
|
@@ -272,8 +280,8 @@ the body.
|
|
|
272
280
|
// path: '/:id', params: { id }, input: { text }
|
|
273
281
|
await api.update({ id: '1', text: 'new' }) // PUT /users/1 body: { text: 'new' }
|
|
274
282
|
|
|
275
|
-
// path: '/:slug/*', params: { slug,
|
|
276
|
-
await api.app({ slug: 'foo',
|
|
283
|
+
// path: '/:slug/*filePath', params: { slug, filePath }
|
|
284
|
+
await api.app({ slug: 'foo', filePath: 'a/b' }) // GET /apps/foo/a/b
|
|
277
285
|
```
|
|
278
286
|
|
|
279
287
|
### Input vs. output types
|
|
@@ -523,11 +531,13 @@ the endpoint is public (fail-open). If every contract in your app must be scoped
|
|
|
523
531
|
// app: one line, once
|
|
524
532
|
export const { defineContract } = createContractFactory<'public' | 'user' | 'admin'>()
|
|
525
533
|
|
|
526
|
-
// scope is
|
|
534
|
+
// scope is mandatory, checked against the union and retained as the 'user' literal
|
|
527
535
|
export const users = defineContract({ prefix: 'users', scope: 'user' }, { … })
|
|
528
536
|
```
|
|
529
537
|
|
|
530
|
-
The vocabulary is yours;
|
|
538
|
+
The vocabulary is yours; each returned `ContractDef` retains its concrete scope
|
|
539
|
+
literal, so scope-aware registries select the exact config without another
|
|
540
|
+
wrapper.
|
|
531
541
|
|
|
532
542
|
## One source of truth
|
|
533
543
|
|
|
@@ -773,11 +783,12 @@ prefix param in the schema, use a non-strict `z.object` (extra keys are dropped
|
|
|
773
783
|
from `ctx.params`, but `ctx.tenantId` still works), or read the param off the
|
|
774
784
|
context root.
|
|
775
785
|
|
|
776
|
-
**Trailing wildcard.** A contract path may end in
|
|
786
|
+
**Trailing wildcard.** A contract path may end in a named wildcard.
|
|
787
|
+
`/app/:slug/*filePath` matches
|
|
777
788
|
both `/app/foo` and nested paths such as `/app/foo/a/b`; the collected params are
|
|
778
|
-
`{ slug: 'foo',
|
|
779
|
-
|
|
780
|
-
`ctx.params`. Each captured segment is URL-decoded before the remainder is
|
|
789
|
+
`{ slug: 'foo', filePath: '' }` and `{ slug: 'foo', filePath: 'a/b' }`
|
|
790
|
+
respectively. Put `filePath` in the endpoint's `params` schema to keep it in
|
|
791
|
+
typed `ctx.params`. Each captured segment is URL-decoded before the remainder is
|
|
781
792
|
joined, so encoded spaces and reserved characters reach the handler as their
|
|
782
793
|
semantic values while `/` remains the segment boundary. Static and named-param
|
|
783
794
|
routes are matched before a catch-all, so a
|
|
@@ -1026,9 +1037,9 @@ createServer({
|
|
|
1026
1037
|
})
|
|
1027
1038
|
```
|
|
1028
1039
|
|
|
1029
|
-
A path may be exact, carry `:param` segments, or end in
|
|
1030
|
-
wildcard — and the two combine: `/app/:slug
|
|
1031
|
-
`ctx.params.slug === 'x'` and the remainder in `ctx.params
|
|
1040
|
+
A path may be exact, carry `:param` segments, or end in `/*filePath` for a prefix
|
|
1041
|
+
wildcard — and the two combine: `/app/:slug/*filePath` matches `/app/x/a/b` with
|
|
1042
|
+
`ctx.params.slug === 'x'` and the remainder in `ctx.params.filePath` (a SPA
|
|
1032
1043
|
deep-link fallback). List more specific routes before the wildcard — the first
|
|
1033
1044
|
match wins. `staticRoute()` builds a raw route that serves a directory.
|
|
1034
1045
|
|
|
@@ -1067,24 +1078,35 @@ seek and cache, use **`serveFile`** (Bun) — it streams the requested byte rang
|
|
|
1067
1078
|
and speaks the conditional-request half of RFC 7233 / 9110:
|
|
1068
1079
|
|
|
1069
1080
|
```ts
|
|
1070
|
-
import {
|
|
1081
|
+
import { defineContract } from 'stitchkit/contract'
|
|
1082
|
+
import { implement, serveFile } from 'stitchkit/server'
|
|
1083
|
+
import { z } from 'zod'
|
|
1071
1084
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
+
const MediaParams = z.object({ id: z.string() })
|
|
1086
|
+
const media = defineContract({ prefix: 'media' }, {
|
|
1087
|
+
download: {
|
|
1088
|
+
method: 'GET', path: '/:id', desc: 'Download media',
|
|
1089
|
+
params: MediaParams, rawResponse: true, contentType: 'video/mp4',
|
|
1090
|
+
},
|
|
1091
|
+
inspect: {
|
|
1092
|
+
method: 'HEAD', path: '/:id', desc: 'Inspect media',
|
|
1093
|
+
params: MediaParams, rawResponse: true, contentType: 'video/mp4',
|
|
1094
|
+
},
|
|
1095
|
+
})
|
|
1096
|
+
|
|
1097
|
+
const mediaService = implement(media, {
|
|
1098
|
+
download: ({ req, params }) =>
|
|
1099
|
+
serveFile(req, { path: pathForId(params.id), filename: 'clip.mp4' }),
|
|
1100
|
+
inspect: ({ req, params }) =>
|
|
1101
|
+
serveFile(req, { path: pathForId(params.id), filename: 'clip.mp4' }),
|
|
1085
1102
|
})
|
|
1086
1103
|
```
|
|
1087
1104
|
|
|
1105
|
+
GET and HEAD are separate operations deliberately: declaring GET never creates
|
|
1106
|
+
a hidden HEAD alias. Both travel through the normal contract router, params,
|
|
1107
|
+
lifecycle/RBAC and request logging. A HEAD handler may inspect the raw query via
|
|
1108
|
+
`ctx.req.url`, but cannot declare a request input schema or body.
|
|
1109
|
+
|
|
1088
1110
|
`serveFile` returns `206` (range, with `Content-Range` + `Content-Length`), `200`
|
|
1089
1111
|
(full), `416` (unsatisfiable, `Content-Range: bytes */size`), `304`
|
|
1090
1112
|
(`If-None-Match` / `If-Modified-Since`), `404` (missing) or `405` (non GET/HEAD).
|
|
@@ -1205,7 +1227,8 @@ createServer({ services: [users, orders], rawRoutes: [openApiRoute('/openapi.jso
|
|
|
1205
1227
|
Only HTTP-exposed methods appear (an MCP/agent-only tool is skipped).
|
|
1206
1228
|
|
|
1207
1229
|
OpenAPI 3.1 has no standard multi-segment path parameter. For a contract path
|
|
1208
|
-
ending in
|
|
1230
|
+
ending in a named wildcard such as `/*filePath`, Stitchkit keeps the literal
|
|
1231
|
+
runtime path, omits `filePath` from the
|
|
1209
1232
|
standard `in: path` parameter list, and emits
|
|
1210
1233
|
`x-stitchkit-trailing-wildcard` on the operation with its parameter name,
|
|
1211
1234
|
schema and semantics. A generic OpenAPI client therefore cannot invent
|
|
@@ -1266,9 +1289,13 @@ and adds cookie auth, SSR cookie forwarding, error parsing into `ApiError`, a
|
|
|
1266
1289
|
`401 → unauthorized` event stream and safe transport retry.
|
|
1267
1290
|
|
|
1268
1291
|
```ts
|
|
1269
|
-
import { createHttpClient } from 'stitchkit'
|
|
1292
|
+
import { contractEndpointMatchers, createHttpClient } from 'stitchkit'
|
|
1293
|
+
import { publicAuth } from '../shared/contracts'
|
|
1270
1294
|
|
|
1271
|
-
const http = createHttpClient({
|
|
1295
|
+
const http = createHttpClient({
|
|
1296
|
+
baseUrl: '/api',
|
|
1297
|
+
suppressUnauthorizedFor: contractEndpointMatchers(publicAuth, ['complete', 'verify']),
|
|
1298
|
+
})
|
|
1272
1299
|
```
|
|
1273
1300
|
|
|
1274
1301
|
The returned `ConfiguredHttpClient` keeps that `baseUrl` as a readonly public
|
|
@@ -1284,13 +1311,22 @@ without repeating transport configuration.
|
|
|
1284
1311
|
| `credentials` | `'include'` | fetch credentials mode |
|
|
1285
1312
|
| `retry` | 2× GET, network errors only | transport retry policy |
|
|
1286
1313
|
| `headers` | — | extra headers — an object, or a function re-run per request |
|
|
1287
|
-
| `
|
|
1314
|
+
| `suppressUnauthorizedFor` | `[]` | exact contract-derived operation matchers whose expected 401 does not emit `unauthorized` |
|
|
1288
1315
|
| `parseError` | built-in | map an error body to `{ code, message, details, hint }` |
|
|
1289
1316
|
| `trace` | `false` | emit a W3C `traceparent` header on every request |
|
|
1290
1317
|
|
|
1291
1318
|
`headers` as a function is the hook for runtime tokens — a bearer token or any
|
|
1292
1319
|
short-lived credential — re-evaluated on every request.
|
|
1293
1320
|
|
|
1321
|
+
Expected 401 policy is explicit and contract-driven. Select individual
|
|
1322
|
+
operations with `contractEndpointMatchers(contract, ['login'])`, or omit the
|
|
1323
|
+
second argument to select every HTTP operation in that contract. Pass the same
|
|
1324
|
+
`ContractClientConfig` as the typed client when routes use a static or dynamic
|
|
1325
|
+
`pathPrefix`; dynamic matchers require `stripPrefixKeys`, so the helper can
|
|
1326
|
+
compile the prefix structure without a concrete tenant id. Matching is exact by
|
|
1327
|
+
path segments, including params and trailing wildcards — a shared prefix never
|
|
1328
|
+
suppresses a neighbouring protected endpoint.
|
|
1329
|
+
|
|
1294
1330
|
`trace: true` mints a fresh root trace per request. The stitchkit server
|
|
1295
1331
|
[continues an inbound `traceparent`](./observability.md#trace-context), so the
|
|
1296
1332
|
browser call, the HTTP handler and every nested tool call share one trace id
|
|
@@ -1316,12 +1352,22 @@ await api.update({ id: '1', name: 'M' }) // PUT /users/1 body: { name }
|
|
|
1316
1352
|
await api.delete({ id: '1' }) // DELETE /users/1
|
|
1317
1353
|
```
|
|
1318
1354
|
|
|
1355
|
+
An explicit contract `HEAD` operation is exposed like any other typed method.
|
|
1356
|
+
Because HEAD endpoints are `rawResponse`, it resolves to the untouched
|
|
1357
|
+
`Response`, giving the caller direct access to status and headers without JSON
|
|
1358
|
+
parsing:
|
|
1359
|
+
|
|
1360
|
+
```ts
|
|
1361
|
+
const response = await assets.head({ name: 'clip.mp4' })
|
|
1362
|
+
console.log(response.headers.get('content-length'))
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1319
1365
|
Each call takes one argument object. The client routes each field by the
|
|
1320
1366
|
contract:
|
|
1321
1367
|
|
|
1322
1368
|
- a **path param** (`:id`) is substituted into the URL,
|
|
1323
|
-
- a terminal wildcard (
|
|
1324
|
-
segments (`{
|
|
1369
|
+
- a named terminal wildcard (`/*filePath`) consumes that field and preserves its
|
|
1370
|
+
path segments (`{ filePath: 'a/b' }` → `/a/b`, not `/%2Fa%2Fb` or a query field),
|
|
1325
1371
|
- for `GET` / `DELETE`, the remaining fields become the **query string**
|
|
1326
1372
|
(arrays become repeated keys),
|
|
1327
1373
|
- for `POST` / `PUT` / `PATCH`, they become the **JSON body**,
|
|
@@ -1340,6 +1386,32 @@ await api.users.list()
|
|
|
1340
1386
|
await api.posts.create({ title: 'Hi' })
|
|
1341
1387
|
```
|
|
1342
1388
|
|
|
1389
|
+
When contracts use different path-prefix rules, route the same registry by the
|
|
1390
|
+
scope already declared in each contract. An array composes contracts with
|
|
1391
|
+
different scopes into one logical namespace:
|
|
1392
|
+
|
|
1393
|
+
```ts
|
|
1394
|
+
const api = createScopedClients(
|
|
1395
|
+
{ auth: [publicAuth, authenticatedAuth], widgets },
|
|
1396
|
+
http,
|
|
1397
|
+
{
|
|
1398
|
+
public: {},
|
|
1399
|
+
user: {},
|
|
1400
|
+
tenant: {
|
|
1401
|
+
stripPrefixKeys: ['tenantId'],
|
|
1402
|
+
pathPrefix: ({ tenantId }) => `tenants/${tenantId}`,
|
|
1403
|
+
},
|
|
1404
|
+
},
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
await api.auth.login()
|
|
1408
|
+
await api.auth.me()
|
|
1409
|
+
await api.widgets.list({ tenantId: 't1' })
|
|
1410
|
+
```
|
|
1411
|
+
|
|
1412
|
+
Every scope present in the registry needs a config. Unknown/missing scopes and
|
|
1413
|
+
duplicate method names inside a composed namespace fail before a request runs.
|
|
1414
|
+
|
|
1343
1415
|
`createClients` builds one typed client per contract from a registry — list the
|
|
1344
1416
|
contracts once, get the whole API typed. It accepts the same optional scoped
|
|
1345
1417
|
config as `createClient`, so a whole registry can share one resource prefix:
|
|
@@ -1362,7 +1434,7 @@ URL, not a fetched response. `createUrlBuilder` derives those URLs from the same
|
|
|
1362
1434
|
contract path planner used by both typed-client transports:
|
|
1363
1435
|
|
|
1364
1436
|
```ts
|
|
1365
|
-
import { createUrlBuilder, createUrlBuilders } from 'stitchkit'
|
|
1437
|
+
import { createScopedUrlBuilders, createUrlBuilder, createUrlBuilders } from 'stitchkit'
|
|
1366
1438
|
|
|
1367
1439
|
const mediaUrls = createUrlBuilder(media, http, {
|
|
1368
1440
|
stripPrefixKeys: ['tenantId'],
|
|
@@ -1375,13 +1447,47 @@ const src = mediaUrls.file({
|
|
|
1375
1447
|
thumbnail: true,
|
|
1376
1448
|
})
|
|
1377
1449
|
|
|
1450
|
+
// Body and multipart fields are intentionally absent: only the URL-bound
|
|
1451
|
+
// params are accepted by URL functions.
|
|
1452
|
+
const formAction = mediaUrls.replace({ tenantId: 't_123', fileId: 'f_456' })
|
|
1453
|
+
const beaconUrl = mediaUrls.track({ tenantId: 't_123' })
|
|
1454
|
+
|
|
1378
1455
|
const urls = createUrlBuilders({ media, exports }, http)
|
|
1379
1456
|
```
|
|
1380
1457
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1458
|
+
When contracts use different dynamic prefixes, route the registry by the same
|
|
1459
|
+
literal `contract.meta.scope` model as `createScopedClients`:
|
|
1460
|
+
|
|
1461
|
+
```ts
|
|
1462
|
+
const urls = createScopedUrlBuilders(
|
|
1463
|
+
{
|
|
1464
|
+
assets: publicAssets,
|
|
1465
|
+
media: [tenantFiles, tenantMetadata], // one composed namespace
|
|
1466
|
+
},
|
|
1467
|
+
http,
|
|
1468
|
+
{
|
|
1469
|
+
public: {},
|
|
1470
|
+
tenant: {
|
|
1471
|
+
stripPrefixKeys: ['tenantId'],
|
|
1472
|
+
pathPrefix: ({ tenantId }) => `tenants/${tenantId}`,
|
|
1473
|
+
},
|
|
1474
|
+
},
|
|
1475
|
+
)
|
|
1476
|
+
|
|
1477
|
+
urls.assets.logo()
|
|
1478
|
+
urls.media.file({ tenantId: 't1', fileId: 'f' })
|
|
1479
|
+
```
|
|
1480
|
+
|
|
1481
|
+
Every reachable scope needs a config. A missing config or duplicate method in a
|
|
1482
|
+
composed namespace fails while the registry is built; URL generation itself
|
|
1483
|
+
continues to use the same request planner as `createClient`.
|
|
1484
|
+
|
|
1485
|
+
Every HTTP-exposed endpoint appears on a URL builder, including body, multipart
|
|
1486
|
+
and raw-response operations. Path and scoped-prefix keys are consumed by the
|
|
1487
|
+
path. `GET` and `DELETE` input becomes the query string, including repeated keys
|
|
1488
|
+
for arrays; body-method input and multipart files are not URL arguments and are
|
|
1489
|
+
never serialized into the URL. Passing such a field through an untyped boundary
|
|
1490
|
+
fails before a URL is returned.
|
|
1385
1491
|
|
|
1386
1492
|
Building a URL is synchronous and performs no request, auth event, header
|
|
1387
1493
|
resolution or output validation. A `ConfiguredHttpClient` created by
|
|
@@ -1417,7 +1523,7 @@ The HTTP client emits events your app can react to globally:
|
|
|
1417
1523
|
|
|
1418
1524
|
```ts
|
|
1419
1525
|
const unsubscribe = http.subscribe((event) => {
|
|
1420
|
-
if (event.type === 'unauthorized') redirectToLogin() // a 401
|
|
1526
|
+
if (event.type === 'unauthorized') redirectToLogin() // a non-suppressed 401
|
|
1421
1527
|
if (event.type === 'network_error') showOfflineBanner()
|
|
1422
1528
|
})
|
|
1423
1529
|
|
|
@@ -1544,7 +1650,8 @@ The same contract that drives the HTTP API also drives AI tooling. An endpoint
|
|
|
1544
1650
|
exposed on `MCP` becomes a [Model Context Protocol](https://modelcontextprotocol.io)
|
|
1545
1651
|
tool — callable from Claude, Cursor and other MCP clients. An endpoint exposed on
|
|
1546
1652
|
`AGENT` becomes a [Vercel AI SDK](https://sdk.vercel.ai) tool — callable from an
|
|
1547
|
-
agent loop.
|
|
1653
|
+
agent loop. Contract operations are never re-described by hand; pathless
|
|
1654
|
+
runtime operations use one framework definition shared by both tool transports.
|
|
1548
1655
|
|
|
1549
1656
|
## Which endpoints become tools
|
|
1550
1657
|
|
|
@@ -1604,6 +1711,51 @@ and after, compare.
|
|
|
1604
1711
|
> HTTP-only shows up in the list, which is the one check that catches it however
|
|
1605
1712
|
> many places the line was forgotten.
|
|
1606
1713
|
|
|
1714
|
+
## In-process calls — `createToolInvoker`
|
|
1715
|
+
|
|
1716
|
+
When one application operation dispatches to a contract tool, do not mount an
|
|
1717
|
+
AI SDK `ToolSet` and call its transport adapter manually. Compile an in-process
|
|
1718
|
+
invoker once and call the shared framework runner directly:
|
|
1719
|
+
|
|
1720
|
+
```ts
|
|
1721
|
+
import { createToolInvoker } from 'stitchkit/tools'
|
|
1722
|
+
|
|
1723
|
+
const invoker = createToolInvoker(services, {
|
|
1724
|
+
transport: 'AGENT', // required exposure policy
|
|
1725
|
+
})
|
|
1726
|
+
|
|
1727
|
+
const result = await invoker.invoke('update_entity', args, {
|
|
1728
|
+
source: 'internal', // default; audit names the real call source
|
|
1729
|
+
context: { identity },
|
|
1730
|
+
lifecycle,
|
|
1731
|
+
hooks,
|
|
1732
|
+
})
|
|
1733
|
+
|
|
1734
|
+
// Composition path: validated data or the exact normalized AppError.
|
|
1735
|
+
const data = await invoker.invokeOrThrow('update_entity', args, {
|
|
1736
|
+
context: { identity },
|
|
1737
|
+
lifecycle,
|
|
1738
|
+
hooks,
|
|
1739
|
+
})
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
`transport` is required and uses the exact existing `MCP`, `AGENT` or `CLI`
|
|
1743
|
+
exposure rules; there is no internal bypass mode. The immutable name lookup is
|
|
1744
|
+
compiled once. Call-specific identity, context, lifecycle and hooks are passed
|
|
1745
|
+
to `invoke` / `invokeOrThrow`, so one registry can safely serve parallel and
|
|
1746
|
+
recursive calls without retaining request state. Every invocation gets a fresh
|
|
1747
|
+
tool-call context and runs the same extension resolution, input/output
|
|
1748
|
+
validation, lifecycle, hooks and output-strip reporter as mounted tools.
|
|
1749
|
+
|
|
1750
|
+
`invoke` returns the canonical discriminated `ToolResult`, not an AI SDK or MCP
|
|
1751
|
+
presentation envelope. `invokeOrThrow` returns validated data and throws the
|
|
1752
|
+
runner's normalized `AppError` on failure, preserving an application error's
|
|
1753
|
+
code, status, message, details and hint without reconstructing it from the
|
|
1754
|
+
model-safe envelope. Unexpected throws remain scrubbed. An unknown name throws
|
|
1755
|
+
`AppError('NOT_FOUND')` before the runner because there is no operation identity
|
|
1756
|
+
against which hooks could run. Duplicate and provider-invalid names fail when
|
|
1757
|
+
the invoker is created.
|
|
1758
|
+
|
|
1607
1759
|
## MCP — `createMcpHandler`
|
|
1608
1760
|
|
|
1609
1761
|
`createMcpHandler` builds a complete Streamable-HTTP MCP server as a single
|
|
@@ -1981,6 +2133,7 @@ the merged `params` + `input`. `context` is merged into every tool handler's
|
|
|
1981
2133
|
| `lifecycle` | `beforeHandle` / `afterHandle` — the tool-side auth gate (see [Guarding tools](#guarding-tools--lifecycle)) |
|
|
1982
2134
|
| `hooks` | tool-call observability hooks — `afterToolCall` fires on every result |
|
|
1983
2135
|
| `extend` | add extra args resolved before the handler runs (see below) |
|
|
2136
|
+
| `runtimeTools` | framework-managed pathless operations from `defineRuntimeTool` |
|
|
1984
2137
|
|
|
1985
2138
|
`lifecycle` works the same as on the MCP server — without it an agent tool call
|
|
1986
2139
|
bypasses the HTTP `beforeHandle` auth gate. Pass your `createAuthHook` result.
|
|
@@ -2031,51 +2184,78 @@ as the HTTP prefix param — see
|
|
|
2031
2184
|
so one handler serves both surfaces. Pair `extend` with `lifecycle` (your
|
|
2032
2185
|
`createAuthHook`) so the tool call is still scope-gated.
|
|
2033
2186
|
|
|
2034
|
-
##
|
|
2187
|
+
## Pathless runtime tools and multimodal results
|
|
2035
2188
|
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2189
|
+
Use `defineRuntimeTool` for an operation that has no HTTP path but still needs
|
|
2190
|
+
the same validation, isolated per-call context, lifecycle/RBAC and hooks as a
|
|
2191
|
+
contract tool. The handler returns one transport-neutral, schema-validated
|
|
2192
|
+
result. Optional presentation callbacks map that result to MCP content and AI
|
|
2193
|
+
SDK model output without coupling the handler to either SDK:
|
|
2039
2194
|
|
|
2040
2195
|
```ts
|
|
2041
|
-
import { createMcpHandler } from 'stitchkit/tools'
|
|
2196
|
+
import { createMcpHandler, defineRuntimeTool, mountAgent } from 'stitchkit/tools'
|
|
2042
2197
|
import { z } from 'zod'
|
|
2043
2198
|
|
|
2199
|
+
const renderPreview = defineRuntimeTool({
|
|
2200
|
+
name: 'render_preview',
|
|
2201
|
+
description: 'Render and inspect a preview',
|
|
2202
|
+
identity: {
|
|
2203
|
+
serviceName: 'mediaTools',
|
|
2204
|
+
action: 'renderPreview',
|
|
2205
|
+
scope: 'admin',
|
|
2206
|
+
method: 'POST',
|
|
2207
|
+
},
|
|
2208
|
+
input: z.object({ prompt: z.string() }),
|
|
2209
|
+
output: z.object({ assetId: z.string(), imageBase64: z.string() }),
|
|
2210
|
+
handler: async ({ input }) => renderAndSave(input.prompt),
|
|
2211
|
+
present: {
|
|
2212
|
+
mcp: (output) => ({
|
|
2213
|
+
content: [
|
|
2214
|
+
{ type: 'image', data: output.imageBase64, mimeType: 'image/png' },
|
|
2215
|
+
{ type: 'text', text: output.assetId },
|
|
2216
|
+
],
|
|
2217
|
+
}),
|
|
2218
|
+
agent: (output) => ({
|
|
2219
|
+
type: 'content',
|
|
2220
|
+
value: [{
|
|
2221
|
+
type: 'file',
|
|
2222
|
+
data: { type: 'data', data: output.imageBase64 },
|
|
2223
|
+
mediaType: 'image/png',
|
|
2224
|
+
filename: `${output.assetId}.png`,
|
|
2225
|
+
}],
|
|
2226
|
+
}),
|
|
2227
|
+
},
|
|
2228
|
+
})
|
|
2229
|
+
|
|
2044
2230
|
const handleMcp = createMcpHandler({
|
|
2045
2231
|
serverInfo: { name: 'my-app', version: '1.0.0' },
|
|
2046
2232
|
auth,
|
|
2047
2233
|
services: [service],
|
|
2048
2234
|
lifecycle: { beforeHandle: authHook },
|
|
2049
2235
|
hooks: audit.toolCall,
|
|
2050
|
-
nativeTools: ({ registerTool }
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
scope: 'admin',
|
|
2058
|
-
method: 'POST',
|
|
2059
|
-
},
|
|
2060
|
-
input: z.object({ prompt: z.string() }),
|
|
2061
|
-
output: z.object({ assetId: z.string() }),
|
|
2062
|
-
handler: async ({ input, traceId }) => ({
|
|
2063
|
-
content: [
|
|
2064
|
-
{ type: 'image', data: await renderBase64(input.prompt), mimeType: 'image/png' },
|
|
2065
|
-
{ type: 'text', text: `trace: ${traceId}` },
|
|
2066
|
-
],
|
|
2067
|
-
structuredContent: { assetId: await saveAsset(identity) },
|
|
2068
|
-
}),
|
|
2069
|
-
})
|
|
2070
|
-
},
|
|
2236
|
+
nativeTools: ({ registerTool }) => registerTool(renderPreview),
|
|
2237
|
+
})
|
|
2238
|
+
|
|
2239
|
+
const agentTools = mountAgent([service], {
|
|
2240
|
+
runtimeTools: [renderPreview],
|
|
2241
|
+
lifecycle: { beforeHandle: authHook },
|
|
2242
|
+
hooks: audit.toolCall,
|
|
2071
2243
|
})
|
|
2072
2244
|
```
|
|
2073
2245
|
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2246
|
+
`transports` defaults to `['MCP', 'AGENT']`; set an explicit subset when an
|
|
2247
|
+
operation belongs on only one surface. The configured identity becomes the
|
|
2248
|
+
hook/lifecycle `OperationIdentity` and the tool `RequestEvent`
|
|
2249
|
+
(`serviceName`, `action`, `httpMethod`). A runtime operation has no HTTP route,
|
|
2250
|
+
so no fake `path` is added.
|
|
2251
|
+
|
|
2252
|
+
When `output` is declared, Stitchkit validates the neutral handler result after
|
|
2253
|
+
`afterHandle`. MCP owns `structuredContent` and `isError`: a presenter supplies
|
|
2254
|
+
only rich `content`/metadata, while the framework inserts the validated
|
|
2255
|
+
structured result and normalises failures. The Agent adapter uses the AI SDK's
|
|
2256
|
+
official `toModelOutput` callback, so `execute` and application UI keep the
|
|
2257
|
+
neutral output while the model receives text/file content. Presentation
|
|
2258
|
+
callbacks require an output schema.
|
|
2079
2259
|
|
|
2080
2260
|
The MCP registration uses an identity carrier: the SDK advertises the compiled
|
|
2081
2261
|
JSON Schema but forwards the raw object into Stitchkit. Input failures therefore
|
|
@@ -2369,7 +2549,7 @@ It returns a handle with three pieces, all wired into `createServer`:
|
|
|
2369
2549
|
createServer({
|
|
2370
2550
|
services,
|
|
2371
2551
|
websocket: socket.websocket, // → Bun.serve websocket handlers
|
|
2372
|
-
rawRoutes: [socket.route], // ready-made /socket.io/* route
|
|
2552
|
+
rawRoutes: [socket.route], // ready-made /socket.io/*socketPath route
|
|
2373
2553
|
})
|
|
2374
2554
|
|
|
2375
2555
|
// elsewhere — broadcast:
|
|
@@ -2380,7 +2560,7 @@ socket.io.emit('note:created', note)
|
|
|
2380
2560
|
|--------------|---------|
|
|
2381
2561
|
| `io` | the typed Socket.IO server — attach `connection` handlers, broadcast |
|
|
2382
2562
|
| `websocket` | Bun WebSocket handlers — pass to `createServer({ websocket })` |
|
|
2383
|
-
| `route` | the `/socket.io
|
|
2563
|
+
| `route` | the `/socket.io/*socketPath` raw route — pass to `createServer({ rawRoutes })` |
|
|
2384
2564
|
|
|
2385
2565
|
`SocketIOServerConfig` also takes `path`, `transports`, `pingTimeout` and
|
|
2386
2566
|
`pingInterval`. For anything else socket.io's `ServerOptions` exposes, use the
|
|
@@ -2556,9 +2736,16 @@ updater per entity:
|
|
|
2556
2736
|
import { createEntityCacheHandlers } from 'stitchkit/react'
|
|
2557
2737
|
|
|
2558
2738
|
const widgetCache = createEntityCacheHandlers<Widget>({
|
|
2559
|
-
getId: (
|
|
2560
|
-
|
|
2561
|
-
|
|
2739
|
+
getId: (widget) => widget.id,
|
|
2740
|
+
getListItemId: (widget) => widget.id,
|
|
2741
|
+
toListItem: (widget) => widget,
|
|
2742
|
+
list: {
|
|
2743
|
+
key: ['widgets'],
|
|
2744
|
+
shape: 'paginated',
|
|
2745
|
+
createAt: 'start',
|
|
2746
|
+
updateMissing: 'skip',
|
|
2747
|
+
},
|
|
2748
|
+
detailKey: (event) => ['widgets', event.id],
|
|
2562
2749
|
})
|
|
2563
2750
|
|
|
2564
2751
|
createCacheBridge({ socket, queryClient, handlers: {
|
|
@@ -2568,10 +2755,52 @@ createCacheBridge({ socket, queryClient, handlers: {
|
|
|
2568
2755
|
}})
|
|
2569
2756
|
```
|
|
2570
2757
|
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2758
|
+
The `list.shape` discriminant supports `array`, `paginated`, `infinite-array`
|
|
2759
|
+
and `infinite-paginated`. Every mutation preserves the surrounding envelope,
|
|
2760
|
+
page metadata and `pageParams`; an infinite create changes only the selected
|
|
2761
|
+
edge page (`createAt: 'start' | 'end'`). Creates are deduplicated across every
|
|
2762
|
+
cached page. `updateMissing` makes an absent update explicitly skip or insert.
|
|
2763
|
+
|
|
2764
|
+
The event entity may be richer than a list row. Keep the full value in detail
|
|
2765
|
+
cache, project it for lists, and provide the same comparator the backend uses:
|
|
2766
|
+
|
|
2767
|
+
```ts
|
|
2768
|
+
const memberCache = createEntityCacheHandlers<Member, MemberListItem>({
|
|
2769
|
+
getId: (member) => member.id,
|
|
2770
|
+
getListItemId: (item) => item.id,
|
|
2771
|
+
toListItem: (member) => ({
|
|
2772
|
+
id: member.id,
|
|
2773
|
+
name: member.name,
|
|
2774
|
+
joinedAt: member.joinedAt,
|
|
2775
|
+
}),
|
|
2776
|
+
list: {
|
|
2777
|
+
key: (event) => {
|
|
2778
|
+
if (event.type !== 'deleted') {
|
|
2779
|
+
return ['workspaces', event.entity.workspaceId, 'members']
|
|
2780
|
+
}
|
|
2781
|
+
if ('workspaceId' in event.payload) {
|
|
2782
|
+
return ['workspaces', event.payload.workspaceId, 'members']
|
|
2783
|
+
}
|
|
2784
|
+
throw new Error('A scoped delete must carry its entity')
|
|
2785
|
+
},
|
|
2786
|
+
shape: 'array',
|
|
2787
|
+
createAt: 'start',
|
|
2788
|
+
updateMissing: 'skip',
|
|
2789
|
+
compare: (left, right) => left.joinedAt.localeCompare(right.joinedAt),
|
|
2790
|
+
},
|
|
2791
|
+
detailKey: (event) => ['members', event.id],
|
|
2792
|
+
})
|
|
2793
|
+
```
|
|
2794
|
+
|
|
2795
|
+
Static `QueryKey` values remain the short path. A key factory receives a typed
|
|
2796
|
+
`created | updated | deleted` event, so scoped keys can use the full entity or
|
|
2797
|
+
deleted payload without guessing. The same resolved detail key drives the
|
|
2798
|
+
`isFresh` echo guard. Shape checks also leave neighbouring detail caches alone
|
|
2799
|
+
when a list key is intentionally used as a partial query-key prefix.
|
|
2800
|
+
|
|
2801
|
+
The helper deliberately does **not** flatten pages, update totals, derive a
|
|
2802
|
+
sort order or replace arbitrary `setQueryData` logic. Those are application
|
|
2803
|
+
policies; this helper only applies declared CRUD semantics.
|
|
2575
2804
|
|
|
2576
2805
|
## Raw binary lane (Bun)
|
|
2577
2806
|
|
|
@@ -2645,7 +2874,7 @@ Notes:
|
|
|
2645
2874
|
- **Bun-only.** On Node, Socket.IO attaches to the `node:http.Server` `upgrade`
|
|
2646
2875
|
event (`serveNode({ socket })`); a raw lane there is a separate upgrade
|
|
2647
2876
|
handler, not this composition. See [ADR 0020](../decisions/0020-raw-websocket-lane.md).
|
|
2648
|
-
- The upgrade path must not collide with `/socket.io
|
|
2877
|
+
- The upgrade path must not collide with `/socket.io/*socketPath`.
|
|
2649
2878
|
- The tuning (`maxPayloadLength`, `idleTimeout`, `backpressureLimit`, …) is
|
|
2650
2879
|
global — keep `idleTimeout` ≥ Socket.IO needs (> 2 × `pingInterval`).
|
|
2651
2880
|
- For high throughput, handle backpressure in the raw lane: `ws.send()` returns
|
|
@@ -2705,6 +2934,19 @@ gives you the contract and the metadata (`idempotent`, the open `source` tag,
|
|
|
2705
2934
|
|
|
2706
2935
|
# Auth & errors
|
|
2707
2936
|
|
|
2937
|
+
Client-side login/session operations may legitimately answer `401`. Declare
|
|
2938
|
+
that policy with contract-owned matchers instead of path strings:
|
|
2939
|
+
|
|
2940
|
+
```ts
|
|
2941
|
+
const http = createHttpClient({
|
|
2942
|
+
baseUrl,
|
|
2943
|
+
suppressUnauthorizedFor: contractEndpointMatchers(publicAuth, ['login', 'verify']),
|
|
2944
|
+
})
|
|
2945
|
+
```
|
|
2946
|
+
|
|
2947
|
+
Only the selected operations suppress the global `unauthorized` event; a 401
|
|
2948
|
+
from any neighbouring protected route still signals session expiry.
|
|
2949
|
+
|
|
2708
2950
|
stitchkit carries no domain model — it does not know what a user is. What it
|
|
2709
2951
|
provides is the *control flow*: a scope on every endpoint, one hook that
|
|
2710
2952
|
enforces it, and one error model shared by every transport. The identity and
|
|
@@ -3857,6 +4099,135 @@ current one *up to* your target, and apply each snippet.
|
|
|
3857
4099
|
(`STITCH_ERROR_STATUS`, `serveFile`, `scopePrefixes`, `afterToolCall`'s
|
|
3858
4100
|
`MethodDef`, `maxUploadBytes`) are available to adopt, not required.
|
|
3859
4101
|
|
|
4102
|
+
## Unreleased breaking migrations
|
|
4103
|
+
|
|
4104
|
+
`createToolInvoker` now separates immutable registry preparation from per-call
|
|
4105
|
+
runtime state. Move source/context/lifecycle/hooks/output-strip reporting from
|
|
4106
|
+
the factory config to the third invocation argument. Use `invokeOrThrow` when a
|
|
4107
|
+
nested operation should preserve the normalized `AppError` instead of returning
|
|
4108
|
+
a model-facing failure envelope:
|
|
4109
|
+
|
|
4110
|
+
```ts
|
|
4111
|
+
// before
|
|
4112
|
+
const invoker = createToolInvoker(services, {
|
|
4113
|
+
transport: 'AGENT', context: { identity }, lifecycle, hooks,
|
|
4114
|
+
})
|
|
4115
|
+
const result = await invoker.invoke(name, args)
|
|
4116
|
+
|
|
4117
|
+
// after
|
|
4118
|
+
const invoker = createToolInvoker(services, { transport: 'AGENT' })
|
|
4119
|
+
const result = await invoker.invoke(name, args, {
|
|
4120
|
+
context: { identity }, lifecycle, hooks,
|
|
4121
|
+
})
|
|
4122
|
+
const data = await invoker.invokeOrThrow(name, args, {
|
|
4123
|
+
context: { identity }, lifecycle, hooks,
|
|
4124
|
+
})
|
|
4125
|
+
```
|
|
4126
|
+
|
|
4127
|
+
There is no static runtime-config overload: request identity must not be retained
|
|
4128
|
+
by a reusable compiled registry.
|
|
4129
|
+
|
|
4130
|
+
Entity cache handlers now require the cached list shape and CRUD policies. Move
|
|
4131
|
+
`listKey` under `list`, make detail keys event-aware, and state the list-item
|
|
4132
|
+
identity/projection explicitly:
|
|
4133
|
+
|
|
4134
|
+
```ts
|
|
4135
|
+
// before
|
|
4136
|
+
createEntityCacheHandlers<Entity>({
|
|
4137
|
+
getId,
|
|
4138
|
+
listKey: ['entities'],
|
|
4139
|
+
detailKey: (id) => ['entities', id],
|
|
4140
|
+
})
|
|
4141
|
+
|
|
4142
|
+
// after
|
|
4143
|
+
createEntityCacheHandlers<Entity, EntityListItem>({
|
|
4144
|
+
getId,
|
|
4145
|
+
getListItemId: (item) => item.id,
|
|
4146
|
+
toListItem: (entity) => ({ id: entity.id, name: entity.name }),
|
|
4147
|
+
list: {
|
|
4148
|
+
key: ['entities'],
|
|
4149
|
+
shape: 'paginated',
|
|
4150
|
+
createAt: 'start',
|
|
4151
|
+
updateMissing: 'skip',
|
|
4152
|
+
},
|
|
4153
|
+
detailKey: (event) => ['entities', event.id],
|
|
4154
|
+
})
|
|
4155
|
+
```
|
|
4156
|
+
|
|
4157
|
+
Choose `array`, `paginated`, `infinite-array` or `infinite-paginated` to match
|
|
4158
|
+
the actual cached data. A dynamic `list.key` / `detailKey` receives a
|
|
4159
|
+
discriminated event and can derive scoped keys from the created/updated entity
|
|
4160
|
+
or deleted payload. Add `compare` only when the backend has a canonical order;
|
|
4161
|
+
the framework does not guess it or mutate pagination metadata.
|
|
4162
|
+
|
|
4163
|
+
Protected native MCP operations now use the transport-neutral runtime tool
|
|
4164
|
+
definition. Return the schema-owned value from the handler and move MCP content
|
|
4165
|
+
or metadata into `present.mcp`; `structuredContent` and `isError` are
|
|
4166
|
+
framework-owned:
|
|
4167
|
+
|
|
4168
|
+
```ts
|
|
4169
|
+
// before
|
|
4170
|
+
registerTool({ input, output, handler: async () => ({
|
|
4171
|
+
content: [{ type: 'image', data, mimeType: 'image/png' }],
|
|
4172
|
+
structuredContent: { assetId },
|
|
4173
|
+
}) })
|
|
4174
|
+
|
|
4175
|
+
// after
|
|
4176
|
+
const preview = defineRuntimeTool({
|
|
4177
|
+
name: 'render_preview', description, identity, input, output,
|
|
4178
|
+
handler: async () => ({ assetId, data }),
|
|
4179
|
+
present: {
|
|
4180
|
+
mcp: (result) => ({
|
|
4181
|
+
content: [{ type: 'image', data: result.data, mimeType: 'image/png' }],
|
|
4182
|
+
}),
|
|
4183
|
+
},
|
|
4184
|
+
})
|
|
4185
|
+
nativeTools: ({ registerTool }) => registerTool(preview)
|
|
4186
|
+
```
|
|
4187
|
+
|
|
4188
|
+
The removed `NativeMcp*` types have no aliases. Use `RuntimeToolDefinition`,
|
|
4189
|
+
`RuntimeToolIdentity`, `RuntimeToolHandlerContext` and
|
|
4190
|
+
`RuntimeMcpPresentation`. The same definition can now be passed to
|
|
4191
|
+
`mountAgent(services, { runtimeTools: [preview] })`; add `present.agent` only
|
|
4192
|
+
when the model needs rich text/file content instead of the neutral JSON result.
|
|
4193
|
+
|
|
4194
|
+
Trailing wildcards must be named consistently across the path and params schema:
|
|
4195
|
+
|
|
4196
|
+
```ts
|
|
4197
|
+
// before
|
|
4198
|
+
path: '/app/:slug/*'
|
|
4199
|
+
params: z.object({ slug: z.string(), '*': z.string() })
|
|
4200
|
+
ctx.params['*']
|
|
4201
|
+
api.app({ slug: 'foo', '*': 'a/b' })
|
|
4202
|
+
|
|
4203
|
+
// after
|
|
4204
|
+
path: '/app/:slug/*filePath'
|
|
4205
|
+
params: z.object({ slug: z.string(), filePath: z.string() })
|
|
4206
|
+
ctx.params.filePath
|
|
4207
|
+
api.app({ slug: 'foo', filePath: 'a/b' })
|
|
4208
|
+
```
|
|
4209
|
+
|
|
4210
|
+
Bare wildcards have no compatibility alias; raw routes use the same named form.
|
|
4211
|
+
|
|
4212
|
+
### Expected-401 matchers
|
|
4213
|
+
|
|
4214
|
+
`HttpClientConfig.authEndpoints` is removed. Replace manual path prefixes with
|
|
4215
|
+
the operations whose 401 response is expected:
|
|
4216
|
+
|
|
4217
|
+
```ts
|
|
4218
|
+
// before
|
|
4219
|
+
createHttpClient({ baseUrl, authEndpoints: ['/api/auth/'] })
|
|
4220
|
+
|
|
4221
|
+
// after
|
|
4222
|
+
createHttpClient({
|
|
4223
|
+
baseUrl,
|
|
4224
|
+
suppressUnauthorizedFor: contractEndpointMatchers(authContract, ['login', 'verify']),
|
|
4225
|
+
})
|
|
4226
|
+
```
|
|
4227
|
+
|
|
4228
|
+
There is no implicit `/auth/` suppression. Omit `suppressUnauthorizedFor` when
|
|
4229
|
+
every 401 should emit the global `unauthorized` event.
|
|
4230
|
+
|
|
3860
4231
|
## The 0.37 migration
|
|
3861
4232
|
|
|
3862
4233
|
Tool presentation is no longer an executable Zod parser. Replace the removed
|
|
@@ -4052,17 +4423,27 @@ The browser-and-server entrypoint. Re-exports everything from
|
|
|
4052
4423
|
|--------|------|---------|
|
|
4053
4424
|
| `createClient` | function | build a typed client from a contract — [guide](../guide/client.md#createclient) |
|
|
4054
4425
|
| `createClients` | function | build one exact typed client per contract from a registry; accepts the same scoped config and transports as `createClient` |
|
|
4055
|
-
| `
|
|
4426
|
+
| `createScopedClients` | function | build one registry routed by contract scope; arrays compose contracts into one namespace |
|
|
4427
|
+
| `createScopedUrlBuilders` | function | build one URL registry routed by contract scope; arrays compose contracts into one namespace |
|
|
4428
|
+
| `ScopeClientConfigs` | _type_ | per-scope routing configuration consumed by scoped clients and URL builders |
|
|
4429
|
+
| `ScopedClientRegistry` | _type_ | exact composed registry returned by `createScopedClients` |
|
|
4430
|
+
| `ScopedUrlBuilderRegistry` | _type_ | exact composed registry returned by `createScopedUrlBuilders` |
|
|
4431
|
+
| `ClientRegistryValue` | _type_ | one contract or a contract array composing one client namespace |
|
|
4432
|
+
| `ClientContract` | _type_ | HTTP-client-compatible contract value used by scoped registries |
|
|
4433
|
+
| `RegistryScope` | _type_ | contract-scope union inferred from a scoped client registry value |
|
|
4434
|
+
| `createUrlBuilder` | function | build synchronous browser-native URLs for all HTTP endpoints; body methods accept URL-bound args only — [guide](../guide/client.md#contract-url-builders) |
|
|
4056
4435
|
| `createUrlBuilders` | function | build one exact URL builder per contract in a registry |
|
|
4057
4436
|
| `UrlBuilderConfig` | _type_ | explicit `{ baseUrl }` source for a URL builder |
|
|
4058
4437
|
| `ClientConfig` | _type_ | config for `createClient`'s bare-fetch mode (2nd arg, no `HttpClient`) |
|
|
4059
4438
|
| `ContractClientConfig` | _type_ | per-tenant / resource-scoped client config — dynamic `pathPrefix` + `stripPrefixKeys` ([guide](../guide/client.md#contractclientconfig--per-tenant--resource-scoped-clients)) |
|
|
4439
|
+
| `contractEndpointMatchers` | function | compile exact pathname matchers for selected HTTP contract operations and expected-401 policy |
|
|
4060
4440
|
| `PathPrefixArgs` | _type_ | required string-valued keys exposed to a typed dynamic `pathPrefix` callback |
|
|
4061
4441
|
| `createHttpClient` | function | the Ky-based HTTP transport — [guide](../guide/client.md#createhttpclient) |
|
|
4062
4442
|
| `ApiError` | class | a non-2xx response, with `code` / `status` / `details` / `hint` |
|
|
4063
4443
|
| `HttpClient` | _type_ | the transport interface `createClient` builds on |
|
|
4064
4444
|
| `ConfiguredHttpClient` | _type_ | a framework-created `HttpClient` carrying its readonly `baseUrl` for URL builders |
|
|
4065
4445
|
| `HttpClientConfig` | _type_ | config for `createHttpClient` |
|
|
4446
|
+
| `UnauthorizedMatcher` | _type_ | exact `(pathname) => boolean` policy accepted by `suppressUnauthorizedFor` |
|
|
4066
4447
|
| `RequestOptions` | _type_ | per-call options — params, timeout, response type |
|
|
4067
4448
|
| `HeaderProvider` | _type_ | static or per-request headers |
|
|
4068
4449
|
| `ApiEvent` | _type_ | a client event — `unauthorized` / `network_error` / `logout` |
|
|
@@ -4107,17 +4488,18 @@ from the root `stitchkit`.
|
|
|
4107
4488
|
| Export | Kind | Summary |
|
|
4108
4489
|
|--------|------|---------|
|
|
4109
4490
|
| `defineContract` | function | declare a contract — [guide](../guide/contracts.md#definecontract) |
|
|
4110
|
-
| `createContractFactory` | function | a `defineContract` with a required
|
|
4491
|
+
| `createContractFactory` | function | a `defineContract` with a required allowed scope that retains each concrete literal — [guide](../guide/contracts.md#scope) |
|
|
4111
4492
|
| `ScopedDefineContract` | _type_ | the `defineContract` `createContractFactory` returns |
|
|
4112
4493
|
| `ALL_TRANSPORTS` | constant | `['HTTP', 'MCP', 'AGENT', 'CLI']` |
|
|
4113
4494
|
| `ContractDef` | _type_ | a defined contract |
|
|
4114
4495
|
| `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` and `meta` (a default every endpoint shallow-merges over) |
|
|
4115
4496
|
| `EndpointDef` | _type_ | a single endpoint definition |
|
|
4497
|
+
| `HeadEndpointDef` | _type_ | explicit HTTP-only, bodyless `HEAD` endpoint definition |
|
|
4116
4498
|
| `EndpointResponseMeta` | _type_ | static success metadata declared by an HTTP-only typed-data endpoint |
|
|
4117
4499
|
| `ResponseMetadata` | _type_ | per-request outbound collector exposed as `ctx.response` only for a `responseMeta` endpoint |
|
|
4118
4500
|
| `HttpSuccessStatus` | _type_ | supported declared 2xx success statuses |
|
|
4119
4501
|
| `BodyHttpSuccessStatus` | _type_ | supported 2xx statuses excluding bodyless 204/205 |
|
|
4120
|
-
| `HttpMethod` | _type_ | `GET \| POST \| PUT \| PATCH \| DELETE` |
|
|
4502
|
+
| `HttpMethod` | _type_ | `GET \| HEAD \| POST \| PUT \| PATCH \| DELETE` |
|
|
4121
4503
|
| `Transport` | _type_ | `HTTP \| MCP \| AGENT \| CLI` |
|
|
4122
4504
|
| `TransportSource` | _type_ | `http \| mcp \| agent \| cli` — the value of `ctx.source` |
|
|
4123
4505
|
| `RuntimeContext` | _type_ | the loose context seen by transport and hooks |
|
|
@@ -4127,7 +4509,7 @@ from the root `stitchkit`.
|
|
|
4127
4509
|
| `TypedHttpClient` | _type_ | the typed client, HTTP endpoints only (`= ScopedHttpClient<C, unknown>`) |
|
|
4128
4510
|
| `ScopedHttpClient` | _type_ | a client whose `stripPrefixKeys` become required args ([guide](../guide/multi-tenant.md)) |
|
|
4129
4511
|
| `ScopedEndpointFn` | _type_ | one method's signature with the consumed keys folded in |
|
|
4130
|
-
| `TypedUrlBuilder` | _type_ | one contract's HTTP
|
|
4512
|
+
| `TypedUrlBuilder` | _type_ | one contract's HTTP endpoints as synchronous, method-aware URL functions |
|
|
4131
4513
|
| `ScopedUrlBuilder` | _type_ | a URL builder whose scoped-prefix keys are required method arguments |
|
|
4132
4514
|
| `ScopedUrlFn` | _type_ | one URL method's signature with scoped-prefix keys folded in |
|
|
4133
4515
|
| `MultipartFile` | _type_ | a `multipart` file field — `Blob \| FileDescriptor` |
|
|
@@ -4372,6 +4754,8 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
|
|
|
4372
4754
|
| `mountMcp` | function | add contract tools to an existing `McpServer` — [guide](../guide/mcp-and-agents.md#mountmcp) |
|
|
4373
4755
|
| `implementRemote` | function | bind a contract to a remote HTTP API — [guide](../guide/mcp-and-agents.md#proxying-a-remote-api--implementremote) |
|
|
4374
4756
|
| `mountAgent` | function | a Vercel AI SDK `ToolSet` from a service — [guide](../guide/mcp-and-agents.md#ai-agents--mountagent) |
|
|
4757
|
+
| `defineRuntimeTool` | function | define one validated pathless operation for MCP, Agent or both — [guide](../guide/mcp-and-agents.md#pathless-runtime-tools-and-multimodal-results) |
|
|
4758
|
+
| `createToolInvoker` | function | compile an exposure-aware in-process dispatcher over the canonical tool runner — [guide](../guide/mcp-and-agents.md#in-process-calls--createtoolinvoker) |
|
|
4375
4759
|
| `createCli` | function | a command-line program from contracts — [guide](../guide/cli.md) (also on `stitchkit/cli`) |
|
|
4376
4760
|
| `createToolkit` | function | context-typed tool mounts — [guide](../guide/cli.md#typed-context) |
|
|
4377
4761
|
| `mountViewFile` | function | a native multimodal "view file" MCP tool |
|
|
@@ -4387,10 +4771,17 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
|
|
|
4387
4771
|
| `McpSchemaValidationConfig` | _type_ | shared `{ policy, requireTypedProperties, allowUntyped, requirePortableFormats, allowFormats }` profile |
|
|
4388
4772
|
| `ValidateMcpSchemasConfig` | _type_ | standalone validation profile plus `services`, `extend`, flattening and logger |
|
|
4389
4773
|
| `NativeMcpRegistrar` | _type_ | protected `registerTool` plus explicit unprotected `rawServer` access |
|
|
4390
|
-
| `
|
|
4391
|
-
| `
|
|
4392
|
-
| `
|
|
4393
|
-
| `
|
|
4774
|
+
| `RuntimeToolDefinition` | _type_ | transport-neutral pathless operation with identity, schemas, handler and optional presenters |
|
|
4775
|
+
| `RuntimeToolDefinitionBase` | _type_ | common name, identity, input, exposure and MCP metadata fields |
|
|
4776
|
+
| `RuntimeToolDefinitionWithOutput` | _type_ | runtime definition whose handler and presenters share a validated output type |
|
|
4777
|
+
| `RuntimeToolDefinitionWithoutOutput` | _type_ | runtime definition without output validation or presentation callbacks |
|
|
4778
|
+
| `RuntimeToolIdentity` | _type_ | `{ serviceName, action, scope?, method, meta? }` for runtime lifecycle/audit |
|
|
4779
|
+
| `RuntimeToolHandlerContext` | _type_ | runtime context with the definition's parsed input |
|
|
4780
|
+
| `RuntimeToolOutput` | _type_ | output inferred from a runtime tool's optional Zod schema |
|
|
4781
|
+
| `RuntimeToolPresenters` | _type_ | optional MCP and AI SDK `toModelOutput` presentation callbacks |
|
|
4782
|
+
| `RuntimeMcpPresentation` | _type_ | MCP content/metadata result without framework-owned `structuredContent` or `isError` |
|
|
4783
|
+
| `RuntimeAgentModelOutput` | _type_ | AI SDK model-facing text/JSON/content output returned by `present.agent` |
|
|
4784
|
+
| `RuntimeToolTransport` | _type_ | runtime exposure: `'MCP' \| 'AGENT'` |
|
|
4394
4785
|
| `AgentMountConfig` | _type_ | config for `mountAgent` |
|
|
4395
4786
|
| `AgentContext` | _type_ | the context merged into agent tool handlers |
|
|
4396
4787
|
| `CliConfig` | _type_ | config for `createCli` |
|
|
@@ -4406,6 +4797,10 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
|
|
|
4406
4797
|
| `ToolErrorOptions` | _type_ | `{ toolName, error, context, endpoint }` for a thrown handler-path value |
|
|
4407
4798
|
| `ErrorHintFn` | _type_ | `(toolName, errorCode) => string \| null` — a per-tool recovery hint, shared by every mount |
|
|
4408
4799
|
| `ToolResult` | _type_ | the result of one tool call |
|
|
4800
|
+
| `ToolInvoker` | _type_ | immutable compiled dispatcher (`names`, envelope `invoke`, throwing `invokeOrThrow`) |
|
|
4801
|
+
| `ToolInvokerConfig` | _type_ | compile-time exposure, extension and presentation options |
|
|
4802
|
+
| `ToolInvocationOptions` | _type_ | per-call source, context, lifecycle, hooks and output-strip reporter |
|
|
4803
|
+
| `ToolInvokerTransport` | _type_ | invoker exposure policy: `MCP \| AGENT \| CLI` |
|
|
4409
4804
|
| `ToolCallContext` | _type_ | the context every tool hook receives — `{ source }` plus whatever the mount's `context` added |
|
|
4410
4805
|
| `ViewFileOptions` | _type_ | options for `mountViewFile` |
|
|
4411
4806
|
| `McpAnnotations` | _type_ | MCP annotations on a media result |
|
|
@@ -4544,6 +4939,10 @@ and `react-query-kit` peers.
|
|
|
4544
4939
|
| `createEntityCacheHandlers` | function | created/updated/deleted cache handlers for one entity — [guide](../guide/realtime.md#entity-cache-handlers) |
|
|
4545
4940
|
| `EntityCacheConfig` | _type_ | config for `createEntityCacheHandlers` |
|
|
4546
4941
|
| `EntityCacheHandlers` | _type_ | the `{ created, updated, deleted }` handlers it returns |
|
|
4942
|
+
| `EntityCacheEvent` | _type_ | discriminated created/updated/deleted input for dynamic cache keys |
|
|
4943
|
+
| `EntityCacheKey` | _type_ | static `QueryKey` or event-aware key factory |
|
|
4944
|
+
| `EntityCacheListConfig` | _type_ | list shape, scoped key, insertion/missing-update policy and comparator |
|
|
4945
|
+
| `EntityCacheListShape` | _type_ | `array \| paginated \| infinite-array \| infinite-paginated` |
|
|
4547
4946
|
| `DeletedPayload` | _type_ | a `deleted` event payload — the entity or a bare `{ id }` |
|
|
4548
4947
|
| `CursorQueryConfig` | _type_ | config for `createCursorQuery` |
|
|
4549
4948
|
| `CacheBridge` | _type_ | the `createCacheBridge` handle |
|