Unified CLI + MCP (Model Context Protocol) tool for MySQL, PostgreSQL, ClickHouse, Redis, MongoDB, HTTP, SSH, and Browser — with composable SSH + SOCKS5 tunnels.
- CLI Mode: Execute commands directly from the command line
- MCP Mode: Run as an MCP server for AI assistant integration
- Configuration: Support for TOML profiles and YAML config files
- SSH Jump Host: Access internal services through bastion hosts
This is the Phase 21 release. Currently implemented:
- All eight service orchestrators (
MysqlOrchestrator,PgsqlOrchestrator,ClickhouseOrchestrator,RedisOrchestrator,MongoOrchestrator,HttpOrchestrator,SshDirectOrchestrator,BrowserOrchestrator) impl thetools4a_core::Servicetrait, defined asasync fn execute(Self::Request, Option<TunnelConfig>) -> Result<ExecutionResult>. Each lives in its own leaf crate (tools4a-mysql,tools4a-pgsql, …) alongside the corresponding<Svc>Mcpimpl oftools4a_core::McpTool. - MySQL CLI mode (
tools4a mysql "...") andmysql_execMCP tool. - PostgreSQL CLI mode (
tools4a pgsql "...") andpgsql_execMCP tool. - ClickHouse CLI mode (
tools4a clickhouse "...") andclickhouse_execMCP tool — SQL over the HTTP interface (default port 8123). - Redis CLI mode (
tools4a redis "...") andredis_execMCP tool. - MongoDB CLI mode (
tools4a mongo '{"find":"coll","filter":{}}') andmongo_execMCP tool — JSON document passed toDatabase::run_command. - HTTP CLI mode (
tools4a http GET https://...) andhttp_execMCP tool. - SSH-direct CLI mode (
tools4a ssh "...") andssh_execMCP tool — run a shell command on a target SSH server, optionally through SSH jump hosts. - Browser CLI mode (
tools4a browser <SUBCOMMAND> [ARGS]...) andbrowser_execMCP tool — thin wrapper around the externally-installedagent-browserbinary (operator installs it separately). Any SSH-containing layer stack stands up aLayeredTunnelwithForwardTarget::Socks5Serverand injects--proxy socks5://127.0.0.1:<rand>automatically; a pure-SOCKS5 stack short-circuits to--proxy socks5://…directly without a local relay. - Configuration via YAML file (
--config=PATH) or TOML profile (--profile=NAME) for MySQL, PostgreSQL, ClickHouse, Redis, and MongoDB. (HTTP, SSH-direct, and Browser profile/YAML not yet supported.) - Composable tunnel layer stack (Phase 21) —
TunnelConfigis nowstruct { layers: Vec<TunnelLayer> }(ordered local→target);TunnelLayeris eitherSocks5 { host, port, user?, password? }orSshHop(JumpHop). Emptylayers= direct. One genericLayeredTunnel(impl Tunnel) driven bybuild_connector(&[TunnelLayer])folds the layers (TcpConnectorbase → each layer wraps the inner connector); SSH layers cache their session in aOnceCell(onedirect-tcpipchannel per client connection → multi-client without head-of-line blocking). The old per-shape impls (DirectTunnel,SshTunnel,Socks5ClientTunnel,SocksTunnel,StreamLocalTunnel) have been removed. - Direct connection (no
--hop/--tunnel, or--tunnel=direct). - SSH tunnel (
--tunnel=ssh) with single- or multi-hop jump (--ssh-jump=h1[,h2,...]), password or key auth. Host keys accepted with a fingerprint warning. Works for all eight services through the unifiedLayeredTunnel/build_connectorlayer model. - External SOCKS5 proxy (
--tunnel=socks5 --socks5-host=H --socks5-port=P [--socks5-user=U --socks5-password=W], default port 1080, RFC 1929 user/pass auth). All services route through theLayeredTunnelconnector chain. Browser short-circuits a pure-SOCKS5 stack by passingsocks5://[user:pass@]host:portdirectly to agent-browser's--proxy.sshno longer errors on--tunnel=socks5(it routes through the layer chain).dockererrors only when a non-empty stack does not end in an SSH hop (a[Socks5, SshHop]underlay is valid; socks5-only still errors for docker since streamlocal needs an SSH tail). - Composable
--hopflag (Phase 21) — repeatable, ordered, URL-form. Mutually exclusive with legacy--tunnel/--ssh-*/--socks5-*; legacy flags still work and are lowered to layers.--tunnel=ssh+--socks5-*now composes (SOCKS5 underlay + SSH hop) instead of being an error. tunnel_layersMCP field (Phase 21) — ordered JSON array of{type:"socks5"|"ssh", host, port, user?, password?, key_path?}. Conflicts with legacytunnel/ssh_jump/ssh_*/socks5_*fields.- MCP server mode (
tools4awith no subcommand) over stdio. SQL tools (mysql/pgsql/clickhouse) and HTTP tool return a secondContent::resource(MCP App UI, MIMEtext/html) alongside the JSON text — clients without MCP Apps support ignore it.
Not yet implemented:
- SSH key passphrases, strict known_hosts verification
- SSH PTY allocation (interactive commands like
topwon't work) - HTTP / SSH-direct / Browser profile/YAML config
- HTTP/SSE MCP transport (the SERVER's transport)
- Redis cluster routing, pub/sub, transactions, scripting (EVAL)
- Per-Value typed mapping for RESP3
Map/Set/Push - SCP/SFTP file transfer
Build a release binary and install it on PATH:
cargo install --path .
# or, for an unpublished build:
cargo build --release && cp target/release/tools4a ~/.local/bin/cargo install --path . puts the binary at
~/.cargo/bin/tools4a, which is on PATH by default after a normal
Rust toolchain install.
This repo is a Cargo workspace. The tools4a binary crate lives at
the repo root (presentation layer only). The lib crates under crates/
are: tools4a-core (everything shared — trait floor + LayeredTunnel
build_connector+ForwardTarget+build_tunnel+ Config/Profile /Loader/Merger + SSHsessionchain helpers +McpTooltrait), and the eight leaf service cratestools4a-mysql/tools4a-pgsql/tools4a-clickhouse/tools4a-redis/tools4a-mongo/tools4a-http/tools4a-ssh/tools4a-browser. Each leaf crate owns its full vertical slice: protocol primitives, the<Svc>Orchestrator: impl Service, and the<Svc>Mcp: impl McpTool. Every leaf depends only ontools4a-core.cargo build/cargo testfrom the root build and test all of them.
The mysql, pgsql, and mongo subcommands (and their MCP equivalents)
reject write operations by default. Use --allow-write
(allow_write: true in MCP) to opt in.
| Service | Reads (always allowed) | Writes (need --allow-write) |
|---|---|---|
| mysql | SELECT, SHOW, EXPLAIN, DESCRIBE, WITH, VALUES, TABLE |
INSERT, UPDATE, DELETE, REPLACE, CREATE, DROP, ALTER, TRUNCATE, GRANT, CALL, SET, … |
| pgsql | same as mysql | same as mysql, plus COPY, VACUUM, ANALYZE, etc. |
| mongo | find, aggregate (no $out/$merge), count, distinct, listCollections, listDatabases, listIndexes, dbStats, collStats, serverStatus, ping, hello, buildInfo, getParameter, … |
insert, update, delete, findAndModify, drop, create, createIndexes, aggregate with $out / $merge, … |
For mysql + pgsql, when --allow-write is not set, the session is
also forced into a DB-level read-only mode (SET SESSION TRANSACTION READ ONLY / SET default_transaction_read_only = on) as a second line
of defense — so a misclassified write will still be rejected by the
database itself. Mongo has no per-session read-only mode, so the
orchestrator-level command whitelist is the only guard.
Redis, HTTP, and SSH are not restricted — they accept any command
without --allow-write.
# Direct connection (read-only by default)
tools4a mysql "SELECT * FROM users" --host=localhost --user=root --password=secret
# Write — requires --allow-write
tools4a mysql "INSERT INTO users (name) VALUES ('alice')" \
--host=localhost --user=root --password=secret --allow-write
# Using YAML config
tools4a --config=mysql.yaml mysql "SELECT * FROM users"
# Using TOML profile
tools4a mysql "SELECT * FROM users" --profile=prod
# Through a single SSH jump
tools4a --tunnel=ssh --ssh-jump=bastion.com --ssh-user=admin --ssh-password=secret \
mysql --host=mysql.internal --user=root --password=dbpass "SELECT 1"
# Through two SSH jumps (comma-separated; all share --ssh-user/--ssh-password)
tools4a --tunnel=ssh --ssh-jump=bastion1.com,bastion2.com --ssh-user=admin \
--ssh-key-path=~/.ssh/jump_key \
mysql --host=mysql.internal --user=root --password=dbpass "SELECT 1"
# Through an external SOCKS5 proxy
tools4a --tunnel=socks5 --socks5-host=172.31.169.108 --socks5-port=2235 \
mysql --host=mysql.internal --user=root --password=dbpass "SELECT 1"
# Through an external SOCKS5 proxy with RFC 1929 user/pass auth
tools4a --tunnel=socks5 --socks5-host=proxy.internal --socks5-user=alice \
--socks5-password=s3cret \
mysql --host=mysql.internal --user=root --password=dbpass "SELECT 1"
# Composable --hop (Phase 21): SOCKS5 underlay → SSH → target (the canonical chain)
tools4a --hop 'socks5://172.31.169.108:2235' \
--hop 'ssh://admin:pass@gateway.internal:22' \
mysql --host=mysql.internal --user=root --password=dbpass "SELECT 1"
# Composable --hop: SSH multi-hop with per-hop credentials
tools4a --hop 'ssh://admin:pass@bastion1.com' \
--hop 'ssh://dbuser:pass@bastion2.internal' \
mysql --host=mysql.internal --user=root --password=dbpass "SELECT 1"
# tunnel-serve: local TCP port-forward through SOCKS5 → SSH (Phase 21)
tools4a tunnel-serve --type tcp --listen 127.0.0.1:13306 \
--target-host mysql.internal --target-port 3306 \
--hop 'socks5://172.31.169.108:2235' \
--hop 'ssh://admin:pass@gateway.internal:22'# Direct connection (read-only)
tools4a pgsql "SELECT * FROM users LIMIT 5" --host=localhost --user=postgres --password=secret --database=myapp
# Write — requires --allow-write
tools4a pgsql "DELETE FROM events WHERE created_at < now() - interval '30 days'" \
--host=localhost --user=postgres --password=secret --database=myapp --allow-write
# Through an SSH jump
tools4a --tunnel=ssh --ssh-jump=bastion.com --ssh-user=admin --ssh-key-path=~/.ssh/id_rsa \
pgsql --host=pg.internal --user=app --password=app_pwd --database=myapp "SELECT NOW()"
# Using a TOML profile
tools4a pgsql "SELECT count(*) FROM events" --profile=prod-postgresMongo commands are JSON documents passed to Database::run_command:
# find (read — works without --allow-write)
tools4a mongo '{"find":"users","filter":{"active":true},"limit":5}' \
--host=localhost --database=myapp
# insert (write — requires --allow-write)
tools4a mongo '{"insert":"events","documents":[{"type":"signup","ts":1}]}' \
--host=mongo.internal --user=app --password=secret --database=analytics \
--allow-write
# Through an SSH jump
tools4a --tunnel=ssh --ssh-jump=bastion.com --ssh-user=admin --ssh-password=jpwd \
mongo '{"listCollections":1}' --host=mongo.internal --database=admin# Direct connection
tools4a redis "GET mykey" --host=localhost --port=6379
# With password + db
tools4a redis "HGETALL myhash" --host=localhost --password=secret --db=2
# Through an SSH jump
tools4a --tunnel=ssh --ssh-jump=bastion.com --ssh-user=admin --ssh-password=secret \
redis "INFO replication" --host=redis.internal --password=cache_pwd
# Using a TOML profile
tools4a redis "KEYS *" --profile=prod-cache# Simple GET
tools4a http GET https://api.example.com/users
# POST with JSON body
tools4a http POST https://api.example.com/users \
--json --data '{"name":"alice"}' \
--bearer "$API_TOKEN"
# Through an SSH jump to an internal HTTPS service
tools4a --tunnel=ssh --ssh-jump=bastion.com --ssh-user=admin --ssh-password=secret \
http GET https://internal-api.local/health
# Self-signed cert internal service (show full status + headers + body)
tools4a http GET https://10.0.0.5/api --insecure -i# Direct connection
tools4a ssh "uname -a" --host=server.com --user=admin --key-path=~/.ssh/id_rsa
# With password
tools4a ssh "df -h" --host=10.0.0.5 --user=root --password=secret
# Through an SSH jump (jump creds are SEPARATE from target creds)
tools4a --tunnel=ssh --ssh-jump=bastion.com --ssh-user=jumper --ssh-password=jpwd \
ssh "systemctl status nginx" --host=internal-server --user=admin --key-path=~/.ssh/target_key
# Show structured output (exit_code/stdout/stderr table)
tools4a ssh "false" --host=h --user=u --key-path=~/.ssh/k -iBy default tools4a's exit code mirrors the remote command's exit code,
so shell-script usage works (e.g. if tools4a ssh "test -f /etc/passwd" ...).
Pre-requisite: install agent-browser
separately (npm i -g agent-browser or the upstream Rust build). tools4a
shells out to it; it does not bundle a browser.
# Open a URL in a named session, then snapshot
tools4a browser open https://example.com --session work
tools4a browser snapshot --session work
# Pass agent-browser flags directly — they sit AFTER the subcommand
tools4a browser open https://example.com --wait
# tools4a-side flags (--session, --proxy, --bin) go BEFORE the subcommand
tools4a browser --session work open https://example.com
# Show structured output (exit_code/stdout/stderr table)
tools4a browser snapshot --session work -i
# Through an SSH bastion (tools4a binds a per-call SOCKS5 listener via LayeredTunnel Socks5Server)
tools4a --tunnel=ssh --ssh-jump=bastion.example.com --ssh-user=admin \
browser open https://internal.local --session work
# Composable --hop for browser: SOCKS5 underlay → SSH bastion → browser
tools4a --hop 'socks5://172.31.169.108:2235' \
--hop 'ssh://admin:pass@bastion.example.com' \
browser open https://internal.local --session worktools4a's exit code mirrors agent-browser's, like the ssh
subcommand. Any SSH-containing layer stack (e.g. --tunnel=ssh or
--hop 'ssh://...') stands up a LayeredTunnel with
ForwardTarget::Socks5Server (no external ssh -D needed) — tools4a
binds 127.0.0.1:<random> and injects --proxy socks5://... into the
agent-browser invocation; the listener is torn down when the call
returns. A pure-SOCKS5 stack (--tunnel=socks5 or a single
--hop 'socks5://...') short-circuits by passing --proxy socks5://…
directly to agent-browser without a local relay. If you set BOTH
--tunnel=ssh AND --proxy ..., that's an Error::Config conflict —
pick one.
Run tools4a with no subcommand to start an MCP server over stdio:
tools4aIt exposes eight tools (mysql_exec, pgsql_exec, clickhouse_exec,
redis_exec, mongo_exec, http_exec, ssh_exec, browser_exec) —
one per service. Each tool accepts the same parameters as the
corresponding CLI subcommand plus the shared tunnel fields. The
preferred form (Phase 21) is tunnel_layers (an ordered JSON array of
{type:"socks5"|"ssh", host, port, user?, password?, key_path?});
legacy flat fields (tunnel, ssh_jump, ssh_user, ssh_password,
ssh_key_path, ssh_port, socks5_host, socks5_port, etc.) still
work and are lowered to layers automatically.
browser_exec additionally requires the agent-browser
binary to be installed on $PATH (or via $AGENT_BROWSER_BIN) — tools4a
shells out to it and captures stdout/stderr/exit_code. AI clients
(Claude Desktop, Cursor, etc.) can call these tools to query databases,
run shell commands through SSH jump hosts, and automate a real browser.
Example MCP configuration entry (e.g. for Claude Desktop):
{
"mcpServers": {
"tools4a": {
"command": "/usr/local/bin/tools4a"
}
}
}This repo ships a Claude Code plugin (.claude-plugin/plugin.json +
.claude-plugin/marketplace.json + .mcp.json + skills/). Once
installed, Claude gets the eight service MCP tools plus the
project-specific skills — all wired up automatically.
Pick one of the two paths below.
Gives you the MCP tools and the bundled skills (tools4a-using,
mysql-debugging, ssh-bastion-checklist).
# 1. Build & install the binary onto $PATH
cargo install --path . # produces ~/.cargo/bin/tools4a
# 2. In a Claude Code session, register this repo as a marketplace
/plugin marketplace add /absolute/path/to/tools4a # one-time
# 3. Install the plugin from that marketplace
/plugin install tools4a@tools4a # enable plugin
# 4. Verify
/mcp # should list `tools4a`To upgrade after pulling new commits, rebuild the binary
(cargo install --path . --force) and re-run /plugin marketplace update tools4a.
Gives you the eight MCP tools only (no skills). Useful if you don't want plugin-level integration.
# 1. Build & install the binary
cargo install --path .
# 2. Register the MCP server with Claude Code
claude mcp add tools4a tools4a # name=tools4a, command=tools4a
# 3. Verify
claude mcp list # should show `tools4a`The tools4a binary speaks MCP over stdio when invoked with no
subcommand, so no extra flags are needed.
- MCP tools auto-registered via
.mcp.json:mysql_exec— run a MySQL query.pgsql_exec— run a PostgreSQL query.clickhouse_exec— run a ClickHouse SQL query (HTTP interface).redis_exec— run a Redis command.mongo_exec— run a MongoDB command (JSON document torunCommand).http_exec— send an HTTP request.ssh_exec— run a shell command on a remote SSH server.browser_exec— run anagent-browsersubcommand (browser automation; requires the externalagent-browserbinary).
- Skills that guide the assistant (Path A only):
tools4a-using— consolidated guide for all eight tools: parameter shape per service, three-layer config priority (mysql + pgsql + clickhouse + redis + mongo), SSH tunnel syntax, output mapping, destructive-command list.mysql-debugging— diagnostic queries for common MySQL errors, locks, slow queries.ssh-bastion-checklist— narrows down SSH-tunnel failures.browser-using— agent-browser daemon model, session reuse, Phase 1 SOCKS workaround for SSH-routed browsing.
YAML Config (mysql.yaml):
type: mysql
host: localhost
port: 3306
user: root
password: secret
database: mydbTOML Config (~/.config/tools4a/config.toml):
[profiles.prod]
type = "mysql"
host = "prod.example.com"
port = 3306
user = "app_user"
password = "secret"Run tests:
cargo testBuild:
cargo buildMIT