Implement manual cli tools option

This commit is contained in:
Danielle Jenkins 2025-03-13 11:20:08 +09:00
parent 85b4116262
commit ebede724ad
4 changed files with 428 additions and 18 deletions

View file

@ -1,16 +1,19 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use cratedocs_mcp::tools::DocRouter;
use mcp_core::Content;
use mcp_server::router::RouterService;
use mcp_server::{ByteTransport, Server};
use mcp_server::{ByteTransport, Router, Server};
use serde_json::json;
use std::net::SocketAddr;
use tokio::io::{stdin, stdout};
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{self, EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(author, version = "0.1.0", about, long_about = None)]
#[command(propagate_version = true)]
#[command(disable_version_flag = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
@ -30,6 +33,36 @@ enum Commands {
#[arg(short, long, default_value = "127.0.0.1:8080")]
address: String,
/// Enable debug logging
#[arg(short, long)]
debug: bool,
},
/// Test tools directly from the CLI
Test {
/// The tool to test (lookup_crate, search_crates, lookup_item)
#[arg(long, default_value = "lookup_crate")]
tool: String,
/// Crate name for lookup_crate and lookup_item
#[arg(long)]
crate_name: Option<String>,
/// Item path for lookup_item (e.g., std::vec::Vec)
#[arg(long)]
item_path: Option<String>,
/// Search query for search_crates
#[arg(long)]
query: Option<String>,
/// Crate version (optional)
#[arg(long)]
version: Option<String>,
/// Result limit for search_crates
#[arg(long)]
limit: Option<u32>,
/// Enable debug logging
#[arg(short, long)]
debug: bool,
@ -43,6 +76,15 @@ async fn main() -> Result<()> {
match cli.command {
Commands::Stdio { debug } => run_stdio_server(debug).await,
Commands::Http { address, debug } => run_http_server(address, debug).await,
Commands::Test {
tool,
crate_name,
item_path,
query,
version,
limit,
debug
} => run_test_tool(tool, crate_name, item_path, query, version, limit, debug).await,
}
}
@ -98,5 +140,113 @@ async fn run_http_server(address: String, debug: bool) -> Result<()> {
let app = cratedocs_mcp::transport::http_sse_server::App::new();
axum::serve(listener, app.router()).await?;
Ok(())
}
/// Run a direct test of a documentation tool from the CLI
async fn run_test_tool(
tool: String,
crate_name: Option<String>,
item_path: Option<String>,
query: Option<String>,
version: Option<String>,
limit: Option<u32>,
debug: bool,
) -> Result<()> {
// Print help information if the tool is "help"
if tool == "help" {
println!("CrateDocs CLI Tool Tester\n");
println!("Usage examples:");
println!(" cargo run --bin cratedocs -- test --tool lookup_crate --crate-name serde");
println!(" cargo run --bin cratedocs -- test --tool lookup_crate --crate-name tokio --version 1.35.0");
println!(" cargo run --bin cratedocs -- test --tool search_crates --query logger\n");
println!("Available tools:");
println!(" lookup_crate - Look up documentation for a Rust crate");
println!(" lookup_item - Look up documentation for a specific item in a crate");
println!(" search_crates - Search for crates on crates.io");
println!(" help - Show this help information\n");
return Ok(());
}
// Set up console logging
let level = if debug { tracing::Level::DEBUG } else { tracing::Level::INFO };
tracing_subscriber::fmt()
.with_max_level(level)
.without_time()
.with_target(false)
.init();
// Create router instance
let router = DocRouter::new();
tracing::info!("Testing tool: {}", tool);
// Prepare arguments based on the tool being tested
let arguments = match tool.as_str() {
"lookup_crate" => {
let crate_name = crate_name.ok_or_else(||
anyhow::anyhow!("--crate-name is required for lookup_crate tool"))?;
json!({
"crate_name": crate_name,
"version": version,
})
},
"lookup_item" => {
let crate_name = crate_name.ok_or_else(||
anyhow::anyhow!("--crate-name is required for lookup_item tool"))?;
let item_path = item_path.ok_or_else(||
anyhow::anyhow!("--item-path is required for lookup_item tool"))?;
json!({
"crate_name": crate_name,
"item_path": item_path,
"version": version,
})
},
"search_crates" => {
let query = query.ok_or_else(||
anyhow::anyhow!("--query is required for search_crates tool"))?;
json!({
"query": query,
"limit": limit,
})
},
_ => return Err(anyhow::anyhow!("Unknown tool: {}", tool)),
};
// Call the tool and get results
tracing::debug!("Calling {} with arguments: {}", tool, arguments);
println!("Executing {} tool...", tool);
let result = match router.call_tool(&tool, arguments).await {
Ok(result) => result,
Err(e) => {
eprintln!("\nERROR: {}", e);
eprintln!("\nTip: The direct item lookup may require very specific path formats. Try these commands instead:");
eprintln!(" - For crate docs: cargo run --bin cratedocs -- test --tool lookup_crate --crate-name tokio");
eprintln!(" - For crate docs with version: cargo run --bin cratedocs -- test --tool lookup_crate --crate-name serde --version 1.0.147");
return Ok(());
}
};
// Print results
if !result.is_empty() {
for content in result {
match content {
Content::Text(text) => {
println!("\n--- TOOL RESULT ---\n");
// Access the raw string from TextContent.text field
println!("{}", text.text);
println!("\n--- END RESULT ---");
},
_ => println!("Received non-text content"),
}
}
} else {
println!("Tool returned no results");
}
Ok(())
}

View file

@ -10,6 +10,7 @@ use mcp_server::router::CapabilitiesBuilder;
use reqwest::Client;
use serde_json::{json, Value};
use tokio::sync::Mutex;
use html2md::parse_html;
// Cache for documentation lookups to avoid repeated requests
#[derive(Clone)]
@ -93,14 +94,17 @@ impl DocRouter {
)));
}
let body = response.text().await.map_err(|e| {
let html_body = response.text().await.map_err(|e| {
ToolError::ExecutionError(format!("Failed to read response body: {}", e))
})?;
// Cache the result
self.cache.set(cache_key, body.clone()).await;
Ok(body)
// Convert HTML to markdown
let markdown_body = parse_html(&html_body);
// Cache the markdown result
self.cache.set(cache_key, markdown_body.clone()).await;
Ok(markdown_body)
}
// Search crates.io for crates matching a query
@ -124,7 +128,14 @@ impl DocRouter {
ToolError::ExecutionError(format!("Failed to read response body: {}", e))
})?;
Ok(body)
// Check if response is JSON (API response) or HTML (web page)
if body.trim().starts_with('{') {
// This is likely JSON data, return as is
Ok(body)
} else {
// This is likely HTML, convert to markdown
Ok(parse_html(&body))
}
}
// Get documentation for a specific item in a crate
@ -159,14 +170,17 @@ impl DocRouter {
)));
}
let body = response.text().await.map_err(|e| {
let html_body = response.text().await.map_err(|e| {
ToolError::ExecutionError(format!("Failed to read response body: {}", e))
})?;
// Cache the result
self.cache.set(cache_key, body.clone()).await;
Ok(body)
// Convert HTML to markdown
let markdown_body = parse_html(&html_body);
// Cache the markdown result
self.cache.set(cache_key, markdown_body.clone()).await;
Ok(markdown_body)
}
}
@ -176,10 +190,11 @@ impl mcp_server::Router for DocRouter {
}
fn instructions(&self) -> String {
"This server provides tools for looking up Rust crate documentation. \
"This server provides tools for looking up Rust crate documentation in markdown format. \
You can search for crates, lookup documentation for specific crates or \
items within crates. Use these tools to find information about Rust libraries \
you are not familiar with.".to_string()
you are not familiar with. All HTML documentation is automatically converted to markdown \
for better compatibility with language models.".to_string()
}
fn capabilities(&self) -> ServerCapabilities {
@ -194,7 +209,7 @@ impl mcp_server::Router for DocRouter {
vec![
Tool::new(
"lookup_crate".to_string(),
"Look up documentation for a Rust crate".to_string(),
"Look up documentation for a Rust crate (returns markdown)".to_string(),
json!({
"type": "object",
"properties": {
@ -212,7 +227,7 @@ impl mcp_server::Router for DocRouter {
),
Tool::new(
"search_crates".to_string(),
"Search for Rust crates on crates.io".to_string(),
"Search for Rust crates on crates.io (returns JSON or markdown)".to_string(),
json!({
"type": "object",
"properties": {
@ -230,7 +245,7 @@ impl mcp_server::Router for DocRouter {
),
Tool::new(
"lookup_item".to_string(),
"Look up documentation for a specific item in a Rust crate".to_string(),
"Look up documentation for a specific item in a Rust crate (returns markdown)".to_string(),
json!({
"type": "object",
"properties": {