#![cfg(feature = "mcp")]
use std::time::Duration;
use aingle_cortex::mcp::AingleMcp;
use aingle_cortex::state::AppState;
use rmcp::model::CallToolRequestParams;
use rmcp::{RoleClient, ServiceExt};
#[tokio::test]
async fn mcp_in_process_client_server() {
let state = AppState::with_db_path(":memory:", None).expect("build in-memory AppState");
state.set_mcp_policy(aingle_cortex::mcp::policy::McpPolicy {
permission: aingle_cortex::mcp::policy::Permission::ReadWrite,
..Default::default()
});
let (server_io, client_io) = tokio::io::duplex(8 * 1024);
let server_task = tokio::spawn(async move {
let running = AingleMcp::new(state)
.serve(server_io)
.await
.expect("server serve handshake");
let _ = running.waiting().await;
});
let client = ServiceExt::<RoleClient>::serve((), client_io)
.await
.expect("client serve handshake");
let tools = client.list_all_tools().await.expect("list_all_tools");
let names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
assert!(
names.contains(&"aingle_query_pattern"),
"tools missing aingle_query_pattern; got {names:?}"
);
assert!(
names.contains(&"aingle_graph_stats"),
"tools missing aingle_graph_stats; got {names:?}"
);
assert!(
names.contains(&"aingle_dag_history"),
"tools missing aingle_dag_history (dag feature); got {names:?}"
);
let stats = client
.call_tool(CallToolRequestParams::new("aingle_graph_stats"))
.await
.expect("call aingle_graph_stats");
assert_ne!(
stats.is_error,
Some(true),
"aingle_graph_stats returned an error result: {stats:?}"
);
let create_args = serde_json::json!({
"subject": "http://example.org/alice",
"predicate": "http://example.org/knows",
"object": "bob",
})
.as_object()
.cloned()
.unwrap();
let create = client
.call_tool(CallToolRequestParams::new("aingle_create_triple").with_arguments(create_args))
.await
.expect("call aingle_create_triple");
assert_ne!(
create.is_error,
Some(true),
"aingle_create_triple returned an error result: {create:?}"
);
let query_args = serde_json::json!({
"subject": "http://example.org/alice",
})
.as_object()
.cloned()
.unwrap();
let query = client
.call_tool(CallToolRequestParams::new("aingle_query_pattern").with_arguments(query_args))
.await
.expect("call aingle_query_pattern");
assert_ne!(
query.is_error,
Some(true),
"aingle_query_pattern returned an error result: {query:?}"
);
let _ = client.cancel().await;
server_task.abort();
}
#[tokio::test]
async fn stdout_is_clean_jsonrpc_only() {
use tokio::io::AsyncWriteExt;
let mut child = tokio::process::Command::new(env!("CARGO_BIN_EXE_aingle-cortex"))
.args(["--mcp", "--memory"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("spawn aingle-cortex --mcp --memory");
let mut stdin = child.stdin.take().expect("child stdin");
stdin
.write_all(b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"t\",\"version\":\"0\"}}}\n")
.await
.expect("write initialize");
drop(stdin);
let out = match tokio::time::timeout(Duration::from_secs(30), child.wait_with_output()).await {
Ok(res) => res.expect("collect child output"),
Err(_) => {
panic!("aingle-cortex did not exit within 30s after stdin EOF");
}
};
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines().filter(|l| !l.trim().is_empty()) {
assert!(
line.trim_start().starts_with('{'),
"non-JSON on stdout: {line}"
);
}
}