1use std::sync::{Arc, OnceLock};
25use std::time::Duration;
26
27use dashmap::DashMap;
28use indexmap::IndexMap;
29use objectiveai_sdk::client_objectiveai_mcp::{
30 McpKind,
31 client_request::{self, McpListChangedKind},
32 client_response,
33 server_request::{self, InitializeRequest, Request as ServerRequest},
34 server_response::{self, JsonRpcResult, Response as ServerResponse},
35};
36use objectiveai_sdk::mcp::resource::{
37 ListResourcesRequest, ReadResourceRequestParams, ReadResourceResult, Resource,
38};
39use objectiveai_sdk::mcp::tool::{
40 CallToolRequestParams, CallToolResult, ListToolsRequest, Tool,
41};
42use objectiveai_sdk::mcp::{Connection, Error as McpError};
43use tokio::sync::{RwLock, mpsc, oneshot};
44
45use crate::session::Session;
46use crate::session_manager::SessionManager;
47
48type ListChangedCb = Arc<dyn Fn() + Send + Sync>;
50
51struct Inner {
52 tx: mpsc::UnboundedSender<ServerRequest>,
55 pending: DashMap<String, oneshot::Sender<ServerResponse>>,
57 timeout: Duration,
59 list_changed: DashMap<McpKind, (Option<ListChangedCb>, Option<ListChangedCb>)>,
62 sessions: OnceLock<Arc<SessionManager>>,
68}
69
70#[derive(Clone)]
72pub struct ReverseChannel(Arc<Inner>);
73
74impl std::fmt::Debug for ReverseChannel {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.debug_struct("ReverseChannel").finish_non_exhaustive()
77 }
78}
79
80impl ReverseChannel {
81 pub fn new(timeout: Duration) -> (Self, mpsc::UnboundedReceiver<ServerRequest>) {
84 let (tx, rx) = mpsc::unbounded_channel();
85 let inner = Inner {
86 tx,
87 pending: DashMap::new(),
88 timeout,
89 list_changed: DashMap::new(),
90 sessions: OnceLock::new(),
91 };
92 (Self(Arc::new(inner)), rx)
93 }
94
95 pub(crate) fn wire_sessions(&self, sessions: Arc<SessionManager>) {
99 let _ = self.0.sessions.set(sessions);
100 }
101
102 async fn lookup_session(
111 &self,
112 response_id: &str,
113 ) -> Result<Arc<Session>, (i64, String)> {
114 let sessions = self
115 .0
116 .sessions
117 .get()
118 .ok_or((-32603i64, "proxy sessions not wired".to_string()))?;
119 sessions
120 .get_or_wait(response_id)
121 .await
122 .ok_or_else(|| (-32001i64, format!("unknown session for response id {response_id:?}")))
123 }
124
125 async fn request(
129 &self,
130 payload: server_request::Payload,
131 headers: IndexMap<String, String>,
132 ) -> Result<ServerResponse, McpError> {
133 let id = uuid::Uuid::new_v4().to_string();
134 let (resp_tx, resp_rx) = oneshot::channel();
135 self.0.pending.insert(id.clone(), resp_tx);
136 let request = ServerRequest {
137 id: id.clone(),
138 headers,
139 payload,
140 };
141 if self.0.tx.send(request).is_err() {
142 self.0.pending.remove(&id);
143 return Err(transport_error("reverse channel closed before send"));
144 }
145 match tokio::time::timeout(self.0.timeout, resp_rx).await {
146 Ok(Ok(response)) => Ok(response),
147 Ok(Err(_)) => {
148 self.0.pending.remove(&id);
149 Err(transport_error("reverse channel dropped before response"))
150 }
151 Err(_) => {
152 self.0.pending.remove(&id);
153 Err(transport_error("reverse channel timed out waiting for response"))
154 }
155 }
156 }
157
158 pub(crate) async fn drop_response(&self, response_id: String) {
163 let _ = self
164 .request(
165 server_request::Payload::Drop(server_request::DropRequest { response_id }),
166 IndexMap::new(),
167 )
168 .await;
169 }
170
171 pub async fn transfer_laboratories(
176 &self,
177 source_id: String,
178 dest_id: String,
179 source_path: String,
180 dest_path: String,
181 ) -> Result<server_response::LaboratoryTransferResult, McpError> {
182 let response = self
183 .request(
184 server_request::Payload::LaboratoryTransfer(
185 server_request::LaboratoryTransferRequest {
186 source_id,
187 dest_id,
188 source_path,
189 dest_path,
190 },
191 ),
192 IndexMap::new(),
193 )
194 .await?;
195 match response.payload {
196 server_response::Payload::LaboratoryTransfer(result) => {
197 unwrap_rpc("laboratory_transfer", result)
198 }
199 other => Err(variant_mismatch(
200 "laboratory_transfer",
201 "laboratory_transfer",
202 &other,
203 )),
204 }
205 }
206
207 pub fn deliver_response(&self, response: ServerResponse) {
211 if let Some((_, tx)) = self.0.pending.remove(&response.id) {
212 let _ = tx.send(response);
213 }
214 }
215
216 pub async fn deliver_client_request(
228 &self,
229 request: client_request::Request,
230 ) -> client_response::Response {
231 let client_request::Request { id, payload } = request;
232 match payload {
233 client_request::Payload::McpListChanged(change) => {
234 if let Some(cbs) = self.0.list_changed.get(&change.mcp_kind) {
235 let cb = match change.kind {
236 McpListChangedKind::Tools => cbs.0.clone(),
237 McpListChangedKind::Resources => cbs.1.clone(),
238 };
239 drop(cbs);
240 if let Some(cb) = cb {
241 cb();
242 }
243 }
244 client_response::Response::Ok { id }
245 }
246 client_request::Payload::ListTools {
252 response_id, name, ..
253 } => {
254 let result = match self.lookup_session(&response_id).await {
255 Ok(session) => {
256 match session.list_tools_filtered(None, name.as_deref()).await {
257 Ok(result) => JsonRpcResult::Ok { result },
258 Err(e) => rpc_err_result(-32603, format!("list_tools: {e}")),
259 }
260 }
261 Err((code, message)) => rpc_err_result(code, message),
262 };
263 client_response::Response::ListTools { id, result }
264 }
265 client_request::Payload::ListResources {
266 response_id, name, ..
267 } => {
268 let result = match self.lookup_session(&response_id).await {
269 Ok(session) => {
270 match session.list_resources_filtered(None, name.as_deref()).await {
271 Ok(result) => JsonRpcResult::Ok { result },
272 Err(e) => rpc_err_result(-32603, format!("list_resources: {e}")),
273 }
274 }
275 Err((code, message)) => rpc_err_result(code, message),
276 };
277 client_response::Response::ListResources { id, result }
278 }
279 client_request::Payload::ListServers { response_id } => {
280 let result = match self.lookup_session(&response_id).await {
281 Ok(session) => JsonRpcResult::Ok {
283 result: session.list_servers(),
284 },
285 Err((code, message)) => rpc_err_result(code, message),
286 };
287 client_response::Response::ListServers { id, result }
288 }
289 client_request::Payload::CallTool { response_id, params } => {
290 let result = match self.lookup_session(&response_id).await {
291 Ok(session) => match session.call_tool(¶ms).await {
294 Ok(result) => JsonRpcResult::Ok { result },
295 Err(crate::session::CallToolError::ToolNotFound(name)) => {
296 rpc_err_result(-32601, format!("tool not found: {name}"))
297 }
298 Err(crate::session::CallToolError::Upstream(e)) => {
299 rpc_err_result(-32603, format!("upstream call_tool: {e}"))
300 }
301 },
302 Err((code, message)) => rpc_err_result(code, message),
303 };
304 client_response::Response::CallTool { id, result }
305 }
306 client_request::Payload::ReadResource { response_id, params } => {
307 let result = match self.lookup_session(&response_id).await {
308 Ok(session) => match session.read_resource(¶ms.uri).await {
309 Ok(result) => JsonRpcResult::Ok { result },
310 Err(crate::session::ReadResourceError::ResourceNotFound(uri)) => {
311 rpc_err_result(-32602, format!("resource not found: {uri}"))
312 }
313 Err(crate::session::ReadResourceError::Upstream(e)) => {
314 rpc_err_result(-32603, format!("upstream read_resource: {e}"))
315 }
316 },
317 Err((code, message)) => rpc_err_result(code, message),
318 };
319 client_response::Response::ReadResource { id, result }
320 }
321 }
322 }
323
324 fn set_tools_list_changed(&self, mcp_kind: McpKind, cb: ListChangedCb) {
325 let mut entry = self.0.list_changed.entry(mcp_kind).or_default();
326 entry.0 = Some(cb);
327 }
328
329 fn set_resources_list_changed(&self, mcp_kind: McpKind, cb: ListChangedCb) {
330 let mut entry = self.0.list_changed.entry(mcp_kind).or_default();
331 entry.1 = Some(cb);
332 }
333}
334
335pub struct WsUpstream {
340 channel: ReverseChannel,
341 mcp_kind: McpKind,
342 pub url: String,
344 pub session_id: String,
346 server_name: String,
349 server_version: String,
350 initialize_result: objectiveai_sdk::mcp::initialize_result::InitializeResult,
355 laboratory: Option<objectiveai_sdk::laboratories::Laboratory>,
359 has_tools_cap: bool,
368 has_resources_cap: bool,
369 base_headers: IndexMap<String, String>,
377 extra_headers: RwLock<IndexMap<String, String>>,
383}
384
385impl std::fmt::Debug for WsUpstream {
386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387 f.debug_struct("WsUpstream")
388 .field("url", &self.url)
389 .field("session_id", &self.session_id)
390 .finish_non_exhaustive()
391 }
392}
393
394impl WsUpstream {
395 async fn headers(&self) -> IndexMap<String, String> {
403 let mut h = self.base_headers.clone();
404 for (k, v) in self.extra_headers.read().await.iter() {
405 h.insert(k.clone(), v.clone());
406 }
407 h.insert(
408 crate::upstream::MCP_SESSION_ID_KEY.to_string(),
409 self.session_id.clone(),
410 );
411 h
412 }
413
414 pub async fn list_tools(&self) -> Result<Arc<Vec<Tool>>, Arc<McpError>> {
415 if !self.has_tools_cap {
418 return Ok(Arc::new(Vec::new()));
419 }
420 let headers = self.headers().await;
421 let response = self
422 .channel
423 .request(
424 server_request::Payload::ToolsList {
425 mcp_kind: self.mcp_kind.clone(),
426 params: ListToolsRequest { cursor: None },
427 },
428 headers,
429 )
430 .await
431 .map_err(Arc::new)?;
432 match response.payload {
433 server_response::Payload::ToolsList { result, .. } => {
434 Ok(Arc::new(unwrap_rpc(&self.url, result).map_err(Arc::new)?.tools))
435 }
436 other => Err(Arc::new(variant_mismatch(&self.url, "tools_list", &other))),
437 }
438 }
439
440 pub async fn list_resources(&self) -> Result<Arc<Vec<Resource>>, Arc<McpError>> {
441 if !self.has_resources_cap {
445 return Ok(Arc::new(Vec::new()));
446 }
447 let headers = self.headers().await;
448 let response = self
449 .channel
450 .request(
451 server_request::Payload::ResourcesList {
452 mcp_kind: self.mcp_kind.clone(),
453 params: ListResourcesRequest { cursor: None },
454 },
455 headers,
456 )
457 .await
458 .map_err(Arc::new)?;
459 match response.payload {
460 server_response::Payload::ResourcesList { result, .. } => {
461 Ok(Arc::new(unwrap_rpc(&self.url, result).map_err(Arc::new)?.resources))
462 }
463 other => Err(Arc::new(variant_mismatch(&self.url, "resources_list", &other))),
464 }
465 }
466
467 pub async fn call_tool(
468 &self,
469 params: &CallToolRequestParams,
470 ) -> Result<CallToolResult, McpError> {
471 let headers = self.headers().await;
472 let response = self
473 .channel
474 .request(
475 server_request::Payload::ToolsCall {
476 mcp_kind: self.mcp_kind.clone(),
477 params: params.clone(),
478 },
479 headers,
480 )
481 .await?;
482 match response.payload {
483 server_response::Payload::ToolsCall { result, .. } => unwrap_rpc(&self.url, result),
484 other => Err(variant_mismatch(&self.url, "tools_call", &other)),
485 }
486 }
487
488 pub async fn read_resource(&self, uri: &str) -> Result<ReadResourceResult, McpError> {
489 let headers = self.headers().await;
490 let response = self
491 .channel
492 .request(
493 server_request::Payload::ResourcesRead {
494 mcp_kind: self.mcp_kind.clone(),
495 params: ReadResourceRequestParams {
496 uri: uri.to_string(),
497 },
498 },
499 headers,
500 )
501 .await?;
502 match response.payload {
503 server_response::Payload::ResourcesRead { result, .. } => unwrap_rpc(&self.url, result),
504 other => Err(variant_mismatch(&self.url, "resources_read", &other)),
505 }
506 }
507
508 pub async fn delete(&self) -> Result<(), McpError> {
509 let headers = self.headers().await;
510 let response = self
511 .channel
512 .request(
513 server_request::Payload::SessionTerminate {
514 mcp_kind: self.mcp_kind.clone(),
515 },
516 headers,
517 )
518 .await?;
519 match response.payload {
520 server_response::Payload::SessionTerminate { result, .. } => unwrap_rpc(&self.url, result),
521 other => Err(variant_mismatch(&self.url, "session_terminate", &other)),
522 }
523 }
524
525 pub fn set_on_tools_list_changed<F>(&self, callback: F)
526 where
527 F: Fn() + Send + Sync + 'static,
528 {
529 self.channel
530 .set_tools_list_changed(self.mcp_kind.clone(), Arc::new(callback));
531 }
532
533 pub fn set_on_resources_list_changed<F>(&self, callback: F)
534 where
535 F: Fn() + Send + Sync + 'static,
536 {
537 self.channel
538 .set_resources_list_changed(self.mcp_kind.clone(), Arc::new(callback));
539 }
540
541 pub async fn set_extra_headers(&self, extras: IndexMap<String, String>) {
542 *self.extra_headers.write().await = extras;
543 }
544}
545
546#[derive(Debug)]
549pub enum Upstream {
550 Http(Connection),
551 Ws(WsUpstream),
552}
553
554impl Upstream {
555 pub fn is_ws(&self) -> bool {
558 matches!(self, Upstream::Ws(_))
559 }
560
561 pub fn url(&self) -> &str {
562 match self {
563 Upstream::Http(c) => &c.url,
564 Upstream::Ws(w) => &w.url,
565 }
566 }
567
568 pub fn session_id(&self) -> &str {
569 match self {
570 Upstream::Http(c) => &c.session_id,
571 Upstream::Ws(w) => &w.session_id,
572 }
573 }
574
575 pub fn server_name(&self) -> &str {
578 match self {
579 Upstream::Http(c) => &c.initialize_result.server_info.name,
580 Upstream::Ws(w) => &w.server_name,
581 }
582 }
583
584 pub fn server_version(&self) -> &str {
586 match self {
587 Upstream::Http(c) => &c.initialize_result.server_info.version,
588 Upstream::Ws(w) => &w.server_version,
589 }
590 }
591
592 pub fn initialize_result(
595 &self,
596 ) -> &objectiveai_sdk::mcp::initialize_result::InitializeResult {
597 match self {
598 Upstream::Http(c) => &c.initialize_result,
599 Upstream::Ws(w) => &w.initialize_result,
600 }
601 }
602
603 pub fn laboratory(&self) -> Option<objectiveai_sdk::laboratories::Laboratory> {
611 match self {
612 Upstream::Http(_) => None,
613 Upstream::Ws(w) => w.laboratory.clone(),
614 }
615 }
616
617 pub fn plugin(&self) -> Option<objectiveai_sdk::mcp::server::Plugin> {
622 match self {
623 Upstream::Http(_) => None,
624 Upstream::Ws(w) => match &w.mcp_kind {
625 McpKind::Plugin {
626 owner,
627 name,
628 version,
629 mcp,
630 } => Some(objectiveai_sdk::mcp::server::Plugin {
631 owner: owner.clone(),
632 name: name.clone(),
633 version: version.clone(),
634 mcp: mcp.clone(),
635 }),
636 McpKind::ObjectiveAi | McpKind::Laboratory { .. } => None,
637 },
638 }
639 }
640
641 pub fn reverse_channel(&self) -> Option<&ReverseChannel> {
646 match self {
647 Upstream::Http(_) => None,
648 Upstream::Ws(w) => Some(&w.channel),
649 }
650 }
651
652 pub async fn list_tools(&self) -> Result<Arc<Vec<Tool>>, Arc<McpError>> {
653 match self {
654 Upstream::Http(c) => c.list_tools().await,
655 Upstream::Ws(w) => w.list_tools().await,
656 }
657 }
658
659 pub async fn list_resources(&self) -> Result<Arc<Vec<Resource>>, Arc<McpError>> {
660 match self {
661 Upstream::Http(c) => c.list_resources().await,
662 Upstream::Ws(w) => w.list_resources().await,
663 }
664 }
665
666 pub async fn call_tool(
667 &self,
668 params: &CallToolRequestParams,
669 ) -> Result<CallToolResult, McpError> {
670 match self {
671 Upstream::Http(c) => c.call_tool(params).await,
672 Upstream::Ws(w) => w.call_tool(params).await,
673 }
674 }
675
676 pub async fn read_resource(&self, uri: &str) -> Result<ReadResourceResult, McpError> {
677 match self {
678 Upstream::Http(c) => c.read_resource(uri).await,
679 Upstream::Ws(w) => w.read_resource(uri).await,
680 }
681 }
682
683 pub async fn delete(&self) -> Result<(), McpError> {
684 match self {
685 Upstream::Http(c) => c.delete().await,
686 Upstream::Ws(w) => w.delete().await,
687 }
688 }
689
690 pub fn set_on_tools_list_changed<F>(&self, callback: F)
691 where
692 F: Fn() + Send + Sync + 'static,
693 {
694 match self {
695 Upstream::Http(c) => c.set_on_tools_list_changed(callback),
696 Upstream::Ws(w) => w.set_on_tools_list_changed(callback),
697 }
698 }
699
700 pub fn set_on_resources_list_changed<F>(&self, callback: F)
701 where
702 F: Fn() + Send + Sync + 'static,
703 {
704 match self {
705 Upstream::Http(c) => c.set_on_resources_list_changed(callback),
706 Upstream::Ws(w) => w.set_on_resources_list_changed(callback),
707 }
708 }
709
710 pub async fn set_extra_headers(&self, extras: IndexMap<String, String>) {
711 match self {
712 Upstream::Http(c) => c.set_extra_headers(extras).await,
713 Upstream::Ws(w) => w.set_extra_headers(extras).await,
714 }
715 }
716}
717
718pub fn parse_ws_mcp_kind(url: &str) -> Option<McpKind> {
727 let rest = url.strip_prefix("ws://")?;
728 let rest = rest.split('?').next().unwrap_or(rest);
730 if rest == "objectiveai" {
732 return Some(McpKind::ObjectiveAi);
733 }
734 let path = rest.strip_prefix('/')?;
736 let parts: Vec<&str> = path.split('/').collect();
737 if let [owner, name, version, mcp] = parts.as_slice() {
738 if !owner.is_empty() && !name.is_empty() && !version.is_empty() && !mcp.is_empty() {
739 return Some(McpKind::Plugin {
740 owner: (*owner).to_string(),
741 name: (*name).to_string(),
742 version: (*version).to_string(),
743 mcp: (*mcp).to_string(),
744 });
745 }
746 }
747 None
748}
749
750pub async fn connect_ws(
756 channel: ReverseChannel,
757 url: String,
758 mcp_kind: McpKind,
759 args: IndexMap<String, Option<String>>,
760 mut headers: IndexMap<String, String>,
761 laboratory: Option<objectiveai_sdk::laboratories::Laboratory>,
762) -> Result<WsUpstream, McpError> {
763 let response = channel
764 .request(
765 server_request::Payload::Initialize {
766 mcp_kind: mcp_kind.clone(),
767 params: InitializeRequest { args },
768 },
769 headers.clone(),
770 )
771 .await?;
772 let reply = match response.payload {
773 server_response::Payload::Initialize { result, .. } => unwrap_rpc(&url, result)?,
774 other => return Err(variant_mismatch(&url, "initialize", &other)),
775 };
776 headers.shift_remove(crate::upstream::MCP_SESSION_ID_KEY);
781 let session_id = reply.mcp_session_id;
782 let initialize_result = reply.result;
783 let has_tools_cap = initialize_result.capabilities.tools.is_some();
784 let has_resources_cap = initialize_result.capabilities.resources.is_some();
785 let server_name = initialize_result.server_info.name.clone();
786 let server_version = initialize_result.server_info.version.clone();
787 Ok(WsUpstream {
788 channel,
789 mcp_kind,
790 url,
791 session_id,
792 server_name,
793 server_version,
794 initialize_result,
795 laboratory,
796 has_tools_cap,
797 has_resources_cap,
798 base_headers: headers,
804 extra_headers: RwLock::new(IndexMap::new()),
805 })
806}
807
808fn unwrap_rpc<R>(url: &str, result: JsonRpcResult<R>) -> Result<R, McpError> {
809 match result {
810 JsonRpcResult::Ok { result } => Ok(result),
811 JsonRpcResult::Err {
812 code,
813 message,
814 data,
815 } => Err(McpError::JsonRpc {
816 url: url.to_string(),
817 code,
818 message,
819 data,
820 }),
821 }
822}
823
824fn transport_error(message: &str) -> McpError {
825 McpError::MalformedResponse {
826 url: "ws".to_string(),
827 message: message.to_string(),
828 }
829}
830
831fn rpc_err_result<R>(code: i64, message: String) -> JsonRpcResult<R> {
835 JsonRpcResult::Err {
836 code,
837 message,
838 data: None,
839 }
840}
841
842fn variant_mismatch(url: &str, expected: &str, got: &server_response::Payload) -> McpError {
843 McpError::MalformedResponse {
844 url: url.to_string(),
845 message: format!(
846 "reverse channel returned wrong payload variant: expected {expected}, got {}",
847 got_variant_name(got),
848 ),
849 }
850}
851
852fn got_variant_name(p: &server_response::Payload) -> &'static str {
853 use server_response::Payload as P;
854 match p {
855 P::Initialize { .. } => "initialize",
856 P::ToolsList { .. } => "tools_list",
857 P::ToolsCall { .. } => "tools_call",
858 P::ResourcesList { .. } => "resources_list",
859 P::ResourcesRead { .. } => "resources_read",
860 P::SessionTerminate { .. } => "session_terminate",
861 P::ReadMessageQueue(_) => "read_message_queue",
862 P::Retrieve(_) => "retrieve",
863 P::Drop(_) => "drop",
864 P::LaboratoryTransfer(_) => "laboratory_transfer",
865 }
866}