#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use embacle::types::{McpServerConfig, McpTransport, RunnerError};
use embacle::{McpToolDefinition, McpToolExecutor};
use embacle_tool_host::{StaticSurface, ToolHost, ToolHostConfig, ToolOutcome, ToolSurface};
use serde_json::{json, Value};
use tokio::time::sleep;
struct RecordingExecutor {
calls: AtomicUsize,
}
#[async_trait]
impl McpToolExecutor for RecordingExecutor {
async fn execute(&self, tool_name: &str, arguments: &Value) -> Result<Value, RunnerError> {
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(json!({ "ran": tool_name, "got": arguments }))
}
}
fn one_tool() -> Vec<McpToolDefinition> {
vec![McpToolDefinition {
name: "get_activities".to_owned(),
description: "List recent activities".to_owned(),
input_schema: json!({ "type": "object", "properties": {} }),
}]
}
fn bearer_of(servers: &[McpServerConfig]) -> String {
match &servers[0].transport {
McpTransport::Http { headers, .. } => headers[0].value.clone(),
_ => panic!("the host must publish an HTTP transport"),
}
}
fn url_of(servers: &[McpServerConfig]) -> String {
match &servers[0].transport {
McpTransport::Http { url, .. } => url.clone(),
_ => panic!("the host must publish an HTTP transport"),
}
}
async fn post(url: &str, bearer: &str, body: Value) -> (u16, Value) {
let response = reqwest::Client::new()
.post(url)
.header("authorization", bearer)
.json(&body)
.send()
.await
.expect("the host answers");
let status = response.status().as_u16();
let parsed = response.json::<Value>().await.unwrap_or(Value::Null);
(status, parsed)
}
fn call(tool: &str) -> Value {
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": tool, "arguments": {} }
})
}
#[tokio::test]
async fn an_agent_lists_and_runs_the_callers_tools() {
let host = ToolHost::bind(ToolHostConfig {
server_name: "dravr".to_owned(),
..ToolHostConfig::default()
})
.await
.expect("binds on loopback");
let executor = Arc::new(RecordingExecutor {
calls: AtomicUsize::new(0),
});
let session = host.open_session(Arc::new(StaticSurface::new(one_tool(), executor.clone())));
let servers = session.mcp_servers();
let (url, bearer) = (url_of(&servers), bearer_of(&servers));
assert_eq!(servers[0].name, "dravr");
let (status, listed) = post(
&url,
&bearer,
json!({"jsonrpc":"2.0","id":1,"method":"tools/list"}),
)
.await;
assert_eq!(
status, 200,
"an authenticated tools/list is served: {listed}"
);
let names = listed["result"]["tools"]
.as_array()
.expect("tools array")
.iter()
.filter_map(|t| t["name"].as_str())
.collect::<Vec<_>>();
assert_eq!(
names,
vec!["get_activities"],
"the agent sees exactly the caller's surface"
);
let (status, result) = post(&url, &bearer, call("get_activities")).await;
assert_eq!(status, 200, "a tools/call is served: {result}");
assert_eq!(
executor.calls.load(Ordering::SeqCst),
1,
"the CALLER's executor ran it — not embacle, not the agent"
);
assert_eq!(session.calls_served(), 1);
}
#[tokio::test]
async fn dropping_the_session_revokes_the_bearer() {
let host = ToolHost::bind(ToolHostConfig::default())
.await
.expect("binds");
let executor = Arc::new(RecordingExecutor {
calls: AtomicUsize::new(0),
});
let session = host.open_session(Arc::new(StaticSurface::new(one_tool(), executor.clone())));
let servers = session.mcp_servers();
let (url, bearer) = (url_of(&servers), bearer_of(&servers));
let (status, _) = post(&url, &bearer, call("get_activities")).await;
assert_eq!(status, 200, "the bearer works while the session is open");
assert_eq!(host.open_sessions(), 1);
drop(session);
assert_eq!(host.open_sessions(), 0, "the guard removed its session");
let (status, _) = post(&url, &bearer, call("get_activities")).await;
assert_eq!(
status, 401,
"the same bearer must be refused once the turn is over"
);
assert_eq!(
executor.calls.load(Ordering::SeqCst),
1,
"the revoked call must not have reached the caller's executor"
);
}
#[tokio::test]
async fn a_tool_outside_the_session_surface_is_refused() {
let host = ToolHost::bind(ToolHostConfig::default())
.await
.expect("binds");
let executor = Arc::new(RecordingExecutor {
calls: AtomicUsize::new(0),
});
let session = host.open_session(Arc::new(StaticSurface::new(one_tool(), executor.clone())));
let servers = session.mcp_servers();
let (status, result) = post(
&url_of(&servers),
&bearer_of(&servers),
call("delete_everything"),
)
.await;
assert_eq!(status, 200, "a refusal is in-band, not a transport error");
assert!(
result["result"]["isError"].as_bool().unwrap_or(false),
"an ungranted tool must come back as an error result: {result}"
);
assert_eq!(
executor.calls.load(Ordering::SeqCst),
0,
"an ungranted tool must never reach the caller's executor"
);
}
#[tokio::test]
async fn an_unauthenticated_call_is_refused() {
let host = ToolHost::bind(ToolHostConfig::default())
.await
.expect("binds");
let executor = Arc::new(RecordingExecutor {
calls: AtomicUsize::new(0),
});
let session = host.open_session(Arc::new(StaticSurface::new(one_tool(), executor.clone())));
let servers = session.mcp_servers();
let (status, _) = post(&url_of(&servers), "", call("get_activities")).await;
assert_eq!(status, 401);
assert_eq!(executor.calls.load(Ordering::SeqCst), 0);
}
struct WithdrawingSurface {
visible: AtomicBool,
calls: AtomicUsize,
}
#[async_trait]
impl ToolSurface for WithdrawingSurface {
async fn list_tools(&self) -> Vec<McpToolDefinition> {
if self.visible.load(Ordering::SeqCst) {
one_tool()
} else {
Vec::new()
}
}
async fn call(&self, tool_name: &str, _arguments: &Value) -> ToolOutcome {
self.calls.fetch_add(1, Ordering::SeqCst);
ToolOutcome::json(json!({ "ran": tool_name }))
}
}
#[tokio::test]
async fn a_tool_withdrawn_mid_session_stops_being_listed_and_callable() {
let host = ToolHost::bind(ToolHostConfig::default())
.await
.expect("binds");
let surface = Arc::new(WithdrawingSurface {
visible: AtomicBool::new(true),
calls: AtomicUsize::new(0),
});
let session = host.open_session(surface.clone());
let servers = session.mcp_servers();
let (url, bearer) = (url_of(&servers), bearer_of(&servers));
let (_, listed) = post(
&url,
&bearer,
json!({"jsonrpc":"2.0","id":1,"method":"tools/list"}),
)
.await;
assert_eq!(
listed["result"]["tools"].as_array().map(Vec::len),
Some(1),
"visible while the surface says so"
);
let (status, _) = post(&url, &bearer, call("get_activities")).await;
assert_eq!(status, 200);
assert_eq!(surface.calls.load(Ordering::SeqCst), 1);
surface.visible.store(false, Ordering::SeqCst);
let (_, listed) = post(
&url,
&bearer,
json!({"jsonrpc":"2.0","id":1,"method":"tools/list"}),
)
.await;
assert_eq!(
listed["result"]["tools"].as_array().map(Vec::len),
Some(0),
"the listing must reflect the withdrawal immediately"
);
let (status, result) = post(&url, &bearer, call("get_activities")).await;
assert_eq!(status, 200, "a refusal is in-band");
assert!(
result["result"]["isError"].as_bool().unwrap_or(false),
"a withdrawn tool must be refused: {result}"
);
assert_eq!(
surface.calls.load(Ordering::SeqCst),
1,
"the withdrawn call must never reach the caller's surface"
);
}
#[tokio::test]
async fn a_refusal_carries_its_reason_and_is_flagged() {
struct RefusingSurface;
#[async_trait]
impl ToolSurface for RefusingSurface {
async fn list_tools(&self) -> Vec<McpToolDefinition> {
one_tool()
}
async fn call(&self, _tool_name: &str, _arguments: &Value) -> ToolOutcome {
ToolOutcome::refused("daily limit reached — try tomorrow")
.with_structured(json!({ "code": "quota_exceeded" }))
}
}
let host = ToolHost::bind(ToolHostConfig::default())
.await
.expect("binds");
let session = host.open_session(Arc::new(RefusingSurface));
let servers = session.mcp_servers();
let (status, result) = post(
&url_of(&servers),
&bearer_of(&servers),
call("get_activities"),
)
.await;
assert_eq!(status, 200, "a refusal is served, not a transport error");
assert!(
result["result"]["isError"].as_bool().unwrap_or(false),
"must be flagged: {result}"
);
let text = result["result"]["content"][0]["text"]
.as_str()
.unwrap_or("");
assert!(
text.contains("daily limit reached"),
"the model must receive the reason, got {text:?}"
);
assert_eq!(
result["result"]["structuredContent"]["code"].as_str(),
Some("quota_exceeded"),
"structured content must survive: {result}"
);
}
#[tokio::test]
async fn concurrent_sessions_do_not_leak_into_each_other() {
let host = ToolHost::bind(ToolHostConfig::default())
.await
.expect("binds");
let alice_exec = Arc::new(RecordingExecutor {
calls: AtomicUsize::new(0),
});
let bob_exec = Arc::new(RecordingExecutor {
calls: AtomicUsize::new(0),
});
let alice_tool = vec![McpToolDefinition {
name: "alice_only".to_owned(),
description: "Alice's tool".to_owned(),
input_schema: json!({ "type": "object", "properties": {} }),
}];
let alice = host.open_session(Arc::new(StaticSurface::new(alice_tool, alice_exec.clone())));
let bob = host.open_session(Arc::new(StaticSurface::new(one_tool(), bob_exec.clone())));
assert_eq!(host.open_sessions(), 2);
let a = alice.mcp_servers();
let b = bob.mcp_servers();
let (a_url, a_bearer) = (url_of(&a), bearer_of(&a));
let (b_url, b_bearer) = (url_of(&b), bearer_of(&b));
assert_eq!(a_url, b_url);
assert_ne!(a_bearer, b_bearer, "each session mints its own bearer");
let (_, a_list) = post(
&a_url,
&a_bearer,
json!({"jsonrpc":"2.0","id":1,"method":"tools/list"}),
)
.await;
assert_eq!(
a_list["result"]["tools"][0]["name"].as_str(),
Some("alice_only")
);
let (status, result) = post(&b_url, &b_bearer, call("alice_only")).await;
assert_eq!(status, 200);
assert!(
result["result"]["isError"].as_bool().unwrap_or(false),
"Bob must not be able to call Alice's tool: {result}"
);
assert_eq!(
alice_exec.calls.load(Ordering::SeqCst),
0,
"Alice's executor must never be reached by Bob's session"
);
let (status, _) = post(&a_url, &a_bearer, call("alice_only")).await;
assert_eq!(status, 200);
let (status, _) = post(&b_url, &b_bearer, call("get_activities")).await;
assert_eq!(status, 200);
assert_eq!(alice_exec.calls.load(Ordering::SeqCst), 1);
assert_eq!(bob_exec.calls.load(Ordering::SeqCst), 1);
drop(alice);
assert_eq!(host.open_sessions(), 1);
let (status, _) = post(&a_url, &a_bearer, call("alice_only")).await;
assert_eq!(status, 401, "Alice's bearer is dead");
let (status, _) = post(&b_url, &b_bearer, call("get_activities")).await;
assert_eq!(status, 200, "Bob is unaffected by Alice's revocation");
}
#[tokio::test]
async fn shutdown_stops_the_listener() {
let host = ToolHost::bind(ToolHostConfig::default())
.await
.expect("binds");
let executor = Arc::new(RecordingExecutor {
calls: AtomicUsize::new(0),
});
let session = host.open_session(Arc::new(StaticSurface::new(one_tool(), executor)));
let servers = session.mcp_servers();
let (url, bearer) = (url_of(&servers), bearer_of(&servers));
let (status, _) = post(&url, &bearer, call("get_activities")).await;
assert_eq!(status, 200, "serving before shutdown");
host.shutdown();
host.shutdown();
for _ in 0..40 {
if reqwest::Client::new()
.post(&url)
.header("authorization", &bearer)
.json(&call("get_activities"))
.send()
.await
.is_err()
{
return; }
sleep(Duration::from_millis(50)).await;
}
panic!("the listener was still accepting two seconds after shutdown");
}
#[tokio::test]
async fn the_static_adapter_preserves_the_error_kind() {
struct FailingExecutor;
#[async_trait]
impl McpToolExecutor for FailingExecutor {
async fn execute(&self, _tool: &str, _args: &Value) -> Result<Value, RunnerError> {
Err(RunnerError::auth_failure("token expired"))
}
}
let host = ToolHost::bind(ToolHostConfig::default())
.await
.expect("binds");
let session = host.open_session(Arc::new(StaticSurface::new(
one_tool(),
Arc::new(FailingExecutor),
)));
let servers = session.mcp_servers();
let (status, result) = post(
&url_of(&servers),
&bearer_of(&servers),
call("get_activities"),
)
.await;
assert_eq!(status, 200, "a failing tool is still an in-band result");
assert!(
result["result"]["isError"].as_bool().unwrap_or(false),
"must be flagged as an error: {result}"
);
assert_eq!(
result["result"]["structuredContent"]["error_kind"].as_str(),
Some("AuthFailure"),
"the kind must survive as a machine-readable discriminator: {result}"
);
let text = result["result"]["content"][0]["text"]
.as_str()
.unwrap_or("");
assert!(
text.contains("token expired"),
"the message must reach the model, got {text:?}"
);
}
#[tokio::test]
async fn instructions_are_served_at_initialize() {
const PERSONA: &str = "You are Dravr, an endurance coach.";
let host = ToolHost::bind(ToolHostConfig {
instructions: Some(PERSONA.to_owned()),
..ToolHostConfig::default()
})
.await
.expect("binds");
let executor = Arc::new(RecordingExecutor {
calls: AtomicUsize::new(0),
});
let session = host.open_session(Arc::new(StaticSurface::new(one_tool(), executor)));
let servers = session.mcp_servers();
let (status, result) = post(
&url_of(&servers),
&bearer_of(&servers),
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "test", "version": "1" }
}
}),
)
.await;
assert_eq!(status, 200, "initialize is served: {result}");
assert_eq!(
result["result"]["instructions"].as_str(),
Some(PERSONA),
"the caller's instructions must reach the agent verbatim: {result}"
);
}
#[tokio::test]
async fn no_instructions_means_the_field_is_absent() {
let host = ToolHost::bind(ToolHostConfig::default())
.await
.expect("binds");
let executor = Arc::new(RecordingExecutor {
calls: AtomicUsize::new(0),
});
let session = host.open_session(Arc::new(StaticSurface::new(one_tool(), executor)));
let servers = session.mcp_servers();
let (_, result) = post(
&url_of(&servers),
&bearer_of(&servers),
json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": { "protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": { "name": "test", "version": "1" } }
}),
)
.await;
assert!(
result["result"]["instructions"].is_null(),
"absent, not empty: {result}"
);
}