@kya-os/mcp 1.17.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +398 -0
  2. package/CONFORMANCE.md +18 -6
  3. package/README.md +59 -0
  4. package/SPEC.md +57 -4
  5. package/dist/card/cimd.js +6 -2
  6. package/dist/card/cimd.js.map +1 -1
  7. package/dist/card/delegation.d.ts +12 -3
  8. package/dist/card/delegation.js +13 -7
  9. package/dist/card/delegation.js.map +1 -1
  10. package/dist/card/index.d.ts +1 -0
  11. package/dist/card/index.js +1 -0
  12. package/dist/card/index.js.map +1 -1
  13. package/dist/card/revocation.d.ts +15 -4
  14. package/dist/card/revocation.js +9 -4
  15. package/dist/card/revocation.js.map +1 -1
  16. package/dist/card/status-list-proof.d.ts +32 -0
  17. package/dist/card/status-list-proof.js +102 -0
  18. package/dist/card/status-list-proof.js.map +1 -0
  19. package/dist/delegation/chain-enforcement.d.ts +16 -0
  20. package/dist/delegation/chain-enforcement.js +17 -0
  21. package/dist/delegation/chain-enforcement.js.map +1 -1
  22. package/dist/delegation/verification-method-key.d.ts +4 -1
  23. package/dist/delegation/verification-method-key.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/middleware/index.d.ts +3 -1
  28. package/dist/middleware/index.js +2 -0
  29. package/dist/middleware/index.js.map +1 -1
  30. package/dist/middleware/kya-os-transport.d.ts +43 -12
  31. package/dist/middleware/kya-os-transport.js +151 -28
  32. package/dist/middleware/kya-os-transport.js.map +1 -1
  33. package/dist/middleware/mcp-tool-callback.d.ts +48 -0
  34. package/dist/middleware/mcp-tool-callback.js +38 -0
  35. package/dist/middleware/mcp-tool-callback.js.map +1 -0
  36. package/dist/middleware/serve-in-process.d.ts +41 -0
  37. package/dist/middleware/serve-in-process.js +86 -0
  38. package/dist/middleware/serve-in-process.js.map +1 -0
  39. package/dist/middleware/with-kya-os-server.d.ts +20 -3
  40. package/dist/middleware/with-kya-os-server.js +16 -1
  41. package/dist/middleware/with-kya-os-server.js.map +1 -1
  42. package/dist/middleware/with-kya-os.config-types.d.ts +34 -0
  43. package/dist/middleware/with-kya-os.d.ts +4 -0
  44. package/dist/middleware/with-kya-os.delegation-gate.d.ts +2 -1
  45. package/dist/middleware/with-kya-os.delegation-gate.js +12 -18
  46. package/dist/middleware/with-kya-os.delegation-gate.js.map +1 -1
  47. package/dist/middleware/with-kya-os.delegation-verify.d.ts +7 -12
  48. package/dist/middleware/with-kya-os.delegation-verify.js +44 -9
  49. package/dist/middleware/with-kya-os.delegation-verify.js.map +1 -1
  50. package/dist/middleware/with-kya-os.deps.d.ts +15 -4
  51. package/dist/middleware/with-kya-os.helpers.d.ts +5 -0
  52. package/dist/middleware/with-kya-os.helpers.js +13 -0
  53. package/dist/middleware/with-kya-os.helpers.js.map +1 -1
  54. package/dist/middleware/with-kya-os.js +23 -0
  55. package/dist/middleware/with-kya-os.js.map +1 -1
  56. package/dist/middleware/with-kya-os.outcome.d.ts +18 -0
  57. package/dist/middleware/with-kya-os.outcome.js +36 -0
  58. package/dist/middleware/with-kya-os.outcome.js.map +1 -0
  59. package/dist/middleware/with-kya-os.policy-gate.d.ts +2 -1
  60. package/dist/middleware/with-kya-os.policy-gate.js +2 -1
  61. package/dist/middleware/with-kya-os.policy-gate.js.map +1 -1
  62. package/dist/middleware/with-kya-os.session.d.ts +76 -10
  63. package/dist/middleware/with-kya-os.session.js +739 -164
  64. package/dist/middleware/with-kya-os.session.js.map +1 -1
  65. package/dist/middleware/with-kya-os.types.d.ts +102 -8
  66. package/dist/proof/generator.d.ts +13 -0
  67. package/dist/proof/generator.js +15 -0
  68. package/dist/proof/generator.js.map +1 -1
  69. package/dist/proof/verifier.js +14 -9
  70. package/dist/proof/verifier.js.map +1 -1
  71. package/package.json +18 -14
