use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::{tool, tool_router};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::db::Db;
use crate::paths;
use crate::query as core_query;
#[derive(Clone)]
pub struct BeckServer {
pub tool_router: ToolRouter<Self>,
}
impl BeckServer {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}
fn default_top() -> usize {
3
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct QueryParams {
pub query: String,
#[serde(default = "default_top")]
pub top: usize,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct LoadParams {
pub name: String,
}
#[tool_router]
impl BeckServer {
#[tool(
name = "skills_query",
description = "Search locally indexed skills by free-text query. Returns up to `top` ranked matches, each with name, short description, and BM25 score. Always call this first when you need a skill but do not know its exact name, then call skills_load with the chosen name to fetch the full body."
)]
async fn tool_query(&self, Parameters(params): Parameters<QueryParams>) -> String {
let result = (|| -> anyhow::Result<String> {
let db_path = paths::db_path().map_err(|e| anyhow::anyhow!("{e}"))?;
if !db_path.exists() {
return Ok(json!({
"error": "beck has not been synced yet. Run `beck sync` from a shell to index skills."
})
.to_string());
}
let db = Db::open(&db_path)?;
let matches = core_query::search(&db, ¶ms.query, params.top)?;
let arr: Vec<_> = matches
.iter()
.map(|m| {
json!({
"name": m.name,
"description": m.description,
"score": m.score,
})
})
.collect();
Ok(serde_json::to_string(&json!({ "matches": arr }))
.unwrap_or_else(|_| "[]".to_string()))
})();
match result {
Ok(s) => s,
Err(e) => json!({ "error": format!("{e}") }).to_string(),
}
}
#[tool(
name = "skills_load",
description = "Load the full markdown body of a skill by exact name. Use skills_query first to find candidate names. Returns {name, path, body} as JSON. Returns {error} if no skill matches the given name."
)]
async fn tool_load(&self, Parameters(params): Parameters<LoadParams>) -> String {
let result = (|| -> anyhow::Result<String> {
let db_path = paths::db_path().map_err(|e| anyhow::anyhow!("{e}"))?;
if !db_path.exists() {
return Ok(json!({
"error": "beck has not been synced yet. Run `beck sync` from a shell to index skills."
})
.to_string());
}
let db = Db::open(&db_path)?;
let row: std::result::Result<(String, String, String), rusqlite::Error> =
db.conn.query_row(
"SELECT name, path, body FROM skills WHERE name = ?1",
[¶ms.name],
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
);
match row {
Ok((name, path, body)) => Ok(json!({
"name": name,
"path": path,
"body": body,
})
.to_string()),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(json!({
"error": format!("skill not found: {}", params.name)
})
.to_string()),
Err(e) => Err(e.into()),
}
})();
match result {
Ok(s) => s,
Err(e) => json!({ "error": format!("{e}") }).to_string(),
}
}
}