use serde_json::{json, Value};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt, BufReader},
net::{TcpListener, TcpStream},
sync::{mpsc, oneshot},
};
use uuid::Uuid;
use crate::{
bindings::host::murmur::tool::run::{Status, ToolInput},
runtime::CapsuleStoreState,
};
pub(super) const BRIDGE_SERVER_NAME: &str = "murmur";
const BRIDGE_PATH: &str = "/bridge";
const GATE_UNREACHABLE: &str =
"Refused: the session stopped answering policy checks, so this tool call was not dispatched.";
pub(super) struct GateRequest {
pub(super) tool_name: String,
pub(super) input_json: String,
pub(super) reply: oneshot::Sender<Option<String>>,
}
async fn ask_gate(
gate: &mpsc::UnboundedSender<GateRequest>,
tool_name: &str,
input_json: &str,
) -> Option<String> {
let (reply, verdict) = oneshot::channel();
if gate
.send(GateRequest {
tool_name: tool_name.to_string(),
input_json: input_json.to_string(),
reply,
})
.is_err()
{
return Some(GATE_UNREACHABLE.to_string());
}
verdict
.await
.unwrap_or_else(|_| Some(GATE_UNREACHABLE.to_string()))
}
fn error_result(text: String) -> Value {
json!({
"content": [{ "type": "text", "text": text }],
"isError": true
})
}
pub(super) struct BridgeHandle {
pub(super) listener: TcpListener,
pub(super) url: String,
pub(super) token: String,
pub(super) session_id: String,
pub(super) tool_names: Vec<String>,
mcp_tools: Vec<Value>,
}
fn inventory_to_mcp_tools(inventory: &[Value]) -> Vec<Value> {
inventory
.iter()
.filter_map(|t| {
let name = t.get("name").and_then(Value::as_str)?;
let schema = t
.get("parameters")
.cloned()
.unwrap_or_else(|| json!({"type": "object"}));
let mut tool = json!({ "name": name, "inputSchema": schema });
if let Some(desc) = t.get("description").and_then(Value::as_str) {
tool["description"] = Value::String(desc.to_string());
}
Some(tool)
})
.collect()
}
pub(super) async fn bind_bridge(bind_addr: &str, inventory: &[Value]) -> Option<BridgeHandle> {
let mcp_tools = inventory_to_mcp_tools(inventory);
if mcp_tools.is_empty() {
return None;
}
let listener = TcpListener::bind(format!("{bind_addr}:0")).await.ok()?;
let port = listener.local_addr().ok()?.port();
let host = if bind_addr.is_empty() {
"127.0.0.1"
} else {
bind_addr
};
let tool_names = mcp_tools
.iter()
.filter_map(|t| t.get("name").and_then(Value::as_str))
.map(str::to_string)
.collect();
Some(BridgeHandle {
listener,
url: format!("http://{host}:{port}{BRIDGE_PATH}"),
token: Uuid::new_v4().simple().to_string(),
session_id: Uuid::new_v4().simple().to_string(),
tool_names,
mcp_tools,
})
}
impl BridgeHandle {
pub(super) async fn serve(
&self,
store: &CapsuleStoreState,
gate: Option<&mpsc::UnboundedSender<GateRequest>>,
on_request: &dyn Fn(),
) {
while let Ok((stream, _)) = self.listener.accept().await {
on_request();
self.handle_connection(stream, store, gate).await;
}
}
async fn handle_connection(
&self,
stream: TcpStream,
store: &CapsuleStoreState,
gate: Option<&mpsc::UnboundedSender<GateRequest>>,
) {
use tokio::io::AsyncBufReadExt;
let (reader_half, mut writer) = stream.into_split();
let mut reader = BufReader::new(reader_half);
let mut request_line = String::new();
if reader.read_line(&mut request_line).await.is_err() {
return;
}
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap_or("").to_string();
let mut content_length = 0usize;
let mut authorized = false;
loop {
let mut line = String::new();
match reader.read_line(&mut line).await {
Ok(0) | Err(_) => break,
Ok(_) => {}
}
if line.trim().is_empty() {
break;
}
let lower = line.to_ascii_lowercase();
let lower = lower.trim_end();
if let Some(rest) = lower.strip_prefix("content-length:") {
content_length = rest.trim().parse().unwrap_or(0);
} else if let Some(rest) = lower.strip_prefix("authorization:") {
authorized = rest.trim() == format!("bearer {}", self.token);
}
}
if method == "GET" {
write_response(&mut writer, "405 Method Not Allowed", None, None).await;
return;
}
if !authorized {
write_response(&mut writer, "401 Unauthorized", None, None).await;
return;
}
let mut body = vec![0u8; content_length];
if content_length > 0 && reader.read_exact(&mut body).await.is_err() {
return;
}
let request: Value = match serde_json::from_slice(&body) {
Ok(v) => v,
Err(_) => {
write_response(&mut writer, "400 Bad Request", None, None).await;
return;
}
};
let rpc_method = request.get("method").and_then(Value::as_str).unwrap_or("");
let id = request.get("id").cloned();
if id.is_none() {
write_response(&mut writer, "202 Accepted", None, None).await;
return;
}
let id = id.unwrap();
match rpc_method {
"initialize" => {
let protocol = request
.get("params")
.and_then(|p| p.get("protocolVersion"))
.and_then(Value::as_str)
.unwrap_or("2024-11-05");
let result = json!({
"protocolVersion": protocol,
"capabilities": { "tools": {} },
"serverInfo": { "name": "murmur-bridge", "version": env!("CARGO_PKG_VERSION") }
});
let session_header = format!("Mcp-Session-Id: {}", self.session_id);
write_json_rpc(&mut writer, &id, result, Some(&session_header)).await;
}
"tools/list" => {
write_json_rpc(&mut writer, &id, json!({ "tools": self.mcp_tools }), None).await;
}
"tools/call" => {
let result = self
.dispatch_tool_call(request.get("params"), store, gate)
.await;
write_json_rpc(&mut writer, &id, result, None).await;
}
_ => {
let err = json!({
"jsonrpc": "2.0", "id": id,
"error": { "code": -32601, "message": "method not found" }
});
write_response(
&mut writer,
"200 OK",
Some("application/json"),
Some(&err.to_string()),
)
.await;
}
}
}
async fn dispatch_tool_call(
&self,
params: Option<&Value>,
store: &CapsuleStoreState,
gate: Option<&mpsc::UnboundedSender<GateRequest>>,
) -> Value {
let name = params
.and_then(|p| p.get("name"))
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let arguments = params
.and_then(|p| p.get("arguments"))
.cloned()
.unwrap_or_else(|| json!({}));
let input_json = serde_json::to_string(&arguments).unwrap_or_else(|_| "{}".to_string());
if let Some(gate) = gate {
if let Some(refusal) = ask_gate(gate, &name, &input_json).await {
return error_result(refusal);
}
}
match store
.dispatch_agent_tool_async(
&name,
ToolInput {
data: Some(input_json),
log_path: None,
},
None,
)
.await
{
Ok(outcome) => {
let is_error = !matches!(outcome.result.status, Status::Passed);
let text = outcome
.result
.data
.or(outcome.result.summary)
.unwrap_or_else(|| "tool returned no data".to_string());
json!({
"content": [{ "type": "text", "text": text }],
"isError": is_error
})
}
Err(err) => error_result(format!("tool '{name}' failed: {err}")),
}
}
}
async fn write_json_rpc(
writer: &mut tokio::net::tcp::OwnedWriteHalf,
id: &Value,
result: Value,
extra_header: Option<&str>,
) {
let payload = json!({ "jsonrpc": "2.0", "id": id, "result": result }).to_string();
write_response_with_header(
writer,
"200 OK",
Some("application/json"),
Some(&payload),
extra_header,
)
.await;
}
async fn write_response(
writer: &mut tokio::net::tcp::OwnedWriteHalf,
status: &str,
content_type: Option<&str>,
body: Option<&str>,
) {
write_response_with_header(writer, status, content_type, body, None).await;
}
async fn write_response_with_header(
writer: &mut tokio::net::tcp::OwnedWriteHalf,
status: &str,
content_type: Option<&str>,
body: Option<&str>,
extra_header: Option<&str>,
) {
let body = body.unwrap_or("");
let mut response = format!("HTTP/1.1 {status}\r\n");
if let Some(ct) = content_type {
response.push_str(&format!("content-type: {ct}\r\n"));
}
if let Some(h) = extra_header {
response.push_str(h);
response.push_str("\r\n");
}
response.push_str(&format!("content-length: {}\r\n", body.len()));
response.push_str("connection: close\r\n\r\n");
response.push_str(body);
let _ = writer.write_all(response.as_bytes()).await;
let _ = writer.shutdown().await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inventory_maps_to_mcp_tool_shape() {
let inventory = vec![json!({
"name": "murmur-tool-editor",
"description": "edits files",
"parameters": {"type": "object", "properties": {"path": {"type": "string"}}}
})];
let mcp = inventory_to_mcp_tools(&inventory);
assert_eq!(mcp.len(), 1);
assert_eq!(mcp[0]["name"], "murmur-tool-editor");
assert_eq!(mcp[0]["description"], "edits files");
assert_eq!(mcp[0]["inputSchema"]["type"], "object");
assert!(mcp[0].get("parameters").is_none());
}
#[test]
fn inventory_without_schema_defaults_to_object() {
let inventory = vec![json!({ "name": "t" })];
let mcp = inventory_to_mcp_tools(&inventory);
assert_eq!(mcp[0]["inputSchema"]["type"], "object");
}
#[tokio::test]
async fn bind_bridge_is_none_without_tools() {
assert!(bind_bridge("127.0.0.1", &[]).await.is_none());
}
#[tokio::test]
async fn bind_bridge_names_tools_bare() {
let inventory = vec![json!({"name": "editor", "parameters": {"type": "object"}})];
let handle = bind_bridge("127.0.0.1", &inventory)
.await
.expect("bridge should bind when tools are declared");
assert_eq!(handle.tool_names, vec!["editor"]);
assert!(handle.url.starts_with("http://127.0.0.1:"));
assert!(!handle.token.is_empty());
}
}