package/CHANGELOG.md CHANGED
@@ -7,6 +7,404 @@ Versioning: https://semver.org/spec/v2.0.0.html
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.20.0] - 2026-10-13
11
+
12
+ 1.19.0 was tagged but never published to npm; its changes (listed under
13
+ 1.19.0 below) ship to npm for the first time in this release.
14
+
15
+ ### Upgrading — BREAKING without an open KYA-OS transport
16
+
17
+ - **Every response proof is now made where the KYA-OS transport sends the
18
+ result, under the default body profile as under the envelope profile, and
19
+ the wrappers refuse to run without one.** While no KYA-OS transport is open
20
+ for the middleware, `wrapWithProof`, `wrapWithDelegation`, `withPolicyGate`
21
+ and `proveOutcome` throw on entry, before they record or run anything, with
22
+ an error that names `withKyaOs` and `createKyaOsTransport`. A transport
23
+ counts as open from `start()` until it closes, by `close()` or by its own
24
+ `onclose`. Once a KYA-OS transport has been created for a middleware, every
25
+ wrapper result must go out through one. Before, the proofs were signed in
26
+ the handler, and they did not verify on 2026-07-28 (envelope) or after an
27
+ SDK reshaped `content` (body). Find your setup:
28
+ - **`withKyaOs(server, …)` before `server.connect()`, `proofAllTools` left
29
+ on:** nothing to change.
30
+ - **The low-level `Server` API with `createKyaOsMiddleware`:** connect
31
+ through the KYA-OS transport, as this repository's examples now do:
32
+
33
+ ```ts
34
+ await server.connect(createKyaOsTransport(transport, kyaos)); // was server.connect(transport)
35
+ ```
36
+
37
+ The transport proves and audits every tool's result, as `withKyaOs`
38
+ does, including tools you registered without a wrapper, which went out
39
+ unproven on 1.19.0. List any to leave unproven in its third argument,
40
+ which adds to the always-excluded `_kyaos` and `_kyaos_handshake`.
41
+ - **A script, walkthrough or test that calls a wrapped handler directly:**
42
+ serve it in process, and read the result a client would receive:
43
+
44
+ ```ts
45
+ const local = serveInProcess(kyaos);
46
+ const checkout = local.tool('checkout', kyaos.wrapWithDelegation('checkout', consent, handler));
47
+ const result = await checkout({ item: 'laptop' }); // proved
48
+ await local.close();
49
+ ```
50
+ - **`withKyaOs` with `proofAllTools: false`:** it wraps no transport, so
51
+ the wrappers refuse. Leave it on and list the tools to leave unproven in
52
+ `excludeTools`.
53
+ - **`withKyaOs` called after `connect()`:** the connected transport is not
54
+ wrapped; call `withKyaOs` first.
55
+ - **Code that reads the proof inside the handler:** the result
56
+ `proveOutcome` or a wrapper returns no longer carries one, under either
57
+ profile. Return it from the handler; the transport adds the proof.
58
+ - **Results delivered later, such as MCP task results:** a result is
59
+ proved the first time the same middleware sends it, within
60
+ `pendingProofTtlMs` (default five minutes, at most 2147483647) of the
61
+ handler returning. Raise it for task results fetched later; a later or
62
+ repeated send goes out with `proofError` (see below).
63
+
64
+ ### Added
65
+
66
+ - **The Entity Card revocation checker can verify a status list's proof.**
67
+ `createRevocationChecker` fetched a Bitstring status list and read its bits
68
+ without checking the list's proof, which Bitstring Status List v1.0 requires
69
+ (§3.2, step 4). Whoever could serve or alter the list could clear a
70
+ revocation. It now takes an optional `verifyStatusList`: a list it rejects,
71
+ by returning anything but `true` or by throwing, resolves to revoked and not
72
+ fresh, like any other unreadable list. `createStatusListProofVerifier({
73
+ didResolver })` is a ready verifier for `DataIntegrityProof` /
74
+ `eddsa-jcs-2022` lists. It finds the key that `proof.verificationMethod`
75
+ names in the issuer's DID document, requires that method to belong to the
76
+ list's `issuer`, and accepts relative method ids and `publicKeyJwk` or
77
+ `publicKeyMultibase` keys. It verifies a status list published by
78
+ builders.kya-os.org, signed by an independent implementation. Without
79
+ `verifyStatusList` the checker behaves as before. The option becomes
80
+ required in 2.0, and SPEC.md §6.10 states the requirement.
81
+ - **`createKyaOsTransport` is exported from `@kya-os/mcp`,** next to
82
+ `createKyaOsMiddleware`, for servers on the low-level `Server` API, which
83
+ now connect through it. It was only on `@kya-os/mcp/middleware`. Its
84
+ `Transport` type declares the callbacks as methods, so an SDK 1 or SDK 2
85
+ transport is accepted, and the wrapper connects to either SDK's server,
86
+ without a cast.
87
+ - **`serveInProcess(kyaos)` calls wrapped handlers outside an MCP server.**
88
+ `local.tool(name, handler)` answers as a server would, through a KYA-OS
89
+ transport, so the result carries its proof; `local.close()` closes the
90
+ transport. It replaces the examples' own helper.
91
+ - **`pendingProofTtlMs`** on `KyaOsConfig` and `WithKyaOsOptions`: how long a
92
+ wrapper's result may wait to be sent before its proof is given up (default
93
+ 300000, five minutes; at most 2147483647, about 24.8 days, the longest timer
94
+ a JavaScript runtime keeps). Memory stays bounded by the caps on waiting
95
+ proofs.
96
+
97
+ ### Changed
98
+
99
+ - **Envelope-profile proofs are made at the transport, over the result as it
100
+ is sent.** On the 2026-07-28 revision, MCP SDK 2 adds `resultType:
101
+ 'complete'` to a tool result after the handler returns it. An
102
+ envelope-profile proof made in the handler, by `proveOutcome`,
103
+ `wrapWithProof` or either gate, did not cover that member, so the client's
104
+ check of the result it received failed. Under the envelope profile these
105
+ paths now leave the proof to the `withKyaOs` transport, or one built with
106
+ `createKyaOsTransport`, which makes it in `send()`, after the SDK has
107
+ finished the result. The proof covers exactly the result the client
108
+ receives, on 2025-11-25 and on 2026-07-28, with SDK 1 or SDK 2. This
109
+ supersedes the 1.19.0 advice to set `resultType: 'complete'` on a result
110
+ before proving it under the envelope profile: that is no longer needed, and
111
+ a result that sets it is proved as sent.
112
+ - What a proof claims stays in the middleware. The result carries only a
113
+ private one-time id, redeemed only on a result that also carries the
114
+ middleware's stamp, and the session is read again when the proof is made,
115
+ which takes its audience from it. A result naming no waiting proof, or
116
+ one whose session is gone by then, goes out without a proof and with
117
+ `_meta.proofError`, as does an outcome whose proof fails at send.
118
+ - A waiting proof holds the request's hash, computed when the call was
119
+ recorded, never the arguments, so a waiting proof is small however large
120
+ they were, and later changes to the arguments cannot reach what is
121
+ signed. Arguments that cannot be hashed fail the proof in the handler
122
+ (`proofError`, `proof.rejected` with `PROOF_GENERATION_FAILED`).
123
+ - The claims, `_meta` keys and audit events are the ones the handler-side
124
+ proof had. For a result that is sent, the audit events are recorded in
125
+ the same order as before: an allowed call's `tool.call.started` in the
126
+ handler, then `proof.generated` and `tool.call.completed` at send.
127
+ - A result the SDK never sends (a cancelled call, or an SDK 1 result
128
+ replaced after an `outputSchema` check) is closed in the audit trail with
129
+ `proof.rejected` under the new code `PROOF_NOT_SENT`. An allowed call
130
+ then records `tool.call.failed` with the reason `RESULT_NOT_SENT`, never
131
+ `tool.call.completed`, since its client did not get the result. An
132
+ outcome's terminal events were recorded in the handler. This happens five
133
+ minutes after the handler returned; when the last KYA-OS transport for
134
+ the middleware closes; or, oldest first, when 10,000 proofs are waiting
135
+ or they hold about 16 MB of audit context.
136
+ - **Body-profile proofs are made at the transport too, so neither profile
137
+ signs in the handler.** The MCP SDK also changes `content` after the
138
+ handler returns. SDK 2, on both revisions, appends a text item when
139
+ `structuredContent` is not an object and `content` has none. Both SDKs fill
140
+ in `content: []` when a handler leaves it out. A body-profile proof made in
141
+ the handler did not cover the appended item, so it failed verification, and
142
+ over a missing `content` it could not be made at all. The body profile now
143
+ takes the same path as the envelope profile above: the claims stay in the
144
+ middleware by request hash, and the transport makes the proof in `send()`
145
+ over the result as sent, with the same claims, `_meta` keys and audit
146
+ events, recorded in the same order. `_meta` keeps its place among the
147
+ result's members. Where the SDK does not reshape the result, the output is
148
+ byte-identical to 1.19.0. An outcome proof that fails now also says so in
149
+ `_meta.proofError`, as an allowed call's already did.
150
+ - **A result is proved once, within the window.** A stamped result whose
151
+ pending proof is no longer waiting, because its window passed, it was
152
+ evicted, or it was already sent (a task result fetched again), goes out
153
+ with `proofError`, and the audit trail records `proof.rejected` with the
154
+ new code `PROOF_EXPIRED`, with the call's audit context when it is still
155
+ known. That record of closed proofs is bounded too: 10,000 entries and
156
+ about 4 MB, oldest first, holding no context over 16 KB.
157
+ - **`createKyaOsTransport`'s `exclude` adds to the protocol tools.** The
158
+ third argument replaced the default `["_kyaos", "_kyaos_handshake"]`, so a
159
+ caller passing its own list had the protocol tools' results proved. It now
160
+ adds to them, as `excludeTools` does in `withKyaOs`; the protocol tools
161
+ are always excluded.
162
+
163
+ ### Security
164
+
165
+ - **SDK 2's per-tool OAuth scope challenges were bypassed under `withKyaOs`
166
+ in earlier versions.** SDK 2's `McpServer.connect()` installs
167
+ the `scopeChallenge` enforcement of `registerTool` only on a transport
168
+ that has `setScopeChallengeResolver`, and the KYA-OS transport did not
169
+ pass that member through. A tool registered with, for example,
170
+ `scopeChallenge: requireScopes('payments:write')` therefore ran, and was
171
+ even proved, for a token without that scope. The caller should have
172
+ received a 403. This affected 1.19.0 and earlier:
173
+ - `withKyaOs` with a server's own `WebStandardStreamableHTTPServerTransport`;
174
+ - `createMcpHandler` serving a client that negotiates 2025-11-25.
175
+
176
+ At 2026-07-28, `createMcpHandler` enforces the challenge at its entry, before
177
+ the transport. The transport now passes the resolver through, and every
178
+ other transport member either SDK feature-detects or reads is passed
179
+ through too.
180
+ - **The private members could leave in a message that is not a result, and
181
+ a copied stamp and id could make the transport sign chosen content.** The
182
+ transport removed the lifecycle stamp and pending-proof id only from
183
+ `result`. A tool that logged a wrapped result in a notification therefore
184
+ sent both to the client. A tool that relays caller-controlled `_meta` into
185
+ its own result could then redeem them, and the client got a server-signed
186
+ proof over content of its choosing, verifying as the wrapped call's
187
+ response. With envelope-profile proofs at the transport this was possible
188
+ before this release; with the body profile there too, in this release
189
+ before the fix. Now:
190
+ - every outgoing message is cleaned at any depth: its `params`,
191
+ `result` and `error.data`;
192
+ - a waiting proof whose id is found anywhere other than its own result is
193
+ closed, so nothing can redeem it;
194
+ - on the answer to a `tools/call` the transport tracked, a pending proof
195
+ left for a different tool is not redeemed. The answer goes out unproven,
196
+ and the audit trail records `proof.rejected` with the new code
197
+ `PROOF_TOOL_MISMATCH`.
198
+
199
+ ### Fixed
200
+
201
+ - **The proof wrappers no longer change the result a handler returns.**
202
+ `wrapWithProof` set its `_meta` members (the pending-proof id, the
203
+ lifecycle stamp, `proofError`, the audit marker) on the handler's own
204
+ object, and its audit-degraded path set `isError: true` on it. A handler
205
+ that returned one object from every call, a module-level constant or a
206
+ cached result, found those members on it, and a later call could go out
207
+ with an earlier call's `isError` and audit marker; a frozen result made the
208
+ wrapper throw. Every member now goes on a shallow copy of the result with a
209
+ new `_meta`, in `wrapWithProof` and in the outcome path behind the gates
210
+ and `proveOutcome`, and the handler's object and its `_meta` are never
211
+ changed. What the client receives is otherwise the same. Closes #249.
212
+
213
+ - **The KYA-OS transport passes the SDK's request context through.** It
214
+ dropped the second argument of `onmessage` and of `send`, and did not
215
+ expose the inner transport's `sessionId`, `setProtocolVersion`,
216
+ `setSupportedProtocolVersions` or `hasPerRequestStream`. Under `withKyaOs`,
217
+ tools lost the request's `sessionId`, `authInfo` and `requestInfo` (SDK 1)
218
+ and `ctx.http.authInfo` (SDK 2, both revisions); SDK 1 task stores stopped
219
+ separating tasks by session; and notifications a tool sent during a call
220
+ lost their `relatedRequestId`, so on Streamable HTTP they missed the
221
+ request's stream. All of these now pass through unchanged. Closes #248.
222
+ - **The proof's scope is the call's, as the wrapper received it.**
223
+ `wrapWithProof` read `context.scopeId` and the audit context after the
224
+ handler ran, so a handler could change the scope a proof claims by changing
225
+ the context object it was handed. They are now read before the handler
226
+ runs.
227
+ - **Private members never reach the wire from a nested result.** A wrapped
228
+ result a handler placed inside its own result, in `structuredContent` for
229
+ example, carried the lifecycle stamp and the pending-proof id to the
230
+ client. They are now removed at any depth: the walk is iterative, with no
231
+ depth limit. A wrapped result serialized into a string, a text item for
232
+ example, cannot be cleaned that way. Such a result is sent without a
233
+ proof and with `proofError`, recorded as `proof.rejected` with the new
234
+ code `PRIVATE_MEMBER_IN_RESULT`. Every waiting proof whose id appears in
235
+ that string is closed, so it cannot be redeemed.
236
+
237
+ ## [1.19.0] - 2026-10-09
238
+
239
+ ### Added
240
+
241
+ - **`proveOutcome` proves the authorization outcomes application code
242
+ decides.** A server that enforces OAuth scopes in its tool handlers
243
+ returns its own challenges and denials as tool results, and the
244
+ `withKyaOs` transport sent such an error result unproven. The middleware
245
+ now has `proveOutcome({ toolName, args, outcome, reason, result,
246
+ sessionId? })`, which returns a copy of `result` carrying the proof the
247
+ gates attach to their outcomes: the same claims, the same `_meta` keys,
248
+ the configured response-proof profile and the same audit events. It runs
249
+ the gates' own code, and the transport passes its result through as
250
+ proven. A `needs_authorization` proof binds the result and a denial or
251
+ step-up proof binds none, as SPEC.md §7.4 has it. The `_kyaos*` control
252
+ arguments are left out of the signed request, `_meta` members only the
253
+ middleware may set are dropped, and no session needs to be opened. Under
254
+ the envelope profile, set `resultType: 'complete'` on the result first: on
255
+ the 2026-07-28 revision the MCP SDK adds it after the handler returns,
256
+ outside a proof made there. It is
257
+ optional on `KyaOsMiddleware`, as `withPolicyGate` is, so structural
258
+ implementers still type-check; the new `KyaOsOutcomeProver` role requires
259
+ it. New types: `KyaOsOutcomeProofRequest`, `KyaOsAuthorizationOutcome`,
260
+ `KyaOsToolResult`.
261
+ - **`formatChallenge` may return the whole challenge result.** Besides the
262
+ content array, the delegation gate's hook may return `{ content,
263
+ structuredContent?, isError?, _meta? }` (`KyaOsChallengeResult`; the hook
264
+ type is `KyaOsChallengeFormatter`). The gate sets all of it before it
265
+ signs the challenge, so the envelope profile covers `isError` and
266
+ `structuredContent`; the body profile still covers `content` only.
267
+ `_meta` members the middleware owns are dropped from what the hook
268
+ returns. The array form and the default challenge are unchanged; the
269
+ default challenge still carries no `isError`, unlike the policy gate's
270
+ step-up.
271
+ - **`toMcpToolCallback` registers a wrapped handler with `registerTool`
272
+ without a cast.** Passed straight to `McpServer.registerTool`, a
273
+ `KyaOsToolHandler` needed `as never`, and the SDK's request context then
274
+ arrived as the KYA-OS session id, so `wrapWithProof` found no session and
275
+ sent the result unproven. The adapter passes the arguments alone. Types:
276
+ `McpToolCallback`, `McpToolCallbackResult`.
277
+
278
+ ### Changed
279
+
280
+ - **A `formatChallenge` hook that returns neither form gets the default
281
+ challenge**, as a throwing hook does. Before, the value was emitted as
282
+ `content`, which `McpServer` then rejected as an invalid result.
283
+ - **`wrapWithProof` keeps an outcome proof it is handed.** A result
284
+ produced by the outcome-proof path, recognized by identity, is returned
285
+ as it is. Before, a challenge that was not an error result was signed
286
+ again there as an allowed call. No gate composition in this repository
287
+ reached that path.
288
+
289
+ ### Fixed
290
+
291
+ - **An allow result the session fallback cannot attribute says so.** A
292
+ call that threads no session, which is every call on the `withKyaOs`
293
+ transport path, is proved under the single established session. With
294
+ more than one session live the middleware refuses to pick one and the
295
+ result went out unproven, with only a log line; opening a session in the
296
+ middleware's `SessionManager` from application code did this to every
297
+ later allow result. Such a result now carries `_meta.proofError`, the
298
+ marker a failed proof generation already sets. `isError`, the content and
299
+ the audit events are unchanged, and a server with no session at all
300
+ emits what it did before.
301
+ - **Three DID-document readers accept a key published as
302
+ `publicKeyMultibase`.** `ProofVerifier.fetchPublicKeyFromDID`, the Entity
303
+ Card `didKeyedJwks` projection, and the conformance adapter's DID-resolution
304
+ check read only `publicKeyJwk`. A verification method that published its
305
+ Ed25519 key as `publicKeyMultibase`, the material property of
306
+ `Ed25519VerificationKey2020` and the form did:cheqd documents use, failed
307
+ with `PUBLIC_KEY_NOT_FOUND`, projected no key, or failed the check. They now
308
+ read the key through `verificationMethodJwk`, as the credential verifiers
309
+ already do: a `publicKeyJwk` is used unchanged, otherwise the key is taken
310
+ from `publicKeyMultibase` or `publicKeyBase58`, and anything that is not a
311
+ 32-byte Ed25519 key is still refused. The `PUBLIC_KEY_NOT_FOUND` message from
312
+ `fetchPublicKeyFromDID` now names all three properties. The did:key resolver's
313
+ output is unchanged.
314
+ - **Conformance suite 1.2.0: the status-list vectors' list verifies, and a
315
+ list altered after signing must be rejected.** The StatusList2021Credential
316
+ in `status-list.json` carried a proof that did not verify, and the reference
317
+ adapter read its bits without checking it. The suite therefore passed an
318
+ implementation that never checked a list's proof and failed one that did.
319
+ The list is re-signed, the reference adapter verifies its proof on the
320
+ delegation credentials' path before reading a bit, and a new negative
321
+ vector, `status-list/tampered-list`, clears the revoked bit after signing.
322
+ SPEC.md §6.6 and CONFORMANCE.md L3.11 state the check. The other eight
323
+ vector files change only their `version`, the suite has 49 vectors, and the
324
+ library is unchanged.
325
+
326
+ ### Documentation
327
+
328
+ - **The README says what a response proof does not cover.** `_meta` is
329
+ outside every response proof (SPEC.md §7.6), so a server should repeat
330
+ security-relevant hints it carries there, such as an OAuth
331
+ `resource_metadata`, `scope` or `error`, in `content` or
332
+ `structuredContent`. The new section on proving application outcomes says
333
+ so, and says which profile covers which members.
334
+ - **SPEC.md §6.2 states how a base-profile credential is signed.** Its
335
+ example labels the proof `Ed25519Signature2020`, and CONFORMANCE.md L3.1
336
+ asked for "Ed25519Signature2020 or equivalent", but the profile has never
337
+ used the W3C suite of that name. §6.2 now specifies the construction the
338
+ reference implementation and the vectors use: Ed25519 over the JCS
339
+ canonicalization of the credential without `proof`, `proofValue` as
340
+ unpadded base64url, the key found by `proof.verificationMethod`, and the
341
+ proof options outside the signature. L3.1 and L3.5 point to it. A new
342
+ conformance test checks the vectors' delegation credentials against that
343
+ text with `node:crypto` rather than the library's verifier. Moving to a
344
+ standard cryptosuite stays open in #184.
345
+
346
+ ## [1.18.0] - 2026-10-08
347
+
348
+ ### Added
349
+
350
+ - **`maxChainLength` for delegation chains.** `validateDelegationChain` and
351
+ the middleware's `delegation` config take an optional cap on how many
352
+ credentials one chain may hold, checked before any of them is verified. A
353
+ resolver could return a chain of any length, and each credential costs a
354
+ signature check and possibly a DID resolution. It is a local cost limit, not
355
+ a protocol rule about how far authority may be delegated: it is unset by
356
+ default, and SPEC.md §6.4 records that the base profile leaves chain length
357
+ unbounded. The middleware refuses a value that is not a positive integer at
358
+ startup rather than rejecting every call. The cap applies to the chain
359
+ `resolveDelegationChain` returns, not to the resolver's own fetches.
360
+
361
+ ### Changed
362
+
363
+ - **The Entity Card chain limit is opt-in, as on the base profile.**
364
+ `validateDelegationChain` and `evaluateDelegationChain` in
365
+ `@kya-os/mcp/card` rejected any chain of more than `MAX_DELEGATION_DEPTH`
366
+ (10) credentials unless the caller passed a larger `maxDepth`. They now
367
+ accept any length unless the caller sets `maxChainLength`, the option's name
368
+ on both profiles. **A verifier that relied on the default now accepts chains
369
+ of 11 credentials or more; pass `maxChainLength: MAX_DELEGATION_DEPTH` to
370
+ keep the previous behavior.** A longer chain broadens nothing: every hop
371
+ still attenuates its parent, so the cap bounded only the verifier's work.
372
+ SPEC.md §6.10 drops the cap from the attenuation invariants and states it,
373
+ non-normatively, as a local limit, as §6.4 does for the base profile. A
374
+ limit that is not a positive integer rejects every chain; previously `NaN`
375
+ accepted every length.
376
+
377
+ ### Deprecated
378
+
379
+ - **`maxDepth` on the Entity Card `DelegationChainContext`.** Use
380
+ `maxChainLength`, which wins when both are set.
381
+
382
+ ### Fixed
383
+
384
+ - **The `example:*` scripts run from a fresh clone.** They resolved
385
+ `@kya-os/mcp` to `dist/`, which a clone does not have until `npm run build`,
386
+ and the node-server and authz-inspector examples, being packages of their
387
+ own, could not resolve it at all without a per-example install. The scripts
388
+ and `scripts/demo.sh` now run the examples from `src/` through
389
+ `tsconfig.examples.json`, so the root `npm install` is all they need. Two
390
+ scripts are added for the consent-persistence scenarios.
391
+ - **`withKyaOs()` warns when the server is already connected.** It wraps
392
+ transports by patching `connect()`, so a transport connected before it ran
393
+ was never wrapped, and its tool results went out without a KYA-OS proof or
394
+ a transport audit record, silently. It now logs a warning saying so, on MCP
395
+ SDK 1.3 and later. When the server had no tools before connecting, the
396
+ warning also explains the SDK's refusal to register `_kyaos`.
397
+
398
+ ### Documentation
399
+
400
+ - **CONFORMANCE.md L2.17 names the current proof keys.** It told verifiers to
401
+ read `org.kya-os/proof`, a key no server writes since the role-named keys
402
+ of SPEC.md §7.6. It now names `org.kya-os/response-proof`, keeps the prior
403
+ keys as 1.x fallbacks, in that order, and separates the request proof under
404
+ `org.kya-os/request-proof`.
405
+ - **SPEC.md §6.10 says how to tell the two delegation profiles apart.** The
406
+ first `@context` entry selects the rule set.
407
+
10
408
  ## [1.17.0] - 2026-10-05
11
409
 
12
410
  ### Added
package/CONFORMANCE.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Compliance levels for KYA-OS implementations**
4
4
 
5
- Version: 1.1.0
5
+ Version: 1.2.0
6
6
  Status: Stable
7
7
 
8
8
  ---
@@ -161,8 +161,14 @@ intact while swapping the `authorizationUrl`, and only the recompute detects it.
161
161
 
162
162
  `_meta` is the MCP per-request metadata channel and is shared with reserved
163
163
  reverse-DNS keys. A conformant verifier MUST:
164
- - Read the KYA-OS proof from `org.kya-os/proof` and, for backward compatibility,
165
- from the legacy bare `proof` key.
164
+ - Read the detached response proof from `org.kya-os/response-proof`. Through
165
+ 1.x, also accept the prior keys `org.kya-os/proof` and bare `proof` (SPEC
166
+ §7.6), in that order: the first key present wins. Servers write `org.kya-os/response-proof` and, unless configured with
167
+ `emitLegacyProofKey: false`, mirror it under bare `proof`; nothing writes
168
+ `org.kya-os/proof`.
169
+ - Treat the holder-of-key request proof, under `org.kya-os/request-proof`
170
+ (SPEC-ENTITY-CARD §8.1), as a separate profile: never read one in place of
171
+ the other.
166
172
  - Process **only** the KYA-OS proof key; ignore every other `_meta` key (never
167
173
  hash it, never trust it).
168
174
  - **Never reject** a response merely because `_meta` also carries non-KYA-OS
@@ -225,7 +231,7 @@ Implementation MUST:
225
231
  - Include `issuer` as DID string or object with `id`
226
232
  - Include `issuanceDate` in ISO 8601 format
227
233
  - Include `credentialSubject` with delegation details
228
- - Include `proof` with Ed25519Signature2020 or equivalent
234
+ - Include `proof` as SPEC.md §6.2 specifies: an Ed25519 signature over the JCS canonicalization of the credential without `proof`, in `proofValue` as unpadded base64url. This is not the W3C `Ed25519Signature2020` suite, despite the `proof.type` value ([#184](https://github.com/decentralized-identity/kya-os-mcp/issues/184))
229
235
 
230
236
  #### L3.5 — VC Verification
231
237
 
@@ -236,12 +242,13 @@ Implementation MUST validate:
236
242
  - `issuanceDate` is present and in the past
237
243
  - `expirationDate` (if present) is in the future
238
244
  - `credentialSubject.delegation` has required fields
239
- - `proof` is present
245
+ - `proof` is present and verifies as SPEC.md §6.2 specifies
240
246
 
241
247
  #### L3.11 — StatusList2021 Checking
242
248
 
243
249
  Implementation MUST:
244
250
  - Fetch StatusList2021 credential from `credentialStatus.statusListCredential`
251
+ - Verify the list's `proof` as SPEC.md §6.2 specifies, and reject the credential when it does not verify
245
252
  - Decompress and decode the bitstring
246
253
  - Check bit at `statusListIndex`
247
254
  - Return revoked status if bit is 1
@@ -424,7 +431,7 @@ so it is reproducible against any implementation without re-signing.
424
431
  |------|----------|----------|----------|
425
432
  | `vectors/signed-proof.json` | Detached proof verification | valid signature + in-window ts; envelope content binding (profile `org.kya-os/response-proof.envelope`) | tampered signature, tampered meta, wrong key, timestamp skew exceeded; tampered `structuredContent` under envelope coverage, `prf` stripped (downgrade), unknown profile |
426
433
  | `vectors/delegation-chain.json` | Delegation chain verification | single-hop, two-hop attenuated | broken issuer↔subject linkage, scope widening, tampered signature, audience mismatch |
427
- | `vectors/status-list.json` | StatusList2021 revocation | active (bit unset) | revoked (bit set) |
434
+ | `vectors/status-list.json` | StatusList2021 revocation | active (bit unset) | revoked (bit set), list altered after signing |
428
435
  | `vectors/did-key-resolution.json` | did:key resolution | valid Ed25519 | malformed multibase, wrong method |
429
436
  | `vectors/did-web-resolution.json` | did:web resolution | well-formed id-matched document | document id mismatch, not found |
430
437
  | `vectors/card-proof.json` | `org.kya-os/proof.v1` holder-of-key proof | valid signed proof, in-window, audience-bound | tampered body, tampered signature, wrong audience, expired, kid⇄did forgery |
@@ -452,6 +459,11 @@ This vector leaves the JWS byte-identical to `valid-basic` and alters only the m
452
459
  A conformant verifier MUST reconcile `meta` against the decoded JWS payload and reject on any mismatch.
453
460
  Signature validity alone does not pass this vector: it is the `requestHash` counterpart of the `responseHash` recompute rule in L2.11.
454
461
 
462
+ **On `status-list/tampered-list` (suite ≥ 1.2.0).**
463
+ This vector carries the revoked credential and its signed list with the revoked bit cleared and the list's proof left as signed.
464
+ A verifier that reads the bits without verifying the list's proof reports the credential active and passes a vector it must reject (L3.11).
465
+ Suites before 1.2.0 shipped a list whose proof did not verify, so an implementation that checked it failed `status-list/active-credential`.
466
+
455
467
  ### Running the reference implementation
456
468
 
457
469
  ```bash
