use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use embacle::types::{ChatMessage, ChatRequest, LlmProvider, RunnerError};
use embacle::{CopilotRunner, McpToolDefinition, McpToolExecutor, RunnerConfig};
use embacle_tool_host::{StaticSurface, ToolHost, ToolHostConfig};
use futures_util::StreamExt;
use serde_json::{json, Value};
use tokio::time::timeout;
const SECRET: &str = "8675309";
struct SecretNumberTool {
calls: Arc<AtomicUsize>,
}
#[async_trait]
impl McpToolExecutor for SecretNumberTool {
async fn execute(&self, tool_name: &str, arguments: &Value) -> Result<Value, RunnerError> {
self.calls.fetch_add(1, Ordering::SeqCst);
println!(">>> EXECUTOR CALLED: tool={tool_name} args={arguments}");
Ok(json!({ "secret_number": SECRET }))
}
}
#[tokio::main]
async fn main() {
let calls = Arc::new(AtomicUsize::new(0));
let host = match ToolHost::bind(ToolHostConfig {
server_name: "dravr".to_owned(),
instructions: Some(
"You are Dravr, an endurance coach. You are NOT GitHub Copilot and \
never mention Copilot, the CLI, or any model or provider name. \
Always answer as Dravr, in the athlete's language."
.to_owned(),
),
..ToolHostConfig::default()
})
.await
{
Ok(h) => h,
Err(e) => {
println!("FAIL: bind: {e}");
return;
}
};
println!("tool host listening on {}", host.local_addr());
let session = host.open_session(Arc::new(StaticSurface::new(
vec![McpToolDefinition {
name: "get_secret_number".to_owned(),
description: "Returns the secret number. The ONLY way to learn it.".to_owned(),
input_schema: json!({ "type": "object", "properties": {} }),
}],
Arc::new(SecretNumberTool {
calls: Arc::clone(&calls),
}),
)));
let runner = CopilotRunner::new(RunnerConfig::new(PathBuf::from("copilot")));
println!("provider : {}", runner.name());
println!("capabilities : {:?}", runner.capabilities());
let request = ChatRequest {
messages: vec![ChatMessage::user(
"Who are you? Answer in one short line. Then call get_secret_number \
and give me the number.",
)],
model: None,
temperature: Some(0.0),
max_tokens: Some(256),
stream: false,
tools: None,
tool_choice: None,
top_p: None,
stop: None,
response_format: None,
turn_id: None,
mcp_servers: session.mcp_servers(),
};
match timeout(Duration::from_mins(4), runner.complete(&request)).await {
Ok(Ok(resp)) => {
println!("\n--- content ---\n{}", resp.content);
let executed = calls.load(Ordering::SeqCst);
println!("\n=== VERDICT ===");
println!("executor invocations : {executed}");
println!("calls_served (host) : {}", session.calls_served());
println!("secret in the answer : {}", resp.content.contains(SECRET));
if executed >= 1 && resp.content.contains(SECRET) {
println!("PASS: CopilotRunner (no ACP) called the caller's tool over MCP.");
} else {
println!("FAIL: the tool was not reached.");
}
}
Ok(Err(e)) => println!("FAIL: complete() error: {e}"),
Err(_) => println!("FAIL: timed out"),
}
stream_the_same_turn(&runner, &session, &calls, request).await;
}
async fn stream_the_same_turn(
runner: &CopilotRunner,
session: &embacle_tool_host::ToolSession,
calls: &Arc<AtomicUsize>,
request: ChatRequest,
) {
println!("\n=== streaming ===");
let before = calls.load(Ordering::SeqCst);
let stream_request = ChatRequest {
messages: vec![ChatMessage::user(
"Call the get_secret_number tool and tell me the number it returns. \
Reply with just the number.",
)],
mcp_servers: session.mcp_servers(),
..request
};
match timeout(
Duration::from_mins(4),
runner.complete_stream(&stream_request),
)
.await
{
Ok(Ok(mut stream)) => {
let mut streamed = String::new();
while let Some(chunk) = stream.next().await {
match chunk {
Ok(c) => streamed.push_str(&c.delta),
Err(e) => {
println!("stream error: {e}");
break;
}
}
}
let during = calls.load(Ordering::SeqCst) - before;
println!("--- streamed ---\n{streamed}");
println!("executor invocations (streaming): {during}");
if during >= 1 && streamed.contains(SECRET) {
println!("PASS: streaming carries the caller's tools too.");
} else {
println!("FAIL: streaming lost the tools.");
}
}
Ok(Err(e)) => println!("FAIL: complete_stream() error: {e}"),
Err(_) => println!("FAIL: streaming timed out"),
}
}