stitchkit 0.43.1 → 0.44.1

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 (63) hide show
  1. package/README.md +12 -4
  2. package/dist/cli.js +2 -2
  3. package/dist/contract/define.d.ts +18 -0
  4. package/dist/contract/define.d.ts.map +1 -1
  5. package/dist/contract/index.d.ts +1 -1
  6. package/dist/contract/index.d.ts.map +1 -1
  7. package/dist/{index-310bfer5.js → index-36f1xkdb.js} +1 -1
  8. package/dist/{index-esqmem78.js → index-4fyn9f09.js} +2 -1
  9. package/dist/{index-h2wdcsby.js → index-mvjyq9h9.js} +176 -12
  10. package/dist/{index-gex6gxhe.js → index-yh0p5ddn.js} +34 -2
  11. package/dist/index.js +4 -1
  12. package/dist/internal/secure-fetch.d.ts +21 -1
  13. package/dist/internal/secure-fetch.d.ts.map +1 -1
  14. package/dist/node.js +2 -2
  15. package/dist/observability/audit.d.ts.map +1 -1
  16. package/dist/observability/event.d.ts +16 -0
  17. package/dist/observability/event.d.ts.map +1 -1
  18. package/dist/observability/index.d.ts +1 -1
  19. package/dist/observability/index.d.ts.map +1 -1
  20. package/dist/observability/index.js +30 -1
  21. package/dist/observability/trace.d.ts +10 -0
  22. package/dist/observability/trace.d.ts.map +1 -1
  23. package/dist/server/implement.d.ts.map +1 -1
  24. package/dist/server/index.js +3 -3
  25. package/dist/server/types.d.ts +14 -2
  26. package/dist/server/types.d.ts.map +1 -1
  27. package/dist/tools/execute.d.ts +1 -1
  28. package/dist/tools/execute.d.ts.map +1 -1
  29. package/dist/tools/mcp-app.d.ts +3 -0
  30. package/dist/tools/mcp-app.d.ts.map +1 -1
  31. package/dist/tools/mcp-handler.d.ts +34 -23
  32. package/dist/tools/mcp-handler.d.ts.map +1 -1
  33. package/dist/tools/mcp-round.d.ts +43 -0
  34. package/dist/tools/mcp-round.d.ts.map +1 -0
  35. package/dist/tools/mcp-stdio.d.ts +11 -18
  36. package/dist/tools/mcp-stdio.d.ts.map +1 -1
  37. package/dist/tools/mcp-trace.d.ts +4 -0
  38. package/dist/tools/mcp-trace.d.ts.map +1 -0
  39. package/dist/tools/mcp.d.ts +24 -3
  40. package/dist/tools/mcp.d.ts.map +1 -1
  41. package/dist/tools/mount-download.d.ts +2 -2
  42. package/dist/tools/mount-download.d.ts.map +1 -1
  43. package/dist/tools/mount-upload.d.ts +1 -1
  44. package/dist/tools/mount-upload.d.ts.map +1 -1
  45. package/dist/tools/mount-wait.d.ts +3 -3
  46. package/dist/tools/mount-wait.d.ts.map +1 -1
  47. package/dist/tools/mount.d.ts +1 -1
  48. package/dist/tools/mount.d.ts.map +1 -1
  49. package/dist/tools/native-mcp.d.ts +3 -2
  50. package/dist/tools/native-mcp.d.ts.map +1 -1
  51. package/dist/tools/oauth-provider.d.ts +77 -7
  52. package/dist/tools/oauth-provider.d.ts.map +1 -1
  53. package/dist/tools/runtime-tool.d.ts +27 -20
  54. package/dist/tools/runtime-tool.d.ts.map +1 -1
  55. package/dist/tools/toolkit.d.ts +5 -5
  56. package/dist/tools/toolkit.d.ts.map +1 -1
  57. package/dist/tools/view-file.d.ts +1 -1
  58. package/dist/tools/view-file.d.ts.map +1 -1
  59. package/dist/tools.d.ts +5 -5
  60. package/dist/tools.d.ts.map +1 -1
  61. package/dist/tools.js +816 -197
  62. package/llms-full.txt +446 -78
  63. package/package.json +5 -4
package/llms-full.txt CHANGED
@@ -27,7 +27,7 @@ bun add stitchkit zod
27
27
 
28
28
  `zod` is a required peer — schemas are the source of truth everywhere. Other
29
29
  peers are optional and pulled in only when you use the matching feature