package/README.md CHANGED
@@ -54,6 +54,24 @@ npm install @kya-os/mcp
54
54
 
55
55
  ---
56
56
 
57
+ ## Upgrading to 1.20: proofs need a KYA-OS transport
58
+
59
+ > **Breaking for servers without a KYA-OS transport.** From 1.20, every response proof (under the default body profile as under the envelope profile) is made where the KYA-OS transport sends the result, after the MCP SDK has finished it. While no KYA-OS transport is open for the middleware, `wrapWithProof`, `wrapWithDelegation`, `withPolicyGate` and `proveOutcome` throw before they run anything.
60
+
61
+ | Your setup | What to do |
62
+ |---|---|
63
+ | `withKyaOs(server, …)` before `server.connect()` | Nothing. |
64
+ | Low-level `Server` with `createKyaOsMiddleware` | `await server.connect(createKyaOsTransport(transport, kyaos))`. It proves every tool's result, unwrapped tools included (unproven on 1.19.0); its third argument adds tools to leave unproven. |
65
+ | A script or test that calls a wrapped handler directly | `const local = serveInProcess(kyaos); const call = local.tool('name', handler); await call(args); await local.close();` |
66
+ | `withKyaOs` with `proofAllTools: false` | Leave it on; list tools to leave unproven in `excludeTools`. |
67
+ | `withKyaOs` after `connect()` | Call it before `connect()`. |
68
+ | Reading the proof inside the handler | The transport adds it when the result is sent; read it from what the client receives. |
69
+ | MCP task results fetched later | A result is proved the first time the same middleware sends it, within `pendingProofTtlMs` (default five minutes, at most 2147483647 ms); raise it for later fetches. |
70
+
71
+ See the [changelog](./CHANGELOG.md) for the details.
72
+
73
+ ---
74
+
57
75
  ## Migrate any MCP server in 2 lines
