mcp.rsannotatedmcp.rssource64 lines · 2.9 KB · raw
1//! The plugin's MCP server: what Claude Code starts once per session (the
2//! plugin's `.mcp.json`). It starts at once, makes sure the shared daemon is
3//! running, and offers two read-only tools over it. The mod does not go
4//! through here: it talks to the daemon's socket directly.
5
6use hyper::Method;
7use rmcp::handler::server::wrapper::Parameters;
8use rmcp::model::{CallToolResult, ContentBlock, Implementation, ServerCapabilities, ServerConfig};
9use rmcp::{ErrorData, ServerHandler, ServiceExt, schemars, tool, tool_handler, tool_router};
10use serde::Deserialize;
11
12use crate::client;
13
14#[derive(Clone)]
15struct Server;
16
17#[derive(Deserialize, schemars::JsonSchema)]
18struct RecentRequest {
19    /// How many records, newest last. 20 when left out.
20    count: Option<u32>,
21}
22
23async fn daemon(method: Method, path: &str) -> Result<CallToolResult, ErrorData> {
24    let (_, value) = client::call(method, path, None).await.map_err(|why| ErrorData::internal_error(why, None))?;
25    Ok(CallToolResult::success(vec![ContentBlock::json(value)?]))
26}
27
28#[tool_router]
29impl Server {
30    #[tool(description = "The jevhooks daemon's status: whether it can ask Jev (key present, client built), the pinned model, how many events it has decided and observed, the spend ledger's line, and the thresholds that turn Jev's probability into a verdict. Read-only.")]
31    async fn status(&self) -> Result<CallToolResult, ErrorData> {
32        daemon(Method::GET, "/status").await
33    }
34
35    #[tool(description = "The most recent jevhooks decisions and outcomes, oldest first: for each judged Bash command or turn end, what was judged, Jev's probability, the verdict, the time taken and the cost; and for judged commands, whether they then ran, failed or were denied. Read-only.")]
36    async fn recent(&self, Parameters(request): Parameters<RecentRequest>) -> Result<CallToolResult, ErrorData> {
37        daemon(Method::GET, &format!("/recent?count={}", request.count.unwrap_or(20))).await
38    }
39}
40
41#[tool_handler]
42impl ServerHandler for Server {
43    fn get_info(&self) -> ServerConfig {
44        ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
45            .with_server_info(Implementation::new("jevhooks", env!("CARGO_PKG_VERSION")))
46            .with_instructions(
47                "jevhooks asks Jev to judge Bash commands before they run and turn ends before they stop. \
48                 status says whether Jev can be asked; recent lists the latest decisions.",
49            )
50    }
51}
52
53/// Serves MCP on stdio until Claude Code closes it.
54pub async fn serve() -> Result<(), Box<dyn std::error::Error>> {
55    // Not awaited: the daemon can take seconds to fetch its key, and the
56    // session should not wait on that to list two tools.
57    tokio::spawn(async {
58        if let Err(why) = client::ensure().await {
59            eprintln!("jevhooks: {why}");
60        }
61    });
62    Server.serve(rmcp::transport::stdio()).await?.waiting().await?;
63    Ok(())
64}