Skip to content

Architecture & Design

Date: 2026-03-11
Status: Implementation Complete (Phase 1-5)
Reference: ContextVM TS SDK · ContextVM Draft Spec · Existing Rust crate

CheckResult
cargo check✅ Clean (2 unused import warnings)
cargo test✅ 8 unit + 3 doc tests pass
cargo build --examples✅ All 3 examples compile
Source LOC1,914 across 17 files
Tasks complete22/22
  • 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

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.


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
TS SDK ModuleTS LOCRust ModuleSourceStatus
core/constants.ts87core/constants.rs (64 LOC)Port from existing Rust✅ done
core/interfaces.ts61core/types.rs (188 LOC)Port from existing Rust✅ done
core/encryption.ts64encryption/mod.rs (86 LOC)Port from existing Rust✅ done
core/utils/serializers.ts~60core/serializers.rs (74 LOC)New✅ done
core/utils/utils.ts~30core/validation.rs (64 LOC)New✅ done
relay/simple-relay-pool.ts~100relay/mod.rs (108 LOC)Port from existing Rust✅ done
signer/private-key-signer.ts~50signer/mod.rs (15 LOC)Port from existing Rust✅ done
transport/base-nostr-transport.ts355transport/base.rs (139 LOC)New✅ done
transport/nostr-client-transport.ts411transport/client.rs (228 LOC)Rewrite from existing✅ done
transport/nostr-server-transport.ts944transport/server.rs (583 LOC)Rewrite from existing✅ done
gateway/index.ts151gateway/mod.rs (82 LOC)New✅ done
proxy/index.ts96proxy/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]
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.


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

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

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

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

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

AspectExisting (clarity/crates/cvm)New SDK
MCP typesRaw JSON stringsTyped JsonRpcMessage enum with serde
Transport traitNoneTransport trait matching MCP SDK pattern
Base transportNoneBaseNostrTransport with shared logic
Server dispatchhandle_event logs onlyFull request correlation + multi-client routing
GatewayNoneNostrMCPGateway (bidirectional bridge)
ProxyNoneNostrMCPProxy (bidirectional bridge)
Announcementsannounce + publish_toolsAll 5 kinds + deletion + discovery
AuthorizationNonePubkey whitelist with capability exclusions
Encryption negotiationEnum defined, not enforcedFull mode enforcement per TS SDK
Session managementBasic HashMapFull session with pending requests, progress tokens, cleanup
ValidationNoneSize + schema validation
nostr-sdk version0.430.43 (same)
AspectTS SDKRust SDK
MCP SDK dependency@modelcontextprotocol/sdkOwn JSON-RPC types (no official Rust MCP SDK)
Relay abstractionRelayHandler interface, multiple implementationsRelayPool using nostr-sdk Client
Async modelCallbacks (onmessage, onerror, onclose)Callbacks + channels (tokio::mpsc for event streams)
Gift wrapManual NIP-44 + finalizenostr-sdk built-in gift_wrap
LoggingPino (JSON)tracing (structured)

| 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.