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 fn deliver_response(&self, response: ServerResponse) {
166 if let Some((_, tx)) = self.0.pending.remove(&response.id) {
167 let _ = tx.send(response);
168 }
169 }
170
171 pub async fn deliver_client_request(
183 &self,
184 request: client_request::Request,
185 ) -> client_response::Response {
186 let client_request::Request { id, payload } = request;
187 match payload {
188 client_request::Payload::McpListChanged(change) => {
189 if let Some(cbs) = self.0.list_changed.get(&change.mcp_kind) {
190 let cb = match change.kind {
191 McpListChangedKind::Tools => cbs.0.clone(),
192 McpListChangedKind::Resources => cbs.1.clone(),
193 };
194 drop(cbs);
195 if let Some(cb) = cb {
196 cb();
197 }
198 }
199 client_response::Response::Ok { id }
200 }
201 client_request::Payload::ListTools { response_id, .. } => {
204 let result = match self.lookup_session(&response_id) {
205 Ok(session) => match session.list_tools_filtered(None).await {
206 Ok(result) => JsonRpcResult::Ok { result },
207 Err(e) => rpc_err_result(-32603, format!("list_tools: {e}")),
208 },
209 Err((code, message)) => rpc_err_result(code, message),
210 };
211 client_response::Response::ListTools { id, result }
212 }
213 client_request::Payload::ListResources { response_id, .. } => {
214 let result = match self.lookup_session(&response_id) {
215 Ok(session) => match session.list_resources_filtered(None).await {
216 Ok(result) => JsonRpcResult::Ok { result },
217 Err(e) => rpc_err_result(-32603, format!("list_resources: {e}")),
218 },
219 Err((code, message)) => rpc_err_result(code, message),
220 };
221 client_response::Response::ListResources { id, result }
222 }
223 client_request::Payload::CallTool { response_id, params } => {
224 let result = match self.lookup_session(&response_id) {
225 Ok(session) => match session.call_tool(¶ms).await {
228 Ok(result) => JsonRpcResult::Ok { result },
229 Err(crate::session::CallToolError::ToolNotFound(name)) => {
230 rpc_err_result(-32601, format!("tool not found: {name}"))
231 }
232 Err(crate::session::CallToolError::Upstream(e)) => {
233 rpc_err_result(-32603, format!("upstream call_tool: {e}"))
234 }
235 },
236 Err((code, message)) => rpc_err_result(code, message),
237 };
238 client_response::Response::CallTool { id, result }
239 }
240 client_request::Payload::ReadResource { response_id, params } => {
241 let result = match self.lookup_session(&response_id) {
242 Ok(session) => match session.read_resource(¶ms.uri).await {
243 Ok(result) => JsonRpcResult::Ok { result },
244 Err(crate::session::ReadResourceError::ResourceNotFound(uri)) => {
245 rpc_err_result(-32602, format!("resource not found: {uri}"))
246 }
247 Err(crate::session::ReadResourceError::Upstream(e)) => {
248 rpc_err_result(-32603, format!("upstream read_resource: {e}"))
249 }
250 },
251 Err((code, message)) => rpc_err_result(code, message),
252 };
253 client_response::Response::ReadResource { id, result }
254 }
255 }
256 }
257
258 fn set_tools_list_changed(&self, mcp_kind: McpKind, cb: ListChangedCb) {
259 let mut entry = self.0.list_changed.entry(mcp_kind).or_default();
260 entry.0 = Some(cb);
261 }
262
263 fn set_resources_list_changed(&self, mcp_kind: McpKind, cb: ListChangedCb) {
264 let mut entry = self.0.list_changed.entry(mcp_kind).or_default();
265 entry.1 = Some(cb);
266 }
267}
268
269pub struct WsUpstream {
274 channel: ReverseChannel,
275 mcp_kind: McpKind,
276 pub url: String,
278 pub session_id: String,
280 server_name: String,
283 server_version: String,
284 has_tools_cap: bool,
293 has_resources_cap: bool,
294 base_headers: IndexMap<String, String>,
302 extra_headers: RwLock<IndexMap<String, String>>,
308}
309
310impl std::fmt::Debug for WsUpstream {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 f.debug_struct("WsUpstream")
313 .field("url", &self.url)
314 .field("session_id", &self.session_id)
315 .finish_non_exhaustive()
316 }
317}
318
319impl WsUpstream {
320 async fn headers(&self) -> IndexMap<String, String> {
328 let mut h = self.base_headers.clone();
329 for (k, v) in self.extra_headers.read().await.iter() {
330 h.insert(k.clone(), v.clone());
331 }
332 h.insert(
333 crate::upstream::MCP_SESSION_ID_KEY.to_string(),
334 self.session_id.clone(),
335 );
336 h
337 }
338
339 pub async fn list_tools(&self) -> Result<Arc<Vec<Tool>>, Arc<McpError>> {
340 if !self.has_tools_cap {
343 return Ok(Arc::new(Vec::new()));
344 }
345 let headers = self.headers().await;
346 let response = self
347 .channel
348 .request(
349 server_request::Payload::ToolsList {
350 mcp_kind: self.mcp_kind.clone(),
351 params: ListToolsRequest { cursor: None },
352 },
353 headers,
354 )
355 .await
356 .map_err(Arc::new)?;
357 match response.payload {
358 server_response::Payload::ToolsList { result, .. } => {
359 Ok(Arc::new(unwrap_rpc(&self.url, result).map_err(Arc::new)?.tools))
360 }
361 other => Err(Arc::new(variant_mismatch(&self.url, "tools_list", &other))),
362 }
363 }
364
365 pub async fn list_resources(&self) -> Result<Arc<Vec<Resource>>, Arc<McpError>> {
366 if !self.has_resources_cap {
370 return Ok(Arc::new(Vec::new()));
371 }
372 let headers = self.headers().await;
373 let response = self
374 .channel
375 .request(
376 server_request::Payload::ResourcesList {
377 mcp_kind: self.mcp_kind.clone(),
378 params: ListResourcesRequest { cursor: None },
379 },
380 headers,
381 )
382 .await
383 .map_err(Arc::new)?;
384 match response.payload {
385 server_response::Payload::ResourcesList { result, .. } => {
386 Ok(Arc::new(unwrap_rpc(&self.url, result).map_err(Arc::new)?.resources))
387 }
388 other => Err(Arc::new(variant_mismatch(&self.url, "resources_list", &other))),
389 }
390 }
391
392 pub async fn call_tool(
393 &self,
394 params: &CallToolRequestParams,
395 ) -> Result<CallToolResult, McpError> {
396 let headers = self.headers().await;
397 let response = self
398 .channel
399 .request(
400 server_request::Payload::ToolsCall {
401 mcp_kind: self.mcp_kind.clone(),
402 params: params.clone(),
403 },
404 headers,
405 )
406 .await?;
407 match response.payload {
408 server_response::Payload::ToolsCall { result, .. } => unwrap_rpc(&self.url, result),
409 other => Err(variant_mismatch(&self.url, "tools_call", &other)),
410 }
411 }
412
413 pub async fn read_resource(&self, uri: &str) -> Result<ReadResourceResult, McpError> {
414 let headers = self.headers().await;
415 let response = self
416 .channel
417 .request(
418 server_request::Payload::ResourcesRead {
419 mcp_kind: self.mcp_kind.clone(),
420 params: ReadResourceRequestParams {
421 uri: uri.to_string(),
422 },
423 },
424 headers,
425 )
426 .await?;
427 match response.payload {
428 server_response::Payload::ResourcesRead { result, .. } => unwrap_rpc(&self.url, result),
429 other => Err(variant_mismatch(&self.url, "resources_read", &other)),
430 }
431 }
432
433 pub async fn delete(&self) -> Result<(), McpError> {
434 let headers = self.headers().await;
435 let response = self
436 .channel
437 .request(
438 server_request::Payload::SessionTerminate {
439 mcp_kind: self.mcp_kind.clone(),
440 },
441 headers,
442 )
443 .await?;
444 match response.payload {
445 server_response::Payload::SessionTerminate { result, .. } => unwrap_rpc(&self.url, result),
446 other => Err(variant_mismatch(&self.url, "session_terminate", &other)),
447 }
448 }
449
450 pub fn set_on_tools_list_changed<F>(&self, callback: F)
451 where
452 F: Fn() + Send + Sync + 'static,
453 {
454 self.channel
455 .set_tools_list_changed(self.mcp_kind.clone(), Arc::new(callback));
456 }
457
458 pub fn set_on_resources_list_changed<F>(&self, callback: F)
459 where
460 F: Fn() + Send + Sync + 'static,
461 {
462 self.channel
463 .set_resources_list_changed(self.mcp_kind.clone(), Arc::new(callback));
464 }
465
466 pub async fn set_extra_headers(&self, extras: IndexMap<String, String>) {
467 *self.extra_headers.write().await = extras;
468 }
469}
470
471#[derive(Debug)]
474pub enum Upstream {
475 Http(Connection),
476 Ws(WsUpstream),
477}
478
479impl Upstream {
480 pub fn is_ws(&self) -> bool {
483 matches!(self, Upstream::Ws(_))
484 }
485
486 pub fn url(&self) -> &str {
487 match self {
488 Upstream::Http(c) => &c.url,
489 Upstream::Ws(w) => &w.url,
490 }
491 }
492
493 pub fn session_id(&self) -> &str {
494 match self {
495 Upstream::Http(c) => &c.session_id,
496 Upstream::Ws(w) => &w.session_id,
497 }
498 }
499
500 pub fn server_name(&self) -> &str {
503 match self {
504 Upstream::Http(c) => &c.initialize_result.server_info.name,
505 Upstream::Ws(w) => &w.server_name,
506 }
507 }
508
509 pub fn server_version(&self) -> &str {
511 match self {
512 Upstream::Http(c) => &c.initialize_result.server_info.version,
513 Upstream::Ws(w) => &w.server_version,
514 }
515 }
516
517 pub async fn list_tools(&self) -> Result<Arc<Vec<Tool>>, Arc<McpError>> {
518 match self {
519 Upstream::Http(c) => c.list_tools().await,
520 Upstream::Ws(w) => w.list_tools().await,
521 }
522 }
523
524 pub async fn list_resources(&self) -> Result<Arc<Vec<Resource>>, Arc<McpError>> {
525 match self {
526 Upstream::Http(c) => c.list_resources().await,
527 Upstream::Ws(w) => w.list_resources().await,
528 }
529 }
530
531 pub async fn call_tool(
532 &self,
533 params: &CallToolRequestParams,
534 ) -> Result<CallToolResult, McpError> {
535 match self {
536 Upstream::Http(c) => c.call_tool(params).await,
537 Upstream::Ws(w) => w.call_tool(params).await,
538 }
539 }
540
541 pub async fn read_resource(&self, uri: &str) -> Result<ReadResourceResult, McpError> {
542 match self {
543 Upstream::Http(c) => c.read_resource(uri).await,
544 Upstream::Ws(w) => w.read_resource(uri).await,
545 }
546 }
547
548 pub async fn delete(&self) -> Result<(), McpError> {
549 match self {
550 Upstream::Http(c) => c.delete().await,
551 Upstream::Ws(w) => w.delete().await,
552 }
553 }
554
555 pub fn set_on_tools_list_changed<F>(&self, callback: F)
556 where
557 F: Fn() + Send + Sync + 'static,
558 {
559 match self {
560 Upstream::Http(c) => c.set_on_tools_list_changed(callback),
561 Upstream::Ws(w) => w.set_on_tools_list_changed(callback),
562 }
563 }
564
565 pub fn set_on_resources_list_changed<F>(&self, callback: F)
566 where
567 F: Fn() + Send + Sync + 'static,
568 {
569 match self {
570 Upstream::Http(c) => c.set_on_resources_list_changed(callback),
571 Upstream::Ws(w) => w.set_on_resources_list_changed(callback),
572 }
573 }
574
575 pub async fn set_extra_headers(&self, extras: IndexMap<String, String>) {
576 match self {
577 Upstream::Http(c) => c.set_extra_headers(extras).await,
578 Upstream::Ws(w) => w.set_extra_headers(extras).await,
579 }
580 }
581}
582
583pub fn parse_ws_mcp_kind(url: &str) -> Option<McpKind> {
586 let rest = url.strip_prefix("ws://")?;
587 let rest = rest.split('?').next().unwrap_or(rest);
589 if rest == "objectiveai" {
591 return Some(McpKind::ObjectiveAi);
592 }
593 let path = rest.strip_prefix('/')?;
595 let parts: Vec<&str> = path.split('/').collect();
596 if let [owner, name, version, mcp] = parts.as_slice() {
597 if !owner.is_empty() && !name.is_empty() && !version.is_empty() && !mcp.is_empty() {
598 return Some(McpKind::Other {
599 owner: (*owner).to_string(),
600 name: (*name).to_string(),
601 version: (*version).to_string(),
602 mcp: (*mcp).to_string(),
603 });
604 }
605 }
606 None
607}
608
609pub async fn connect_ws(
615 channel: ReverseChannel,
616 url: String,
617 mcp_kind: McpKind,
618 args: IndexMap<String, Option<String>>,
619 mut headers: IndexMap<String, String>,
620) -> Result<WsUpstream, McpError> {
621 let response = channel
622 .request(
623 server_request::Payload::Initialize {
624 mcp_kind: mcp_kind.clone(),
625 params: InitializeRequest { args },
626 },
627 headers.clone(),
628 )
629 .await?;
630 let reply = match response.payload {
631 server_response::Payload::Initialize { result, .. } => unwrap_rpc(&url, result)?,
632 other => return Err(variant_mismatch(&url, "initialize", &other)),
633 };
634 headers.shift_remove(crate::upstream::MCP_SESSION_ID_KEY);
639 let has_tools_cap = reply.result.capabilities.tools.is_some();
640 let has_resources_cap = reply.result.capabilities.resources.is_some();
641 Ok(WsUpstream {
642 channel,
643 mcp_kind,
644 url,
645 session_id: reply.mcp_session_id,
646 server_name: reply.result.server_info.name,
647 server_version: reply.result.server_info.version,
648 has_tools_cap,
649 has_resources_cap,
650 base_headers: headers,
656 extra_headers: RwLock::new(IndexMap::new()),
657 })
658}
659
660fn unwrap_rpc<R>(url: &str, result: JsonRpcResult<R>) -> Result<R, McpError> {
661 match result {
662 JsonRpcResult::Ok { result } => Ok(result),
663 JsonRpcResult::Err {
664 code,
665 message,
666 data,
667 } => Err(McpError::JsonRpc {
668 url: url.to_string(),
669 code,
670 message,
671 data,
672 }),
673 }
674}
675
676fn transport_error(message: &str) -> McpError {
677 McpError::MalformedResponse {
678 url: "ws".to_string(),
679 message: message.to_string(),
680 }
681}
682
683fn rpc_err_result<R>(code: i64, message: String) -> JsonRpcResult<R> {
687 JsonRpcResult::Err {
688 code,
689 message,
690 data: None,
691 }
692}
693
694fn variant_mismatch(url: &str, expected: &str, got: &server_response::Payload) -> McpError {
695 McpError::MalformedResponse {
696 url: url.to_string(),
697 message: format!(
698 "reverse channel returned wrong payload variant: expected {expected}, got {}",
699 got_variant_name(got),
700 ),
701 }
702}
703
704fn got_variant_name(p: &server_response::Payload) -> &'static str {
705 use server_response::Payload as P;
706 match p {
707 P::Initialize { .. } => "initialize",
708 P::ToolsList { .. } => "tools_list",
709 P::ToolsCall { .. } => "tools_call",
710 P::ResourcesList { .. } => "resources_list",
711 P::ResourcesRead { .. } => "resources_read",
712 P::SessionTerminate { .. } => "session_terminate",
713 P::ReadMessageQueue(_) => "read_message_queue",
714 P::Retrieve(_) => "retrieve",
715 P::Drop(_) => "drop",
716 }
717}