- Rust 95.8%
- Shell 3.8%
- JavaScript 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
russh 0.62.7 → 0.63.2: picks up the upstream fix for the RSA-signing trap we reported (Eugeny/russh#758 — russh now fails loudly at signing time when built without `rsa`). Our compile guard and the aborted-vs-rejected auth discrimination stay: russh still collapses a session that dies mid-auth into a plain Failure, so the typed AuthAborted error remains the caller-visible fix (verified against 0.63.2's wait_recv_reply). One trait adaptation (check_server_key now receives PublicKeyOrCertificate) and one deliberate behavior change: a bastion presenting an SSH CERTIFICATE as its host key is refused — no known_hosts entry can match one, and reducing it to its signing key would bypass the pin. rmcp =3.1.4 → =3.2.0 (lifecycle suite green). sha2/sha1 0.11, hmac 0.13, base64 0.23 — digest outputs unchanged by construction (audit chains, known_hosts stamps, credential keys stay identical); hmac 0.13 moved key construction to KeyInit. rand held at 0.9 (0.10 drops the RngCore re-export for zero gain); sqlparser already latest. Full lockfile refresh. Gates: fmt, clippy -D warnings, 230 workspace tests, SSH matrix both variants (incl. RSA-4096 + rsa-sha2-256 pin), no-default-features compile guard. |
||
| .codex | ||
| .github/workflows | ||
| docs/rust-rewrite | ||
| scripts | ||
| skills/using-sequel-mcp | ||
| src | ||
| tests | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| deny.toml | ||
| LICENSE | ||
| README.md | ||
| rust-toolchain.toml | ||
| SECURITY.md | ||
sequel-mcp
A native-Rust Model Context Protocol server for MySQL/MariaDB and SQLite with policy-gated action sets, table-level rules, pre-mutation backups, an append-only audit log, macOS Keychain credential storage, optional Touch ID, and authenticated approval companions (CLI + native GUI). Designed so Claude Code, Codex CLI, or any MCP client can run real SQL safely enough to use every day.
0.10.0 is a full native-Rust rewrite of the former TypeScript implementation (0.9.x). The Node/npm install path is gone; see Migration from 0.9.x. Published to crates.io —
cargo install sequel-mcp --lockedworks today.
Sequel Ace is OPTIONAL. This is a fully standalone MCP. Sequel Ace integration is a bootstrap convenience (one-time import of saved MySQL/MariaDB favorites) and a history augment. Without Sequel Ace, everything else works — only
import_from_sequel_aceandsequel_ace_historyreport "not found".
Capabilities
Current release: v0.10.0. Full history: CHANGELOG.md.
- Single static binary — pure Rust (
sequel-mcp), no Node runtime, nodist/, no npm. - Two-layer permissions — connection baseline + table rules (exact
db.tableor wildcarddb.*; exact beats wildcard; strictest-wins across everything a statement touches; fail-closed). - Elevation still confirms — a table rule can relax what the baseline denied, but elevated categories always require per-statement confirmation.
- 28 tools + 2 prompts + 1 no-secrets resource (
sequel-mcp://connections). - Approvals, three ways — MCP elicitation first; when the client cannot elicit, an authenticated same-user local IPC channel answers instead (
sequel-mcp approveCLI or the native GUI window). Modern clients use server-side opaquerequestStatehandles (MRTR) bound to the exact operation digest. Every path fails closed — nothing is ever auto-approved. - Pre-mutation backups for UPDATE / DELETE / REPLACE / INSERT / TRUNCATE / DROP / ALTER, multi-table aware;
restore_backupreplays through the same policy gate (dryRun=truedefault). - Append-only audit log (SQLite, optional SHA-256 chain) with per-category retention and boot-time auto-cleanup.
- Three transports — direct TCP (TLS, private CA via
sslCaPath), SSH tunnels (russh; strict/lenient host-key policy, keepalives, tunnel-lease generation pool keys), and SSH +docker execstdio bridge (nc/ncat/socat) for closed containers — works even underAllowTcpForwarding no. - macOS-native security — Keychain (
WhenUnlockedThisDeviceOnly, non-syncable), optional Touch ID per connection, fail-closed if unavailable. - Sequel Ace import + unified history — one-time favorites/Keychain import;
history_searchmerges Sequel Ace's query history with the audit log.
Install
Requirements
- macOS 12+ (Apple Silicon or Intel) for Keychain + Touch ID. Linux builds compile (CI check), but Keychain/Touch ID degrade to explicit errors — fail-closed, no plaintext fallback.
- Rust toolchain matching
rust-toolchain.toml(1.97.1) —rustuppicks it up automatically.
From crates.io (primary)
cargo install sequel-mcp --locked
That builds and installs the sequel-mcp binary into ~/.cargo/bin (make sure it's on your PATH). To update: re-run the same command after a new release.
From source (developer path)
git clone https://github.com/xicv/sequel-mcp.git
cd sequel-mcp
cargo install --path . --locked
Use this to track main or hack on the server locally.
Wire into an MCP client
sequel-mcp serve speaks MCP over stdio — the only transport; there is no --stdio flag to pass.
Claude Code
claude mcp add --scope user sequel-mcp -- sequel-mcp serve
Codex CLI
codex mcp add sequel-mcp -- sequel-mcp serve
Equivalent ~/.codex/config.toml entry (also what this repo's project-scoped .codex/config.toml now contains):
[mcp_servers.sequel-mcp]
command = "sequel-mcp"
args = ["serve"]
startup_timeout_sec = 20
tool_timeout_sec = 120
Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"sequel-mcp": { "command": "sequel-mcp", "args": ["serve"] }
}
}
Any other MCP client
Point command at the sequel-mcp binary with args ["serve"].
CLI reference
sequel-mcp serve # MCP stdio server (the primary transport)
sequel-mcp doctor [--json] # sanitized local-install diagnostics
sequel-mcp approve [--socket P] [--choice once|session|decline]
# answer a pending confirmation over the
# authenticated approval IPC socket
sequel-mcp gui [--socket P] # native approvals companion window
doctor reports config path/version/revision, connection count, default connection — no passwords.
How approvals resolve
When a statement's category is confirm:
- MCP elicitation (preferred) — the client shows the statement and a three-choice prompt: Allow once, Allow for session, Decline. Session grants are per
(connection, database, category)and die with the server process. - Authenticated approval IPC (fallback) — if the client cannot elicit, the server asks over a Unix socket restricted to the same effective uid (
runtime/approval.sock). Answer it withsequel-mcp approveor leave the GUI window (sequel-mcp gui) open — it watches the socket, shows the statement, and turns your click into an id-bound reply. Replies are single-use and bound to the exact pending request; a 60-second deadline fails the ask closed (audited as unavailable, never declined, never approved). - Modern
requestState(MRTR) — clients that support round-trip confirmations receive a server-side opaque handle bound to the operation digest; the approved statement replays only if the digest, connection, and policy revision all match.
If no channel can answer, the statement does not run — the audit log records an unavailable prompt with the reason.
Action sets — the permission model
Each connection has a baseline policy; each category is allow | confirm | deny.
| Category | What it covers |
|---|---|
read |
SELECT, SHOW, DESCRIBE, EXPLAIN, read-only SQLite PRAGMA |
write |
INSERT, UPDATE, DELETE, REPLACE |
ddl |
CREATE, ALTER, DROP, TRUNCATE, RENAME |
admin |
GRANT, REVOKE, SET GLOBAL, KILL, FLUSH, LOAD |
txCtrl |
BEGIN, COMMIT, ROLLBACK, SAVEPOINT |
Presets
| Preset | read | write | ddl | admin | rowCap | timeout | Touch ID |
|---|---|---|---|---|---|---|---|
read-only |
allow | deny | deny | deny | 1000 | 10s | off |
development |
allow | confirm | confirm | deny | 5000 | 30s | off |
administration |
allow | confirm | confirm | confirm | 5000 | 60s | on |
Table rules (layer two)
set_table_policy writes an exact (database.table) or wildcard (database.*) rule per connection. Resolution for every table a statement touches: exact rule → wildcard rule → baseline, strictest action wins across tables. A rule may elevate a denied category, but elevated statements always confirm. explain_policy previews the classification and per-table resolution of any statement without executing it.
"Set baseline on acme-prod to read-only."
"Set table policy on acme-prod: staging.* write=confirm."
"Set table policy on acme-prod: staging.migrations ddl=confirm."
"Explain policy for: UPDATE staging.users SET email='x' WHERE id=1"
Tools (28)
| Tool | What it does |
|---|---|
query |
Single read statement; MySQL reads under START TRANSACTION READ ONLY, SQLite via read-only handle. |
execute |
Single non-read statement; gated; backup captured automatically. |
explain_policy |
Classify a statement and show per-table resolution without executing. |
describe_table |
DESCRIBE (MySQL/MariaDB) / PRAGMA table_info (SQLite). |
list_databases |
SHOW DATABASES / PRAGMA database_list. |
list_connections |
Saved connections, no secrets; marks default + hasStoredPassword. |
add_connection |
Add/update a MySQL/MariaDB connection; password elicited separately → Keychain. |
add_sqlite_connection |
Add/update a SQLite file connection (no password). |
remove_connection |
Forget a connection + delete its Keychain entry. |
set_policy |
Change a connection's baseline. |
set_database_policy / clear_database_policy / list_database_policies |
Legacy per-database override view; stored as db.* table rules. |
set_table_policy / clear_table_policy / list_table_policies |
Layer-two exact/wildcard rules. |
set_default_connection / get_default_connection |
Default used when connection is omitted. |
select_database |
Per-connection default database. |
audit_search |
Query the local audit log. |
audit_cleanup / set_retention |
Prune past retention; configure windows/caps. |
list_backups / restore_backup |
Inspect and replay pre-mutation backups. |
history_search |
Unified timeline: audit log + Sequel Ace history. |
import_from_sequel_ace |
One-time favorites + Keychain import. Requires Sequel Ace. |
sequel_ace_history |
Read Sequel Ace's query history. Requires Sequel Ace. |
doctor |
Sanitized diagnostics. No passwords. |
Prompts: setup-connection (guided connection setup), analyze-table (read-only investigation). Resource: sequel-mcp://connections (JSON, no secrets).
Adding connections
SQLite
"Add a SQLite connection 'local' at ~/Projects/app/dev.sqlite, read-only preset."
MySQL/MariaDB
The guided path is add_connection — arguments carry everything except the password; the server elicits the password separately (it never appears in tool arguments or logs) and stores it in the macOS Keychain:
"Add a connection named local, host 127.0.0.1, port 3306, user root, database app, read-only preset."
Optional ssh_* arguments build the tunnel in the same call (ssh_host, ssh_port, ssh_user, ssh_key_path, ssh_docker_container + ssh_docker_bridge_tool, ssh_host_key_policy, ssh_known_hosts_path); ssl, ssl_server_name, and ssl_ca_path control TLS. Declining or dismissing the password prompt cancels — nothing is saved.
Alternatives: import_from_sequel_ace (one-time favorites + Keychain import on macOS) or manual setup — add the connection to ~/.config/sequel-mcp/config.json (v2) and store the password yourself:
-
Config entry (camelCase fields):
{ "version": 2, "revision": 1, "connections": [{ "driver": "mysql", "name": "local", "host": "127.0.0.1", "port": 3306, "user": "root", "database": "app", "ssl": false, "policy": { "read": "allow", "write": "confirm", "ddl": "confirm", "admin": "deny", "txCtrl": "allow", "rowCap": 1000, "stmtTimeoutMs": 10000, "requireTouchID": false, "maxBackupRows": 10000, "maxBackupBytes": 52428800, "onBackupOverflow": "abort" }, "tablePolicies": {} }], "retention": {} } -
Store the password in the macOS Keychain under the service name the server looks up —
sequel-mcp : <connection-name>, account = DB user:security add-generic-password -s "sequel-mcp : local" -a root -w -
SSH passwords/passphrases go under service
<connection-name>::ssh, account = SSH user.
No credentials ever live in the config file.
SSH tunnels and Docker bridge
A connection may carry ssh: { host, port, user, authMethod: "key"|"password", privateKeyPath, hostKeyPolicy: "strict"|"lenient", knownHostsPath, docker: { container, bridgeTool } }.
- Direct tunnel — local forward onto multiplexed SSH channels.
hostKeyPolicy: "strict"rejects unknown and changed host keys (fail-closed MitM signal);lenientstill enforces@revokedmarkers. TLS hostname preservation viasslServerName; private CAs viasslCaPath. - Docker bridge — for containers with no published port:
docker: { container: "mysql_prod", bridgeTool: "nc" }pipes MySQL overdocker exec -i … nc 127.0.0.1 3306through SSH exec channels. Works underAllowTcpForwarding no. Ops prerequisites: the SSH user can rundocker, andnc/ncat/socatexists inside the container.
Security playbook (short form)
- Default to
read-onlyfor production connections — writes are denied outright, not "confirm". - Scope relaxations to one table or database (
set_table_policy/ wildcard), preferconfirmoverallow, revert afterwards. - Review the audit log weekly —
audit_searchfiltered byoutcome=deniedshows what was blocked; skim confirmed writes. - Restore drills — always
restore_backupwithdryRun=truefirst; check schema-drift warnings. - Touch ID on the most sensitive connection (
requireTouchIDviaset_policy); shrinkrowCap/stmtTimeoutMsfurther if you want tighter caps. - Docker-in-bastion setups — strict host-key policy +
sslServerName+ read-only preset = every leg fails visibly rather than silently.
Defence in depth
AST classification (closed-world, unknown denied) · multi-statement rejection · read-only driver enforcement · two-layer strictest-wins gate · server-issued confirmations (elicitation / same-uid IPC / MRTR) · same-transaction backups · append-only audit · row caps + wall-clock statement timeouts · Touch ID · SSH host-key verification · TLS server-name preservation · bridge argv validation (no shell metacharacters).
Audit, backups, retention
- Audit DB:
~/.local/share/sequel-mcp/audit.sqlite— one row per tool call: identity, redacted SQL, decision, outcome, duration,backup_id, optional hash chain. - Backup caps:
maxBackupRows10000 /maxBackupBytes50 MB, overflow aborts the mutation (configurable). - Retention defaults: read 7d, write 30d, ddl 90d, admin 180d, txCtrl 7d; audit hard cap 500 MB; auto-cleanup on boot every 24 h.
Migration from 0.9.x (Node)
- Install: replace the
node dist/index.jswiring with thesequel-mcpbinary (cargo install --path . --locked; see above). Update anycommand = "node"MCP config tocommand = "sequel-mcp". - Config: v1 configs migrate in place on first load —
databasePoliciesbecomedb.*table rules; SSH host-key policy inherited unset is markedhostKeyPolicyMigratedand treated as lenient-with-warning. - Keychain: unchanged service names (
sequel-mcp : <name>,<name>::ssh) — existing stored passwords keep working. - Behavior: confirmations now resolve elicitation-first with the approve CLI / GUI fallback and MRTR for modern clients; unavailable prompts are audited as errors, never as declines.
Doctor / debugging
sequel-mcp doctor # text report
sequel-mcp doctor --json # machine-readable
Reports config path/version/revision, connection count, default connection. No secrets.
Development
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
bash scripts/ci-local.sh # the full local gate incl. cargo package
bash scripts/test-db.sh both # docker MySQL/MariaDB matrices
bash scripts/test-ssh.sh mariadb # SSH transport matrix
bash scripts/check-secrets.sh # local secret scan
CI (.github/workflows/ci.yml) runs fmt + clippy + check on macOS and Ubuntu, tests on macOS, plus secret scans. All actions are pinned to commit SHAs.
Security policy
See SECURITY.md for the threat model and CONTRIBUTING.md for contributor rules around credentials and PII. Quick summary: no credential, no PII, no environment-specific identifier may ever enter this repository.
License
MIT — see LICENSE.