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 and native GUI) and runs the boot-time retention auto-cleanup when due.
78    pub fn with_approval_ipc() -> Self {
79        Self::with_approval_gui(true)
80    }
81
82    pub fn with_approval_gui(launch_gui: bool) -> Self {
83        let mut server = Self::with_defaults();
84        match crate::approval::ipc::ApprovalIpc::start_with_gui(launch_gui) {
85            Ok(hub) => {
86                server.ctx.approval_ipc = Some(hub);
87            }
88            Err(e) => {
89                eprintln!(
90                    "[sequel-mcp] approval IPC unavailable ({}); elicitation-only approvals",
91                    e
92                );
93            }
94        }
95        // Boot retention: runs only when the configured interval elapsed
96        // since the last recorded cleanup.
97        if let Ok(cfg) = server.ctx.config.load()
98            && let Some(report) =
99                crate::audit::retention::maybe_auto_cleanup(&server.ctx.audit, &cfg.retention)
100        {
101            eprintln!(
102                "[sequel-mcp] auto-cleanup: pruned {} audit row(s), {} backup(s), reclaimed {} byte(s)",
103                report.audit_deleted, report.backup_deleted, report.bytes_reclaimed
104            );
105        }
106        server
107    }
108}
109
110/// A single JSON-returning tool result: structuredContent plus the
111/// spec-recommended text fallback (legacy `jsonResult`).
112pub fn json_tool_result(value: serde_json::Value) -> CallToolResult {
113    CallToolResult::structured(value)
114}
115
116/// Error result with `isError: true` and a text block (legacy `toolError`).
117pub fn error_tool_result(text: impl Into<String>) -> CallToolResult {
118    CallToolResult::error(vec![ContentBlock::text(text)])
119}
120
121/// Plain text success result (legacy `textResult`).
122pub fn text_tool_result(text: impl Into<String>) -> CallToolResult {
123    CallToolResult::success(vec![ContentBlock::text(text)])
124}