use std::sync::Arc;
use async_trait::async_trait;
use serde_json::Value;
use crate::protocol::ToolResultContent;
use crate::server::McpServer;
use crate::speculation::{PrefetchDispatcher, PrefetchError};
pub struct McpPrefetchDispatcher {
server: Arc<McpServer>,
}
impl McpPrefetchDispatcher {
pub fn new(server: Arc<McpServer>) -> Self {
Self { server }
}
}
#[async_trait]
impl PrefetchDispatcher for McpPrefetchDispatcher {
async fn dispatch(&self, tool_name: &str, args: Value) -> Result<String, PrefetchError> {
let result = self
.server
.execute_for_prefetch(tool_name, Some(args))
.await;
if result.is_error == Some(true) {
let detail = result
.content
.iter()
.map(|c| match c {
ToolResultContent::Text { text } => text.clone(),
})
.next()
.unwrap_or_default();
return Err(PrefetchError::Rejected(detail));
}
let body: String = result
.content
.into_iter()
.map(|c| match c {
ToolResultContent::Text { text } => text,
})
.collect::<Vec<_>>()
.join("\n");
Ok(body)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layered::SessionPipeline;
use crate::protocol::ToolCallResult;
use devboy_format_pipeline::adaptive_config::AdaptiveConfig;
use serde_json::json;
use std::sync::Arc;
#[tokio::test]
async fn dispatcher_routes_through_server_and_returns_body() {
let server = Arc::new(McpServer::new());
let dispatcher = McpPrefetchDispatcher::new(Arc::clone(&server));
let err = dispatcher
.dispatch("Read", json!({"file_path": "/tmp/x"}))
.await;
assert!(matches!(err, Err(PrefetchError::Rejected(_))));
}
#[tokio::test]
async fn dispatcher_blocks_internal_context_tools() {
let server = Arc::new(McpServer::new());
let dispatcher = McpPrefetchDispatcher::new(Arc::clone(&server));
for tool in ["use_context", "list_contexts", "get_current_context"] {
let r = dispatcher.dispatch(tool, json!({})).await;
assert!(
matches!(&r, Err(PrefetchError::Rejected(msg)) if msg.contains("internal")),
"{tool} must be rejected as internal, got: {r:?}"
);
}
}
#[tokio::test]
async fn session_pipeline_can_attach_real_dispatcher() {
let server = Arc::new(McpServer::new());
let dispatcher: Arc<dyn PrefetchDispatcher> = Arc::new(McpPrefetchDispatcher::new(server));
let cfg = AdaptiveConfig::default();
let _pipeline = SessionPipeline::new(cfg).with_speculation(dispatcher).await;
}
#[test]
fn tool_call_result_text_fields_concatenate_with_newline() {
let r = ToolCallResult {
content: vec![
ToolResultContent::Text {
text: "first".into(),
},
ToolResultContent::Text {
text: "second".into(),
},
],
is_error: Some(false),
};
let body: String = r
.content
.into_iter()
.map(|c| match c {
ToolResultContent::Text { text } => text,
})
.collect::<Vec<_>>()
.join("\n");
assert_eq!(body, "first\nsecond");
}
}