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