Skip to main content

contextvm_sdk/proxy/
mod.rs

1//! ContextVM Proxy — connect to a remote Nostr MCP server as if local.
2//!
3//! The proxy sends MCP requests over Nostr to a remote server and
4//! receives responses, making the remote server accessible locally.
5
6use crate::core::error::{Error, Result};
7use crate::core::types::JsonRpcMessage;
8use crate::transport::client::{NostrClientTransport, NostrClientTransportConfig};
9
10/// Configuration for the proxy.
11#[derive(Debug, Clone)]
12#[non_exhaustive]
13pub struct ProxyConfig {
14    /// Nostr client transport configuration.
15    pub nostr_config: NostrClientTransportConfig,
16}
17
18impl ProxyConfig {
19    /// Create a new proxy configuration.
20    pub fn new(nostr_config: NostrClientTransportConfig) -> Self {
21        Self { nostr_config }
22    }
23}
24
25/// Proxy that connects to a remote MCP server via Nostr.
26pub struct NostrMCPProxy {
27    transport: NostrClientTransport,
28    is_running: bool,
29}
30
31impl NostrMCPProxy {
32    /// Create a new proxy.
33    pub async fn new<T>(signer: T, config: ProxyConfig) -> Result<Self>
34    where
35        T: nostr_sdk::prelude::IntoNostrSigner,
36    {
37        let transport = NostrClientTransport::new(signer, config.nostr_config).await?;
38
39        Ok(Self {
40            transport,
41            is_running: false,
42        })
43    }
44
45    /// Start the proxy. Returns a receiver for incoming responses/notifications.
46    pub async fn start(&mut self) -> Result<tokio::sync::mpsc::UnboundedReceiver<JsonRpcMessage>> {
47        if self.is_running {
48            return Err(Error::Other("Proxy already running".to_string()));
49        }
50
51        self.transport.start().await?;
52        self.is_running = true;
53
54        self.transport
55            .take_message_receiver()
56            .ok_or_else(|| Error::Other("Message receiver already taken".to_string()))
57    }
58
59    /// Send an MCP request to the remote server.
60    pub async fn send(&self, message: &JsonRpcMessage) -> Result<()> {
61        self.transport.send(message).await
62    }
63
64    /// Stop the proxy.
65    pub async fn stop(&mut self) -> Result<()> {
66        if !self.is_running {
67            return Ok(());
68        }
69        self.transport.close().await?;
70        self.is_running = false;
71        Ok(())
72    }
73
74    /// Check if the proxy is active.
75    pub fn is_active(&self) -> bool {
76        self.is_running
77    }
78}
79
80#[cfg(feature = "rmcp")]
81impl NostrMCPProxy {
82    /// Start a proxy directly from an rmcp client handler.
83    ///
84    /// This additive API keeps the existing `new/start/send` flow intact,
85    /// while also allowing direct `handler.serve(transport)` style usage.
86    pub async fn serve_client_handler<T, H>(
87        signer: T,
88        config: ProxyConfig,
89        handler: H,
90    ) -> Result<rmcp::service::RunningService<rmcp::RoleClient, H>>
91    where
92        T: nostr_sdk::prelude::IntoNostrSigner,
93        H: rmcp::ClientHandler,
94    {
95        use crate::NostrClientTransport;
96        use rmcp::ServiceExt;
97
98        let transport = NostrClientTransport::new(signer, config.nostr_config).await?;
99        handler
100            .serve(transport)
101            .await
102            .map_err(|e| Error::Other(format!("rmcp client initialization failed: {e}")))
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::core::types::*;
110    use crate::transport::client::NostrClientTransportConfig;
111    use std::time::Duration;
112
113    #[test]
114    fn test_proxy_config_construction() {
115        let keys = nostr_sdk::Keys::generate();
116        let server_pubkey = keys.public_key().to_hex();
117
118        let nostr_config = NostrClientTransportConfig {
119            relay_urls: vec!["wss://relay.example.com".to_string()],
120            server_pubkey: server_pubkey.clone(),
121            encryption_mode: EncryptionMode::Required,
122            gift_wrap_mode: GiftWrapMode::Optional,
123            is_stateless: true,
124            timeout: Duration::from_secs(60),
125        };
126
127        let config = ProxyConfig { nostr_config };
128
129        assert_eq!(
130            config.nostr_config.relay_urls,
131            vec!["wss://relay.example.com"]
132        );
133        assert_eq!(config.nostr_config.server_pubkey, server_pubkey);
134        assert_eq!(
135            config.nostr_config.encryption_mode,
136            EncryptionMode::Required
137        );
138        assert!(config.nostr_config.is_stateless);
139        assert_eq!(config.nostr_config.timeout, Duration::from_secs(60));
140    }
141
142    #[test]
143    fn test_proxy_config_with_defaults() {
144        let config = ProxyConfig {
145            nostr_config: NostrClientTransportConfig::default(),
146        };
147        assert!(!config.nostr_config.is_stateless);
148        assert_eq!(
149            config.nostr_config.encryption_mode,
150            EncryptionMode::Optional
151        );
152    }
153}