use clap::Args;
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::{ServerHandler, ServiceExt, tool, tool_handler, tool_router};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::output::AgError;
#[derive(Args)]
pub struct McpArgs {}
pub fn run(_args: McpArgs) -> Result<serde_json::Value, AgError> {
let rt = tokio::runtime::Runtime::new().map_err(|e| AgError::Internal {
message: format!("failed to create tokio runtime: {e}"),
})?;
rt.block_on(async {
let server = AgMcpServer::new();
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let service = server
.serve((stdin, stdout))
.await
.map_err(|e| AgError::Internal {
message: format!("MCP server error: {e}"),
})?;
let _ = service.waiting().await;
Ok(serde_json::json!({"status": "stopped"}))
})
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct SearchParams {
pub query: String,
#[serde(default = "default_path")]
pub path: String,
#[serde(default = "default_top_k")]
pub top_k: Option<usize>,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct ReadParams {
pub file: String,
#[serde(default)]
pub skeleton: bool,
#[serde(default)]
pub outline: bool,
#[serde(default)]
pub hash: bool,
#[serde(default)]
pub if_changed: Option<String>,
#[serde(default)]
pub mode: Option<String>,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct FindParams {
#[serde(default = "default_path")]
pub path: String,
pub pattern: Option<String>,
pub depth: Option<usize>,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct ExistsParams {
pub pattern: String,
#[serde(default = "default_path")]
pub path: String,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct OutlineParams {
pub path: String,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct RunParams {
pub command: Vec<String>,
}
fn default_path() -> String {
".".to_string()
}
fn default_top_k() -> Option<usize> {
Some(5)
}
#[derive(Debug, Clone)]
pub struct AgMcpServer {
tool_router: ToolRouter<Self>,
}
impl AgMcpServer {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}
impl Default for AgMcpServer {
fn default() -> Self {
Self::new()
}
}
#[tool_handler(router = self.tool_router)]
impl ServerHandler for AgMcpServer {}
#[tool_router(router = tool_router)]
impl AgMcpServer {
#[tool(
name = "search",
description = "Search codebase with natural language or code query"
)]
async fn search(&self, params: Parameters<SearchParams>) -> String {
let p = params.0;
let args = super::search::SearchArgs {
query: p.query,
path: p.path,
literal: false,
semantic: false,
structural: false,
top_k: p.top_k.unwrap_or(5),
budget: None,
context: None,
exists: false,
continue_token: None,
alpha: None,
};
match super::search::run(args) {
Ok(v) => serde_json::to_string(&v).unwrap_or_else(|e| format!("error: {e}")),
Err(e) => format!("error: {e}"),
}
}
#[tool(
name = "read",
description = "Read file with optional skeleton/outline/hash mode"
)]
async fn read(&self, params: Parameters<ReadParams>) -> String {
let p = params.0;
let args = super::read::ReadArgs {
file: p.file,
lines: None,
snap: None,
skeleton: p.skeleton,
outline: p.outline,
hash: p.hash,
budget: None,
meta: false,
if_changed: p.if_changed,
mode: p.mode,
};
match super::read::run(args) {
Ok(v) => serde_json::to_string(&v).unwrap_or_else(|e| format!("error: {e}")),
Err(e) => format!("error: {e}"),
}
}
#[tool(name = "find", description = "List and filter files in workspace")]
async fn find(&self, params: Parameters<FindParams>) -> String {
let p = params.0;
let args = super::find::FindArgs {
path: p.path,
pattern: p.pattern,
depth: p.depth,
related_to: None,
changed_since: None,
outline: false,
tree: false,
flat: false,
budget: None,
};
match super::find::run(args) {
Ok(v) => serde_json::to_string(&v).unwrap_or_else(|e| format!("error: {e}")),
Err(e) => format!("error: {e}"),
}
}
#[tool(
name = "exists",
description = "Check if a pattern exists in the codebase (O(1) bloom filter)"
)]
async fn exists(&self, params: Parameters<ExistsParams>) -> String {
let p = params.0;
let args = super::exists::ExistsArgs {
pattern: p.pattern,
path: p.path,
};
match super::exists::run(args) {
Ok(v) => serde_json::to_string(&v).unwrap_or_else(|e| format!("error: {e}")),
Err(e) => format!("error: {e}"),
}
}
#[tool(
name = "outline",
description = "Get symbol table for a file or directory"
)]
async fn outline(&self, params: Parameters<OutlineParams>) -> String {
let p = params.0;
let args = super::outline::OutlineArgs {
path: p.path,
depth: None,
kind: None,
};
match super::outline::run(args) {
Ok(v) => serde_json::to_string(&v).unwrap_or_else(|e| format!("error: {e}")),
Err(e) => format!("error: {e}"),
}
}
#[tool(
name = "run",
description = "Run a command and return structured output (test/build/lint)"
)]
async fn run_cmd(&self, params: Parameters<RunParams>) -> String {
let p = params.0;
let args = super::run::RunArgs {
command: p.command,
raw: false,
full: false,
auto_json: false,
timeout: 300,
};
match super::run::run(args) {
Ok(v) => serde_json::to_string(&v).unwrap_or_else(|e| format!("error: {e}")),
Err(e) => format!("error: {e}"),
}
}
}