58
76
 
59
77
  **Before**, a standard MCP server with no identity or proofs:
@@ -106,6 +124,45 @@ When an agent calls `checkout` without a delegation credential, it gets back a `
106
124
 
107
125
  > Try it yourself: [examples/consent-basic](./examples/consent-basic/) walks through the full consent flow end-to-end.
108
126
 
127
+ ### Prove the outcomes your own code decides
128
+
129
+ A server that enforces authorization itself, checking OAuth scopes in the tool handler for example, returns its own challenges and denials as tool results. `withKyaOs` proves the results it can attribute, but it sends an error result its own gates did not produce as the handler returned it, without a proof. `proveOutcome` gives that result the proof the gates attach to theirs, with the same claims, `_meta` keys and audit events:
130
+
131
+ ```typescript
132
+ import { toMcpToolCallback } from '@kya-os/mcp';
133
+
134
+ server.registerTool('get_report', { inputSchema }, toMcpToolCallback(async (args) => {
135
+ if (!grantedScopes.includes('reports:read')) {
136
+ return kyaos.proveOutcome!({
137
+ toolName: 'get_report',
138
+ args,
139
+ outcome: 'needs_authorization', // or 'step_up_required', 'denied'
140
+ reason: 'insufficient_scope',
141
+ result: {
142
+ content: [{ type: 'text', text: 'Sign in to see this report.' }],
143
+ structuredContent: { error: 'insufficient_scope', scope: 'reports:read', resource_metadata },
144
+ isError: true,
145
+ _meta: { 'mcp/www_authenticate': [`Bearer resource_metadata="${resource_metadata}"`] },
146
+ },
147
+ });
148
+ }
149
+ return { content: [{ type: 'text', text: report }] };
150
+ }));
151
+ ```
152
+
153
+ - **Build the whole result before proving it**, and return what `proveOutcome` gives you from the handler.
154
+ - **Every proof is made where the result is sent.** The `withKyaOs` transport makes it when it sends the result, after the MCP SDK has finished the result, so it covers what the client receives. Under the default body profile it covers `content`, including a text item SDK 2 appends when `structuredContent` is not an object, and the `content: []` either SDK fills in when a handler leaves it out. Under the envelope response-proof profile (`responseProofProfile: RESPONSE_PROOF_PROFILE_ENVELOPE`) it covers every member except `_meta`, including the `resultType` that SDK 2 adds on the 2026-07-28 revision. This applies to `proveOutcome`, `wrapWithProof` and both gates.
155
+ - **Every wrapper needs an open KYA-OS transport.** `withKyaOs` wraps the transport you connect, unless `proofAllTools` is `false`; for a transport you connect yourself (the low-level `Server` API with `createKyaOsMiddleware`), wrap it with `createKyaOsTransport`, exported from `@kya-os/mcp`. Once a KYA-OS transport has been created for a middleware, every wrapper result must go out through one. While none is open (none was started, or all have closed), the wrappers and `proveOutcome` throw before they run anything, instead of sending a result they cannot prove.
156
+ - **`_meta` is outside every response proof** (SPEC §7.6). Repeat anything a client must be able to trust, such as `resource_metadata`, `scope` and `error`, in `content` or `structuredContent`.
157
+ - A `needs_authorization` proof binds the result; a denial or step-up proof binds none (SPEC §7.4).
158
+ - The result carries private members until the transport sends it, and the transport removes them. One of them is a one-time id: what the proof claims (by request hash) and its session stay in the middleware, and the transport redeems the id once. A result the SDK never sends, such as a cancelled call's, is closed in the audit trail with `proof.rejected` (`PROOF_NOT_SENT`) and, for an allowed call, `tool.call.failed` (`RESULT_NOT_SENT`).
159
+ - **A result is proved once, by the middleware that sends it, within `pendingProofTtlMs`** (default five minutes, at most 2147483647 ms). A task result fetched again, or later than the window, goes out with `proofError`, recorded as `PROOF_EXPIRED`. What the proof claims lives in that middleware's memory, so only the process that recorded the call can make the proof; [#250](https://github.com/decentralized-identity/kya-os-mcp/issues/250) proposes sealed claims so that any process holding the identity could.
160
+ - **Do not serialize a wrapped result into a string.** Its private members cannot be removed from inside the string, so the result that carries it goes out unproven (`PRIVATE_MEMBER_IN_RESULT`), and the serialized result's own proof is voided. Nest the result as an object instead: private members are removed at any depth from every outgoing message (results, notification and request `params`, and error `data`), and a wrapped result that goes out anywhere but as its own answer has its proof closed. On the answer to a `tools/call`, only a proof left for that tool is made.
161
+ - **Connect every server that shares a middleware through a KYA-OS transport.** The refusal checks only that some KYA-OS transport is open for the middleware. A second server sharing it over a plain transport therefore runs its wrappers, and its results leave with the lifecycle stamp and the pending-proof id in `_meta` and no proof. Those proofs are later closed as `PROOF_NOT_SENT`, although the results were sent.
162
+ - Leave `kyaos.sessionManager` alone. A second live session makes the fallback session ambiguous, and allow results then go out unproven, marked with `_meta.proofError`.
163
+ - `wrapWithDelegation`'s `formatChallenge` can return the same `{ content, structuredContent, isError, _meta }` shape for the library's own challenge, and all of it is set before signing.
164
+ - `toMcpToolCallback` adapts any wrapped handler to `registerTool` without a cast. The SDK's request context never reaches the handler as its session id.
165
+
109
166
  ---
110
167
 
111
168
  ## Turn proofs into a verifiable audit trail
@@ -217,6 +274,8 @@ This starts all example servers and opens [MCP Inspector](https://github.com/mod
217
274
 
218
275
  Also available: [outbound-delegation](./examples/outbound-delegation/) (gateway pattern), [verify-proof](./examples/verify-proof/) (standalone verification), [statuslist](./examples/statuslist/) (revocation lifecycle), [cheqd-dlr](./examples/cheqd-dlr/) (operator DID linkage + DLR publishing).
219
276
 
277
+ To run one example on its own, use its `npm run example:*` script, for example `npm run example:server`. The scripts and `scripts/demo.sh` run the examples from `src/` through `tsconfig.examples.json`, so they need no build step. The scripts also need nothing beyond the root `npm install`; `demo.sh` additionally installs each example's own dependencies.
278
+
220
279
  ## Try it against the live server
221
280
 
222
281
  [![Live deployment probe](https://github.com/decentralized-identity/kya-os-mcp/actions/workflows/live-probe.yml/badge.svg)](https://github.com/decentralized-identity/kya-os-mcp/actions/workflows/live-probe.yml)