Rust SDK for the ContextVM protocol — MCP over Nostr.
A complete implementation enabling Model Context Protocol (MCP) servers and clients to communicate over the Nostr network with decentralized discovery, cryptographic identity, and optional end-to-end encryption.
┌──────────────────────────────────────────────────────────┐
│ Your Application │
├──────────────┬───────────────┬────────────────────────────┤
│ Gateway │ Proxy │ Discovery │
│ (server → │ (nostr → │ (find servers & │
│ nostr) │ client) │ capabilities) │
├──────────────┴───────────────┴────────────────────────────┤
│ Transport Layer │
│ NostrServerTransport / NostrClientTransport │
├───────────────────────────────────────────────────────────┤
│ Core │ Encryption │ Relay │ Signer │
│ (types, │ (NIP-44, │ (pool │ (key │
│ JSON-RPC, │ NIP-59 │ mgmt) │ mgmt) │
│ validation) │ gift wrap) │ │ │
├────────────────┴─────────────────┴───────────┴────────────┤
│ Nostr Network (relays) │
└───────────────────────────────────────────────────────────┘
ContextVM maps MCP's JSON-RPC 2.0 messages onto Nostr events:
| Kind | Name | Type | Description |
|---|---|---|---|
25910 | ContextVM Messages | Ephemeral | MCP request/response/notification |
1059 | Gift Wrap (NIP-59) | Regular | Encrypted MCP messages |
21059 | Ephemeral Gift Wrap | Ephemeral | Encrypted MCP messages (CEP-19) |
10002 | Relay List Metadata | Replaceable | Server's advertised relays (CEP-17) |
11316 | Server Announcement | Addressable | Server identity & metadata |
11317 | Tools List | Addressable | Published tool capabilities |
11318 | Resources List | Addressable | Published resource capabilities |
11319 | Resource Templates | Addressable | Published resource template list |
11320 | Prompts List | Addressable | Published prompt capabilities |
Messages are routed using Nostr p tags (recipient pubkey) and correlated with e tags (request event ID).
Add to your Cargo.toml:
[dependencies]
contextvm-sdk = { git = "https://github.com/ContextVM/rs-sdk" }Or pin a published release from crates.io:
[dependencies]
contextvm-sdk = "0.2.2"use contextvm_sdk::gateway::{NostrMCPGateway,GatewayConfig};use contextvm_sdk::transport::server::NostrServerTransportConfig;use contextvm_sdk::core::types::{ServerInfo,EncryptionMode};use contextvm_sdk::signer;#[tokio::main]asyncfnmain() -> contextvm_sdk::Result<()>{let keys = signer::generate();let config = GatewayConfig::new(NostrServerTransportConfig::default().with_encryption_mode(EncryptionMode::Optional).with_server_info(ServerInfo::default().with_name("My MCP Server").with_about("Tools via Nostr"),).with_announced_server(true),);letmut gateway = NostrMCPGateway::new(keys, config).await?;letmut requests = gateway.start().await?;
gateway.announce().await?;whileletSome(req) = requests.recv().await{println!("Request: {:?}", req.message);// Process and respond:// gateway.send_response(&req.event_id, response).await?;}Ok(())}use contextvm_sdk::proxy::{NostrMCPProxy,ProxyConfig};use contextvm_sdk::transport::client::NostrClientTransportConfig;use contextvm_sdk::core::types::EncryptionMode;use contextvm_sdk::signer;#[tokio::main]asyncfnmain() -> contextvm_sdk::Result<()>{let keys = signer::generate();let config = ProxyConfig::new(NostrClientTransportConfig::default().with_server_pubkey("abc123...server_hex_pubkey").with_encryption_mode(EncryptionMode::Optional),);letmut proxy = NostrMCPProxy::new(keys, config).await?;letmut responses = proxy.start().await?;// Send an MCP requestlet request = contextvm_sdk::JsonRpcMessage::Request(contextvm_sdk::JsonRpcRequest{jsonrpc:"2.0".into(),id: serde_json::json!(1),method:"tools/list".into(),params:None,});
proxy.send(&request).await?;// Receive responseifletSome(msg) = responses.recv().await{println!("Response: {:?}", msg);}Ok(())}use contextvm_sdk::{discovery, signer,RelayPool};#[tokio::main]asyncfnmain() -> contextvm_sdk::Result<()>{let keys = signer::generate();let pool = RelayPool::new(keys).await?;let relays = vec!["wss://relay.damus.io".into()];
pool.connect(&relays).await?;let servers = discovery::discover_servers(pool.client(),&relays).await?;for server in&servers {println!("Server: {} ({:?})", server.pubkey, server.server_info.name);let tools = discovery::discover_tools(pool.client(),&server.pubkey_parsed,&relays).await?;println!(" Tools: {}", tools.len());}Ok(())}The in-repo Rust SDK guides live in docs/README.md:
For most users, the main pattern is: build an
rmcpserver or client, then attachNostrServerTransportorNostrClientTransport.
| Module | Description |
|---|---|
core | Protocol constants, JSON-RPC types, error types, validation |
transport | Client/server Nostr transports with event loop and correlation |
gateway | High-level gateway bridging local MCP servers to Nostr |
proxy | High-level proxy connecting to remote MCP servers via Nostr |
discovery | Server/capability discovery via addressable Nostr events |
encryption | NIP-44 encryption and NIP-59 gift wrapping |
relay | Nostr relay pool management (connect, publish, subscribe) |
signer | Key generation and management utilities |
| Mode | Behavior |
|---|---|
Optional | Encrypt responses if the incoming request was encrypted |
Required | All messages must be encrypted (rejects plaintext) |
Disabled | No encryption; all messages sent as plaintext kind 25910 |
Encryption uses NIP-44 for payload encryption and NIP-59 (Gift Wrap) for metadata-private delivery. Server announcements (kinds 11316–11320) are always public.
Messages too large for a single relay event are fragmented into ordered frames
and reassembled by the receiver (CEP-22 oversized transfer, enabled by default;
it adds no event kind — frames ride inside notifications/progress messages).
See docs/oversized-transfer.md for the timeout
model and tuning.
Open-ended streaming (CEP-41) lets a server tool emit an ordered sequence of
chunks back to the client while a request is in flight. The client consumes them
as an async Stream via call_tool_stream. Unlike CEP-22, the stream supplements
the final JSON-RPC response rather than replacing it. Disabled by default; opt in
with with_open_stream(OpenStreamConfig::enabled()). See
docs/open-stream.md for the writer and client APIs and the
keepalive timer model.
| Field | Default | Description |
|---|---|---|
relay_urls | ["wss://relay.damus.io"] | Nostr relays to connect to |
encryption_mode | Optional | Encryption policy |
gift_wrap_mode | Optional | Gift-wrap policy (CEP-19): persistent (1059) vs ephemeral (21059) |
server_info | None | Server metadata for announcements |
is_announced_server | false | Auto-publish announcements on start (CEP-6) |
allowed_public_keys | [] (allow all) | Client pubkey allowlist (hex) |
excluded_capabilities | [] | Methods exempt from allowlist |
session_timeout | 300s | Inactive session expiry |
relay_list_urls | None | Relay URLs for kind 10002 (CEP-17); defaults to relay_urls |
bootstrap_relay_urls | None | Additional relays for publishing announcements (CEP-6/17) |
publish_relay_list | true | Whether to publish kind 10002 relay list metadata |
profile_metadata | None | Profile metadata for kind 0 publication (CEP-23) |
oversized_transfer | enabled | CEP-22 oversized payload transfer config (guide) |
open_stream | disabled | CEP-41 open-stream config; opt-in (guide) |
| Field | Default | Description |
|---|---|---|
relay_urls | [] | Nostr relays to connect to (empty = use relay resolution) |
server_pubkey | (required) | Target server's public key (hex, npub, or nprofile) |
encryption_mode | Optional | Encryption policy |
gift_wrap_mode | Optional | Gift-wrap policy (CEP-19): persistent (1059) vs ephemeral (21059) |
is_stateless | false | Emulate initialize locally |
timeout | 30s | Response timeout |
discovery_relay_urls | None (bootstrap relays) | Relays for CEP-17 kind 10002 discovery |
fallback_operational_relay_urls | None | Relays probed in parallel with CEP-17 discovery |
oversized_transfer | enabled | CEP-22 oversized payload transfer config (guide) |
open_stream | disabled | CEP-41 open-stream config; opt-in (guide) |
When relay_urls is empty, start() runs automatic relay resolution: configured relays > nprofile hints > CEP-17 kind 10002 discovery > fallback probing > bootstrap defaults.