Skip to main content

sequel_mcp/mcp/
mod.rs

1//! MCP server layer: registration, framing, result mapping.
2//!
3//! stdout carries protocol frames only; logs go to stderr. Tool handlers
4//! call the shared library services — no policy or SQL logic lives here.
5
6pub mod confirm;
7pub mod limits;
8pub mod mrtr;
9pub mod tools;
10
11use rmcp::model::{CallToolResult, ContentBlock, Implementation, ServerCapabilities, ServerInfo};
12use std::sync::Arc;
13
14use crate::approval::ApprovalEngine;
15use crate::audit::AuditDb;
16use crate::config::ConfigStore;
17use crate::vault::keychain::SecretStore;
18use crate::vault::touchid::SessionAuthenticator;
19
20/// Shared state handed to every tool handler.
21#[derive(Clone)]
22pub struct AppCtx {
23    pub config: Arc<ConfigStore>,
24    pub audit: Arc<AuditDb>,
25    pub approvals: Arc<ApprovalEngine>,
26    pub auth: Arc<SessionAuthenticator>,
27    pub secrets: Arc<dyn SecretStore>,
28    /// Approval IPC hub (companion approvals). `None` when the runtime
29    /// socket could not be bound — approvals then rely on elicitation
30    /// alone and fail closed without a client prompt.
31    pub approval_ipc: Option<Arc<crate::approval::ipc::ApprovalIpc>>,
32}
33
34pub fn build_server_info() -> ServerInfo {
35    let mut info = ServerInfo::default();
36    info.capabilities = ServerCapabilities::builder()
37        .enable_tools()
38        .enable_prompts()
39        .enable_resources()
40        .build();
41    info.server_info = Implementation::new(crate::PACKAGE_NAME, crate::PACKAGE_VERSION);
42    info.instructions = Some(
43        "Policy-gated MySQL/MariaDB and SQLite access. Reads are allowed by default; \
44         writes require policy + user confirmation; ambiguous statements fail closed. \
45         Prefer `query` for reads; `execute` for everything else."
46            .into(),
47    );
48    info
49}
50
51#[derive(Clone)]
52pub struct SequelServer {
53    ctx: AppCtx,
54}
55
56impl SequelServer {
57    pub fn new(ctx: AppCtx) -> Self {
58        Self { ctx }
59    }
60
61    pub fn with_defaults() -> Self {
62        Self {
63            ctx: AppCtx {
64                config: Arc::new(ConfigStore::new()),
65                audit: AuditDb::shared(),
66                approvals: Arc::new(ApprovalEngine::new()),
67                auth: Arc::new(SessionAuthenticator::new(
68                    crate::vault::touchid::system_touch_id(),
69                )),
70                secrets: crate::vault::keychain::default_store(),
71                approval_ipc: None,
72            },
73        }
74    }
75
76    /// `with_defaults` plus the approval IPC hub: binds the runtime
77    /// socket for companion approvals (the `approve` CLI now, the GUI
78    /// later) and runs the boot-time retention auto-cleanup when due.
79    pub fn with_approval_ipc() -> Self {
80        let mut server = Self::with_defaults();
81        match crate::approval::ipc::ApprovalIpc::start() {
82            Ok(hub) => {
83                server.ctx.approval_ipc = Some(hub);
84            }
85            Err(e) => {
86                eprintln!(
87                    "[sequel-mcp] approval IPC unavailable ({}); elicitation-only approvals",
88                    e
89                );
90            }
91        }
92        // Boot retention: runs only when the configured interval elapsed
93        // since the last recorded cleanup.
94        if let Ok(cfg) = server.ctx.config.load()
95            && let Some(report) =
96                crate::audit::retention::maybe_auto_cleanup(&server.ctx.audit, &cfg.retention)
97        {
98            eprintln!(
99                "[sequel-mcp] auto-cleanup: pruned {} audit row(s), {} backup(s), reclaimed {} byte(s)",
100                report.audit_deleted, report.backup_deleted, report.bytes_reclaimed
101            );
102        }
103        server
104    }
105}
106
107/// A single JSON-returning tool result: structuredContent plus the
108/// spec-recommended text fallback (legacy `jsonResult`).
109pub fn json_tool_result(value: serde_json::Value) -> CallToolResult {
110    CallToolResult::structured(value)
111}
112
113/// Error result with `isError: true` and a text block (legacy `toolError`).
114pub fn error_tool_result(text: impl Into<String>) -> CallToolResult {
115    CallToolResult::error(vec![ContentBlock::text(text)])
116}
117
118/// Plain text success result (legacy `textResult`).
119pub fn text_tool_result(text: impl Into<String>) -> CallToolResult {
120    CallToolResult::success(vec![ContentBlock::text(text)])
121}