contextvm-sdk 0.1.0

Rust SDK for the ContextVM protocol — MCP over Nostr
Documentation
# Proxy Guide

`NostrMCPProxy` is the simplest way to talk to a remote ContextVM server from Rust.

It wraps `NostrClientTransport`, gives you a receiver for responses and notifications, and handles transport startup and shutdown.

For native Rust applications, this is usually not the primary path. Most users should build an `rmcp` client and attach `NostrClientTransport` directly, as described in the native client guide.

## When to use it

Use the proxy when:

- you already know the target server pubkey
- you want a lightweight request/response flow
- you do not need low-level transport hooks

Do not start here if you are writing a new native Rust MCP client from scratch.

## Loading an existing private key

Like the native client transport, the proxy can reuse an existing Nostr identity instead of generating a new one. Load the signer with `from_sk()`:

```rust
use contextvm_sdk::signer;

let signer = signer::from_sk("<hex-or-nsec-private-key>")?;
```

Pass that signer to `NostrMCPProxy::new()` exactly as you would pass a freshly generated keypair.

## Minimal example

This follows the repository proxy example.

```rust
use contextvm_sdk::core::types::{JsonRpcMessage, JsonRpcRequest};
use contextvm_sdk::proxy::{NostrMCPProxy, ProxyConfig};
use contextvm_sdk::signer;
use contextvm_sdk::transport::client::NostrClientTransportConfig;

#[tokio::main]
async fn main() -> contextvm_sdk::Result<()> {
    let keys = signer::from_sk("<hex-or-nsec-private-key>")?;

    let config = ProxyConfig {
        nostr_config: NostrClientTransportConfig {
            relay_urls: vec!["wss://relay.damus.io".to_string()],
            server_pubkey: "<server-hex-pubkey>".to_string(),
            ..Default::default()
        },
    };

    let mut proxy = NostrMCPProxy::new(keys, config).await?;
    let mut rx = proxy.start().await?;

    let request = JsonRpcMessage::Request(JsonRpcRequest {
        jsonrpc: "2.0".to_string(),
        id: serde_json::json!(1),
        method: "tools/list".to_string(),
        params: None,
    });

    proxy.send(&request).await?;

    if let Some(message) = rx.recv().await {
        println!("{}", serde_json::to_string_pretty(&message)?);
    }

    proxy.stop().await?;
    Ok(())
}
```

## Client config

The main options live on `NostrClientTransportConfig`:

- `relay_urls`: relays used for direct transport
- `server_pubkey`: target server identity in hex form
- `encryption_mode`: client encryption policy
- `gift_wrap_mode`: preferred gift-wrap kind policy
- `is_stateless`: emulate the initialize response locally
- `timeout`: pending request correlation retention

## Stateless mode

`is_stateless` is a major behavior switch.

When enabled, the client can emulate initialize handling locally instead of waiting for a network roundtrip. This behavior is covered by the conformance tests.

Use it when:

- you want faster startup for short-lived clients
- you control the server behavior and know stateless operation is acceptable

Avoid assuming that every server workflow depends only on stateless behavior.

## Behavioral notes

- responses are correlated at the transport level, not just by raw receive order
- the client learns peer capabilities from discovery tags on inbound messages
- encrypted traffic is deduplicated by outer gift-wrap event id before delivery

## When not to use it

Prefer the native client transport path when:

- your application is already modeled as an `rmcp` `ClientHandler`
- you want the normal running-client workflow from `ServiceExt`
- you want examples that match the rest of the `rmcp` client ecosystem

## rmcp path

If you are building on `rmcp`, use `serve_client_handler()` instead of manually sending and receiving raw `JsonRpcMessage` values.

That said, the preferred native architecture is still `rmcp` client first and ContextVM transport second.