pub(crate) mod schemas;
pub(crate) mod tools;
use std::sync::Arc;
use std::sync::Mutex;
use rmcp::{
ServerHandler,
handler::server::router::tool::ToolRouter,
model::{ProtocolVersion, ServerCapabilities, ServerInfo},
};
use crate::db::DbPool;
use crate::db::models::AuthUser;
static MCP_HANDLER_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
static MCP_REQUEST_USER: Mutex<Option<AuthUser>> = Mutex::new(None);
pub async fn with_request_user<F, Fut, R>(user: Option<AuthUser>, f: F) -> R
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = R>,
{
let _guard = MCP_HANDLER_LOCK.lock().await;
*MCP_REQUEST_USER
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = user;
let result = f().await;
*MCP_REQUEST_USER
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
result
}
pub(crate) fn current_auth_user() -> Option<AuthUser> {
MCP_REQUEST_USER
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
#[derive(Clone)]
pub struct LificMcp {
db: Arc<DbPool>,
tool_router: ToolRouter<Self>,
}
impl LificMcp {
pub fn new(db: DbPool) -> Self {
Self {
db: Arc::new(db),
tool_router: Self::create_tool_router(),
}
}
fn read<F, T>(&self, f: F) -> Result<T, String>
where
F: FnOnce(&rusqlite::Connection) -> Result<T, crate::error::LificError>,
{
let conn = self.db.read().map_err(|e| e.to_string())?;
f(&conn).map_err(|e| e.to_string())
}
fn write<F, T>(&self, f: F) -> Result<T, String>
where
F: FnOnce(&rusqlite::Connection) -> Result<T, crate::error::LificError>,
{
let conn = self.db.write().map_err(|e| e.to_string())?;
f(&conn).map_err(|e| e.to_string())
}
}
impl ServerHandler for LificMcp {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_protocol_version(ProtocolVersion::V_2025_03_26)
.with_instructions(
"Lific is a local-first issue tracker. Use list_resources(type='project') to discover projects. \
Use list_issues to browse issues with filters. Use get_issue with an identifier like 'PRO-42' \
for details. Use workable=true to find issues ready to work on (no unresolved blockers). \
Use search to find anything by text across issues and pages.",
)
}
fn list_tools(
&self,
_request: Option<rmcp::model::PaginatedRequestParams>,
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
) -> impl std::future::Future<Output = Result<rmcp::model::ListToolsResult, rmcp::ErrorData>>
+ rmcp::service::MaybeSendFuture
+ '_ {
std::future::ready(Ok(rmcp::model::ListToolsResult {
tools: self.tool_router.list_all(),
..Default::default()
}))
}
fn call_tool(
&self,
request: rmcp::model::CallToolRequestParams,
context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
) -> impl std::future::Future<Output = Result<rmcp::model::CallToolResult, rmcp::ErrorData>>
+ rmcp::service::MaybeSendFuture
+ '_ {
let tool_context =
rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
self.tool_router.call(tool_context)
}
fn get_tool(&self, name: &str) -> Option<rmcp::model::Tool> {
self.tool_router.get(name).cloned()
}
}