Files
amethyst/contextvm/README.md
T
Claude 4b49198190 refactor(contextvm): one package per CEP
`quartz` puts a NIP's implementation under `nipXX`, `marmot` puts a MIP's
under `mipXX`, and both keep named packages only for what no spec number
covers. ContextVM had neither: CEP-4 sat in `crypto/`, CEP-6/17/23/24/35 were
four unrelated specs sharing one `ServerDiscovery.kt`, CEP-8 in `payment/`.
"Which file implements CEP-17" was a grep rather than a path. Now it is
`cep17RelayList/ServerRelay`.

Four placements are judgment calls, recorded in the README so they can be
argued with:

- CEP-19 stays inside `cep04Encryption/CvmGiftWrap`. Picking the wrap kind
  (21059, falling back to 1059) and building the wrap are one negotiation in
  one class; its own package would separate a method from its only caller.
- CEP-16 gets no package. It is a `_meta` key name plus the server-side
  obligation to inject it -- a constant and fixture behaviour, not a subsystem.
- `transfer/ProgressEnvelope` stays cross-cutting: CEP-22 and CEP-41 share that
  framing, so it is the `marmot/foundation/` analogue.
- `core/`, `jsonrpc/`, `transport/` and `mcp/` keep names because the core
  draft spec is not a CEP and has no number to carry.

Pure moves and package/import rewrites; no behaviour changed. Also drops a
redundant cast the compiler flagged in `CvmMcpClient`. Suite unchanged at 172
on jvm and 143 on the Android target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n
2026-09-18 14:54:00 +00:00

5.9 KiB

:contextvm

A Kotlin Multiplatform client for ContextVM — the Model Context Protocol (MCP) carried over Nostr.

This is a general MCP-over-Nostr implementation, not a client for any one server. It exists because Amethyst needs to talk to cordn coordinators (see quartz/plans/2026-09-17-cordn-interop.md), but nothing in it is cordn-specific.

Implemented specification revision

Built from the specification documents in ContextVM/contextvm-docs at commit e63bce6, read on 2026-09-17.

Most of these are Draft, including both transfer profiles, so the text can still move. When bumping the revision, diff against the CVM-* rule ids in the plan's §6.5 catalog — the test names carry them, so a spec change shows up as named failures rather than as silent divergence.

Spec Status Implemented in
Core draft spec Draft core/, jsonrpc/, transport/, mcp/
CEP-4 Encryption Final cep04Encryption/CvmGiftWrap
CEP-6 Public Announcements Final cep06Announcements/
CEP-8 Pricing and Payment Draft cep08Payments/
CEP-15 Common Tool Schemas Draft cep15CommonSchemas/CommonToolSchema
CEP-16 Client Pubkey Injection Final mcp/McpMethods (the _meta key), fixture/ (server role)
CEP-17 Relay List Metadata Draft cep17RelayList/ServerRelay
CEP-19 Ephemeral Gift Wraps Draft cep04Encryption/CvmGiftWrap
CEP-21 PMI Recommendations Draft cep08Payments/PaymentSession
CEP-22 Oversized Transfer Draft cep22OversizedTransfer/
CEP-23 Server Profile Metadata Draft cep06Announcements/DiscoverySurface (kind 0 via quartz)
CEP-24 Server Reviews Draft cep24Reviews/ServerReview
CEP-35 Stateless Discovery Draft cep35Discovery/SessionDiscovery
CEP-41 Open Streams Draft cep41OpenStreams/

Why the packages are named this way

Each CEP gets its own cepNNName/ package, the way quartz uses nipXX and marmot uses mipXX: a spec number in the path is what makes "is this rule implemented, and where" answerable without grep. Four placements are judgment calls rather than mechanics:

  • CEP-19 lives in cep04Encryption/. Choosing the wrap kind (21059 with the 1059 fallback) and building the wrap are one negotiation inside CvmGiftWrap; a separate package would split a single method from its caller.
  • CEP-16 has no package. It is a _meta key name plus the server-side obligation to inject it — a key constant and fixture behaviour, not a subsystem.
  • transfer/ProgressEnvelope stays cross-cutting because CEP-22 and CEP-41 share that framing; it is the marmot/foundation/ analogue.
  • core/, jsonrpc/, transport/, mcp/ keep names because the core draft spec is not a CEP and has no number to carry.

RFC 8785 (JCS), required by CEP-8 and CEP-15, lives in quartz/…/utils/jcs/JsonCanonicalization.kt — it is a generic primitive, not a ContextVM concern.

Licensing

Implemented clean-room from the specification. The reference ContextVM/sdk is LGPL-3.0; Amethyst ships under MIT, so translating that source would carry copyleft terms into Quartz. Do not read it while working here — the plan's §6 was written so you do not have to.

The spec repository carries no LICENSE file. Implementing a published protocol is fine; do not paste spec prose into this repo.

Three things that are easy to get wrong

  1. Kind 25910 is ephemeral, so relays do not store it. A subscription opened after the peer published has missed the response permanently. This is why CvmTransport exposes request() and no public publish/subscribe pair: the ordering is the transport's job, not the caller's. InMemoryRelayPool drops an event nobody is listening for, so the property is tested rather than assumed.

  2. CEP-41 has two independent ordering fields. progress orders every frame, control frames included, and is explicitly not a chunk counter; chunkIndex (contiguous from 0) is what validates completeness. Progress sequences are also per-sender, so a pong's progress bears no relation to the ping it answers — they match by nonce alone.

  3. close does not complete the request. After a CEP-41 stream closes, the originating JSON-RPC request still needs its own response, and a client must never synthesize success from close. ToolCallResult carries streamed fragments and the result separately for exactly this reason.

Testing

./gradlew :contextvm:jvmTest              # 172 tests
./gradlew :contextvm:testAndroidHostTest  # 143 tests

The Android run is smaller because the tests needing real secp256k1 and NIP-44 — the gift wrap round trip, the transport and the MCP client — live in src/jvmTest where the JVM JNI artifact is on the classpath. Everything protocol-level is in commonTest and runs on both.

The suite is Tier A and Tier C from the plan's §6.4: rule-derived unit tests, plus adversarial tests against fixture/CvmFixtureServer, which plays the server role and can be told to violate any rule on demand (FixtureFaults). Most tests are negative, because the CEPs are written as failure conditions.

Still open:

  • Tier B — live integration against ghcr.io/cordn-msg/cordn:latest.
  • Tier D — cross-implementation vector exchange for CEP-4 wraps and CEP-22/41 framing. Worth offering upstream; no official vectors exist.
  • Tier E — a real Lightning wallet behind CEP-8 (NIP-47 NWC is already in Quartz).

Not in scope

  • A production server. fixture/ plays the server role for tests only and is deliberately not hardened; for a real coordinator, cordn-rs exists.
  • Full MCP. Lifecycle, tool listing and tool calling are implemented because that is what the CEPs define; sampling, roots and elicitation are not.