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 fn lookup_session(&self, response_id: &str) -> Result<Arc<Session>, (i64, String)> {
106 let sessions = self
107 .0
108 .sessions
109 .get()
110 .ok_or((-32603i64, "proxy sessions not wired".to_string()))?;
111 sessions
112 .get(response_id)
113 .ok_or_else(|| (-32001i64, format!("unknown session for response id {response_id:?}")))
114 }
115
116 async fn request(
120 &self,
121 payload: server_request::Payload,
122 headers: IndexMap<String, String>,
123 ) -> Result<ServerResponse, McpError> {
124 let id = uuid::Uuid::new_v4().to_string();
125 let (resp_tx, resp_rx) = oneshot::channel();
126 self.0.pending.insert(id.clone(), resp_tx);
127 let request = ServerRequest {
128 id: id.clone(),
129 headers,
130 payload,
131 };
132 if self.0.tx.send(request).is_err() {
133 self.0.pending.remove(&id);
134 return Err(transport_error("reverse channel closed before send"));
135 }
136 match tokio::time::timeout(self.0.timeout, resp_rx).await {
137 Ok(Ok(response)) => Ok(response),
138 Ok(Err(_)) => {
139 self.0.pending.remove(&id);
140 Err(transport_error("reverse channel dropped before response"))
141 }
142 Err(_) => {
143 self.0.pending.remove(&id);
144 Err(transport_error("reverse channel timed out waiting for response"))
145 }
146 }
147 }
148
149 pub(crate) async fn drop_response(&self, response_id: String) {
154 let _ = self
155 .request(
156 server_request::Payload::Drop(server_request::DropRequest { response_id }),
157 IndexMap::new(),
158 )
159 .await;
160 }
161
162 pub async fn transfer_laboratories(
167 &self,
168 source_id: String,
169 dest_id: String,
170 source_path: String,
171 dest_path: String,
172 ) -> Result<server_response::LaboratoryTransferResult, McpError> {
173 let response = self
174 .request(
175 server_request::Payload::LaboratoryTransfer(
176 server_request::LaboratoryTransferRequest {
177 source_id,
178 dest_id,
179 source_path,
180 dest_path,
181 },
182 ),
183 IndexMap::new(),
184 )
185 .await?;
186 match response.payload {
187 server_response::Payload::LaboratoryTransfer(result) => {
188 unwrap_rpc("laboratory_transfer", result)
189 }
190 other => Err(variant_mismatch(
191 "laboratory_transfer",
192 "laboratory_transfer",
193 &other,
194 )),
195 }
196 }
197
198 pub fn deliver_response(&self, response: ServerResponse) {
202 if let Some((_, tx)) = self.0.pending.remove(&response.id) {
203 let _ = tx.send(response);
204 }
205 }
206
207 pub async fn deliver_client_request(
219 &self,
220 request: client_request::Request,
221 ) -> client_response::Response {
222 let client_request::Request { id, payload } = request;
223 match payload {
224 client_request::Payload::McpListChanged(change) => {
225 if let Some(cbs) = self.0.list_changed.get(&change.mcp_kind) {
226 let cb = match change.kind {
227 McpListChangedKind::Tools => cbs.0.clone(),
228 McpListChangedKind::Resources => cbs.1.clone(),
229 };
230 drop(cbs);
231 if let Some(cb) = cb {
232 cb();
233 }
234 }
235 client_response::Response::Ok { id }
236 }
237 client_request::Payload::ListTools {
243 response_id, name, ..
244 } => {
245 let result = match self.lookup_session(&response_id) {
246 Ok(session) => {
247 match session.list_tools_filtered(None, name.as_deref()).await {
248 Ok(result) => JsonRpcResult::Ok { result },
249 Err(e) => rpc_err_result(-32603, format!("list_tools: {e}")),
250 }
251 }
252 Err((code, message)) => rpc_err_result(code, message),
253 };
254 client_response::Response::ListTools { id, result }
255 }
256 client_request::Payload::ListResources {
257 response_id, name, ..
258 } => {
259 let result = match self.lookup_session(&response_id) {
260 Ok(session) => {
261 match session.list_resources_filtered(None, name.as_deref()).await {
262 Ok(result) => JsonRpcResult::Ok { result },
263 Err(e) => rpc_err_result(-32603, format!("list_resources: {e}")),
264 }
265 }
266 Err((code, message)) => rpc_err_result(code, message),
267 };
268 client_response::Response::ListResources { id, result }
269 }
270 client_request::Payload::ListServers { response_id } => {
271 let result = match self.lookup_session(&response_id) {
272 Ok(session) => JsonRpcResult::Ok {
274 result: session.list_servers(),
275 },
276 Err((code, message)) => rpc_err_result(code, message),
277 };
278 client_response::Response::ListServers { id, result }
279 }
280 client_request::Payload::CallTool { response_id, params } => {
281 let result = match self.lookup_session(&response_id) {
282 Ok(session) => match session.call_tool(¶ms).await {
285 Ok(result) => JsonRpcResult::Ok { result },
286 Err(crate::session::CallToolError::ToolNotFound(name)) => {
287 rpc_err_result(-32601, format!("tool not found: {name}"))
288 }
289 Err(crate::session::CallToolError::Upstream(e)) => {
290 rpc_err_result(-32603, format!("upstream call_tool: {e}"))
291 }
292 },
293 Err((code, message)) => rpc_err_result(code, message),
294 };
295 client_response::Response::CallTool { id, result }
296 }
297 client_request::Payload::ReadResource { response_id, params } => {
298 let result = match self.lookup_session(&response_id) {
299 Ok(session) => match session.read_resource(¶ms.uri).await {
300 Ok(result) => JsonRpcResult::Ok { result },
301 Err(crate::session::ReadResourceError::ResourceNotFound(uri)) => {
302 rpc_err_result(-32602, format!("resource not found: {uri}"))
303 }
304 Err(crate::session::ReadResourceError::Upstream(e)) => {
305 rpc_err_result(-32603, format!("upstream read_resource: {e}"))
306 }
307 },
308 Err((code, message)) => rpc_err_result(code, message),
309 };
310 client_response::Response::ReadResource { id, result }
311 }
312 }
313 }
314
315 fn set_tools_list_changed(&self, mcp_kind: McpKind, cb: ListChangedCb) {
316 let mut entry = self.0.list_changed.entry(mcp_kind).or_default();
317 entry.0 = Some(cb);
318 }
319
320 fn set_resources_list_changed(&self, mcp_kind: McpKind, cb: ListChangedCb) {
321 let mut entry = self.0.list_changed.entry(mcp_kind).or_default();
322 entry.1 = Some(cb);
323 }
324}
325
326pub struct WsUpstream {
331 channel: ReverseChannel,
332 mcp_kind: McpKind,
333 pub url: String,
335 pub session_id: String,
337 server_name: String,
340 server_version: String,
341 initialize_result: objectiveai_sdk::mcp::initialize_result::InitializeResult,
346 laboratory: Option<objectiveai_sdk::laboratories::Laboratory>,
350 has_tools_cap: bool,
359 has_resources_cap: bool,
360 base_headers: IndexMap<String, String>,
368 extra_headers: RwLock<IndexMap<String, String>>,
374}
375
376impl std::fmt::Debug for WsUpstream {
377 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378 f.debug_struct("WsUpstream")
379 .field("url", &self.url)
380 .field("session_id", &self.session_id)
381 .finish_non_exhaustive()
382 }
383}
384
385impl WsUpstream {
386 async fn headers(&self) -> IndexMap<String, String> {
394 let mut h = self.base_headers.clone();
395 for (k, v) in self.extra_headers.read().await.iter() {
396 h.insert(k.clone(), v.clone());
397 }
398 h.insert(
399 crate::upstream::MCP_SESSION_ID_KEY.to_string(),
400 self.session_id.clone(),
401 );
402 h
403 }
404
405 pub async fn list_tools(&self) -> Result<Arc<Vec<Tool>>, Arc<McpError>> {
406 if !self.has_tools_cap {
409 return Ok(Arc::new(Vec::new()));
410 }
411 let headers = self.headers().await;
412 let response = self
413 .channel
414 .request(
415 server_request::Payload::ToolsList {
416 mcp_kind: self.mcp_kind.clone(),
417 params: ListToolsRequest { cursor: None },
418 },
419 headers,
420 )
421 .await
422 .map_err(Arc::new)?;
423 match response.payload {
424 server_response::Payload::ToolsList { result, .. } => {
425 Ok(Arc::new(unwrap_rpc(&self.url, result).map_err(Arc::new)?.tools))
426 }
427 other => Err(Arc::new(variant_mismatch(&self.url, "tools_list", &other))),
428 }
429 }
430
431 pub async fn list_resources(&self) -> Result<Arc<Vec<Resource>>, Arc<McpError>> {
432 if !self.has_resources_cap {
436 return Ok(Arc::new(Vec::new()));
437 }
438 let headers = self.headers().await;
439 let response = self
440 .channel
441 .request(
442 server_request::Payload::ResourcesList {
443 mcp_kind: self.mcp_kind.clone(),
444 params: ListResourcesRequest { cursor: None },
445 },
446 headers,
447 )
448 .await
449 .map_err(Arc::new)?;
450 match response.payload {
451 server_response::Payload::ResourcesList { result, .. } => {
452 Ok(Arc::new(unwrap_rpc(&self.url, result).map_err(Arc::new)?.resources))
453 }
454 other => Err(Arc::new(variant_mismatch(&self.url, "resources_list", &other))),
455 }
456 }
457
458 pub async fn call_tool(
459 &self,
460 params: &CallToolRequestParams,
461 ) -> Result<CallToolResult, McpError> {
462 let headers = self.headers().await;
463 let response = self
464 .channel
465 .request(
466 server_request::Payload::ToolsCall {
467 mcp_kind: self.mcp_kind.clone(),
468 params: params.clone(),
469 },
470 headers,
471 )
472 .await?;
473 match response.payload {
474 server_response::Payload::ToolsCall { result, .. } => unwrap_rpc(&self.url, result),
475 other => Err(variant_mismatch(&self.url, "tools_call", &other)),
476 }
477 }
478
479 pub async fn read_resource(&self, uri: &str) -> Result<ReadResourceResult, McpError> {
480 let headers = self.headers().await;
481 let response = self
482 .channel
483 .request(
484 server_request::Payload::ResourcesRead {
485 mcp_kind: self.mcp_kind.clone(),
486 params: ReadResourceRequestParams {
487 uri: uri.to_string(),
488 },
489 },
490 headers,
491 )
492 .await?;
493 match response.payload {
494 server_response::Payload::ResourcesRead { result, .. } => unwrap_rpc(&self.url, result),
495 other => Err(variant_mismatch(&self.url, "resources_read", &other)),
496 }
497 }
498
499 pub async fn delete(&self) -> Result<(), McpError> {
500 let headers = self.headers().await;
501 let response = self
502 .channel
503 .request(
504 server_request::Payload::SessionTerminate {
505 mcp_kind: self.mcp_kind.clone(),
506 },
507 headers,
508 )
509 .await?;
510 match response.payload {
511 server_response::Payload::SessionTerminate { result, .. } => unwrap_rpc(&self.url, result),
512 other => Err(variant_mismatch(&self.url, "session_terminate", &other)),
513 }
514 }
515
516 pub fn set_on_tools_list_changed<F>(&self, callback: F)
517 where
518 F: Fn() + Send + Sync + 'static,
519 {
520 self.channel
521 .set_tools_list_changed(self.mcp_kind.clone(), Arc::new(callback));
522 }
523
524 pub fn set_on_resources_list_changed<F>(&self, callback: F)
525 where
526 F: Fn() + Send + Sync + 'static,
527 {
528 self.channel
529 .set_resources_list_changed(self.mcp_kind.clone(), Arc::new(callback));
530 }
531
532 pub async fn set_extra_headers(&self, extras: IndexMap<String, String>) {
533 *self.extra_headers.write().await = extras;
534 }
535}
536
537#[derive(Debug)]
540pub enum Upstream {
541 Http(Connection),
542 Ws(WsUpstream),
543}
544
545impl Upstream {
546 pub fn is_ws(&self) -> bool {
549 matches!(self, Upstream::Ws(_))
550 }
551
552 pub fn url(&self) -> &str {
553 match self {
554 Upstream::Http(c) => &c.url,
555 Upstream::Ws(w) => &w.url,
556 }
557 }
558
559 pub fn session_id(&self) -> &str {
560 match self {
561 Upstream::Http(c) => &c.session_id,
562 Upstream::Ws(w) => &w.session_id,
563 }
564 }
565
566 pub fn server_name(&self) -> &str {
569 match self {
570 Upstream::Http(c) => &c.initialize_result.server_info.name,
571 Upstream::Ws(w) => &w.server_name,
572 }
573 }
574
575 pub fn server_version(&self) -> &str {
577 match self {
578 Upstream::Http(c) => &c.initialize_result.server_info.version,
579 Upstream::Ws(w) => &w.server_version,
580 }
581 }
582
583 pub fn initialize_result(
586 &self,
587 ) -> &objectiveai_sdk::mcp::initialize_result::InitializeResult {
588 match self {
589 Upstream::Http(c) => &c.initialize_result,
590 Upstream::Ws(w) => &w.initialize_result,
591 }
592 }
593
594 pub fn laboratory(&self) -> Option<objectiveai_sdk::laboratories::Laboratory> {
602 match self {
603 Upstream::Http(_) => None,
604 Upstream::Ws(w) => w.laboratory.clone(),
605 }
606 }
607
608 pub fn plugin(&self) -> Option<objectiveai_sdk::mcp::server::Plugin> {
613 match self {
614 Upstream::Http(_) => None,
615 Upstream::Ws(w) => match &w.mcp_kind {
616 McpKind::Plugin {
617 owner,
618 name,
619 version,
620 mcp,
621 } => Some(objectiveai_sdk::mcp::server::Plugin {
622 owner: owner.clone(),
623 name: name.clone(),
624 version: version.clone(),
625 mcp: mcp.clone(),
626 }),
627 McpKind::ObjectiveAi | McpKind::Laboratory { .. } => None,
628 },
629 }
630 }
631
632 pub fn reverse_channel(&self) -> Option<&ReverseChannel> {
637 match self {
638 Upstream::Http(_) => None,
639 Upstream::Ws(w) => Some(&w.channel),
640 }
641 }
642
643 pub async fn list_tools(&self) -> Result<Arc<Vec<Tool>>, Arc<McpError>> {
644 match self {
645 Upstream::Http(c) => c.list_tools().await,
646 Upstream::Ws(w) => w.list_tools().await,
647 }
648 }
649
650 pub async fn list_resources(&self) -> Result<Arc<Vec<Resource>>, Arc<McpError>> {
651 match self {
652 Upstream::Http(c) => c.list_resources().await,
653 Upstream::Ws(w) => w.list_resources().await,
654 }
655 }
656
657 pub async fn call_tool(
658 &self,
659 params: &CallToolRequestParams,
660 ) -> Result<CallToolResult, McpError> {
661 match self {
662 Upstream::Http(c) => c.call_tool(params).await,
663 Upstream::Ws(w) => w.call_tool(params).await,
664 }
665 }
666
667 pub async fn read_resource(&self, uri: &str) -> Result<ReadResourceResult, McpError> {
668 match self {
669 Upstream::Http(c) => c.read_resource(uri).await,
670 Upstream::Ws(w) => w.read_resource(uri).await,
671 }
672 }
673
674 pub async fn delete(&self) -> Result<(), McpError> {
675 match self {
676 Upstream::Http(c) => c.delete().await,
677 Upstream::Ws(w) => w.delete().await,
678 }
679 }
680
681 pub fn set_on_tools_list_changed<F>(&self, callback: F)
682 where
683 F: Fn() + Send + Sync + 'static,
684 {
685 match self {
686 Upstream::Http(c) => c.set_on_tools_list_changed(callback),
687 Upstream::Ws(w) => w.set_on_tools_list_changed(callback),
688 }
689 }
690
691 pub fn set_on_resources_list_changed<F>(&self, callback: F)
692 where
693 F: Fn() + Send + Sync + 'static,
694 {
695 match self {
696 Upstream::Http(c) => c.set_on_resources_list_changed(callback),
697 Upstream::Ws(w) => w.set_on_resources_list_changed(callback),
698 }
699 }
700
701 pub async fn set_extra_headers(&self, extras: IndexMap<String, String>) {
702 match self {
703 Upstream::Http(c) => c.set_extra_headers(extras).await,
704 Upstream::Ws(w) => w.set_extra_headers(extras).await,
705 }
706 }
707}
708
709pub fn parse_ws_mcp_kind(url: &str) -> Option<McpKind> {
718 let rest = url.strip_prefix("ws://")?;
719 let rest = rest.split('?').next().unwrap_or(rest);
721 if rest == "objectiveai" {
723 return Some(McpKind::ObjectiveAi);
724 }
725 let path = rest.strip_prefix('/')?;
727 let parts: Vec<&str> = path.split('/').collect();
728 if let [owner, name, version, mcp] = parts.as_slice() {
729 if !owner.is_empty() && !name.is_empty() && !version.is_empty() && !mcp.is_empty() {
730 return Some(McpKind::Plugin {
731 owner: (*owner).to_string(),
732 name: (*name).to_string(),
733 version: (*version).to_string(),
734 mcp: (*mcp).to_string(),
735 });
736 }
737 }
738 None
739}
740
741pub async fn connect_ws(
747 channel: ReverseChannel,
748 url: String,
749 mcp_kind: McpKind,
750 args: IndexMap<String, Option<String>>,
751 mut headers: IndexMap<String, String>,
752 laboratory: Option<objectiveai_sdk::laboratories::Laboratory>,
753) -> Result<WsUpstream, McpError> {
754 let response = channel
755 .request(
756 server_request::Payload::Initialize {
757 mcp_kind: mcp_kind.clone(),
758 params: InitializeRequest { args },
759 },
760 headers.clone(),
761 )
762 .await?;
763 let reply = match response.payload {
764 server_response::Payload::Initialize { result, .. } => unwrap_rpc(&url, result)?,
765 other => return Err(variant_mismatch(&url, "initialize", &other)),
766 };
767 headers.shift_remove(crate::upstream::MCP_SESSION_ID_KEY);
772 let session_id = reply.mcp_session_id;
773 let initialize_result = reply.result;
774 let has_tools_cap = initialize_result.capabilities.tools.is_some();
775 let has_resources_cap = initialize_result.capabilities.resources.is_some();
776 let server_name = initialize_result.server_info.name.clone();
777 let server_version = initialize_result.server_info.version.clone();
778 Ok(WsUpstream {
779 channel,
780 mcp_kind,
781 url,
782 session_id,
783 server_name,
784 server_version,
785 initialize_result,
786 laboratory,
787 has_tools_cap,
788 has_resources_cap,
789 base_headers: headers,
795 extra_headers: RwLock::new(IndexMap::new()),
796 })
797}
798
799fn unwrap_rpc<R>(url: &str, result: JsonRpcResult<R>) -> Result<R, McpError> {
800 match result {
801 JsonRpcResult::Ok { result } => Ok(result),
802 JsonRpcResult::Err {
803 code,
804 message,
805 data,
806 } => Err(McpError::JsonRpc {
807 url: url.to_string(),
808 code,
809 message,
810 data,
811 }),
812 }
813}
814
815fn transport_error(message: &str) -> McpError {
816 McpError::MalformedResponse {
817 url: "ws".to_string(),
818 message: message.to_string(),
819 }
820}
821
822fn rpc_err_result<R>(code: i64, message: String) -> JsonRpcResult<R> {
826 JsonRpcResult::Err {
827 code,
828 message,
829 data: None,
830 }
831}
832
833fn variant_mismatch(url: &str, expected: &str, got: &server_response::Payload) -> McpError {
834 McpError::MalformedResponse {
835 url: url.to_string(),
836 message: format!(
837 "reverse channel returned wrong payload variant: expected {expected}, got {}",
838 got_variant_name(got),
839 ),
840 }
841}
842
843fn got_variant_name(p: &server_response::Payload) -> &'static str {
844 use server_response::Payload as P;
845 match p {
846 P::Initialize { .. } => "initialize",
847 P::ToolsList { .. } => "tools_list",
848 P::ToolsCall { .. } => "tools_call",
849 P::ResourcesList { .. } => "resources_list",
850 P::ResourcesRead { .. } => "resources_read",
851 P::SessionTerminate { .. } => "session_terminate",
852 P::ReadMessageQueue(_) => "read_message_queue",
853 P::Retrieve(_) => "retrieve",
854 P::Drop(_) => "drop",
855 P::LaboratoryTransfer(_) => "laboratory_transfer",
856 }
857}