MCPL 0.5 + RFC-003: channels capability object, capability paths, ManifestTracker - #6
Conversation
…acker Implements MCPL 0.5.0-draft and RFC-003 (SPEC §17). Closes the two issues together because #5's digest covers the manifest #4 reshapes. #4 — capability advertisement - `channels` is an OBJECT (§14.1): register / lifecycle / publish / incoming / streaming / acknowledge / typing. `channels.streaming` was previously UNDECLARABLE because the member was typed `boolean`. - `CAPABILITY_PATHS` is the closed §6.2 / App. B.2 vocabulary and `uses` is typed against it. `validateUses` fails closed: absent, empty, or containing an unrecognised value is `invalid_uses` (§6.4, §6.6). - Advertisement is walked RECURSIVELY (§5.1) from a tree derived from the path vocabulary, not a hardcoded key set — §5.4 calls hardcoded nestable keys non-conforming. `true` at any level is shorthand for every leaf beneath it. - `effectiveCapabilities` is the sole normative allowlist; `capabilityGranted` returns false for an empty/absent grant and never consults a deny-list. `conflictingCapabilityEntries` implements §5.4's fail-closed rejection. - `context/afterInference`, `modifiedResponse` and the blocking hook form are gone; `inference/lifecycle` (§10.5) replaces them — metadata only, and documented as BEST-EFFORT: dedupe terminals by `inferenceId`, keep a timeout. - §14.4 channel context moved onto `context/beforeInference`, where the spec puts it. - `FeatureSetDeclaration`: `description` required, `hostState` and `scoped` dropped. `featureSets` is an object keyed by name (§6.1). - `featureSets/update` typed dual-mode with the §6.7 degradation receipt. #5 — ManifestTracker (RFC-003 / §17) - `manifestDigest` implements §17.2 exactly: JCS (RFC 8785) over the manifest with the ROOT `revision` removed, set-semantic arrays deduped and sorted by UTF-8 byte order, SHA-256, unpadded base64url. - `ManifestTracker.setManifest(next)` canonicalizes, digests, diffs to derive changed domains, installs atomically, emits per connection and answers `mcpl/manifest` from the same snapshot. `transaction()` coalesces N edits into one announcement. `attach({announcedRevision})` seeds from the handshake so a fresh connection does not fire a redundant announcement after initialize. - `mcpl/manifestChanged` carries revision + domains and NO payload; `mcpl/manifest` returns the COMPLETE flat `experimental.mcpl` object, never a delta. - The digest REFUSES on an identifier outside `[A-Za-z0-9._:*-]` (`ManifestDigestError`), rather than emitting a revision two libraries could disagree about. Distinct from §6.4 `invalid_uses`, which still yields one. - No resident-facing prose or policy conclusions are generated server-side (RFC-003 §12). Interop: the advertisement is FLAT — no `capabilities` wrapper object. An earlier RFC-003 draft invented one; SPEC §5.1 and §17.1 are authoritative. Tests: `test/vectors/manifest-digest-vectors.json` is a VERBATIM copy of the frozen interop vectors from anima-research/mcpl `conformance/manifest-digest-vectors.json` (branch `mcpl-0.5/rfc-003-conformance-vectors`, commit 8d9c0bd). All 20 manifest vectors (15 positive, 5 negative) and all 4 set-comparator vectors pass, with canonical string, SHA-256 hex and digest asserted separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Interop differ results — changes required before merge. A differential harness ran both this branch and Anarchid/mcpl-core#3 through 46 manifests, 16 advertisement objects, 13 grant patterns, and 8 diff pairs. 41/46, 16/16, 8/13, 6/8 agree. The divergences and the adjudicated direction: 1. Trailing- 2. Set-declared field of wrong JSON type — this library's behaviour becomes normative. Hashing a non-array 3. Missing API — 4. Confirmed clean, for the record: capability path strings character-identical with Rust; four-phase lifecycle vocab identical, both sides explicitly best-effort with dedupe+timeout; channels object identical 7 leaves; receipt types here are correct ( |
Four changes pinned by the 2026-08-02 SPEC amendment (mcpl e869744) and the review on PR #6: 1. §5.4 wildcard: `*` matches exactly one segment and segment counts must be equal — a trailing `*` is no longer a subtree match. `contextHooks.*` now grants none of the depth-4 injection leaves; tests assert exactly that. 2. featureSets/update → grant: new `grantFromUpdate(previous, params, form)` owns the message-to-grant step. Request form: absent `effectiveCapabilities` is a grant of NOTHING (absence is denial, never "no change"); `enabled` is an allowlist only when present; `disabled` always subtracts; conflicts fail closed. Notification form: never alters the grant except `disabled` reductions, never establishes ready; other grant fields are discarded with a diagnostic. Plus `CapabilityGrantState`, `emptyGrantState`, `featureSetSelected`. 3. sendError gains an optional `data` param so servers can emit §6.6's documented `data:{capability}` / `data:{featureSet}` shapes; `RpcError` now carries `error.data` through to the requesting side. 4. Re-vendor conformance vectors from mcpl main a77be49 (23 vectors + 4 comparators, byte-identical), adding the three adjudication pins: wrong-typed set fields and array-form featureSets hash VERBATIM (already this library's behavior — now covered), and empty-vs-absent members digest differently. Added a changedDomains test pinning that featureSets appearing announces featureSets + tagOntology (§17.3). typecheck clean; 98/98 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
{"body": "REVISE \u2014 four material interop/security findings at exact head |
1. `tools` is never minted from the experimental manifest (SPEC §5.1): the advertisement tree is built without PATHS_NOT_ADVERTISED_IN_MCPL, and `advertisedCapabilitiesFromInitialize` adds `tools` only from the outer standard MCP `capabilities.tools`. Runtime proof reversed: outer absent, nested tools → []. 2. Grant fields carry `CapabilityPattern` (plain string), so legal §5.4 wildcards like `channels.*` typecheck in `FeatureSetsUpdateParams. effectiveCapabilities`/`deniedCapabilities`, while `FeatureSetDeclaration.uses` stays closed to exact `CapabilityPath`s. 3. Digest totality for malformed set arrays (adjudicated 2026-08-03): a set-declared array containing ANY non-string member is hashed VERBATIM — no sort, no dedupe, no identifier check. `set_member_not_string` is removed as a digest failure. Vector file re-vendored byte-for-byte from mcpl main 2cdc7fb (25 vectors, incl. non-string-set-member-hashed-verbatim / -single); all pass. 4. Domain diff tracks presence separately from value (§17.3): absent vs explicit `null` `featureSets` is a change — `changedDomains` on the review probe now returns ['featureSets','tagOntology'] both ways. `null` is no longer the internal absence sentinel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
ACCEPT at exact head Independent re-review:
No remaining blocker found. |
Implements MCPL 0.5.0-draft and RFC-003 in one branch. Closes #4 and #5 —
they are done together because #5's canonical digest covers the manifest #4
reshapes, and #5 says so ("Depends on the
channelscapability object from #4").Branched off
origin/main.package-lock.jsonhad pre-existing localmodifications that are not mine; it is untouched and uncommitted.
#4 — capability advertisement
channelsis an objectChannelsCap=register / lifecycle / publish / incoming / streaming / acknowledge / typing(SPEC §14.1).channels.streamingis now declarable — it was unreachable while the member was typedboolean.usesis the capability-path vocabularyCAPABILITY_PATHSis the closed §6.2 / App. B.2 set of 17 paths;FeatureSetDeclaration.usesisCapabilityPath[].validateUsesfails closed — absent, empty, or containing an unrecognised value isinvalid_uses(§6.4, §6.6).context/afterInferenceremovedmodifiedResponseand the blocking form.InferenceLifecycleParams(§10.5) is metadata-only:inferenceId,conversationId,turnIndex,phase, optionalmodel/usage. No content fields exist.FeatureSetDeclarationdescriptionrequired;hostStatedropped (removed in 0.4.1);scopeddropped (§7 removed).featureSetsis an object keyed by name (§6.1) — this is a breaking type change and it broketest/connection.test.ts, which is updated in the same commit.channelscontext fieldContextBeforeInferenceParams, where the spec puts it.FeatureSetsUpdateParamsgainseffectiveCapabilities/deniedCapabilities; the result is the §6.7 degradation receipt (accepted/mode/unavailableFeatures[], oraccepted: falsewithfallback: "mcp-only" | "close").state/*andbranches/*are carried unchanged and commented as out of scope for 0.5.0, pending the separate decision the issue asks for.channels/acknowledgeandchannels/typingare promoted into §14 and now have capability paths.Recursive advertisement (§5.1). The walk is generic over a tree derived
from
CAPABILITY_PATHS, not a hardcoded key set — §5.4 calls a hardcoded set ofnestable keys non-conforming "since the vocabulary is depth 3 and will grow".
trueat any level is shorthand for every leaf beneath it;falseand absenceadvertise nothing. An unrecognised member cannot mint a capability.
Absence is denial.
capabilityGrantedreturnsfalsefor an empty,undefinedornullgrant and never consults a deny-list.conflictingCapabilityEntriesimplements §5.4's requirement that a path in botheffectiveCapabilitiesanddeniedCapabilitiesmakes the message malformed.inference/lifecycleis documented as best-effort, per §10.5 as amended:consumers MUST dedupe terminals on
inferenceIdand MUST keep a safety timeout.No replay, outbox, acknowledgement or event identity is implied anywhere.
#5 — ManifestTracker (RFC-003 / SPEC §17)
src/manifest.ts:manifestDigest— §17.2 exactly:"sha256:" + base64url_unpadded(SHA-256(JCS(manifest_without_revision))).revisionis stripped from the root object only;version,falseandnullmembers are content. Set-valued arrays (featureSets.*.uses,tagOntology.coreTags,tags.*.implies) are deduped and sorted by UTF-8byte order — not
Array.prototype.sort(), which is UTF-16 and diverges aboveU+FFFF.
keyed.*.valuesand every other array keep their order.changedDomains—capabilities(every member butversion,revision,featureSets),featureSets(excludingtagOntologywithin it),tagOntology.ManifestTracker—setManifest(next)canonicalizes, digests, diffs,installs atomically, and emits per connection.
transaction()coalesces Nrelated edits into one announcement.
attach(conn, { announcedRevision })seeds from the
initializehandshake so a fresh connection does not fire aredundant announcement immediately after initialize already carried the
manifest.
handleManifestRequest()answers from the same snapshot.mcpl/manifestChangedcarriesrevision+domainsand nothing else —no diff, no added/removed list, no policy conclusion.
mcpl/manifestreturns the complete current
experimental.mcplobject, never a delta.conclusions (RFC-003 §12).
ChangeImpact/ChangeDispositionare exportedfor hosts and are documented as host-derived.
Interop note: the advertisement is FLAT
There is no
capabilitieswrapper object anywhere. SPEC §5.1 and §17.1 areexplicit ("capability members at the top level, with
featureSetsas one memberamong them. There is no nested
capabilitieswrapper"). An earlier RFC-003 draftinvented one; it was wrong. A test asserts
mcpl/manifest's result has nocapabilitiesmember.Conformance vectors
test/vectors/manifest-digest-vectors.jsonis a verbatim copy ofanima-research/mcpl→conformance/manifest-digest-vectors.json, branchmcpl-0.5/rfc-003-conformance-vectors, commit8d9c0bd(sha256
1337968815fc3e817a8b92cb656525d16d1902a20bb43bc496b3989adbcf71fd).Not edited here — per
conformance/CONSUMING.md§8, a vector believed wrong isan issue against that repo, not a local fix.
test/manifest.test.tsiterates the file rather than transcribing it, andasserts
canonicalJson,sha256Hexanddigestseparately so a failurelocalizes to canonicalization vs. encoding vs. base64url, plus
sameDigestAs/differentDigestFromand the foursortVectors.All 20 manifest vectors (15 positive, 5 negative) and all 4 comparator vectors
pass. That includes
unicode-and-json-escapes,number-canonicalisation,set-sort-is-utf8-byte-order,nested-featuresets-and-list-arrays,dot-in-capability-member-name, both boolean-shorthand vectors, bothnull/false vectors, and
utf8-vs-utf16-divergence-above-bmp.Point at a live checkout with
MCPL_DIGEST_VECTORS=/path/to/mcpl/conformance/manifest-digest-vectors.json npm test.channels.streamingis now declarable#4 item 1 warns that making
channels.streamingdeclarable activates aproduction double-post in
discord-mcpl, whoseoutgoing/completehandlercalls
sendMessage()directly. That fix is in another repository and is notin this PR. Nothing in this library can send it — a host still has to grant
channels.streaming— but fixanima-research/discord-mcplbefore grantingit to a Discord server. SPEC §14.5 is quoted in the
ChannelsOutgoingCompletedoc comment: delivery is never a side effect of a lifecycle event, and a
server-side send triggered by it is a conformance defect.
What I did NOT do, and why
discord-mcpl'soutgoing/completedouble-post. Different repository,explicitly out of scope for this issue. Flagged above.
state/*andbranches/*. MCPL 0.5: channels capability object, capability paths, inference/lifecycle #4 item 7 asks for a separatedecision, not a removal. They are carried unchanged with comments marking
them out of scope.
hostStateandscoped, which the issue does ask todrop, are dropped.
channels/typingparam shape. §14 promotes the method andits capability but specifies no params beyond the channel it targets, so
ChannelsTypingParamshaschannelIdonly. Guessing astate/durationfield would have been fabrication.
library. Validation ordering, the change receipt, and grant recomputation are
host work;
ChangeImpact/ChangeDispositionare exported so a host cantype against them.
0.2.2.
featureSetsgoing from array to object is breaking and wants adeliberate major/minor decision.
package-lock.json. Pre-existing local modification, not mine.Open questions (answered fail-closed)
[A-Za-z0-9._:*-], and what happens on violation?The spec states the MUST and names no actor. Chosen: the digest function
refuses (
ManifestDigestError, codeidentifier_charset), matching thefrozen vectors'
errorCodes. This is stricter than §6.4'sinvalid_uses,which degrades one feature set and still yields a revision — the two failures
are deliberately distinct and there is a test for that.
a chain of object member names, so the recursive validator descends plain
objects at every depth (catching
contextHooks.beforeInference.inject.before user) but does not descendinto arrays under a capability member, where "path segment" has no meaning.
No vector covers it.
set_member_not_string/manifest_not_object. Named in the vectors'errorCodesbut exercised by no vector. Implemented as errors, fail-closed.inferenceRequest: true. Read as §5.1 shorthand for every leaf, so itadvertises
inferenceRequest.streamingtoo. A server that does not streamshould say
{ streaming: false }.(recursive-vs-root
revisionstripping, shorthand expansion, set-nessscoping,
tagsAny/tagsAll/tagsNoneas lists, …). This implementationfollows the frozen vectors on every one of them, so it and
Anarchid/mcpl-coremove together if the RFC later answers differently.I found no vector I disagree with. The one process note: the vectors were
read from
anima-research/mcplbranchmcpl-0.5/rfc-003-conformance-vectorsbecause
conformance/does not exist onmainyet. Merging that branch wouldmake the path in #5 resolve.
Closes #4
Closes #5