use std::path::PathBuf;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
use tokio::time::sleep;
const PACKAGE: &str = "@sylphx/pdf-reader-mcp";
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let pdf = std::env::args().nth(1).map_or_else(
|| std::env::current_dir().unwrap().join("examples/sample.pdf"),
PathBuf::from,
);
let pdf = pdf
.canonicalize()
.expect("PDF not found — pass a path as an argument or place a PDF at examples/sample.pdf");
println!("PDF under test: {}", pdf.display());
run_scenario(
"PROBE — initialize + tools/list",
PACKAGE,
&[
init_msg(1),
r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#.into(),
],
Duration::from_secs(5),
)
.await?;
run_scenario(
"READ PDF — happy path",
PACKAGE,
&[
init_msg(1),
serde_json::to_string(&serde_json::json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "read_pdf",
"arguments": {
"sources": [{ "path": pdf }],
"include_metadata": true,
"include_page_count": true,
"include_full_text": true
}
}
}))?,
],
Duration::from_secs(20),
)
.await?;
run_scenario(
"OUT-OF-SCOPE PATH — expect error -32602",
PACKAGE,
&[
init_msg(1),
r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_pdf","arguments":{"sources":[{"path":"/etc/passwd"}]}}}"#.into(),
],
Duration::from_secs(5),
)
.await?;
Ok(())
}
fn init_msg(id: u32) -> String {
serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "mcp-probe", "version": "0.1.0" }
}
})
.to_string()
}
async fn run_scenario(
title: &str,
package: &str,
messages: &[String],
wait: Duration,
) -> anyhow::Result<()> {
let bar = "═".repeat(title.len() + 4);
println!("\n╔{bar}╗");
println!("║ {title} ║");
println!("╚{bar}╝\n");
let npxc = find_npxc();
let mut child = Command::new(&npxc)
.arg(package)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.spawn()?;
let mut stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let printer = tokio::spawn(async move {
let mut lines = BufReader::new(stdout).lines();
while let Ok(Some(line)) = lines.next_line().await {
match serde_json::from_str::<serde_json::Value>(&line) {
Ok(v) => println!("{}", serde_json::to_string_pretty(&v).unwrap_or(line)),
Err(_) => println!("{line}"),
}
}
});
for msg in messages {
stdin.write_all(msg.as_bytes()).await?;
stdin.write_all(b"\n").await?;
stdin.flush().await?;
sleep(Duration::from_millis(200)).await;
}
sleep(wait).await;
drop(stdin);
printer.await?;
let _ = child.wait().await;
Ok(())
}
fn find_npxc() -> PathBuf {
if let Ok(exe) = std::env::current_exe() {
if let Some(examples_dir) = exe.parent() {
if let Some(profile_dir) = examples_dir.parent() {
let candidate = profile_dir.join("npxc");
if candidate.exists() {
return candidate;
}
}
}
}
for profile in ["release", "debug"] {
let p = PathBuf::from("target").join(profile).join("npxc");
if p.exists() {
return p;
}
}
panic!(
"npxc binary not found — run `cargo build --release` before `cargo run --example mcp_probe`"
);
}