#![cfg(all(
feature = "proto-2026-07-28-rc",
feature = "http-server-volga",
feature = "http-client",
feature = "di"
))]
use neva::App;
use neva::di::Dc;
struct Counter {
value: i64,
}
impl Counter {
fn get(&self) -> i64 {
self.value
}
}
#[neva::tool]
async fn read_counter(counter: Dc<Counter>) -> String {
counter.get().to_string()
}
#[neva::tool]
async fn add_to_counter(delta: i64, counter: Dc<Counter>) -> String {
(counter.get() + delta).to_string()
}
#[tokio::test(flavor = "multi_thread")]
async fn dc_extractor_is_injected_not_advertised() {
let port = pick_free_port();
let addr = format!("127.0.0.1:{port}");
let app = App::new()
.with_options(|opt| opt.with_http(|http| http.bind(&addr).with_endpoint("/mcp")))
.add_singleton(Counter { value: 41 });
let handle = tokio::spawn(async move { app.run().await });
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
let client = reqwest::Client::new();
let url = format!("http://{addr}/mcp");
let list_body = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
});
let resp = client
.post(&url)
.header("MCP-Protocol-Version", "2026-07-28")
.json(&list_body)
.send()
.await
.expect("tools/list failed");
assert!(resp.status().is_success());
let body: serde_json::Value = resp.json().await.unwrap();
let tools = body
.pointer("/result/tools")
.and_then(|v| v.as_array())
.expect("missing tools array");
let by_name = |name: &str| -> serde_json::Value {
tools
.iter()
.find(|t| t["name"] == serde_json::json!(name))
.unwrap_or_else(|| panic!("tool {name} not listed"))
.clone()
};
let read = by_name("read_counter");
let props = &read["inputSchema"]["properties"];
assert!(
props.get("counter").is_none(),
"Dc dependency must not be advertised as a property: {read}"
);
let required = read["inputSchema"]["required"]
.as_array()
.cloned()
.unwrap_or_default();
assert!(
!required.iter().any(|v| v == "counter"),
"Dc dependency must not be required: {read}"
);
let add = by_name("add_to_counter");
let add_props = &add["inputSchema"]["properties"];
assert!(
add_props.get("delta").is_some(),
"real arg `delta` must be advertised: {add}"
);
assert!(
add_props.get("counter").is_none(),
"Dc dependency must not be advertised alongside real args: {add}"
);
let add_required: Vec<String> =
serde_json::from_value(add["inputSchema"]["required"].clone()).unwrap_or_default();
assert_eq!(
add_required,
vec!["delta".to_string()],
"only the real arg must be required"
);
let call_body = serde_json::json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": { "name": "read_counter" }
});
let resp = client
.post(&url)
.header("MCP-Protocol-Version", "2026-07-28")
.json(&call_body)
.send()
.await
.expect("tools/call read_counter failed");
assert!(resp.status().is_success());
let body: serde_json::Value = resp.json().await.unwrap();
let text = body
.pointer("/result/content/0/text")
.and_then(|v| v.as_str())
.expect("missing tool result text");
assert_eq!(text, "41", "Dc<Counter> was not resolved from context");
let call_body = serde_json::json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": { "name": "add_to_counter", "arguments": { "delta": 1 } }
});
let resp = client
.post(&url)
.header("MCP-Protocol-Version", "2026-07-28")
.json(&call_body)
.send()
.await
.expect("tools/call add_to_counter failed");
assert!(resp.status().is_success());
let body: serde_json::Value = resp.json().await.unwrap();
let text = body
.pointer("/result/content/0/text")
.and_then(|v| v.as_str())
.expect("missing tool result text");
assert_eq!(text, "42", "Dc<Counter> + real arg did not both resolve");
handle.abort();
}
fn pick_free_port() -> u16 {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
port
}