use std::{collections::VecDeque, sync::Mutex};
use sim_codec_mcp::{McpEnvelope, McpErrorEnvelope, McpRequest, McpResponse};
use sim_kernel::{Cx, Error, Expr, Result};
use crate::McpRouter;
pub trait McpClientPeer: Send + Sync {
fn exchange(&self, cx: &mut Cx, envelope: McpEnvelope) -> Result<McpEnvelope>;
}
pub(crate) fn request_peer(
cx: &mut Cx,
peer: &dyn McpClientPeer,
id: Expr,
method: &str,
params: Expr,
) -> Result<Expr> {
match peer.exchange(
cx,
McpEnvelope::Request(McpRequest {
id,
method: method.to_owned(),
params,
}),
)? {
McpEnvelope::Response(McpResponse { result, .. }) => Ok(result),
McpEnvelope::Error(error) => Err(mcp_error(error)),
_ => Err(Error::Eval(
"foreign MCP peer returned non-response".to_owned(),
)),
}
}
pub struct RouterMcpPeer {
router: Mutex<McpRouter>,
}
impl RouterMcpPeer {
pub fn new(router: McpRouter) -> Self {
Self {
router: Mutex::new(router),
}
}
}
impl McpClientPeer for RouterMcpPeer {
fn exchange(&self, cx: &mut Cx, envelope: McpEnvelope) -> Result<McpEnvelope> {
self.router
.lock()
.map_err(|_| Error::PoisonedLock("mcp client router peer"))?
.handle(cx, envelope)?
.ok_or_else(|| Error::Eval("foreign MCP peer returned no response".to_owned()))
}
}
pub struct McpClientCassettePeer {
frames: Mutex<VecDeque<McpCassetteFrame>>,
}
struct McpCassetteFrame {
method: String,
result: Expr,
}
impl McpClientCassettePeer {
pub fn new(frames: Vec<(String, Expr)>) -> Self {
Self {
frames: Mutex::new(
frames
.into_iter()
.map(|(method, result)| McpCassetteFrame { method, result })
.collect(),
),
}
}
pub fn remaining(&self) -> Result<usize> {
Ok(self
.frames
.lock()
.map_err(|_| Error::PoisonedLock("mcp client cassette"))?
.len())
}
}
impl McpClientPeer for McpClientCassettePeer {
fn exchange(&self, _cx: &mut Cx, envelope: McpEnvelope) -> Result<McpEnvelope> {
let McpEnvelope::Request(request) = envelope else {
return Err(Error::TypeMismatch {
expected: "MCP request",
found: "non-request",
});
};
let frame = self
.frames
.lock()
.map_err(|_| Error::PoisonedLock("mcp client cassette"))?
.pop_front()
.ok_or_else(|| Error::Eval("MCP client cassette exhausted".to_owned()))?;
if frame.method != request.method {
return Err(Error::Eval(format!(
"MCP client cassette expected {}, got {}",
frame.method, request.method
)));
}
Ok(McpEnvelope::Response(McpResponse {
id: request.id,
result: frame.result,
}))
}
}
fn mcp_error(error: McpErrorEnvelope) -> Error {
Error::Eval(format!(
"foreign MCP error {}: {}",
error.error.code, error.error.message
))
}