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>>,
62 list_changed: DashMap<McpKind, (Option<ListChangedCb>, Option<ListChangedCb>)>,
65 sessions: OnceLock<Arc<SessionManager>>,
71}
72
73#[derive(Clone)]
75pub struct ReverseChannel(Arc<Inner>);
76
77impl std::fmt::Debug for ReverseChannel {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_struct("ReverseChannel").finish_non_exhaustive()
80 }
81}
82
83impl ReverseChannel {
84 pub fn new() -> (Self, mpsc::UnboundedReceiver<ServerRequest>) {
87 let (tx, rx) = mpsc::unbounded_channel();
88 let inner = Inner {
89 tx,
90 pending: DashMap::new(),
91 list_changed: DashMap::new(),
92 sessions: OnceLock::new(),
93 };
94 (Self(Arc::new(inner)), rx)
95 }
96
97 pub(crate) fn wire_sessions(&self, sessions: Arc<SessionManager>) {
101 let _ = self.0.sessions.set(sessions);
102 }
103
104 async fn lookup_session(
113 &self,
114 response_id: &str,
115 ) -> Result<Arc<Session>, (i64, String)> {
116 let sessions = self
117 .0
118 .sessions
119 .get()
120 .ok_or((-32603i64, "proxy sessions not wired".to_string()))?;
121 sessions
122 .get_or_wait(response_id)
123 .await
124 .ok_or_else(|| (-32001i64, format!("unknown session for response id {response_id:?}")))
125 }
126
127 async fn request(
135 &self,
136 payload: server_request::Payload,
137 headers: IndexMap<String, String>,
138 timeout: Option<Duration>,
139 ) -> Result<ServerResponse, McpError> {
140 let id = uuid::Uuid::new_v4().to_string();
141 let (resp_tx, resp_rx) = oneshot::channel();
142 self.0.pending.insert(id.clone(), resp_tx);
143 let request = ServerRequest {
144 id: id.clone(),
145 headers,
146 payload,
147 };
148 if self.0.tx.send(request).is_err() {
149 self.0.pending.remove(&id);
150 return Err(transport_error("reverse channel closed before send"));
151 }
152 let received = match timeout {
153 Some(timeout) => match tokio::time::timeout(timeout, resp_rx).await
154 {
155 Ok(received) => received,
156 Err(_) => {
157 self.0.pending.remove(&id);
158 return Err(transport_error(
159 "reverse channel timed out waiting for response",
160 ));
161 }
162 },
163 None => resp_rx.await,
164 };
165 match received {
166 Ok(response) => Ok(response),
167 Err(_) => {
168 self.0.pending.remove(&id);
169 Err(transport_error("reverse channel dropped before response"))
170 }
171 }
172 }
173
174 pub(crate) async fn drop_response(&self, response_id: String) {
179 let _ = self
180 .request(
181 server_request::Payload::Drop(server_request::DropRequest { response_id }),
182 IndexMap::new(),
183 None,
184 )
185 .await;
186 }
187
188 #[allow(clippy::too_many_arguments)]
199 pub async fn laboratory_transfer(
200 &self,
201 request: server_request::LaboratoryTransferRequest,
202 ) -> Result<u64, McpError> {
203 let response = self
204 .request(
205 server_request::Payload::LaboratoryTransfer(request),
206 IndexMap::new(),
207 None,
208 )
209 .await?;
210 match response.payload {
211 server_response::Payload::LaboratoryTransfer(result) => {
212 unwrap_rpc("laboratory_transfer", result).map(|r| r.bytes)
213 }
214 other => Err(variant_mismatch(
215 "laboratory_transfer",
216 "laboratory_transfer",
217 &other,
218 )),
219 }
220 }
221
222 pub async fn laboratory_local_transfer(
226 &self,
227 request: server_request::LaboratoryLocalTransferRequest,
228 ) -> Result<u64, McpError> {
229 let response = self
230 .request(
231 server_request::Payload::LaboratoryLocalTransfer(request),
232 IndexMap::new(),
233 None,
234 )
235 .await?;
236 match response.payload {
237 server_response::Payload::LaboratoryLocalTransfer(result) => {
238 unwrap_rpc("laboratory_local_transfer", result).map(|r| r.bytes)
239 }
240 other => Err(variant_mismatch(
241 "laboratory_local_transfer",
242 "laboratory_local_transfer",
243 &other,
244 )),
245 }
246 }
247
248 pub async fn laboratory_export_begin(
249 &self,
250 laboratory_id: String,
251 machine: Option<String>,
252 machine_state: Option<String>,
253 path: String,
254 ) -> Result<String, McpError> {
255 let response = self
256 .request(
257 server_request::Payload::LaboratoryExportBegin(
258 server_request::LaboratoryExportBeginRequest {
259 laboratory_id,
260 machine,
261 machine_state,
262 path,
263 },
264 ),
265 IndexMap::new(),
266 None,
267 )
268 .await?;
269 match response.payload {
270 server_response::Payload::LaboratoryExportBegin(result) => {
271 unwrap_rpc("laboratory_export_begin", result)
272 .map(|r| r.transfer_id)
273 }
274 other => Err(variant_mismatch(
275 "laboratory_export_begin",
276 "laboratory_export_begin",
277 &other,
278 )),
279 }
280 }
281
282 pub async fn laboratory_export_read(
284 &self,
285 transfer_id: String,
286 ) -> Result<server_response::LaboratoryExportChunk, McpError> {
287 let response = self
288 .request(
289 server_request::Payload::LaboratoryExportRead(
290 server_request::LaboratoryExportReadRequest { transfer_id },
291 ),
292 IndexMap::new(),
293 None,
294 )
295 .await?;
296 match response.payload {
297 server_response::Payload::LaboratoryExportRead(result) => {
298 unwrap_rpc("laboratory_export_read", result)
299 }
300 other => Err(variant_mismatch(
301 "laboratory_export_read",
302 "laboratory_export_read",
303 &other,
304 )),
305 }
306 }
307
308 pub async fn laboratory_export_abort(&self, transfer_id: String) {
310 let _ = self
311 .request(
312 server_request::Payload::LaboratoryExportAbort(
313 server_request::LaboratoryExportAbortRequest { transfer_id },
314 ),
315 IndexMap::new(),
316 None,
317 )
318 .await;
319 }
320
321 pub async fn laboratory_import_begin(
324 &self,
325 laboratory_id: String,
326 machine: Option<String>,
327 machine_state: Option<String>,
328 path: String,
329 ) -> Result<String, McpError> {
330 let response = self
331 .request(
332 server_request::Payload::LaboratoryImportBegin(
333 server_request::LaboratoryImportBeginRequest {
334 laboratory_id,
335 machine,
336 machine_state,
337 path,
338 },
339 ),
340 IndexMap::new(),
341 None,
342 )
343 .await?;
344 match response.payload {
345 server_response::Payload::LaboratoryImportBegin(result) => {
346 unwrap_rpc("laboratory_import_begin", result)
347 .map(|r| r.transfer_id)
348 }
349 other => Err(variant_mismatch(
350 "laboratory_import_begin",
351 "laboratory_import_begin",
352 &other,
353 )),
354 }
355 }
356
357 pub async fn laboratory_import_write(
359 &self,
360 transfer_id: String,
361 data: String,
362 ) -> Result<(), McpError> {
363 let response = self
364 .request(
365 server_request::Payload::LaboratoryImportWrite(
366 server_request::LaboratoryImportWriteRequest { transfer_id, data },
367 ),
368 IndexMap::new(),
369 None,
370 )
371 .await?;
372 match response.payload {
373 server_response::Payload::LaboratoryImportWrite(result) => {
374 unwrap_rpc("laboratory_import_write", result).map(|_| ())
375 }
376 other => Err(variant_mismatch(
377 "laboratory_import_write",
378 "laboratory_import_write",
379 &other,
380 )),
381 }
382 }
383
384 pub async fn laboratory_import_end(
386 &self,
387 transfer_id: String,
388 ) -> Result<u64, McpError> {
389 let response = self
390 .request(
391 server_request::Payload::LaboratoryImportEnd(
392 server_request::LaboratoryImportEndRequest { transfer_id },
393 ),
394 IndexMap::new(),
395 None,
396 )
397 .await?;
398 match response.payload {
399 server_response::Payload::LaboratoryImportEnd(result) => {
400 unwrap_rpc("laboratory_import_end", result).map(|r| r.bytes)
401 }
402 other => Err(variant_mismatch(
403 "laboratory_import_end",
404 "laboratory_import_end",
405 &other,
406 )),
407 }
408 }
409
410 pub async fn laboratory_import_abort(&self, transfer_id: String) {
412 let _ = self
413 .request(
414 server_request::Payload::LaboratoryImportAbort(
415 server_request::LaboratoryImportAbortRequest { transfer_id },
416 ),
417 IndexMap::new(),
418 None,
419 )
420 .await;
421 }
422
423 pub fn deliver_response(&self, response: ServerResponse) {
427 if let Some((_, tx)) = self.0.pending.remove(&response.id) {
428 let _ = tx.send(response);
429 }
430 }
431
432 pub async fn deliver_client_request(
444 &self,
445 request: client_request::Request,
446 ) -> client_response::Response {
447 let client_request::Request { id, payload } = request;
448 match payload {
449 client_request::Payload::McpListChanged(change) => {
450 if let Some(cbs) = self.0.list_changed.get(&change.mcp_kind) {
451 let cb = match change.kind {
452 McpListChangedKind::Tools => cbs.0.clone(),
453 McpListChangedKind::Resources => cbs.1.clone(),
454 };
455 drop(cbs);
456 if let Some(cb) = cb {
457 cb();
458 }
459 }
460 client_response::Response::Ok { id }
461 }
462 client_request::Payload::ListTools {
468 response_id, name, ..
469 } => {
470 let result = match self.lookup_session(&response_id).await {
471 Ok(session) => {
472 match session.list_tools_filtered(None, name.as_deref()).await {
473 Ok(result) => JsonRpcResult::Ok { result },
474 Err(e) => rpc_err_result(-32603, format!("list_tools: {e}")),
475 }
476 }
477 Err((code, message)) => rpc_err_result(code, message),
478 };
479 client_response::Response::ListTools { id, result }
480 }
481 client_request::Payload::ListResources {
482 response_id, name, ..
483 } => {
484 let result = match self.lookup_session(&response_id).await {
485 Ok(session) => {
486 match session.list_resources_filtered(None, name.as_deref()).await {
487 Ok(result) => JsonRpcResult::Ok { result },
488 Err(e) => rpc_err_result(-32603, format!("list_resources: {e}")),
489 }
490 }
491 Err((code, message)) => rpc_err_result(code, message),
492 };
493 client_response::Response::ListResources { id, result }
494 }
495 client_request::Payload::ListServers { response_id } => {
496 let result = match self.lookup_session(&response_id).await {
497 Ok(session) => JsonRpcResult::Ok {
499 result: session.list_servers(),
500 },
501 Err((code, message)) => rpc_err_result(code, message),
502 };
503 client_response::Response::ListServers { id, result }
504 }
505 client_request::Payload::CallTool { response_id, params } => {
506 let result = match self.lookup_session(&response_id).await {
507 Ok(session) => match session.call_tool(¶ms).await {
510 Ok(result) => JsonRpcResult::Ok { result },
511 Err(crate::session::CallToolError::ToolNotFound(name)) => {
512 rpc_err_result(-32601, format!("tool not found: {name}"))
513 }
514 Err(crate::session::CallToolError::Upstream(e)) => {
515 rpc_err_result(-32603, format!("upstream call_tool: {e}"))
516 }
517 },
518 Err((code, message)) => rpc_err_result(code, message),
519 };
520 client_response::Response::CallTool { id, result }
521 }
522 client_request::Payload::ReadResource { response_id, params } => {
523 let result = match self.lookup_session(&response_id).await {
524 Ok(session) => match session.read_resource(¶ms.uri).await {
525 Ok(result) => JsonRpcResult::Ok { result },
526 Err(crate::session::ReadResourceError::ResourceNotFound(uri)) => {
527 rpc_err_result(-32602, format!("resource not found: {uri}"))
528 }
529 Err(crate::session::ReadResourceError::Upstream(e)) => {
530 rpc_err_result(-32603, format!("upstream read_resource: {e}"))
531 }
532 },
533 Err((code, message)) => rpc_err_result(code, message),
534 };
535 client_response::Response::ReadResource { id, result }
536 }
537 }
538 }
539
540 fn set_tools_list_changed(&self, mcp_kind: McpKind, cb: ListChangedCb) {
541 let mut entry = self.0.list_changed.entry(mcp_kind).or_default();
542 entry.0 = Some(cb);
543 }
544
545 fn set_resources_list_changed(&self, mcp_kind: McpKind, cb: ListChangedCb) {
546 let mut entry = self.0.list_changed.entry(mcp_kind).or_default();
547 entry.1 = Some(cb);
548 }
549}
550
551pub struct WsUpstream {
556 channel: ReverseChannel,
557 mcp_kind: McpKind,
558 call_timeout: Option<Duration>,
563 pub url: String,
565 pub session_id: String,
567 server_name: String,
570 server_version: String,
571 initialize_result: objectiveai_sdk::mcp::initialize_result::InitializeResult,
576 laboratory: Option<objectiveai_sdk::laboratories::Laboratory>,
580 has_tools_cap: bool,
589 has_resources_cap: bool,
590 base_headers: IndexMap<String, String>,
598 extra_headers: RwLock<IndexMap<String, String>>,
604}
605
606impl std::fmt::Debug for WsUpstream {
607 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608 f.debug_struct("WsUpstream")
609 .field("url", &self.url)
610 .field("session_id", &self.session_id)
611 .finish_non_exhaustive()
612 }
613}
614
615impl WsUpstream {
616 async fn headers(&self) -> IndexMap<String, String> {
624 let mut h = self.base_headers.clone();
625 for (k, v) in self.extra_headers.read().await.iter() {
626 h.insert(k.clone(), v.clone());
627 }
628 h.insert(
629 crate::upstream::MCP_SESSION_ID_KEY.to_string(),
630 self.session_id.clone(),
631 );
632 h
633 }
634
635 pub async fn list_tools(&self) -> Result<Arc<Vec<Tool>>, Arc<McpError>> {
636 if !self.has_tools_cap {
639 return Ok(Arc::new(Vec::new()));
640 }
641 let headers = self.headers().await;
642 let response = self
643 .channel
644 .request(
645 server_request::Payload::ToolsList {
646 mcp_kind: self.mcp_kind.clone(),
647 params: ListToolsRequest { cursor: None },
648 },
649 headers,
650 self.call_timeout,
651 )
652 .await
653 .map_err(Arc::new)?;
654 match response.payload {
655 server_response::Payload::ToolsList { result, .. } => {
656 Ok(Arc::new(unwrap_rpc(&self.url, result).map_err(Arc::new)?.tools))
657 }
658 other => Err(Arc::new(variant_mismatch(&self.url, "tools_list", &other))),
659 }
660 }
661
662 pub async fn list_resources(&self) -> Result<Arc<Vec<Resource>>, Arc<McpError>> {
663 if !self.has_resources_cap {
667 return Ok(Arc::new(Vec::new()));
668 }
669 let headers = self.headers().await;
670 let response = self
671 .channel
672 .request(
673 server_request::Payload::ResourcesList {
674 mcp_kind: self.mcp_kind.clone(),
675 params: ListResourcesRequest { cursor: None },
676 },
677 headers,
678 self.call_timeout,
679 )
680 .await
681 .map_err(Arc::new)?;
682 match response.payload {
683 server_response::Payload::ResourcesList { result, .. } => {
684 Ok(Arc::new(unwrap_rpc(&self.url, result).map_err(Arc::new)?.resources))
685 }
686 other => Err(Arc::new(variant_mismatch(&self.url, "resources_list", &other))),
687 }
688 }
689
690 pub async fn call_tool(
691 &self,
692 params: &CallToolRequestParams,
693 ) -> Result<CallToolResult, McpError> {
694 let headers = self.headers().await;
695 let response = self
696 .channel
697 .request(
698 server_request::Payload::ToolsCall {
699 mcp_kind: self.mcp_kind.clone(),
700 params: params.clone(),
701 },
702 headers,
703 self.call_timeout,
704 )
705 .await?;
706 match response.payload {
707 server_response::Payload::ToolsCall { result, .. } => unwrap_rpc(&self.url, result),
708 other => Err(variant_mismatch(&self.url, "tools_call", &other)),
709 }
710 }
711
712 pub async fn read_resource(&self, uri: &str) -> Result<ReadResourceResult, McpError> {
713 let headers = self.headers().await;
714 let response = self
715 .channel
716 .request(
717 server_request::Payload::ResourcesRead {
718 mcp_kind: self.mcp_kind.clone(),
719 params: ReadResourceRequestParams {
720 uri: uri.to_string(),
721 },
722 },
723 headers,
724 self.call_timeout,
725 )
726 .await?;
727 match response.payload {
728 server_response::Payload::ResourcesRead { result, .. } => unwrap_rpc(&self.url, result),
729 other => Err(variant_mismatch(&self.url, "resources_read", &other)),
730 }
731 }
732
733 pub async fn delete(&self) -> Result<(), McpError> {
734 let headers = self.headers().await;
735 let response = self
736 .channel
737 .request(
738 server_request::Payload::SessionTerminate {
739 mcp_kind: self.mcp_kind.clone(),
740 },
741 headers,
742 self.call_timeout,
743 )
744 .await?;
745 match response.payload {
746 server_response::Payload::SessionTerminate { result, .. } => unwrap_rpc(&self.url, result),
747 other => Err(variant_mismatch(&self.url, "session_terminate", &other)),
748 }
749 }
750
751 pub fn set_on_tools_list_changed<F>(&self, callback: F)
752 where
753 F: Fn() + Send + Sync + 'static,
754 {
755 self.channel
756 .set_tools_list_changed(self.mcp_kind.clone(), Arc::new(callback));
757 }
758
759 pub fn set_on_resources_list_changed<F>(&self, callback: F)
760 where
761 F: Fn() + Send + Sync + 'static,
762 {
763 self.channel
764 .set_resources_list_changed(self.mcp_kind.clone(), Arc::new(callback));
765 }
766
767 pub async fn set_extra_headers(&self, extras: IndexMap<String, String>) {
768 *self.extra_headers.write().await = extras;
769 }
770}
771
772#[derive(Debug)]
775pub enum Upstream {
776 Http(Connection),
777 Ws(WsUpstream),
778}
779
780impl Upstream {
781 pub fn is_ws(&self) -> bool {
784 matches!(self, Upstream::Ws(_))
785 }
786
787 pub fn url(&self) -> &str {
788 match self {
789 Upstream::Http(c) => &c.url,
790 Upstream::Ws(w) => &w.url,
791 }
792 }
793
794 pub fn session_id(&self) -> &str {
795 match self {
796 Upstream::Http(c) => &c.session_id,
797 Upstream::Ws(w) => &w.session_id,
798 }
799 }
800
801 pub fn server_name(&self) -> &str {
804 match self {
805 Upstream::Http(c) => &c.initialize_result.server_info.name,
806 Upstream::Ws(w) => &w.server_name,
807 }
808 }
809
810 pub fn server_version(&self) -> &str {
812 match self {
813 Upstream::Http(c) => &c.initialize_result.server_info.version,
814 Upstream::Ws(w) => &w.server_version,
815 }
816 }
817
818 pub fn initialize_result(
821 &self,
822 ) -> &objectiveai_sdk::mcp::initialize_result::InitializeResult {
823 match self {
824 Upstream::Http(c) => &c.initialize_result,
825 Upstream::Ws(w) => &w.initialize_result,
826 }
827 }
828
829 pub fn laboratory(&self) -> Option<objectiveai_sdk::laboratories::Laboratory> {
837 match self {
838 Upstream::Http(_) => None,
839 Upstream::Ws(w) => w.laboratory.clone(),
840 }
841 }
842
843 pub fn plugin(&self) -> Option<objectiveai_sdk::mcp::server::Plugin> {
848 match self {
849 Upstream::Http(_) => None,
850 Upstream::Ws(w) => match &w.mcp_kind {
851 McpKind::Plugin {
852 owner,
853 name,
854 version,
855 mcp,
856 } => Some(objectiveai_sdk::mcp::server::Plugin {
857 owner: owner.clone(),
858 name: name.clone(),
859 version: version.clone(),
860 mcp: mcp.clone(),
861 }),
862 McpKind::ObjectiveAi | McpKind::Laboratory { .. } => None,
863 },
864 }
865 }
866
867 pub fn reverse_channel(&self) -> Option<&ReverseChannel> {
872 match self {
873 Upstream::Http(_) => None,
874 Upstream::Ws(w) => Some(&w.channel),
875 }
876 }
877
878 pub async fn list_tools(&self) -> Result<Arc<Vec<Tool>>, Arc<McpError>> {
879 match self {
880 Upstream::Http(c) => c.list_tools().await,
881 Upstream::Ws(w) => w.list_tools().await,
882 }
883 }
884
885 pub async fn list_resources(&self) -> Result<Arc<Vec<Resource>>, Arc<McpError>> {
886 match self {
887 Upstream::Http(c) => c.list_resources().await,
888 Upstream::Ws(w) => w.list_resources().await,
889 }
890 }
891
892 pub async fn call_tool(
893 &self,
894 params: &CallToolRequestParams,
895 ) -> Result<CallToolResult, McpError> {
896 match self {
897 Upstream::Http(c) => c.call_tool(params).await,
898 Upstream::Ws(w) => w.call_tool(params).await,
899 }
900 }
901
902 pub async fn read_resource(&self, uri: &str) -> Result<ReadResourceResult, McpError> {
903 match self {
904 Upstream::Http(c) => c.read_resource(uri).await,
905 Upstream::Ws(w) => w.read_resource(uri).await,
906 }
907 }
908
909 pub async fn delete(&self) -> Result<(), McpError> {
910 match self {
911 Upstream::Http(c) => c.delete().await,
912 Upstream::Ws(w) => w.delete().await,
913 }
914 }
915
916 pub fn set_on_tools_list_changed<F>(&self, callback: F)
917 where
918 F: Fn() + Send + Sync + 'static,
919 {
920 match self {
921 Upstream::Http(c) => c.set_on_tools_list_changed(callback),
922 Upstream::Ws(w) => w.set_on_tools_list_changed(callback),
923 }
924 }
925
926 pub fn set_on_resources_list_changed<F>(&self, callback: F)
927 where
928 F: Fn() + Send + Sync + 'static,
929 {
930 match self {
931 Upstream::Http(c) => c.set_on_resources_list_changed(callback),
932 Upstream::Ws(w) => w.set_on_resources_list_changed(callback),
933 }
934 }
935
936 pub async fn set_extra_headers(&self, extras: IndexMap<String, String>) {
937 match self {
938 Upstream::Http(c) => c.set_extra_headers(extras).await,
939 Upstream::Ws(w) => w.set_extra_headers(extras).await,
940 }
941 }
942}
943
944pub fn parse_ws_mcp_kind(url: &str) -> Option<McpKind> {
953 let rest = url.strip_prefix("ws://")?;
954 let rest = rest.split('?').next().unwrap_or(rest);
956 if rest == "objectiveai" {
958 return Some(McpKind::ObjectiveAi);
959 }
960 let path = rest.strip_prefix('/')?;
962 let parts: Vec<&str> = path.split('/').collect();
963 if let [owner, name, version, mcp] = parts.as_slice() {
964 if !owner.is_empty() && !name.is_empty() && !version.is_empty() && !mcp.is_empty() {
965 return Some(McpKind::Plugin {
966 owner: (*owner).to_string(),
967 name: (*name).to_string(),
968 version: (*version).to_string(),
969 mcp: (*mcp).to_string(),
970 });
971 }
972 }
973 None
974}
975
976pub async fn connect_ws(
986 channel: ReverseChannel,
987 url: String,
988 mcp_kind: McpKind,
989 args: IndexMap<String, Option<String>>,
990 mut headers: IndexMap<String, String>,
991 laboratory: Option<objectiveai_sdk::laboratories::Laboratory>,
992 connect_timeout: Option<Duration>,
993 call_timeout: Option<Duration>,
994) -> Result<WsUpstream, McpError> {
995 let response = channel
996 .request(
997 server_request::Payload::Initialize {
998 mcp_kind: mcp_kind.clone(),
999 params: InitializeRequest { args },
1000 },
1001 headers.clone(),
1002 connect_timeout,
1003 )
1004 .await?;
1005 let reply = match response.payload {
1006 server_response::Payload::Initialize { result, .. } => unwrap_rpc(&url, result)?,
1007 other => return Err(variant_mismatch(&url, "initialize", &other)),
1008 };
1009 headers.shift_remove(crate::upstream::MCP_SESSION_ID_KEY);
1014 let session_id = reply.mcp_session_id;
1015 let initialize_result = reply.result;
1016 let has_tools_cap = initialize_result.capabilities.tools.is_some();
1017 let has_resources_cap = initialize_result.capabilities.resources.is_some();
1018 let server_name = initialize_result.server_info.name.clone();
1019 let server_version = initialize_result.server_info.version.clone();
1020 Ok(WsUpstream {
1021 channel,
1022 mcp_kind,
1023 call_timeout,
1024 url,
1025 session_id,
1026 server_name,
1027 server_version,
1028 initialize_result,
1029 laboratory,
1030 has_tools_cap,
1031 has_resources_cap,
1032 base_headers: headers,
1038 extra_headers: RwLock::new(IndexMap::new()),
1039 })
1040}
1041
1042fn unwrap_rpc<R>(url: &str, result: JsonRpcResult<R>) -> Result<R, McpError> {
1043 match result {
1044 JsonRpcResult::Ok { result } => Ok(result),
1045 JsonRpcResult::Err {
1046 code,
1047 message,
1048 data,
1049 } => Err(McpError::JsonRpc {
1050 url: url.to_string(),
1051 code,
1052 message,
1053 data,
1054 }),
1055 }
1056}
1057
1058fn transport_error(message: &str) -> McpError {
1059 McpError::MalformedResponse {
1060 url: "ws".to_string(),
1061 message: message.to_string(),
1062 }
1063}
1064
1065fn rpc_err_result<R>(code: i64, message: String) -> JsonRpcResult<R> {
1069 JsonRpcResult::Err {
1070 code,
1071 message,
1072 data: None,
1073 }
1074}
1075
1076fn variant_mismatch(url: &str, expected: &str, got: &server_response::Payload) -> McpError {
1077 McpError::MalformedResponse {
1078 url: url.to_string(),
1079 message: format!(
1080 "reverse channel returned wrong payload variant: expected {expected}, got {}",
1081 got_variant_name(got),
1082 ),
1083 }
1084}
1085
1086fn got_variant_name(p: &server_response::Payload) -> &'static str {
1087 use server_response::Payload as P;
1088 match p {
1089 P::Initialize { .. } => "initialize",
1090 P::ToolsList { .. } => "tools_list",
1091 P::ToolsCall { .. } => "tools_call",
1092 P::ResourcesList { .. } => "resources_list",
1093 P::ResourcesRead { .. } => "resources_read",
1094 P::SessionTerminate { .. } => "session_terminate",
1095 P::ReadMessageQueue(_) => "read_message_queue",
1096 P::Retrieve(_) => "retrieve",
1097 P::Script(_) => "script",
1098 P::Drop(_) => "drop",
1099 P::LaboratoryTransfer(_) => "laboratory_transfer",
1100 P::LaboratoryLocalTransfer(_) => "laboratory_local_transfer",
1101 P::LaboratoryExportBegin(_) => "laboratory_export_begin",
1102 P::LaboratoryExportRead(_) => "laboratory_export_read",
1103 P::LaboratoryExportAbort(_) => "laboratory_export_abort",
1104 P::LaboratoryImportBegin(_) => "laboratory_import_begin",
1105 P::LaboratoryImportWrite(_) => "laboratory_import_write",
1106 P::LaboratoryImportEnd(_) => "laboratory_import_end",
1107 P::LaboratoryImportAbort(_) => "laboratory_import_abort",
1108 }
1109}