mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
`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
124 lines
5.9 KiB
Markdown
124 lines
5.9 KiB
Markdown
# :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](https://cordn.net) 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`](https://github.com/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`](https://github.com/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
|
|
|
|
```bash
|
|
./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.
|