• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

joaoh82 / rust_sqlite / 25277076831

03 May 2026 10:47AM UTC coverage: 56.877% (-0.4%) from 57.232%
25277076831

push

github

web-flow
Phase 7h: sqlrite-mcp — MCP server adapter (incl. 7g.8 ask tool) (#73)

* Phase 7h: sqlrite-mcp — MCP server adapter (incl. 7g.8 ask tool)

Adds a new workspace-member crate `sqlrite-mcp` with a single
`[[bin]]` target that exposes a SQLRite database as a Model Context
Protocol (MCP) server over stdio. LLM agents (Claude Code, Cursor,
Codex, mcp-inspector) spawn it as a subprocess and get seven tools
for driving the database without any custom integration:

- list_tables    — discover what's in the DB
- describe_table — column metadata + row count for one table
- query          — run a SELECT, return rows as JSON
- execute        — DDL / DML / transactions (hidden under --read-only)
- schema_dump    — full CREATE TABLE script (same dump `ask` uses)
- vector_search  — k-NN lookup against a VECTOR column
                   (uses HNSW index if present, brute-force otherwise)
- ask            — natural-language → SQL via sqlrite-ask
                   (Phase 7g.8 — gated behind default-on `ask` feature)

Hand-rolled JSON-RPC 2.0 over line-delimited JSON on stdio. ~1100
LOC for the whole binary; no tokio, no async runtime, no third-party
MCP framework — same dep-frugal theme as sqlrite-ask's hand-rolled
JSON over ureq. Sync, single-client, strictly serial dispatch.

Notable implementation details:

- **stdio_redirect.rs**: the engine's `process_command` calls
  `print!`/`println!` for REPL-convenience output (CREATE schema
  dump, INSERT row dump, SELECT result table) — those would corrupt
  the MCP protocol channel. Solved with a `dup2(2, 1)` dance at
  startup that redirects fd 1 to fd 2; JSON-RPC responses go through
  a saved-off duplicate of the original fd 1. The same pollution
  affects the existing SDKs but isn't visible there because their
  stdout doesn't matter; fixing it in the engine is a future cleanup.

- **Read-only mode**: `--read-only` opens the DB with a shared lock
  via `Connection::open_read_only` AND hides `execute` from
  `tools/lis... (continued)

0 of 60 new or added lines in 3 files covered. (0.0%)

5492 of 9656 relevant lines covered (56.88%)

1.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/sqlrite-mcp/src/error.rs
1
//! Error taxonomy for the MCP server.
2
//!
3
//! Two layers, deliberately separate:
4
//!
5
//! - [`ProtocolError`] — JSON-RPC-level failures the LLM client can't
6
//!   recover from (malformed request, unknown method, server in wrong
7
//!   lifecycle state). Surfaced as JSON-RPC `error` responses with
8
//!   the standard codes from the JSON-RPC 2.0 spec.
9
//!
10
//! - [`ToolError`] — failures inside a tool handler (SQL parse error,
11
//!   table not found, vector dimension mismatch, ask returned empty
12
//!   SQL). Surfaced inside the tool result with `isError: true` and
13
//!   the error message in a `text` content block — that's MCP's
14
//!   convention for "the tool ran but the operation failed", and it
15
//!   lets the LLM read the error and retry with adjusted arguments.
16
//!
17
//! See `docs/mcp.md` § "Errors and how they surface" for the
18
//! user-facing reference.
19

20
/// JSON-RPC 2.0 standard error codes
21
/// (https://www.jsonrpc.org/specification#error_object).
22
///
23
/// MCP itself reserves the `-32000..=-32099` server-error range for
24
/// protocol-specific errors; we use a small subset of those.
25
#[allow(dead_code)]
26
pub mod jsonrpc_codes {
27
    /// Invalid JSON was received by the server.
28
    pub const PARSE_ERROR: i64 = -32700;
29
    /// The JSON sent is not a valid Request object.
30
    pub const INVALID_REQUEST: i64 = -32600;
31
    /// The method does not exist / is not available.
32
    pub const METHOD_NOT_FOUND: i64 = -32601;
33
    /// Invalid method parameter(s).
34
    pub const INVALID_PARAMS: i64 = -32602;
35
    /// Internal JSON-RPC error.
36
    pub const INTERNAL_ERROR: i64 = -32603;
37
    /// Server is in the wrong lifecycle state for the request
38
    /// (e.g. `tools/list` before `initialize`).
39
    pub const SERVER_NOT_INITIALIZED: i64 = -32002;
40
}
41

42
/// Protocol-level error. Becomes a JSON-RPC `error` response.
43
#[derive(Debug)]
44
pub struct ProtocolError {
45
    pub code: i64,
46
    pub message: String,
47
}
48

49
#[allow(dead_code)] // round-out-the-API constructors, used selectively
50
impl ProtocolError {
NEW
51
    pub fn new(code: i64, message: impl Into<String>) -> Self {
×
52
        Self {
53
            code,
NEW
54
            message: message.into(),
×
55
        }
56
    }
57

NEW
58
    pub fn parse_error(message: impl Into<String>) -> Self {
×
NEW
59
        Self::new(jsonrpc_codes::PARSE_ERROR, message)
×
60
    }
61

NEW
62
    pub fn invalid_request(message: impl Into<String>) -> Self {
×
NEW
63
        Self::new(jsonrpc_codes::INVALID_REQUEST, message)
×
64
    }
65

NEW
66
    pub fn method_not_found(method: &str) -> Self {
×
67
        Self::new(
NEW
68
            jsonrpc_codes::METHOD_NOT_FOUND,
×
NEW
69
            format!("method not found: {method}"),
×
70
        )
71
    }
72

NEW
73
    pub fn invalid_params(message: impl Into<String>) -> Self {
×
NEW
74
        Self::new(jsonrpc_codes::INVALID_PARAMS, message)
×
75
    }
76

NEW
77
    pub fn internal_error(message: impl Into<String>) -> Self {
×
NEW
78
        Self::new(jsonrpc_codes::INTERNAL_ERROR, message)
×
79
    }
80

NEW
81
    pub fn server_not_initialized(message: impl Into<String>) -> Self {
×
NEW
82
        Self::new(jsonrpc_codes::SERVER_NOT_INITIALIZED, message)
×
83
    }
84
}
85

86
impl std::fmt::Display for ProtocolError {
NEW
87
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
NEW
88
        write!(f, "[{}] {}", self.code, self.message)
×
89
    }
90
}
91

92
impl std::error::Error for ProtocolError {}
93

94
/// Tool-level error. Becomes a tool result with `isError: true`.
95
///
96
/// This is intentionally just a wrapper around a string — the LLM
97
/// reads the message and decides how to react. Structured fields
98
/// would be over-engineering for a surface the LLM treats as a
99
/// natural-language response anyway.
100
#[derive(Debug)]
101
pub struct ToolError(pub String);
102

103
impl ToolError {
NEW
104
    pub fn new(message: impl Into<String>) -> Self {
×
NEW
105
        Self(message.into())
×
106
    }
107
}
108

109
impl std::fmt::Display for ToolError {
NEW
110
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
NEW
111
        f.write_str(&self.0)
×
112
    }
113
}
114

115
impl std::error::Error for ToolError {}
116

117
impl From<sqlrite::SQLRiteError> for ToolError {
NEW
118
    fn from(err: sqlrite::SQLRiteError) -> Self {
×
NEW
119
        Self::new(err.to_string())
×
120
    }
121
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc