Architecture & Design
--- title: "Architecture & Design" description: "ContextVM Rust SDK documentation for Architecture & Design" --- **Date:** 2026-03-11 **Status:** Implementation Complete (Phase 1-5) **Reference:** [ContextVM TS SDK](https://github.com/ContextVM/sdk) · [ContextVM Draft Spec](https://contextvm.org) · [Existing Rust crate](https://github.com/k0sti/clarity/tree/main/crates/cvm) ## Verification Summary (2026-03-11) | Check | Result | | ------------------------ | ----------------------------------- | | `cargo check` | ✅ Clean (2 unused import warnings) | | `cargo test` | ✅ 8 unit + 3 doc tests pass | | `cargo build --examples` | ✅ All 3 examples compile | | Source LOC | 1,914 across 17 files | | Tasks complete | 22/22 | ### Remaining Polish - Fix 2 unused import warnings (`Error` in base.rs, `Instant` in server.rs) - Add more unit tests for transport/gateway/proxy (currently only core + encryption tested) - Integration tests with live relay (requires test relay setup) - `cargo doc` warnings check + doc completeness audit ## Goal A complete Rust SDK for the ContextVM protocol that matches the TypeScript SDK feature-for-feature, enabling any Rust MCP server or client to communicate over Nostr. --- ## Architecture ``` rust-contextvm-sdk/ ├── src/ │ ├── lib.rs # Re-exports, crate root │ ├── core/ │ │ ├── mod.rs │ │ ├── constants.rs # Event kinds, tags, limits │ │ ├── types.rs # EncryptionMode, ServerInfo, ClientSession │ │ ├── error.rs # Error types │ │ ├── serializers.rs # mcpToNostrEvent, nostrEventToMcpMessage, getTag │ │ └── validation.rs # validateMessage, validateMessageSize │ ├── transport/ │ │ ├── mod.rs │ │ ├── base.rs # BaseNostrTransport (shared logic) │ │ ├── client.rs # NostrClientTransport │ │ └── server.rs # NostrServerTransport │ ├── gateway/ │ │ ├── mod.rs # NostrMCPGateway │ ├── proxy/ │ │ ├── mod.rs # NostrMCPProxy │ ├── relay/ │ │ ├── mod.rs # RelayPool │ ├── signer/ │ │ ├── mod.rs # Signer trait + KeysSigner │ └── encryption/ │ ├── mod.rs # NIP-44 encrypt/decrypt, NIP-59 gift wrap ├── examples/ │ ├── gateway.rs # Example: expose a local MCP server via Nostr │ ├── proxy.rs # Example: connect to remote MCP server via Nostr │ └── discovery.rs # Example: discover servers on relay ├── tests/ │ ├── transport_test.rs │ ├── gateway_test.rs │ └── encryption_test.rs ├── Cargo.toml ├── README.md ├── DESIGN.md └── LICENSE ``` ## Module Mapping: TS SDK → Rust SDK | TS SDK Module | TS LOC | Rust Module | Source | Status | | ------------------------------------- | ------ | ------------------------------- | ----------------------- | ------- | | `core/constants.ts` | 87 | `core/constants.rs` (64 LOC) | Port from existing Rust | ✅ done | | `core/interfaces.ts` | 61 | `core/types.rs` (188 LOC) | Port from existing Rust | ✅ done | | `core/encryption.ts` | 64 | `encryption/mod.rs` (86 LOC) | Port from existing Rust | ✅ done | | `core/utils/serializers.ts` | ~60 | `core/serializers.rs` (74 LOC) | New | ✅ done | | `core/utils/utils.ts` | ~30 | `core/validation.rs` (64 LOC) | New | ✅ done | | `relay/simple-relay-pool.ts` | ~100 | `relay/mod.rs` (108 LOC) | Port from existing Rust | ✅ done | | `signer/private-key-signer.ts` | ~50 | `signer/mod.rs` (15 LOC) | Port from existing Rust | ✅ done | | `transport/base-nostr-transport.ts` | 355 | `transport/base.rs` (139 LOC) | New | ✅ done | | `transport/nostr-client-transport.ts` | 411 | `transport/client.rs` (228 LOC) | Rewrite from existing | ✅ done | | `transport/nostr-server-transport.ts` | 944 | `transport/server.rs` (583 LOC) | Rewrite from existing | ✅ done | | `gateway/index.ts` | 151 | `gateway/mod.rs` (82 LOC) | New | ✅ done | | `proxy/index.ts` | 96 | `proxy/mod.rs` (71 LOC) | New | ✅ done | | _(n/a — new)_ | — | `discovery/mod.rs` (154 LOC) | New | ✅ done | | _(n/a — new)_ | — | `core/error.rs` (40 LOC) | New | ✅ done | **TS SDK total:** ~2,169 LOC (non-test) **Existing Rust (reference):** ~782 LOC **Actual Rust SDK:** 1,914 LOC (+ tests + examples) --- ## Dependencies ```toml [dependencies] tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" nostr-sdk = { version = "0.43", features = ["nip59"] } thiserror = "2" async-trait = "0.1" tracing = "0.1" [dev-dependencies] tokio-test = "0.4" tracing-subscriber = "0.3" ``` **Note on MCP types:** The TS SDK imports from `@modelcontextprotocol/sdk`. There's no official Rust MCP SDK yet. We define our own JSON-RPC message types that are protocol-compatible. If/when an official Rust MCP SDK exists, we can add it as an optional integration. --- ## Tasks ### Phase 1: Core Foundation _Port and enhance existing code. Establish project structure._ - **1.1** Initialize Cargo project with workspace structure and dependencies - **Verify:** `cargo check` passes with empty lib.rs - **1.2** Port `core/constants.rs` from existing crate - All event kinds (25910, 1059, 11316-11320) - All tag constants (p, e, cap, name, website, picture, about, support_encryption) - MAX_MESSAGE_SIZE - **Verify:** Constants match TS SDK `core/constants.ts` values exactly - **1.3** Port `core/error.rs` from existing crate - Error variants: Transport, Encryption, Decryption, Timeout, Validation, Unauthorized, Other - **Verify:** All error types cover TS SDK error scenarios - **1.4** Port and extend `core/types.rs` - `EncryptionMode` (Optional, Required, Disabled) - `ServerInfo` (name, version, picture, website, about) - `ClientSession` (is_initialized, is_encrypted, last_activity, pending_requests, event_to_progress_token) - Add JSON-RPC types: `JsonRpcMessage`, `JsonRpcRequest`, `JsonRpcResponse`, `JsonRpcNotification`, `JsonRpcError` - **Verify:** `serde_json::from_str` round-trips for all MCP message types - **1.5** Create `core/serializers.rs` - `mcp_to_nostr_event(message, pubkey, kind, tags) → UnsignedEvent` - `nostr_event_to_mcp_message(event) → Option<JsonRpcMessage>` - `get_tag_value(tags, name) → Option<String>` - **Verify:** Unit tests: serialize MCP request → Nostr event → deserialize back, content matches - **1.6** Create `core/validation.rs` - `validate_message_size(content) → bool` (≤1MB) - `validate_message(value) → Option<JsonRpcMessage>` (check jsonrpc="2.0", has method or result/error) - **Verify:** Unit tests: valid/invalid messages, oversize content rejected - **1.7** Port `encryption/mod.rs` from existing crate - `encrypt_nip44(signer, pubkey, plaintext) → Result<String>` - `decrypt_nip44(signer, pubkey, ciphertext) → Result<String>` - `encrypt_gift_wrap(content, recipient_pubkey) → Event` (NIP-59) - `decrypt_gift_wrap(event, signer) → Result<String>` (NIP-59) - **Verify:** Unit test: encrypt → decrypt round-trip for both NIP-44 and gift wrap - **1.8** Port `signer/mod.rs` from existing crate - Re-export `Keys`, `NostrSigner`, `PublicKey` from nostr-sdk - `from_sk(sk) → Result<Keys>` - `generate() → Keys` - **Verify:** Generate keys, sign event, verify signature - **1.9** Port `relay/mod.rs` from existing crate - `RelayPool::new(signer) → Result<Self>` - `connect(urls) → Result<()>` - `disconnect() → Result<()>` - `publish(event) → Result<EventId>` - `subscribe(filters) → Result<Events>` - `client() → &Arc<Client>` - **Verify:** Integration test with a local relay (or mock): connect, publish, fetch ### Phase 2: Transport Layer _The core of the SDK. Implements MCP Transport trait over Nostr._ - **2.1** Define Rust `Transport` trait - ```rust #[async_trait] pub trait Transport: Send + Sync { async fn start(&mut self) -> Result<()>; async fn close(&mut self) -> Result<()>; async fn send(&self, message: JsonRpcMessage) -> Result<()>; fn set_message_handler(&mut self, handler: Box<dyn Fn(JsonRpcMessage) + Send + Sync>); fn set_error_handler(&mut self, handler: Box<dyn Fn(Error) + Send + Sync>); fn set_close_handler(&mut self, handler: Box<dyn Fn() + Send + Sync>); } ``` - **Verify:** Trait compiles, can be used as `dyn Transport` - **2.2** Implement `BaseNostrTransport` (shared logic) - Fields: signer, relay_pool, encryption_mode, is_connected - Methods: - `connect() / disconnect()` - `get_public_key() → String` - `subscribe(filters, on_event)` - `convert_nostr_event_to_mcp(event) → Option<JsonRpcMessage>` (validate size + structure) - `create_signed_nostr_event(message, kind, tags) → Event` - `publish_event(event)` - `send_mcp_message(message, recipient, kind, tags, is_encrypted) → String` (encrypt if needed) - `should_encrypt_message(kind, is_encrypted) → bool` - `create_subscription_filters(pubkey) → Vec<Filter>` - `create_recipient_tags(pubkey) → Vec<Tag>` - `create_response_tags(pubkey, event_id) → Vec<Tag>` - **Verify:** Unit tests for `should_encrypt_message` with all EncryptionMode variants - **2.3** Implement `NostrClientTransport` - Config: relay_urls, server_pubkey, encryption_mode, is_stateless - Implements `Transport` trait - Features: - Subscribe to events targeting client pubkey - `send()`: create event with recipient tags, publish (encrypt if needed) - Response correlation via pending_request_ids set + `e` tag matching - Stateless mode: emulate initialize response locally - Process incoming: decrypt gift wrap → verify server pubkey → correlate → dispatch - Notification handling (no `e` tag → notification) - **Verify:** - Unit test: send request, receive correlated response - Unit test: stateless mode emulates initialize - Unit test: rejects events from wrong server pubkey - **2.4** Implement `NostrServerTransport` - Config: relay_urls, encryption_mode, server_info, is_announced_server, allowed_public_keys, excluded_capabilities, cleanup_interval_ms, session_timeout_ms - Implements `Transport` trait - Features: - Subscribe to events targeting server pubkey - Multi-client session management (HashMap<String, ClientSession>) - Request correlation: replace request.id with event_id, store original → restore on response - Progress token tracking for streaming responses - Authorization: allowedPublicKeys whitelist with capability exclusions - Encryption mode enforcement (reject unencrypted if Required, reject encrypted if Disabled) - `send()`: route responses back to correct client, route notifications to all/specific clients - Public server announcements: initialize → fetch tools/resources/prompts → publish as kinds 11316-11320 - Announcement deletion (NIP-09 kind 5) - Periodic session cleanup (configurable interval/timeout) - **Verify:** - Unit test: request → response correlation with original ID restoration - Unit test: multi-client sessions don't cross-contaminate - Unit test: unauthorized pubkey rejected (with error response for public servers) - Unit test: encryption mode enforcement - Unit test: session cleanup removes stale sessions ### Phase 3: Gateway & Proxy _Higher-level components that compose transports._ - **3.1** Implement `NostrMCPGateway` - Takes: local MCP transport (any `Transport`) + NostrServerTransportConfig - Creates NostrServerTransport internally - Bidirectional message forwarding: - Nostr → local MCP server (via onmessage) - Local MCP server → Nostr (via onmessage) - Lifecycle: `start()` starts both transports, `stop()` closes both - Error propagation from both sides - `is_active() → bool` - **Verify:** - Integration test: mock MCP transport ↔ gateway ↔ mock Nostr transport - Test: start/stop lifecycle - Test: error from one side doesn't crash the other - **3.2** Implement `NostrMCPProxy` - Takes: local MCP host transport + NostrClientTransportConfig - Creates NostrClientTransport internally - Bidirectional message forwarding: - Local host → Nostr (forward to remote server) - Nostr → local host (relay responses back) - Lifecycle: `start()` starts both, `stop()` closes both - **Verify:** - Integration test: mock local host ↔ proxy ↔ mock Nostr transport - Test: messages flow both directions ### Phase 4: Discovery & Announcements _Server discovery features from the ContextVM spec._ - **4.1** Implement server announcement publishing - Publish kind 11316 (server info) with name/about/website/picture/support_encryption tags - Publish kind 11317 (tools list) from MCP tools/list response - Publish kind 11318 (resources list) from MCP resources/list response - Publish kind 11319 (resource templates list) - Publish kind 11320 (prompts list) from MCP prompts/list response - **Verify:** Published events have correct kinds, tags, and parseable content - **4.2** Implement server discovery client - `discover_servers(relay_urls) → Vec<ServerAnnouncement>` — fetch kind 11316 events - `discover_tools(server_pubkey, relay_urls) → Vec<Tool>` — fetch kind 11317 - `discover_resources(server_pubkey, relay_urls) → Vec<Resource>` — fetch kind 11318 - `discover_prompts(server_pubkey, relay_urls) → Vec<Prompt>` — fetch kind 11320 - **Verify:** Integration test: publish announcements → discover them - **4.3** Implement announcement deletion - `delete_announcements(reason) → Vec<Event>` — publish NIP-09 kind 5 for all announcement kinds - **Verify:** After deletion, discovery returns empty ### Phase 5: Examples & Documentation _Working examples and API docs._ - **5.1** Create `examples/gateway.rs` - Expose a simple MCP server (echo tool) via Nostr gateway - **Verify:** Runs without errors, publishes announcement - **5.2** Create `examples/proxy.rs` - Connect to a remote Nostr MCP server and call a tool - **Verify:** Runs, sends request, receives response - **5.3** Create `examples/discovery.rs` - Discover servers and their tools on a relay - **Verify:** Finds published servers - **5.4** Write API documentation - Doc comments on all public types and methods - Crate-level documentation with usage examples - **Verify:** `cargo doc --open` produces clean docs, no missing docs warnings --- ## Key Differences from Existing Rust Crate | Aspect | Existing (clarity/crates/cvm) | New SDK | | ---------------------- | ----------------------------- | ------------------------------------------------------------ | | MCP types | Raw JSON strings | Typed `JsonRpcMessage` enum with serde | | Transport trait | None | `Transport` trait matching MCP SDK pattern | | Base transport | None | `BaseNostrTransport` with shared logic | | Server dispatch | `handle_event` logs only | Full request correlation + multi-client routing | | Gateway | None | `NostrMCPGateway` (bidirectional bridge) | | Proxy | None | `NostrMCPProxy` (bidirectional bridge) | | Announcements | `announce` + `publish_tools` | All 5 kinds + deletion + discovery | | Authorization | None | Pubkey whitelist with capability exclusions | | Encryption negotiation | Enum defined, not enforced | Full mode enforcement per TS SDK | | Session management | Basic HashMap | Full session with pending requests, progress tokens, cleanup | | Validation | None | Size + schema validation | | nostr-sdk version | 0.43 | 0.43 (same) | ## Key Differences from TS SDK | Aspect | TS SDK | Rust SDK | | ------------------ | -------------------------------------------------- | ---------------------------------------------------- | | MCP SDK dependency | `@modelcontextprotocol/sdk` | Own JSON-RPC types (no official Rust MCP SDK) | | Relay abstraction | `RelayHandler` interface, multiple implementations | `RelayPool` using nostr-sdk Client | | Async model | Callbacks (`onmessage`, `onerror`, `onclose`) | Callbacks + channels (tokio::mpsc for event streams) | | Gift wrap | Manual NIP-44 + finalize | nostr-sdk built-in gift_wrap | | Logging | Pino (JSON) | tracing (structured) | --- ## Estimation | Phase | Effort | Dependencies | | ------------------------ | -------- | ------------ | ------------ | | Phase 1: Core | ~4h | None | ✅ Done | | Phase 2: Transport | ~8h | Phase 1 | ✅ Done | | Phase 3: Gateway & Proxy | ~3h | Phase 2 | ✅ Done | | Phase 4: Discovery | ~3h | Phase 2 | ✅ Done | | Phase 5: Examples & Docs | ~2h | Phase 3+4 | ✅ Done | | **Total** | **~20h** | | **Complete** | --- _This page was ported from the [ContextVM Rust SDK repository](https://github.com/ContextVM/rs-sdk/tree/main/DESIGN.md)._Date: 2026-03-11
Status: Implementation Complete (Phase 1-5)
Reference: ContextVM TS SDK · ContextVM Draft Spec · Existing Rust crate
Verification Summary (2026-03-11)
Section titled “Verification Summary (2026-03-11)”| Check | Result |
|---|---|
cargo check | ✅ Clean (2 unused import warnings) |
cargo test | ✅ 8 unit + 3 doc tests pass |
cargo build --examples | ✅ All 3 examples compile |
| Source LOC | 1,914 across 17 files |
| Tasks complete | 22/22 |
Remaining Polish
Section titled “Remaining Polish”- Fix 2 unused import warnings (
Errorin base.rs,Instantin server.rs) - Add more unit tests for transport/gateway/proxy (currently only core + encryption tested)
- Integration tests with live relay (requires test relay setup)
cargo docwarnings check + doc completeness audit
A complete Rust SDK for the ContextVM protocol that matches the TypeScript SDK feature-for-feature, enabling any Rust MCP server or client to communicate over Nostr.
Architecture
Section titled “Architecture”rust-contextvm-sdk/├── src/│ ├── lib.rs # Re-exports, crate root│ ├── core/│ │ ├── mod.rs│ │ ├── constants.rs # Event kinds, tags, limits│ │ ├── types.rs # EncryptionMode, ServerInfo, ClientSession│ │ ├── error.rs # Error types│ │ ├── serializers.rs # mcpToNostrEvent, nostrEventToMcpMessage, getTag│ │ └── validation.rs # validateMessage, validateMessageSize│ ├── transport/│ │ ├── mod.rs│ │ ├── base.rs # BaseNostrTransport (shared logic)│ │ ├── client.rs # NostrClientTransport│ │ └── server.rs # NostrServerTransport│ ├── gateway/│ │ ├── mod.rs # NostrMCPGateway│ ├── proxy/│ │ ├── mod.rs # NostrMCPProxy│ ├── relay/│ │ ├── mod.rs # RelayPool│ ├── signer/│ │ ├── mod.rs # Signer trait + KeysSigner│ └── encryption/│ ├── mod.rs # NIP-44 encrypt/decrypt, NIP-59 gift wrap├── examples/│ ├── gateway.rs # Example: expose a local MCP server via Nostr│ ├── proxy.rs # Example: connect to remote MCP server via Nostr│ └── discovery.rs # Example: discover servers on relay├── tests/│ ├── transport_test.rs│ ├── gateway_test.rs│ └── encryption_test.rs├── Cargo.toml├── README.md├── DESIGN.md└── LICENSEModule Mapping: TS SDK → Rust SDK
Section titled “Module Mapping: TS SDK → Rust SDK”| TS SDK Module | TS LOC | Rust Module | Source | Status |
|---|---|---|---|---|
core/constants.ts | 87 | core/constants.rs (64 LOC) | Port from existing Rust | ✅ done |
core/interfaces.ts | 61 | core/types.rs (188 LOC) | Port from existing Rust | ✅ done |
core/encryption.ts | 64 | encryption/mod.rs (86 LOC) | Port from existing Rust | ✅ done |
core/utils/serializers.ts | ~60 | core/serializers.rs (74 LOC) | New | ✅ done |
core/utils/utils.ts | ~30 | core/validation.rs (64 LOC) | New | ✅ done |
relay/simple-relay-pool.ts | ~100 | relay/mod.rs (108 LOC) | Port from existing Rust | ✅ done |
signer/private-key-signer.ts | ~50 | signer/mod.rs (15 LOC) | Port from existing Rust | ✅ done |
transport/base-nostr-transport.ts | 355 | transport/base.rs (139 LOC) | New | ✅ done |
transport/nostr-client-transport.ts | 411 | transport/client.rs (228 LOC) | Rewrite from existing | ✅ done |
transport/nostr-server-transport.ts | 944 | transport/server.rs (583 LOC) | Rewrite from existing | ✅ done |
gateway/index.ts | 151 | gateway/mod.rs (82 LOC) | New | ✅ done |
proxy/index.ts | 96 | proxy/mod.rs (71 LOC) | New | ✅ done |
| (n/a — new) | — | discovery/mod.rs (154 LOC) | New | ✅ done |
| (n/a — new) | — | core/error.rs (40 LOC) | New | ✅ done |
TS SDK total: ~2,169 LOC (non-test)
Existing Rust (reference): ~782 LOC
Actual Rust SDK: 1,914 LOC (+ tests + examples)
Dependencies
Section titled “Dependencies”[dependencies]tokio = { version = "1", features = ["full"] }serde = { version = "1", features = ["derive"] }serde_json = "1"nostr-sdk = { version = "0.43", features = ["nip59"] }thiserror = "2"async-trait = "0.1"tracing = "0.1"
[dev-dependencies]tokio-test = "0.4"tracing-subscriber = "0.3"Note on MCP types: The TS SDK imports from @modelcontextprotocol/sdk. There’s no official Rust MCP SDK yet. We define our own JSON-RPC message types that are protocol-compatible. If/when an official Rust MCP SDK exists, we can add it as an optional integration.
Phase 1: Core Foundation
Section titled “Phase 1: Core Foundation”Port and enhance existing code. Establish project structure.
-
1.1 Initialize Cargo project with workspace structure and dependencies
- Verify:
cargo checkpasses with empty lib.rs
- Verify:
-
1.2 Port
core/constants.rsfrom existing crate- All event kinds (25910, 1059, 11316-11320)
- All tag constants (p, e, cap, name, website, picture, about, support_encryption)
- MAX_MESSAGE_SIZE
- Verify: Constants match TS SDK
core/constants.tsvalues exactly
-
1.3 Port
core/error.rsfrom existing crate- Error variants: Transport, Encryption, Decryption, Timeout, Validation, Unauthorized, Other
- Verify: All error types cover TS SDK error scenarios
-
1.4 Port and extend
core/types.rsEncryptionMode(Optional, Required, Disabled)ServerInfo(name, version, picture, website, about)ClientSession(is_initialized, is_encrypted, last_activity, pending_requests, event_to_progress_token)- Add JSON-RPC types:
JsonRpcMessage,JsonRpcRequest,JsonRpcResponse,JsonRpcNotification,JsonRpcError - Verify:
serde_json::from_strround-trips for all MCP message types
-
1.5 Create
core/serializers.rsmcp_to_nostr_event(message, pubkey, kind, tags) → UnsignedEventnostr_event_to_mcp_message(event) → Option<JsonRpcMessage>get_tag_value(tags, name) → Option<String>- Verify: Unit tests: serialize MCP request → Nostr event → deserialize back, content matches
-
1.6 Create
core/validation.rsvalidate_message_size(content) → bool(≤1MB)validate_message(value) → Option<JsonRpcMessage>(check jsonrpc=“2.0”, has method or result/error)- Verify: Unit tests: valid/invalid messages, oversize content rejected
-
1.7 Port
encryption/mod.rsfrom existing crateencrypt_nip44(signer, pubkey, plaintext) → Result<String>decrypt_nip44(signer, pubkey, ciphertext) → Result<String>encrypt_gift_wrap(content, recipient_pubkey) → Event(NIP-59)decrypt_gift_wrap(event, signer) → Result<String>(NIP-59)- Verify: Unit test: encrypt → decrypt round-trip for both NIP-44 and gift wrap
-
1.8 Port
signer/mod.rsfrom existing crate- Re-export
Keys,NostrSigner,PublicKeyfrom nostr-sdk from_sk(sk) → Result<Keys>generate() → Keys- Verify: Generate keys, sign event, verify signature
- Re-export
-
1.9 Port
relay/mod.rsfrom existing crateRelayPool::new(signer) → Result<Self>connect(urls) → Result<()>disconnect() → Result<()>publish(event) → Result<EventId>subscribe(filters) → Result<Events>client() → &Arc<Client>- Verify: Integration test with a local relay (or mock): connect, publish, fetch
Phase 2: Transport Layer
Section titled “Phase 2: Transport Layer”The core of the SDK. Implements MCP Transport trait over Nostr.
-
2.1 Define Rust
Transporttrait-
#[async_trait]pub trait Transport: Send + Sync {async fn start(&mut self) -> Result<()>;async fn close(&mut self) -> Result<()>;async fn send(&self, message: JsonRpcMessage) -> Result<()>;fn set_message_handler(&mut self, handler: Box<dyn Fn(JsonRpcMessage) + Send + Sync>);fn set_error_handler(&mut self, handler: Box<dyn Fn(Error) + Send + Sync>);fn set_close_handler(&mut self, handler: Box<dyn Fn() + Send + Sync>);}
- Verify: Trait compiles, can be used as
dyn Transport
-
-
2.2 Implement
BaseNostrTransport(shared logic)- Fields: signer, relay_pool, encryption_mode, is_connected
- Methods:
connect() / disconnect()get_public_key() → Stringsubscribe(filters, on_event)convert_nostr_event_to_mcp(event) → Option<JsonRpcMessage>(validate size + structure)create_signed_nostr_event(message, kind, tags) → Eventpublish_event(event)send_mcp_message(message, recipient, kind, tags, is_encrypted) → String(encrypt if needed)should_encrypt_message(kind, is_encrypted) → boolcreate_subscription_filters(pubkey) → Vec<Filter>create_recipient_tags(pubkey) → Vec<Tag>create_response_tags(pubkey, event_id) → Vec<Tag>
- Verify: Unit tests for
should_encrypt_messagewith all EncryptionMode variants
-
2.3 Implement
NostrClientTransport- Config: relay_urls, server_pubkey, encryption_mode, is_stateless
- Implements
Transporttrait - Features:
- Subscribe to events targeting client pubkey
send(): create event with recipient tags, publish (encrypt if needed)- Response correlation via pending_request_ids set +
etag matching - Stateless mode: emulate initialize response locally
- Process incoming: decrypt gift wrap → verify server pubkey → correlate → dispatch
- Notification handling (no
etag → notification)
- Verify:
- Unit test: send request, receive correlated response
- Unit test: stateless mode emulates initialize
- Unit test: rejects events from wrong server pubkey
-
2.4 Implement
NostrServerTransport- Config: relay_urls, encryption_mode, server_info, is_announced_server, allowed_public_keys, excluded_capabilities, cleanup_interval_ms, session_timeout_ms
- Implements
Transporttrait - Features:
- Subscribe to events targeting server pubkey
- Multi-client session management (HashMap<String, ClientSession>)
- Request correlation: replace request.id with event_id, store original → restore on response
- Progress token tracking for streaming responses
- Authorization: allowedPublicKeys whitelist with capability exclusions
- Encryption mode enforcement (reject unencrypted if Required, reject encrypted if Disabled)
send(): route responses back to correct client, route notifications to all/specific clients- Public server announcements: initialize → fetch tools/resources/prompts → publish as kinds 11316-11320
- Announcement deletion (NIP-09 kind 5)
- Periodic session cleanup (configurable interval/timeout)
- Verify:
- Unit test: request → response correlation with original ID restoration
- Unit test: multi-client sessions don’t cross-contaminate
- Unit test: unauthorized pubkey rejected (with error response for public servers)
- Unit test: encryption mode enforcement
- Unit test: session cleanup removes stale sessions
Phase 3: Gateway & Proxy
Section titled “Phase 3: Gateway & Proxy”Higher-level components that compose transports.
-
3.1 Implement
NostrMCPGateway- Takes: local MCP transport (any
Transport) + NostrServerTransportConfig - Creates NostrServerTransport internally
- Bidirectional message forwarding:
- Nostr → local MCP server (via onmessage)
- Local MCP server → Nostr (via onmessage)
- Lifecycle:
start()starts both transports,stop()closes both - Error propagation from both sides
is_active() → bool- Verify:
- Integration test: mock MCP transport ↔ gateway ↔ mock Nostr transport
- Test: start/stop lifecycle
- Test: error from one side doesn’t crash the other
- Takes: local MCP transport (any
-
3.2 Implement
NostrMCPProxy- Takes: local MCP host transport + NostrClientTransportConfig
- Creates NostrClientTransport internally
- Bidirectional message forwarding:
- Local host → Nostr (forward to remote server)
- Nostr → local host (relay responses back)
- Lifecycle:
start()starts both,stop()closes both - Verify:
- Integration test: mock local host ↔ proxy ↔ mock Nostr transport
- Test: messages flow both directions
Phase 4: Discovery & Announcements
Section titled “Phase 4: Discovery & Announcements”Server discovery features from the ContextVM spec.
-
4.1 Implement server announcement publishing
- Publish kind 11316 (server info) with name/about/website/picture/support_encryption tags
- Publish kind 11317 (tools list) from MCP tools/list response
- Publish kind 11318 (resources list) from MCP resources/list response
- Publish kind 11319 (resource templates list)
- Publish kind 11320 (prompts list) from MCP prompts/list response
- Verify: Published events have correct kinds, tags, and parseable content
-
4.2 Implement server discovery client
discover_servers(relay_urls) → Vec<ServerAnnouncement>— fetch kind 11316 eventsdiscover_tools(server_pubkey, relay_urls) → Vec<Tool>— fetch kind 11317discover_resources(server_pubkey, relay_urls) → Vec<Resource>— fetch kind 11318discover_prompts(server_pubkey, relay_urls) → Vec<Prompt>— fetch kind 11320- Verify: Integration test: publish announcements → discover them
-
4.3 Implement announcement deletion
delete_announcements(reason) → Vec<Event>— publish NIP-09 kind 5 for all announcement kinds- Verify: After deletion, discovery returns empty
Phase 5: Examples & Documentation
Section titled “Phase 5: Examples & Documentation”Working examples and API docs.
-
5.1 Create
examples/gateway.rs- Expose a simple MCP server (echo tool) via Nostr gateway
- Verify: Runs without errors, publishes announcement
-
5.2 Create
examples/proxy.rs- Connect to a remote Nostr MCP server and call a tool
- Verify: Runs, sends request, receives response
-
5.3 Create
examples/discovery.rs- Discover servers and their tools on a relay
- Verify: Finds published servers
-
5.4 Write API documentation
- Doc comments on all public types and methods
- Crate-level documentation with usage examples
- Verify:
cargo doc --openproduces clean docs, no missing docs warnings
Key Differences from Existing Rust Crate
Section titled “Key Differences from Existing Rust Crate”| Aspect | Existing (clarity/crates/cvm) | New SDK |
|---|---|---|
| MCP types | Raw JSON strings | Typed JsonRpcMessage enum with serde |
| Transport trait | None | Transport trait matching MCP SDK pattern |
| Base transport | None | BaseNostrTransport with shared logic |
| Server dispatch | handle_event logs only | Full request correlation + multi-client routing |
| Gateway | None | NostrMCPGateway (bidirectional bridge) |
| Proxy | None | NostrMCPProxy (bidirectional bridge) |
| Announcements | announce + publish_tools | All 5 kinds + deletion + discovery |
| Authorization | None | Pubkey whitelist with capability exclusions |
| Encryption negotiation | Enum defined, not enforced | Full mode enforcement per TS SDK |
| Session management | Basic HashMap | Full session with pending requests, progress tokens, cleanup |
| Validation | None | Size + schema validation |
| nostr-sdk version | 0.43 | 0.43 (same) |
Key Differences from TS SDK
Section titled “Key Differences from TS SDK”| Aspect | TS SDK | Rust SDK |
|---|---|---|
| MCP SDK dependency | @modelcontextprotocol/sdk | Own JSON-RPC types (no official Rust MCP SDK) |
| Relay abstraction | RelayHandler interface, multiple implementations | RelayPool using nostr-sdk Client |
| Async model | Callbacks (onmessage, onerror, onclose) | Callbacks + channels (tokio::mpsc for event streams) |
| Gift wrap | Manual NIP-44 + finalize | nostr-sdk built-in gift_wrap |
| Logging | Pino (JSON) | tracing (structured) |
Estimation
Section titled “Estimation”| Phase | Effort | Dependencies | | ------------------------ | -------- | ------------ | ------------ | | Phase 1: Core | ~4h | None | ✅ Done | | Phase 2: Transport | ~8h | Phase 1 | ✅ Done | | Phase 3: Gateway & Proxy | ~3h | Phase 2 | ✅ Done | | Phase 4: Discovery | ~3h | Phase 2 | ✅ Done | | Phase 5: Examples & Docs | ~2h | Phase 3+4 | ✅ Done | | Total | ~20h | | Complete |
This page was ported from the ContextVM Rust SDK repository.