30
- (`@modelcontextprotocol/sdk` for MCP, `ai` for agents, `socket.io*` for
30
+ (`@modelcontextprotocol/server` for MCP servers, `ai` for agents, `socket.io*` for
31
31
  realtime, `@tanstack/react-query` + `react-query-kit` for React). See
32
32
  [deps](#dependencies) below.
33
33
 
@@ -141,9 +141,9 @@ map — feature → packages:
141
141
  | anything (validation) | `zod` |
142
142
  | `createServer` (Bun) | — (uses `Bun.serve`) |
143
143
  | `serveNode` (Node ≥ 22) | `srvx` (+ `@types/bun` dev) |
144
- | MCP tools (`stitchkit/tools`) | `@modelcontextprotocol/sdk` |
144
+ | MCP / agent tools (`stitchkit/tools`) | `@modelcontextprotocol/server` `ai` |
145
+ | MCP host/client tests | `@modelcontextprotocol/client` |
145
146
  | MCP Apps UI widgets | `@modelcontextprotocol/ext-apps` |
146
- | agent tools (`stitchkit/tools`) | `ai` |
147
147
  | React data layer (`stitchkit/react`) | `@tanstack/react-query` `react-query-kit` |
148
148
  | **Socket.IO server on Bun** | `socket.io` `@socket.io/bun-engine` |
149
149
  | **Socket.IO server on Node** | `socket.io` |
@@ -153,9 +153,22 @@ map — feature → packages:
153
153
  bun add socket.io @socket.io/bun-engine # e.g. the Socket.IO server on Bun
154
154
  ```
155
155
 
156
- If an optional peer is missing, the feature that needs it fails with an
157
- actionable error naming the package and the install command — not a bare
158
- `Cannot find module`.
156
+ Dynamic optional-peer adapters fail with an actionable error naming the package
157
+ and install command. Static entrypoints such as `stitchkit/tools` fail during
158
+ ESM resolution by naming the exact missing package; the feature-to-peer matrix
159
+ below is the canonical install command.
160
+
161
+ The combined tools entry owns MCP and AI-agent adapters, so install both runtime
162
+ peers before importing `stitchkit/tools`:
163
+
164
+ ```bash
165
+ bun add @modelcontextprotocol/server@^2 ai@^7
166
+ ```
167
+
168
+ Browser, HTTP-client and React entrypoints remain usable without either MCP
169
+ package. MCP hosts and client E2E additionally install
170
+ `@modelcontextprotocol/client@^2`; Apps additionally install
171
+ `@modelcontextprotocol/ext-apps`.
159
172
 
160
173
  ## Next
161
174
 
@@ -1804,27 +1817,28 @@ the invoker is created.
1804
1817
 
1805
1818
  ## MCP — `createMcpHandler`
1806
1819
 
1807
- `createMcpHandler` builds a complete Streamable-HTTP MCP server as a single
1808
- `Request Response` handler. It owns the SDK server and transport lifecycle, so
1809
- your app never imports `@modelcontextprotocol/sdk` itself.
1820
+ `createMcpHandler` builds a complete stateless Streamable-HTTP MCP endpoint on
1821
+ the official TypeScript SDK v2 server package. It owns server creation,
1822
+ protocol-era negotiation and shutdown; application code does not import the SDK
1823
+ to mount a normal endpoint.
1810
1824
 
1811
1825
  ```ts
1812
- import { createMcpHandler } from 'stitchkit/tools'
1826
+ import { createMcpHandler, createMcpHttpRoute } from 'stitchkit/tools'
1827
+ import { createServer } from 'stitchkit/server'
1813
1828
 
1814
- const handleMcp = createMcpHandler({
1829
+ const mcp = createMcpHandler({
1815
1830
  serverInfo: { name: 'my-app', version: '1.0.0' },
1816
1831
  auth: (req) => resolveApiKey(req), // → an identity, or null for 401
1817
1832
  services: [usersService, catalogService],
1818
1833
  })
1819
- ```
1820
-
1821
- Mount the returned handler on a raw route — typically `/mcp`:
1822
1834
 
1823
- ```ts
1824
1835
  createServer({
1825
1836
  services,
1826
- rawRoutes: [{ method: 'ALL', path: '/mcp', handler: (req) => handleMcp(req) }],
1837
+ rawRoutes: [createMcpHttpRoute({ path: '/mcp', handler: mcp })],
1827
1838
  })
1839
+
1840
+ // During graceful shutdown:
1841
+ await mcp.close()
1828
1842
  ```
1829
1843
 
1830
1844
  ### `McpHandlerConfig`
@@ -1839,6 +1853,7 @@ createServer({
1839
1853
  | `selectSurface` | `(auth) => declaredKey` — required with `surfaces` |
1840
1854
  | `context` | `(auth) => {…}` — values merged into every tool handler's `ctx` |
1841
1855
  | `lifecycle` | `beforeHandle` / `afterHandle` — the tool-side auth gate (see below) |
1856
+ | `multiRound` | Typed MRTR state policy for guarded multi-round tool calls |
1842
1857
  | `hooks` | tool-call observability hooks — `afterToolCall` fires on every result |
1843
1858
  | `extend` | extra advertised arguments resolved into handler context |
1844
1859
  | `schemaValidation` | compatibility policy, typed-property guard and portable-format guard |
@@ -1851,7 +1866,10 @@ createServer({
1851
1866
  | `errorHint` | add a project-owned hint to failed tool results |
1852
1867
  | `onOutputStrip` | observe output keys removed by contract validation |
1853
1868
  | `protectedResource` | RFC 9728 metadata used by HTTP `401` responses |
1854
- | `sessionMode` | `'stateless'` (default) or explicit `'stateful'` session/SSE continuity |
1869
+ | `legacy` | `'serve'` (default) or `'reject'` for pre-2026 clients |
1870
+ | `security` | optional Host/Origin allowlists at the Fetch boundary |
1871
+ | `onTransportRejected` | observe HTTP/protocol rejections without entering lifecycle or tool hooks |
1872
+ | `cache` | explicit operation cache hints; omitted means zero/private |
1855
1873
 
1856
1874
  Direct `services` / `runtimeTools` factories, `context`, `selectSurface` and
1857
1875
  `rawTools` receive the resolved identity, so a tenant can be shown only its own
@@ -1876,20 +1894,106 @@ const handleMcp = createMcpHandler({
1876
1894
  Every declared entry is schema-validated and prepared once when the handler is
1877
1895
  constructed. The selected immutable descriptors are shared; the SDK server,
1878
1896
  transport, auth-derived context, lifecycle runner and tool-call context are
1879
- fresh for every stateless request (or every stateful session). Unknown keys
1897
+ fresh for every request. Unknown keys
1880
1898
  fail before the server connects. The registry never retains auth values.
1881
1899
 
1882
- ### Stateless by default; stateful only when required
1883
-
1884
- The default `sessionMode: 'stateless'` creates a fresh SDK server, transport,
1885
- resolved auth/context and runner for each HTTP request. Static direct and finite
1886
- registry schemas are still prepared once when the handler is constructed. There is no session
1887
- map, event store, sweep timer or `Mcp-Session-Id`, so process replacement and
1888
- load balancing cannot strand a client on an in-memory session.
1900
+ ### Stateless transport and protocol eras
1901
+
1902
+ Every request gets a fresh SDK server, resolved auth/context and tool-call
1903
+ runner. Static direct and finite registry schemas are still prepared once when
1904
+ the handler is constructed. There is no session map, event store,
1905
+ `Mcp-Session-Id` or resumable cross-request SSE state. Process replacement and
1906
+ load balancing therefore cannot strand a client on an in-memory session.
1907
+
1908
+ The default `legacy: 'serve'` lets the official SDK negotiate either modern
1909
+ `2026-07-28` or the supported legacy stateless opening on this same endpoint.
1910
+ Set `legacy: 'reject'` when every host is modern. Legacy support is protocol
1911
+ translation inside the SDK, not a second Stitchkit transport or compatibility
1912
+ API. Stateful MCP and subscriptions are intentionally outside this layer.
1913
+
1914
+ `createMcpHttpRoute` is the framework-owned raw-route adapter. It registers
1915
+ `ALL` because Streamable HTTP uses multiple HTTP methods, delegates only to
1916
+ `handler.fetch`, and leaves lifecycle ownership with the returned handler.
1917
+
1918
+ By default, a present `Host` header must match the request URL and a present
1919
+ browser `Origin` must be same-origin. Non-browser hosts may omit `Origin`. Behind
1920
+ a trusted reverse proxy, reconstruct the public request URL before calling the
1921
+ handler or configure exact `security.allowedHosts` / `allowedOrigins`; never
1922
+ copy arbitrary forwarded headers into the trusted URL. If a browser calls MCP
1923
+ cross-origin, its HTTP server or gateway must also answer CORS preflight and
1924
+ expose the protocol headers. `onTransportRejected` is the audit boundary for
1925
+ HTTP/security rejections; it is deliberately separate from tool lifecycle and
1926
+ must not be used as authorization input.
1927
+
1928
+ ### Modern discovery, routing and cache hints
1929
+
1930
+ Stitchkit forwards the negotiated protocol era and validated MCP request data
1931
+ to `RequestEvent.mcp`; untrusted routing headers never become application
1932
+ identity. The SDK validates `MCP-Protocol-Version`, JSON-RPC shape and routing
1933
+ metadata before lifecycle or tool handlers run. Framework-owned tool and
1934
+ resource registration preserves declaration order across HTTP and stdio. Tool
1935
+ manifests preserve mount order; `listToolNames` is a sorted diagnostics view.
1936
+ Consumer-owned raw SDK registrations are outside this guarantee.
1937
+
1938
+ Caching is opt-in. `cache.operations` maps directly to SDK v2 operation cache
1939
+ hints, while each `McpResourceDef` may declare its own `cacheHint`. Omission
1940
+ means zero/private: neither a prepared surface nor a deterministic response is
1941
+ silently treated as public. Unbounded auth-selected surface factories are
1942
+ always forced to zero/private even when an operation policy is present; use a
1943
+ finite `surfaces` registry when identities share a provably immutable surface.
1944
+ Stitchkit never advertises list-change or subscription capabilities
1945
+ without an implementation.
1946
+
1947
+ ### Multi-round tool input (`input_required`)
1948
+
1949
+ An MCP-only operation can require typed user input before its final side effect:
1950
+
1951
+ ```ts
1952
+ const DeleteConfirmationSchema = z.object({ confirmed: z.boolean() })
1953
+
1954
+ deleteProject: {
1955
+ method: 'DELETE',
1956
+ path: '/projects/:id',
1957
+ expose: ['MCP'],
1958
+ input: DeleteProjectSchema,
1959
+ output: DeleteProjectResultSchema,
1960
+ mcp: {
1961
+ inputRequired: [
1962
+ {
1963
+ key: 'confirmation',
1964
+ schema: DeleteConfirmationSchema,
1965
+ message: 'Confirm permanent deletion',
1966
+ },
1967
+ {
1968
+ key: 'reason',
1969
+ schema: z.object({ value: z.string().min(3) }),
1970
+ message: 'Record the reason',
1971
+ },
1972
+ ],
1973
+ },
1974
+ }
1975
+ ```
1889
1976
 
1890
- Opt into `sessionMode: 'stateful'` only when the client needs server-initiated
1891
- messages, cross-request progress or resumable SSE. That mode issues a server
1892
- session id and retains the bounded session/event stores until idle expiry.
1977
+ Each continuation accepts exactly the currently requested item. Only after the
1978
+ ordered sequence completes does the handler receive the exact aggregate as
1979
+ `ctx.mcpInput.confirmation` and `ctx.mcpInput.reason`; before then the framework
1980
+ returns `input_required` without calling it. Set
1981
+ `multiRound.serving.maxRounds` to the maximum declaration size you accept
1982
+ (default `10`); empty sequences, duplicate keys and overflow fail first. State
1983
+ is bound to principal, full operation identity, current round, accepted
1984
+ aggregate and canonical original-argument digest and is
1985
+ expiry-checked. Tampered, expired, cross-user or cross-tool state fails before
1986
+ the side effect. Output validation applies only to the final result.
1987
+
1988
+ Every attempt re-runs auth/lifecycle and emits its own tool hooks; audit metadata
1989
+ contains the attempt's `mcp.outcome` and `mcp.round`. Stitchkit does not merge
1990
+ separate HTTP attempts into one logical trace. Parallel
1991
+ rounds have isolated context. HTTP/stdio modern hosts and the supported legacy
1992
+ stdio bridge may use this declaration; Agent, CLI and ordinary HTTP invocation
1993
+ remain single-round and unchanged. Hosts without the capability receive an
1994
+ ordinary unsupported result rather than a falsely advertised round. Signed
1995
+ state is tamper-resistant, not an exactly-once replay store; destructive
1996
+ handlers must still be idempotent where retries matter.
1893
1997
 
1894
1998
  ### Guarding tools — `lifecycle`
1895
1999
 
@@ -1948,9 +2052,9 @@ cannot become a tool. `schemaValidation.policy` decides what happens:
1948
2052
  startup assertion or a test.
1949
2053
 
1950
2054
  For a static `services` array, collection, schema conversion and every enabled
1951
- validation guard run once when the handler is created. Each HTTP request or
1952
- stateful session still receives a fresh `McpServer`, runner, context and native
1953
- registration over that immutable prepared surface. A `services(auth)` factory
2055
+ validation guard run once when the handler is created. Each HTTP request still
2056
+ receives a fresh `McpServer`, runner, context and native registration over that
2057
+ immutable prepared surface. A `services(auth)` factory
1954
2058
  is deliberately prepared after resolving each identity because its tool set may
1955
2059
  change by tenant.
1956
2060
 
@@ -2057,11 +2161,14 @@ it can reach the local filesystem.
2057
2161
  ```ts
2058
2162
  import { createStdioMcpServer } from 'stitchkit/tools'
2059
2163
 
2060
- await createStdioMcpServer({
2164
+ const stdio = await createStdioMcpServer({
2061
2165
  serverInfo: { name: 'my-app', version: '1.0.0' },
2062
2166
  auth: resolveIdentity(), // resolved once at startup, not per request
2063
2167
  services: [usersService],
2064
2168
  })
2169
+
2170
+ // During graceful shutdown:
2171
+ await stdio.close()
2065
2172
  ```
2066
2173
 
2067
2174
  A stdio server is a single process serving one client, so `auth` is a value (or
@@ -2069,6 +2176,11 @@ a promise of one) resolved once at startup — typically from an env var — rat
2069
2176
  than a per-request `(req) => …`. Keep all logging on **stderr**: stdout is the
2070
2177
  JSON-RPC channel.
2071
2178
 
2179
+ The default `legacy: 'serve'` negotiates the modern or supported legacy opening
2180
+ through the official stdio adapter. Use `legacy: 'reject'` for a modern-only
2181
+ binary. Each invocation builds a fresh server; the returned handle owns
2182
+ transport shutdown.
2183
+
2072
2184
  Both transports build the server through the shared `buildMcpServer` — same
2073
2185
  contract/runtime pipeline, same surface selection, context, hooks, raw escape
2074
2186
  hatch and instructions.
@@ -2077,41 +2189,45 @@ hatch and instructions.
2077
2189
 
2078
2190
  A remote MCP server is connectable from Claude (Desktop / web "custom
2079
2191
  connector") only through the MCP authorization spec: OAuth 2.1 with PKCE, plus
2080
- the discovery documents (RFC 9728 / 8414), Dynamic Client Registration
2081
- (RFC 7591) and resource indicators (RFC 8707). A Bearer-only server returns a
2192
+ the discovery documents (RFC 9728 / 8414), Client ID Metadata Documents (CIMD)
2193
+ and resource indicators (RFC 8707). A Bearer-only server returns a
2082
2194
  bare `401` and the connector never establishes.
2083
2195
 
2084
2196
  stitchkit ships the OAuth **protocol mechanics**; the app supplies only
2085
2197
  **identity and storage**. Three pieces wire it together:
2086
2198
 
2087
2199
  ```ts
2088
- import { createMcpHandler, mountOAuthProvider, oauthProtectedResourceRoute } from 'stitchkit/tools'
2200
+ import { createMcpHandler, createMcpHttpRoute, mountOAuthProvider, oauthProtectedResourceRoute } from 'stitchkit/tools'
2089
2201
  import { createServer } from 'stitchkit/server'
2090
2202
 
2091
2203
  const resource = 'https://api.example.com/mcp'
2092
2204
  const issuer = 'https://api.example.com'
2093
2205
 
2094
2206
  // 1. Resource server — the 401 now points at the metadata.
2095
- const handleMcp = createMcpHandler({
2207
+ const mcp = createMcpHandler({
2096
2208
  serverInfo: { name: 'my-app', version: '1.0.0' },
2097
2209
  auth: resolveOAuthToken, // validate the Bearer JWT (verifyJwt + audience)
2098
2210
  services,
2099
2211
  protectedResource: { resource, authorizationServers: [issuer] },
2100
2212
  })
2101
2213
 
2102
- // 2. Authorization server — DCR, /authorize (PKCE), /token.
2214
+ // 2. Authorization server — CIMD, /authorize (PKCE), /token.
2103
2215
  const oauthRoutes = mountOAuthProvider({
2104
2216
  issuer,
2105
2217
  resource,
2106
2218
  signingSecret: env.OAUTH_SECRET,
2107
- clients, codes, refreshTokens, // your stores (DB or in-memory)
2108
- authorizeUser, // your login + consent → { userId } | Response
2219
+ clientRegistration: {
2220
+ preRegistered: { get: getFirstPartyClient },
2221
+ // CIMD is enabled with secure defaults; DCR remains disabled.
2222
+ },
2223
+ codes, refreshTokens, // your stores (DB or in-memory)
2224
+ authorizeUser, // → { userId, approvedScopes } | Response
2109
2225
  })
2110
2226
 
2111
2227
  createServer({
2112
2228
  services,
2113
2229
  rawRoutes: [
2114
- { method: 'ALL', path: '/mcp', handler: (req) => handleMcp(req) },
2230
+ createMcpHttpRoute({ path: '/mcp', handler: mcp }),
2115
2231
  oauthProtectedResourceRoute({ resource, authorizationServers: [issuer] }),
2116
2232
  ...oauthRoutes,
2117
2233
  ],
@@ -2122,11 +2238,27 @@ Access tokens are signed HS256 JWTs (`signJwt`) whose `aud` is the resource and
2122
2238
  whose `iss` is the issuer — validate both in `auth` with
2123
2239
  `verifyJwt(token, secret, { audience: resource, issuer })`. `authorizeUser` is
2124
2240
  where the app authenticates the user (reuse an existing session) and records
2125
- consent; return `{ userId }` to issue a code, or a `Response` to redirect the
2126
- browser to a login page first. The AS and resource server can co-locate or live
2241
+ consent; return `{ userId, approvedScopes }`, where approved scopes are a subset
2242
+ of the requested/supported values, or a `Response` to redirect the browser to a
2243
+ login page first. The AS and resource server can co-locate or live
2127
2244
  on separate origins. See
2128
2245
  [ADR 0015](../decisions/0015-oauth-resource-server.md).
2129
2246
 
2247
+ Client resolution is deterministic: exact pre-registered client, then an HTTPS
2248
+ URL client id resolved as CIMD, then explicitly enabled DCR. CIMD is on by
2249
+ default; DCR is off unless `clientRegistration.dcr` supplies both `register` and
2250
+ `get`. A CIMD-only deployment therefore exposes neither `/register` nor a
2251
+ `registration_endpoint`. The metadata document must declare a `client_id` that
2252
+ exactly equals its URL, explicit redirect URIs and
2253
+ `token_endpoint_auth_method: 'none'`.
2254
+
2255
+ The production fetcher rejects credentials, fragments, non-HTTPS URLs and
2256
+ private/reserved targets. Every redirect is DNS-resolved and IP-pinned again;
2257
+ timeouts, redirect count and body size are bounded. Its bounded cache respects
2258
+ `Cache-Control` (including `no-store`/`no-cache`), `Age`, `Expires`, `ETag` and
2259
+ `Last-Modified`, coalesces concurrent misses and reports sanitized cache events;
2260
+ invalid or unavailable identity receives only a short fail-closed cache entry.
2261
+
2130
2262
  ### Authorization hardening (MCP 2026-07-28)
2131
2263
 
2132
2264
  - **`iss` on every authorization response (RFC 9207, SEP-2468).** Success *and*
@@ -2144,9 +2276,9 @@ on separate origins. See
2144
2276
  behaves exactly as before (loopback allowed); an unknown value is rejected
2145
2277
  rather than silently defaulted.
2146
2278
 
2147
- > Dynamic Client Registration is **deprecated** in the 2026-07-28 spec in favour
2148
- > of Client ID Metadata Documents (CIMD), with a ≥12-month window. DCR keeps
2149
- > working and stays supported here; CIMD support is tracked separately.
2279
+ > Dynamic Client Registration is an explicit interoperability mode, not the
2280
+ > default. Enabling it changes public discovery and adds `/register`; keep it off
2281
+ > unless a client that cannot publish CIMD requires it.
2150
2282
 
2151
2283
  ## Proxying a remote API — `implementRemote`
2152
2284
 
@@ -2163,11 +2295,14 @@ const http = createHttpClient({
2163
2295
  headers: () => ({ Authorization: `Bearer ${apiKey}` }),
2164
2296
  })
2165
2297
 
2166
- await createStdioMcpServer({
2298
+ const stdio = await createStdioMcpServer({
2167
2299
  serverInfo: { name: 'my-app', version: '1.0.0' },
2168
2300
  auth: null,
2169
2301
  services: contracts.map((c) => implementRemote(c, http)),
2170
2302
  })
2303
+
2304
+ // During graceful shutdown:
2305
+ await stdio.close()
2171
2306
  ```
2172
2307
 
2173
2308
  This is how you ship a thin **local** MCP server for an API that already runs in
@@ -2181,9 +2316,16 @@ no duplicated business logic.
2181
2316
 
2182
2317
  ## Structured output
2183
2318
 
2184
- When a contract endpoint declares an object `output`, its MCP tool registers
2185
- that as the tool `outputSchema` and the result carries `structuredContent`
2186
- alongside the text block the structured payload an MCP App UI consumes.
2319
+ When a contract endpoint or runtime tool declares `output`, its MCP tool
2320
+ registers that exact schema as `outputSchema` and returns the validated value as
2321
+ `structuredContent` alongside a JSON text block. Every JSON root type is kept
2322
+ unchanged: object, array, string, number, boolean and `null`. Stitchkit does not
2323
+ wrap non-object values in `{ result }`; protocol-era adaptation belongs to the
2324
+ official SDK.
2325
+
2326
+ A tool without an output contract advertises no `outputSchema` and returns no
2327
+ invented structured value. Its successful MCP result has an empty `content`
2328
+ list.
2187
2329
 
2188
2330
  ## AI agents — `mountAgent`
2189
2331
 
@@ -3759,8 +3901,25 @@ cross-origin `trace: true` client works out of the box. If you set a custom
3759
3901
  `cors.headers`, extend `DEFAULT_CORS_ALLOW_HEADERS` rather than replacing it, or
3760
3902
  the preflight will reject the trace header.
3761
3903
 
3904
+ MCP tools use the same context. On a modern HTTP or stdio `tools/call`, the
3905
+ official SDK exposes W3C propagation values from request `_meta`; Stitchkit
3906
+ continues a valid `_meta.traceparent` before validation, lifecycle and hooks.
3907
+ If MCP metadata omits `traceparent`, an HTTP call keeps its ambient HTTP trace
3908
+ and a stdio call starts a new root. If the key is present but malformed or
3909
+ all-zero, the call still runs under a fresh local trace instead of trusting the
3910
+ ambient one. This makes explicit MCP propagation authoritative without allowing
3911
+ invalid input to break the tool.
3912
+
3913
+ Bounded `tracestate` and `baggage` are available through
3914
+ `getRequestContext()?.trace` for outbound propagation. Their contents are not
3915
+ copied into the standard `RequestEvent`; never use them for auth, RBAC, tenant
3916
+ selection or rate-limit identity. Every MRTR attempt reads its current request
3917
+ metadata, so callers that want one trace across rounds must propagate it on
3918
+ each request.
3919
+
3762
3920
  > **Span ids live in the request context, not on `ctx`.** The handler `ctx`
3763
- > carries a single `traceId`; the full `{ traceId, spanId, parentSpanId }` is on
3921
+ > carries a single `traceId`; the full
3922
+ > `{ traceId, spanId, parentSpanId, tracestate?, baggage? }` is on
3764
3923
  > the observability request context. To stamp `spanId` / `parentSpanId` into an
3765
3924
  > audit row read `getRequestContext()?.trace`, not `ctx.spanId`:
3766
3925
  >
@@ -4099,9 +4258,11 @@ an independently built Next.js frontend and Bun/Stitchkit API.
4099
4258
 
4100
4259
  ### MCP
4101
4260
 
4102
- If the app exposes MCP tools, `createMcpHandler` is mounted as a raw route
4103
- (`/mcp`). It needs the `@modelcontextprotocol/sdk` peer installed in production
4104
- it is optional only for apps that do not use MCP.
4261
+ If the app exposes MCP tools, mount `createMcpHandler` with
4262
+ `createMcpHttpRoute({ path: '/mcp', handler })` and close the handler during
4263
+ graceful shutdown. Production needs the `@modelcontextprotocol/server` v2 peer;
4264
+ apps without MCP do not. Only an MCP host or integration-test package needs
4265
+ `@modelcontextprotocol/client`.
4105
4266
 
4106
4267
 
4107
4268
  ==============================================================================
@@ -4370,6 +4531,208 @@ current one *up to* your target, and apply each snippet.
4370
4531
 
4371
4532
  ## Unreleased breaking migrations
4372
4533
 
4534
+ ### MCP TypeScript SDK v2 and protocol `2026-07-28`
4535
+
4536
+ This is a hard cut: there is one Stitchkit API and no v1 aliases. Applications
4537
+ that expose MCP install the split server package; applications that implement an
4538
+ MCP host or run client E2E install the split client package.
4539
+
4540
+ ```bash
4541
+ bun remove @modelcontextprotocol/sdk
4542
+ bun add @modelcontextprotocol/server@^2 ai@^7
4543
+ # MCP hosts and client-side E2E only:
4544
+ bun add -d @modelcontextprotocol/client@^2
4545
+ ```
4546
+
4547
+ Direct SDK imports move to the split packages too:
4548
+
4549
+ ```ts
4550
+ // before
4551
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
4552
+
4553
+ // after
4554
+ import { McpServer } from '@modelcontextprotocol/server'
4555
+ ```
4556
+
4557
+ MCP Apps additionally install `@modelcontextprotocol/ext-apps`. That adapter may
4558
+ carry its own isolated v1-era transitive/peer relationship while the ecosystem
4559
+ finishes its cutover; it does not permit application code to import the removed
4560
+ monolithic SDK. Application-owned MCP server and client code uses the v2 split
4561
+ packages exclusively.
4562
+
4563
+ ```ts
4564
+ // before
4565
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
4566
+ import { createMcpHandler } from 'stitchkit/tools'
4567
+
4568
+ const handleMcp = createMcpHandler({
4569
+ serverInfo,
4570
+ auth,
4571
+ services,
4572
+ sessionMode: 'stateless',
4573
+ })
4574
+ rawRoutes: [{ method: 'ALL', path: '/mcp', handler: handleMcp }]
4575
+
4576
+ // after
4577
+ import { Client } from '@modelcontextprotocol/client'
4578
+ import { createMcpHandler, createMcpHttpRoute } from 'stitchkit/tools'
4579
+
4580
+ const mcp = createMcpHandler({
4581
+ serverInfo,
4582
+ auth,
4583
+ services,
4584
+ legacy: 'serve',
4585
+ })
4586
+ rawRoutes: [createMcpHttpRoute({ path: '/mcp', handler: mcp })]
4587
+ // graceful shutdown: await mcp.close()
4588
+ ```
4589
+
4590
+ Keep the owned handle and close it from the same shutdown path as the HTTP
4591
+ server. A minimal runtime smoke should list tools before shutdown and prove the
4592
+ closed handler no longer serves requests:
4593
+
4594
+ ```ts
4595
+ const mcp = createMcpHandler(config)
4596
+ const route = createMcpHttpRoute({ path: '/mcp', handler: mcp })
4597
+
4598
+ const beforeClose = await route.handler(listToolsRequest)
4599
+ if (!beforeClose.ok) throw new Error('MCP list-tools smoke failed')
4600
+
4601
+ await mcp.close()
4602
+ ```
4603
+
4604
+ Remove `@modelcontextprotocol/sdk`, `sessionMode`, `McpSessionMode`, session
4605
+ stores and all `Mcp-Session-Id` handling. HTTP is always request-isolated and
4606
+ stateless. `legacy: 'serve'` (default) lets the official SDK negotiate supported
4607
+ pre-2026 stateless clients on the same endpoint; `legacy: 'reject'` makes it
4608
+ modern-only. This is not a stateful compatibility transport.
4609
+
4610
+ #### Output shape depends on the negotiated protocol era
4611
+
4612
+ Stitchkit always validates the handler result against the declared output
4613
+ schema. The negotiated MCP era determines only its wire representation:
4614
+
4615
+ | Negotiated era | Non-object `structuredContent` |
4616
+ |---|---|
4617
+ | MCP `2026-07-28` | the exact schema-valid JSON root: array, scalar or `null` |
4618
+ | supported legacy era | the official SDK codec adapts it to `{ result: value }` |
4619
+
4620
+ Object roots keep their object shape in both eras. Do not change every consumer
4621
+ expectation to the modern shape while `legacy: 'serve'` remains enabled. Pin
4622
+ both protocol versions in the consumer's transport E2E and assert the boundary
4623
+ explicitly:
4624
+
4625
+ ```ts
4626
+ await expectToolOutputForProtocol('2026-07-28', ['a', 'b'])
4627
+ await expectToolOutputForProtocol('2025-11-25', { result: ['a', 'b'] })
4628
+
4629
+ async function expectToolOutputForProtocol(
4630
+ protocolVersion: '2026-07-28' | '2025-11-25',
4631
+ expected: unknown,
4632
+ ) {
4633
+ const client = await connectConsumerMcpClient({ protocolVersion })
4634
+ const result = await client.callTool({ name: 'list_notes', arguments: {} })
4635
+ expect(result.structuredContent).toEqual(expected)
4636
+ await client.close()
4637
+ }
4638
+ ```
4639
+
4640
+ `connectConsumerMcpClient` represents the consumer's real HTTP or stdio setup;
4641
+ configure its official client with `versionNegotiation.mode.pin` so the test
4642
+ cannot silently negotiate a different era.
4643
+
4644
+ The stdio helper now returns an owned lifecycle handle:
4645
+
4646
+ ```ts
4647
+ // before
4648
+ await createStdioMcpServer(config)
4649
+
4650
+ // after
4651
+ const stdio = await createStdioMcpServer({ ...config, legacy: 'serve' })
4652
+ await stdio.close()
4653
+ ```
4654
+
4655
+ OAuth client registration is now one explicit policy object. CIMD is secure and
4656
+ enabled by default; DCR is disabled unless supplied:
4657
+
4658
+ ```ts
4659
+ // before
4660
+ mountOAuthProvider({ ...oauth, clients, codes, refreshTokens })
4661
+
4662
+ // after
4663
+ mountOAuthProvider({
4664
+ ...oauth,
4665
+ clientRegistration: {
4666
+ preRegistered: { get: clients.get },
4667
+ // optional: dcr: { register: clients.register, get: clients.get }
4668
+ },
4669
+ codes,
4670
+ refreshTokens,
4671
+ })
4672
+ ```
4673
+
4674
+ A URL client id must be HTTPS and serve a document whose `client_id` exactly
4675
+ matches that URL, with explicit `redirect_uris` and
4676
+ `token_endpoint_auth_method: 'none'`. Do not keep a consumer-side metadata
4677
+ fetcher or DCR fallback; Stitchkit owns SSRF-safe resolution and caching.
4678
+
4679
+ Multi-round input is opt-in on the operation and does not change Agent, CLI or
4680
+ ordinary HTTP handlers:
4681
+
4682
+ ```ts
4683
+ const ConfirmationSchema = z.object({ confirmed: z.boolean() })
4684
+
4685
+ mcp: {
4686
+ inputRequired: [{
4687
+ key: 'confirmation',
4688
+ message: 'Confirm this action',
4689
+ schema: ConfirmationSchema,
4690
+ }],
4691
+ }
4692
+ ```
4693
+
4694
+ Configure `multiRound.state` on the MCP server with a key of at least 32 bytes
4695
+ and a stable authenticated `principal`. Read accepted typed content from
4696
+ `ctx.mcpInput.confirmation`. Multiple declarations run in array order, the
4697
+ aggregate remains exactly typed by key, keys must be unique, and declarations
4698
+ must fit `multiRound.serving.maxRounds` (default `10`). Do not execute a
4699
+ destructive side effect before the complete aggregate is accepted. A modern
4700
+ request missing the declared elicitation capability receives JSON-RPC error
4701
+ code `-32021`. The official per-request legacy HTTP bridge cannot issue
4702
+ server-to-client elicitation, so it returns a deterministic failed tool result;
4703
+ multi-round input is never silently treated as complete.
4704
+
4705
+ #### Compatibility matrix
4706
+
4707
+ | Host / transport | Tools and Apps | Multi-round input | Continuity |
4708
+ |---|---|---|---|
4709
+ | `2026-07-28` HTTP | yes | yes | request-isolated |
4710
+ | supported legacy stateless HTTP | yes | unsupported result | request-isolated |
4711
+ | `2026-07-28` stdio | yes | yes | one process connection |
4712
+ | supported legacy stdio | yes | official SDK bridge | one process connection |
4713
+ | Agent / CLI / ordinary HTTP | unchanged | not exposed | unchanged |
4714
+
4715
+ Subscriptions, cross-request progress and resumable stateful SSE are not
4716
+ implemented or advertised.
4717
+
4718
+ #### Consumer checklist
4719
+
4720
+ 1. Replace the monolithic SDK with `@modelcontextprotocol/server@^2` and, only
4721
+ for hosts/tests, `@modelcontextprotocol/client@^2`.
4722
+ 2. Replace raw MCP route wiring with `createMcpHttpRoute`; retain and close the
4723
+ returned handler/stdio handle during shutdown.
4724
+ 3. Delete all session mode, event-store and session-id code.
4725
+ 4. Move OAuth client policy under `clientRegistration`; publish CIMD or enable
4726
+ DCR explicitly.
4727
+ 5. Snapshot `listToolNames`, run one contract tool, one runtime tool, any raw
4728
+ multimodal tool and every MCP App resource you use.
4729
+ 6. Exercise modern HTTP and stdio with protocol `2026-07-28`; exercise legacy
4730
+ only if `legacy: 'serve'` is part of your support policy.
4731
+ 7. Run the consumer's typecheck and runtime gates. A browser/HTTP-only consumer
4732
+ must continue to work without either MCP package.
4733
+
4734
+ ## Historical breaking migrations through 0.43.1
4735
+
4373
4736
  HTTP observability now completes inside the framework handler instead of a
4374
4737
  nested fetch wrapper. Configure request and tool sinks explicitly:
4375
4738
 
@@ -4717,23 +5080,6 @@ If a tool hook annotated `endpoint` as `MethodDef`, remove that annotation or
4717
5080
  use `OperationIdentity`: native operations have service/action/scope/method but
4718
5081
  no HTTP path.
4719
5082
 
4720
- ### MCP HTTP sessions
4721
-
4722
- Finally, replace the MCP HTTP session boolean. Omission changed meaning:
4723
-
4724
- ```ts
4725
- // before → after
4726
- stateless: true // → sessionMode: 'stateless'
4727
- stateless: false // → sessionMode: 'stateful'
4728
-
4729
- // before: omission was stateful
4730
- // after: omission is stateless
4731
- ```
4732
-
4733
- If the client relies on `Mcp-Session-Id`, server push, progress across requests
4734
- or resumable SSE, set `sessionMode: 'stateful'` explicitly. Synchronous tool
4735
- servers should omit it and use the new restart-safe default.
4736
-
4737
5083
  ### Node-facing server types
4738
5084
 
4739
5085
  `stitchkit/server` remains Bun-concrete: an explicitly annotated `RawRoute`
@@ -4882,7 +5228,7 @@ can emit / propagate a `traceparent` (see `HttpClientConfig.trace`).
4882
5228
  | `formatTraceparent` | function | render a `traceparent` header value |
4883
5229
  | `parseTraceparent` | function | parse a `traceparent` header |
4884
5230
  | `childSpan` | function | a child span of a parent trace |
4885
- | `TraceContext` | _type_ | `{ traceId, spanId, parentSpanId? }` |
5231
+ | `TraceContext` | _type_ | `{ traceId, spanId, parentSpanId?, tracestate?, baggage? }` |
4886
5232
 
4887
5233
  ---
4888
5234
 
@@ -4929,6 +5275,8 @@ from the root `stitchkit`.
4929
5275
  | `FileDescriptor` | _type_ | a React Native / Expo file — `{ uri, name, type }` |
4930
5276
  | `EndpointToolAnnotations` | _type_ | MCP behavioural hints on an endpoint (`readOnlyHint` / `destructiveHint` / `title`) |
4931
5277
  | `EndpointUiMeta` | _type_ | MCP Apps widget metadata on an endpoint |
5278
+ | `EndpointMcpInputRequired` | _type_ | typed MCP multi-round input request (`key`, message and Zod object schema) |
5279
+ | `EndpointMcpPolicy` | _type_ | MCP-only endpoint policy containing `inputRequired` |
4932
5280
 
4933
5281
  ### Errors
4934
5282
 
@@ -5146,11 +5494,12 @@ audit event. See the [Observability guide](../guide/observability.md).
5146
5494
  | Export | Kind | Summary |
5147
5495
  |--------|------|---------|
5148
5496
  | `resolveTraceContext` | function | the trace for a request — `traceparent` continued or fresh |
5497
+ | `resolvePropagationContext` | function | continue bounded MCP/W3C propagation metadata with optional ambient fallback |
5149
5498
  | `parseTraceparent` | function | parse a `traceparent` header |
5150
5499
  | `formatTraceparent` | function | render a `traceparent` header value |
5151
5500
  | `createTraceContext` | function | a fresh root trace |
5152
5501
  | `childSpan` | function | a child span of a parent trace |
5153
- | `TraceContext` | _type_ | `{ traceId, spanId, parentSpanId? }` |
5502
+ | `TraceContext` | _type_ | `{ traceId, spanId, parentSpanId?, tracestate?, baggage? }` |
5154
5503
 
5155
5504
  ### Sanitisation
5156
5505
 
@@ -5168,12 +5517,19 @@ audit event. See the [Observability guide](../guide/observability.md).
5168
5517
 
5169
5518
  ## `stitchkit/tools`
5170
5519
 
5171
- Server-only. Turns contracts into MCP and AI-agent tools. Needs the
5172
- `@modelcontextprotocol/sdk` peer (for MCP) and the `ai` peer (for agents).
5520
+ Server-only. Turns contracts into MCP and AI-agent tools. MCP server surfaces
5521
+ need the `@modelcontextprotocol/server` v2 peer; MCP hosts/tests use the separate
5522
+ `@modelcontextprotocol/client` package. Agent surfaces need the `ai` peer.
5523
+
5524
+ Framework-owned MCP tools advertise the exact declared output schema and return
5525
+ the validated value unchanged as `structuredContent`, including arrays, scalars
5526
+ and `null`. Tools without an output contract advertise and return no structured
5527
+ payload.
5173
5528
 
5174
5529
  | Export | Kind | Summary |
5175
5530
  |--------|------|---------|
5176
- | `createMcpHandler` | function | a complete Streamable-HTTP MCP server — [guide](../guide/mcp-and-agents.md#mcp--createmcphandler) |
5531
+ | `createMcpHandler` | function | a stateless dual-era Streamable-HTTP MCP handler — [guide](../guide/mcp-and-agents.md#mcp--createmcphandler) |
5532
+ | `createMcpHttpRoute` | function | framework-owned `RawRoute` adapter for an MCP HTTP handler |
5177
5533
  | `createStdioMcpServer` | function | a complete stdio MCP server — [guide](../guide/mcp-and-agents.md#mcp-over-stdio--createstdiomcpserver) |
5178
5534
  | `buildMcpServer` | function | build an `McpServer` from contract/runtime surfaces — the transport-neutral core |
5179
5535
  | `mountMcp` | function | add contract tools to an existing `McpServer` — [guide](../guide/mcp-and-agents.md#mountmcp) |
@@ -5188,9 +5544,12 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
5188
5544
  | `resolveMedia` | function | resolve a media reference for a tool result |
5189
5545
  | `validateMcpSchemas` | function | object-shaped assertion over the exact advertised schema surface — compatibility, typed properties and portable formats ([guide](../guide/mcp-and-agents.md#mcp-schema-validation-profile)) |
5190
5546
  | `listToolNames` | function | every contract/runtime tool name with origin, identity and transports — for stable snapshots — [guide](../guide/mcp-and-agents.md#pinning-tool-names--listtoolnames) |
5191
- | `McpHandlerConfig` | _type_ | config for `createMcpHandler` |
5192
- | `McpHttpConfig` | _type_ | HTTP auth, protected-resource and session options composed into `McpHandlerConfig` |
5193
- | `McpSessionMode` | _type_ | `'stateless' \| 'stateful'`; HTTP defaults to request-isolated stateless mode |
5547
+ | `McpHandlerConfig` | _type_ | server surface plus stateless HTTP transport config |
5548
+ | `McpHttpConfig` | _type_ | HTTP auth, protected-resource, legacy-era and security options |
5549
+ | `McpHttpHandler` | _type_ | framework-owned `{ fetch(request), close() }` lifecycle |
5550
+ | `McpHttpSecurityConfig` | _type_ | Fetch-boundary Host and Origin allowlists |
5551
+ | `McpLegacyPolicy` | _type_ | `'serve' \| 'reject'` protocol-era compatibility policy |
5552
+ | `McpStdioHandle` | _type_ | closeable official stdio transport handle |
5194
5553
  | `StdioMcpServerConfig` | _type_ | config for `createStdioMcpServer` |
5195
5554
  | `McpServerBuildConfig` | _type_ | shared config for `buildMcpServer` |
5196
5555
  | `McpServerSharedConfig` | _type_ | transport-neutral options shared by direct and finite surface configs |
@@ -5218,6 +5577,15 @@ Server-only. Turns contracts into MCP and AI-agent tools. Needs the
5218
5577
  | `RuntimeToolOutput` | _type_ | output inferred from a runtime tool's optional Zod schema |
5219
5578
  | `RuntimeToolPresenters` | _type_ | optional MCP and AI SDK `toModelOutput` presentation callbacks |
5220
5579
  | `RuntimeMcpPresentation` | _type_ | MCP content/metadata result without framework-owned `structuredContent` or `isError` |
5580
+ | `RuntimeMcpInput` | _type_ | typed accepted multi-round input for an opted-in runtime tool |
5581
+ | `OAuthClientRegistrationConfig` | _type_ | deterministic pre-registered → CIMD → explicit-DCR client resolution |
5582
+ | `CimdClientMetadata` | _type_ | validated Client ID Metadata Document |
5583
+ | `CimdClientMetadataFetcher` | _type_ | injectable secure network boundary for metadata loading |
5584
+ | `CimdFetchResponse` | _type_ | bounded metadata fetch result passed across the injectable network boundary |
5585
+ | `CimdFetchPolicy` | _type_ | CIMD timeout, redirect and size limits |
5586
+ | `CimdCachePolicy` | _type_ | bounded HTTP-aware positive/negative cache policy |
5587
+ | `CimdCacheEvent` | _type_ | observable CIMD cache hit, miss, revalidation and eviction event |
5588
+ | `createSecureClientMetadataFetcher` | function | production HTTPS, DNS/IP-pinned CIMD fetcher |
5221
5589
  | `RuntimeAgentModelOutput` | _type_ | AI SDK model-facing text/JSON/content output returned by `present.agent` |
5222
5590
  | `RuntimeToolTransport` | _type_ | runtime exposure: `'MCP' \| 'AGENT'` |
5223
5591
  | `AgentMountConfig` | _type_ | config for `mountAgent` |