Skip to content

Latest commit

 

History

History
354 lines (300 loc) · 16.4 KB

DESIGN.md

File metadata and controls

354 lines (300 loc) · 16.4 KB

Design Plan — rust-contextvm-sdk

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)

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

[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

    • #[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
Phase 2: Transport ~8h Phase 1
Phase 3: Gateway & Proxy ~3h Phase 2
Phase 4: Discovery ~3h Phase 2
Phase 5: Examples & Docs ~2h Phase 3+4
Total ~20h