#![cfg_attr(not(test), deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::arithmetic_side_effects,
))]
use clap::Parser;
use droidsaw::mcp::{DroidsawServer, McpToolClass, McpToolTier};
use rmcp::ServiceExt;
#[derive(Parser, Debug)]
#[command(
name = "droidsaw-mcp",
about = "DROIDSAW MCP server (stdio transport)."
)]
struct Cli {
#[arg(
long,
env = "MCP_ALLOWED_CLASSES",
default_value = "read-only,writes-tempfile",
value_parser = parse_allowed_classes,
)]
allowed_tool_classes: std::collections::BTreeSet<McpToolClass>,
#[arg(
long = "allowed-read-path",
env = "DROIDSAW_MCP_ALLOWED_READ_PATH",
value_delimiter = ':',
action = clap::ArgAction::Append,
)]
allowed_read_paths: Vec<String>,
#[arg(
long = "allowed-write-path",
env = "DROIDSAW_MCP_ALLOWED_WRITE_PATH",
value_delimiter = ':',
action = clap::ArgAction::Append,
)]
allowed_write_paths: Vec<String>,
#[arg(
long,
env = "DROIDSAW_MCP_TOOL_TIER",
default_value = "full",
value_parser = parse_tool_tier,
)]
tool_tier: McpToolTier,
}
fn parse_tool_tier(s: &str) -> Result<McpToolTier, String> {
use std::str::FromStr;
McpToolTier::from_str(s)
}
fn parse_allowed_classes(s: &str) -> Result<std::collections::BTreeSet<McpToolClass>, String> {
use std::str::FromStr;
if s.trim() == "all" {
return Ok(McpToolClass::all().into_iter().collect());
}
let mut set = std::collections::BTreeSet::new();
for part in s.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
set.insert(McpToolClass::from_str(part)?);
}
if set.is_empty() {
return Err("at least one tool class required (or pass 'all')".to_string());
}
Ok(set)
}
const TOKIO_THREAD_STACK_SIZE_DEBUG: usize = 16 * 1024 * 1024;
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
droidsaw_common::diag::init();
let mut rt_builder = tokio::runtime::Builder::new_multi_thread();
rt_builder.enable_all();
if cfg!(debug_assertions) {
rt_builder.thread_stack_size(TOKIO_THREAD_STACK_SIZE_DEBUG);
}
let runtime = rt_builder.build()?;
runtime.block_on(async_main(cli))
}
async fn async_main(cli: Cli) -> anyhow::Result<()> {
if !cli.allowed_read_paths.is_empty() {
let joined = cli.allowed_read_paths.join(":");
unsafe { std::env::set_var("DROIDSAW_MCP_ALLOWED_READ_PATH", joined) };
}
if !cli.allowed_write_paths.is_empty() {
let joined = cli.allowed_write_paths.join(":");
unsafe { std::env::set_var("DROIDSAW_MCP_ALLOWED_WRITE_PATH", joined) };
}
run_stdio(cli.allowed_tool_classes, cli.tool_tier).await
}
async fn run_stdio(
allowed_classes: std::collections::BTreeSet<McpToolClass>,
tool_tier: McpToolTier,
) -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_max_level(tracing::Level::WARN)
.init();
tracing::info!(
"droidsaw-mcp server starting (transport=stdio); allowed-classes={:?}; tool-tier={:?}",
allowed_classes,
tool_tier,
);
let service = DroidsawServer::with_allowed_classes(allowed_classes)
.with_tool_tier(tool_tier)
.serve(rmcp::transport::stdio())
.await?;
service.waiting().await?;
Ok(())
}