use rmcp::ServerHandler;
use rmcp::model::{
CacheScope, CompleteRequestParams, CompleteResult, GetPromptRequestParams, GetPromptResponse, ListPromptsResult,
PaginatedRequestParams, ServerCapabilities, ServerConfig,
};
use rmcp::tool_handler;
use super::{BasemindServer, lean, tasks};
pub(super) const LIST_CACHE_TTL_MS: u64 = 300_000;
#[tool_handler(router = self.tool_router.clone())]
impl ServerHandler for BasemindServer {
async fn list_tools(
&self,
_request: Option<rmcp::model::PaginatedRequestParams>,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListToolsResult, rmcp::ErrorData> {
if self.lean_enabled() {
return Ok(lean::lean_list_tools());
}
Ok(
rmcp::model::ListToolsResult::with_all_items(self.tool_router.list_all())
.with_ttl_ms(LIST_CACHE_TTL_MS)
.with_cache_scope(CacheScope::Public),
)
}
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParams,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::CallToolResponse, rmcp::ErrorData> {
if self.lean_enabled() {
let inner_name = request
.arguments
.as_ref()
.and_then(|arguments| arguments.get("tool_name"))
.and_then(serde_json::Value::as_str);
let inner_arguments = request
.arguments
.as_ref()
.and_then(|arguments| arguments.get("tool_input"))
.and_then(serde_json::Value::as_object);
let work_class = if inner_name.is_some_and(|name| tasks::is_slow_tool(name, inner_arguments)) {
super::admission::WorkClass::Heavy
} else {
super::admission::WorkClass::Control
};
let _admission = self
.heavy_admission
.admit(work_class)
.await
.map_err(rmcp::ErrorData::from)?;
return lean::lean_call_tool(self, &self.tool_router, request, context).await;
}
let work_class = if tasks::is_slow_tool(&request.name, request.arguments.as_ref()) {
super::admission::WorkClass::Heavy
} else {
super::admission::WorkClass::Control
};
let admission = self
.heavy_admission
.admit(work_class)
.await
.map_err(rmcp::ErrorData::from)?;
let client_supports_tasks = context.client_capabilities().is_some_and(|caps| caps.supports_tasks());
if client_supports_tasks && tasks::is_slow_tool(&request.name, request.arguments.as_ref()) {
return Ok(rmcp::model::CallToolResponse::Task(tasks::spawn_slow_tool(
self, request, context, admission,
)));
}
let should_deliver_comms = request.name != "agents";
let cancel_aware = tasks::is_cancel_aware_read(&request.name, request.arguments.as_ref());
let _admission = admission;
let tcc = rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
let response = if cancel_aware {
let ct = tcc.request_context.ct.clone();
tasks::run_until_cancelled(ct.cancelled(), self.tool_router.call(tcc)).await?
} else {
self.tool_router.call(tcc).await?
};
#[cfg(all(feature = "comms", any(unix, windows)))]
let mut response = response;
#[cfg(all(feature = "comms", any(unix, windows)))]
if should_deliver_comms
&& let rmcp::model::CallToolResponse::Complete(result) = &mut response
&& let Some(notice) = super::helpers_comms::take_delivery_notice(&self.state).await
{
result.content.push(rmcp::model::ContentBlock::text(notice));
}
#[cfg(not(all(feature = "comms", any(unix, windows))))]
let _ = should_deliver_comms;
Ok(response)
}
async fn get_task(
&self,
request: rmcp::model::GetTaskParams,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::GetTaskResult, rmcp::ErrorData> {
Ok(rmcp::model::GetTaskResult::new(self.tasks.get_task(&request.task_id)?))
}
async fn update_task(
&self,
request: rmcp::model::UpdateTaskParams,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<(), rmcp::ErrorData> {
self.tasks.update_task(&request.task_id, request.input_responses)
}
async fn cancel_task(
&self,
request: rmcp::model::CancelTaskParams,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<(), rmcp::ErrorData> {
self.tasks.cancel_task(&request.task_id)
}
fn get_tool(&self, name: &str) -> Option<rmcp::model::Tool> {
if self.lean_enabled() {
return lean::lean_get_tool(name);
}
self.tool_router.get(name).cloned()
}
async fn list_prompts(
&self,
_request: Option<PaginatedRequestParams>,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<ListPromptsResult, rmcp::ErrorData> {
Ok(ListPromptsResult::with_all_items(self.prompt_router.list_all())
.with_ttl_ms(LIST_CACHE_TTL_MS)
.with_cache_scope(CacheScope::Public))
}
async fn get_prompt(
&self,
request: GetPromptRequestParams,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<GetPromptResponse, rmcp::ErrorData> {
let prompt_context =
rmcp::handler::server::prompt::PromptContext::new(self, request.name, request.arguments, context);
self.prompt_router.get_prompt(prompt_context).await
}
async fn list_resources(
&self,
_request: Option<PaginatedRequestParams>,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListResourcesResult, rmcp::ErrorData> {
Ok(self.list_resources_result())
}
async fn list_resource_templates(
&self,
_request: Option<PaginatedRequestParams>,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListResourceTemplatesResult, rmcp::ErrorData> {
Ok(self.list_resource_templates_result())
}
async fn read_resource(
&self,
request: rmcp::model::ReadResourceRequestParams,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ReadResourceResponse, rmcp::ErrorData> {
Ok(self.read_resource_uri(&request.uri).await?.into())
}
async fn complete(
&self,
request: CompleteRequestParams,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<CompleteResult, rmcp::ErrorData> {
self.state.await_cache_ready().await;
Ok(self.complete_argument(&request))
}
fn get_info(&self) -> ServerConfig {
ServerConfig::new(
ServerCapabilities::builder()
.enable_tools()
.enable_prompts()
.enable_resources()
.enable_completions()
.enable_tasks()
.build(),
)
.with_instructions(
"basemind is this repository's indexed context layer: a tree-sitter code map (symbols, \
references, callers, call graphs), git history and blame at symbol resolution, \
full-text and semantic search, document RAG, and shared cross-session memory. The \
index lives in a machine-global cache (override BASEMIND_DATA_HOME); nothing is \
written into the repo and any number of sessions share it.\n\
Every tool takes a required mode; read its description for the modes it offers.\n\
basemind first, shell/grep/git fallback. These tools return paths, line numbers, and \
signatures rather than file bodies, so they cost a fraction of the tokens of reading \
source. Prefer code mode outline over opening a file, then read only the span you \
need; code mode symbols over grep for a definition; code modes references and callers \
over grepping call sites; code mode grep over ripgrep; code mode find to locate a file \
by name; git modes recent, blame, blame_symbol, diff and touching over git log or git \
blame; graph modes calls, neighbors and map to see what reaches what; memory mode \
documents over opening PDFs, markdown, config or data files (the code map holds code \
only); web modes scrape, crawl and map for the web. Do not \
re-read a file basemind already mapped. Run admin mode rescan after edits; if a tool \
reports no indexed files, run basemind scan first.\n\
You may be one of several agents in this repo, so coordinate rather than assuming you \
are alone. Coordination runs over threads: scoped conversations addressed by at least \
two of subject, path-glob, and members, discovered by scope and never globally, and \
joined explicitly. On start call agents mode inbox, then mode thread_list; both return \
front-matter only, so use mode message with an id to read a body. Use mode post when \
you begin, finish, or hit a decision, reply with reply_to to messages about your work, \
and poll again while you work so replies do not go stale.\n\
All paths are repository-relative with forward-slash separators. Paginate by passing a \
response's next_cursor back as cursor.",
)
}
}