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 |
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/k0sti/rust-contextvm-sdk" }Or clone and use as a path dependency:
[dependencies]
contextvm-sdk = { path = "../rust-contextvm-sdk" }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]
async fn main() -> contextvm_sdk::Result<()> {
let keys = signer::generate();
let config = GatewayConfig {
nostr_config: NostrServerTransportConfig {
relay_urls: vec!["wss://relay.damus.io".into()],
encryption_mode: EncryptionMode::Optional,
server_info: Some(ServerInfo {
name: Some("My MCP Server".into()),
about: Some("Tools via Nostr".into()),
..Default::default()
}),
is_announced_server: true,
..Default::default()
},
};
let mut gateway = NostrMCPGateway::new(keys, config).await?;
let mut requests = gateway.start().await?;
gateway.announce().await?;
while let Some(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]
async fn main() -> contextvm_sdk::Result<()> {
let keys = signer::generate();
let config = ProxyConfig {
nostr_config: NostrClientTransportConfig {
relay_urls: vec!["wss://relay.damus.io".into()],
server_pubkey: "abc123...server_hex_pubkey".into(),
encryption_mode: EncryptionMode::Optional,
..Default::default()
},
};
let mut proxy = NostrMCPProxy::new(keys, config).await?;
let mut responses = proxy.start().await?;
// Send an MCP request
let 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 response
if let Some(msg) = responses.recv().await {
println!("Response: {:?}", msg);
}
Ok(())
}use contextvm_sdk::{discovery, signer, RelayPool};
#[tokio::main]
async fn main() -> 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(())
}| 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.
| Field | Default | Description |
|---|---|---|
relay_urls |
["wss://relay.damus.io"] |
Nostr relays to connect to |
encryption_mode |
Optional |
Encryption policy |
server_info |
None |
Server metadata for announcements |
is_announced_server |
false |
Whether to publish announcements (CEP-6) |
allowed_public_keys |
[] (allow all) |
Client pubkey allowlist (hex) |
excluded_capabilities |
[] |
Methods exempt from allowlist |
session_timeout |
300s |
Inactive session expiry |
| Field | Default | Description |
|---|---|---|
relay_urls |
["wss://relay.damus.io"] |
Nostr relays to connect to |
server_pubkey |
(required) | Target server's public key (hex) |
encryption_mode |
Optional |
Encryption policy |
is_stateless |
false |
Emulate initialize locally |
timeout |
30s |
Response timeout |