use rmcp::{
handler::server::wrapper::Parameters,
model::{CallToolResult, Content},
tool, tool_router,
};
use schemars::JsonSchema;
use serde::Deserialize;
use crate::cron_jobs::{self, CreateRequest, CronStore, HandlerRegistry, UpdateRequest};
use crate::routines::{self, CreateRoutineRequest, RoutineStore, UpdateRoutineRequest};
use crate::utils::time::now_secs;
#[derive(Clone)]
pub struct MoadimMcp {
store: CronStore,
handlers: HandlerRegistry,
routines: RoutineStore,
uptime_start: u64,
}
#[derive(Deserialize, JsonSchema)]
struct EchoInput {
message: String,
}
#[derive(Deserialize, JsonSchema)]
struct IdInput {
id: String,
}
#[derive(Deserialize, JsonSchema)]
struct UpdateInput {
id: String,
schedule: Option<String>,
handler: Option<String>,
#[schemars(schema_with = "crate::utils::schema::metadata_schema")]
metadata: Option<serde_json::Value>,
enabled: Option<bool>,
}
#[derive(Deserialize, JsonSchema)]
struct UpdateRoutineInput {
id: String,
schedule: Option<String>,
title: Option<String>,
agent: Option<String>,
prompt: Option<String>,
repositories: Option<Vec<crate::routines::Repository>>,
enabled: Option<bool>,
}
fn ok(val: impl serde::Serialize) -> CallToolResult {
CallToolResult::success(vec![Content::text(
serde_json::to_string(&val).unwrap_or_default(),
)])
}
fn err(msg: impl std::fmt::Display) -> CallToolResult {
CallToolResult::error(vec![Content::text(msg.to_string())])
}
#[tool_router(server_handler)]
impl MoadimMcp {
pub fn new(
store: CronStore,
handlers: HandlerRegistry,
routines: RoutineStore,
uptime_start: u64,
) -> Self {
Self {
store,
handlers,
routines,
uptime_start,
}
}
#[tool(description = "Get server health, uptime, and filesystem locations")]
fn health(&self) -> Result<CallToolResult, rmcp::ErrorData> {
let loc = crate::filesystem::FsLocation::current();
let mut val = serde_json::json!({
"status": "ok",
"uptime_secs": now_secs() - self.uptime_start,
"running": true,
});
if let (Some(obj), Ok(serde_json::Value::Object(loc_map))) =
(val.as_object_mut(), serde_json::to_value(&loc))
{
obj.extend(loc_map);
}
Ok(ok(val))
}
#[tool(description = "Echo a message back with a server timestamp")]
fn echo(
&self,
Parameters(EchoInput { message }): Parameters<EchoInput>,
) -> Result<CallToolResult, rmcp::ErrorData> {
Ok(ok(serde_json::json!({
"message": message,
"timestamp": now_secs(),
})))
}
#[tool(description = "List all managed cron jobs")]
fn list_cron_jobs(&self) -> Result<CallToolResult, rmcp::ErrorData> {
Ok(ok(cron_jobs::svc_list(&self.store, &self.handlers)))
}
#[tool(description = "Get a cron job by ID")]
fn get_cron_job(
&self,
Parameters(IdInput { id }): Parameters<IdInput>,
) -> Result<CallToolResult, rmcp::ErrorData> {
Ok(match cron_jobs::svc_get(&self.store, &self.handlers, &id) {
Ok(resp) => ok(resp),
Err(e) => err(e),
})
}
#[tool(description = "Create a new cron job")]
fn create_cron_job(
&self,
Parameters(req): Parameters<CreateRequest>,
) -> Result<CallToolResult, rmcp::ErrorData> {
Ok(
match cron_jobs::svc_create(&self.store, &self.handlers, req) {
Ok(resp) => ok(resp),
Err(e) => err(e),
},
)
}
#[tool(description = "Update fields of an existing cron job")]
fn update_cron_job(
&self,
Parameters(input): Parameters<UpdateInput>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let req = UpdateRequest {
schedule: input.schedule,
handler: input.handler,
metadata: input.metadata,
enabled: input.enabled,
};
Ok(
match cron_jobs::svc_update(&self.store, &self.handlers, &input.id, req) {
Ok(resp) => ok(resp),
Err(e) => err(e),
},
)
}
#[tool(description = "Delete a cron job by ID")]
fn delete_cron_job(
&self,
Parameters(IdInput { id }): Parameters<IdInput>,
) -> Result<CallToolResult, rmcp::ErrorData> {
Ok(
match cron_jobs::svc_delete(&self.store, &self.handlers, &id) {
Ok(resp) => ok(resp),
Err(e) => err(e),
},
)
}
#[tool(
description = "Manually trigger a cron job outside its schedule, recording last_triggered_at"
)]
fn trigger_cron_job(
&self,
Parameters(IdInput { id }): Parameters<IdInput>,
) -> Result<CallToolResult, rmcp::ErrorData> {
Ok(match cron_jobs::svc_trigger(&self.store, &id) {
Ok(job) => ok(job),
Err(e) => err(e),
})
}
#[tool(description = "List all managed routines (agent-driven jobs)")]
fn list_routines(&self) -> Result<CallToolResult, rmcp::ErrorData> {
Ok(ok(routines::svc_list(&self.routines)))
}
#[tool(description = "Get a routine by ID")]
fn get_routine(
&self,
Parameters(IdInput { id }): Parameters<IdInput>,
) -> Result<CallToolResult, rmcp::ErrorData> {
Ok(match routines::svc_get(&self.routines, &id) {
Ok(resp) => ok(resp),
Err(e) => err(e),
})
}
#[tool(description = "Create a new routine (agent-driven job)")]
fn create_routine(
&self,
Parameters(req): Parameters<CreateRoutineRequest>,
) -> Result<CallToolResult, rmcp::ErrorData> {
Ok(match routines::svc_create(&self.routines, req) {
Ok(resp) => ok(resp),
Err(e) => err(e),
})
}
#[tool(description = "Update fields of an existing routine")]
fn update_routine(
&self,
Parameters(input): Parameters<UpdateRoutineInput>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let req = UpdateRoutineRequest {
schedule: input.schedule,
title: input.title,
agent: input.agent,
prompt: input.prompt,
repositories: input.repositories,
enabled: input.enabled,
};
Ok(match routines::svc_update(&self.routines, &input.id, req) {
Ok(resp) => ok(resp),
Err(e) => err(e),
})
}
#[tool(description = "Delete a routine by ID")]
fn delete_routine(
&self,
Parameters(IdInput { id }): Parameters<IdInput>,
) -> Result<CallToolResult, rmcp::ErrorData> {
Ok(match routines::svc_delete(&self.routines, &id) {
Ok(resp) => ok(resp),
Err(e) => err(e),
})
}
#[tool(
description = "Manually trigger a routine outside its schedule, recording last_triggered_at"
)]
fn trigger_routine(
&self,
Parameters(IdInput { id }): Parameters<IdInput>,
) -> Result<CallToolResult, rmcp::ErrorData> {
Ok(match routines::svc_trigger(&self.routines, &id) {
Ok(routine) => ok(routine),
Err(e) => err(e),
})
}
}
#[cfg(test)]
#[path = "mcp_tests.rs"]
mod mcp_tests;