narwhal

mcp
Guvenlik Denetimi
Gecti
Health Gecti
  • License — License: NOASSERTION
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Community trust — 10 GitHub stars
Code Gecti
  • Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Gecti
  • Permissions — No dangerous permissions requested

Bu listing icin henuz AI raporu yok.

SUMMARY

TUI database client with a built-in MCP server. Five databases (postgres, mysql, sqlite, duckdb, clickhouse), vim editing, Lua plugins.

README.md

narwhal

CI
License: MIT OR Apache-2.0
Version
Rust 1.75+

A TUI database client with a built-in MCP server. Five databases, vim editing, Lua plugins.

narwhal demo

Why narwhal

  • Built-in MCP server. Run narwhal mcp and any
    Model Context Protocol client (Claude
    Desktop, Cursor, your own agent) gets list_connections,
    describe_schema, describe_table, run_query, explain_query over
    stdio. Read-only by default, with a three-layer SQL guard and a
    workspace ACL (.narwhal/workspace.toml) so an agent can only see
    the connections you explicitly listed.
  • One TUI, five databases. Postgres, MySQL, SQLite, DuckDB,
    ClickHouse. No driver-juggling, no context-switching between psql,
    mysql, and DataGrip.
  • Vim editing + auto-pair + completion. Modal input (Normal,
    Insert, Visual), schema-aware tab-completion, alias-resolved column
    hints, a proper : command palette.
  • Lua plugin runtime. The bits that should be yours, stay yours.
    Write a .lua file, drop it in ~/.config/narwhal/plugins/, and it
    is live.
  • SSH tunnels, ~/.pgpass, OS keyring. The auth ergonomics you
    already configured for psql work here too. Set
    ssh_host=jump.example.com and the connect path forwards a loopback
    port for you.

Install

Cargo (any platform with Rust)

cargo install narwhaldb

The crates.io name is narwhaldb (the bare narwhal slot is held
by an unrelated 2018 docker library); the installed binary is still
just narwhal.

For users without a Rust toolchain, cargo-binstall fetches the
prebuilt binary instead of compiling:

cargo binstall narwhaldb

Homebrew (macOS, Linuxbrew)

brew tap Nonanti/tap
brew install narwhal

Arch Linux (AUR)

yay -S narwhal      # or: paru -S narwhal

Nix

nix run github:Nonanti/narwhal

Or add the flake to your inputs and reference the default package.

Pre-built binaries

Download native tarballs (with SHA-256 checksums) from the
latest GitHub Release:

  • x86_64-unknown-linux-gnu
  • aarch64-apple-darwin (Apple Silicon)

Intel Mac users: build from source with cargo install narwhaldb for
now; a prebuilt x86_64-apple-darwin tarball is on the roadmap once
the macos-13 runner backlog clears.

curl -LO https://github.com/Nonanti/narwhal/releases/latest/download/narwhal-1.0.0-x86_64-unknown-linux-gnu.tar.gz
tar -xzf narwhal-1.0.0-*.tar.gz
mv narwhal-1.0.0-*/narwhal ~/.local/bin/

Build from source

git clone https://github.com/Nonanti/narwhal.git
cd narwhal
cargo build --release
# binary at target/release/narwhal

Quick start

  1. Run narwhal. The TUI opens with an empty editor and a sidebar.
  2. Hit :add. The connection wizard appears. Pick a driver, fill in host + database (or use :url postgres://user:pass@host/db to skip the form).
  3. :open <name>. The saved entry connects; the sidebar fills with schemas and tables.
  4. F6 to run. The whole buffer executes; results appear in the lower pane. Press F1 any time for the full keymap reference.

connections.toml schema

Named connections live in ~/.config/narwhal/connections.toml. One
[[connection]] block per database; [connection.params] carries the
driver-specific options. The field names match the
ConnectionParams struct —
in particular username (not user) is the canonical name.

# Local SQLite — the file path is the only required param.
[[connection]]
id     = "00000000-0000-0000-0000-000000000001"
name   = "smoke"
driver = "sqlite"

[connection.params]
path = "/tmp/narwhal-smoke.db"

# Postgres on a non-default port, no TLS — typical local docker setup.
[[connection]]
id     = "00000000-0000-0000-0000-000000000002"
name   = "demo-pg"
driver = "postgres"

[connection.params]
host     = "127.0.0.1"
port     = 5433
username = "postgres"        # NOTE: `username`, not `user`
password = "narwhal"
database = "demo"
ssl_mode = "disable"         # disable | prefer (default) | require | verify-ca | verify-full

File-local drivers (sqlite, duckdb) tolerate the default prefer
so pre-TLS configs still load; the wire layer ignores it. Network
drivers (postgres, mysql, clickhouse) accept any of the five
ssl_mode values plus optional ssl_root_cert, ssl_cert, ssl_key
paths for mutual TLS.

config.toml (settings)

A ~/.config/narwhal/config.toml lets you override the renderer
theme and a few editor toggles. Missing fields fall back to their
defaults so a one-line file is enough.

theme = "dark"           # "dark" (default) | "light" | "high-contrast"

[editor]
tab_width    = 4         # reserved, v1.1 will honour this
use_spaces   = true      # reserved, v1.1 will honour this
line_numbers = true      # reserved, v1.1 will honour this

[keybindings]
vim_mode = true          # reserved, v1.1 will allow opt-out

v1.0 wires only the theme field; the rest are persisted and
load-validated so the file stays stable across upgrades. The renderer
warns at start-up if the file is malformed instead of silently
falling back to defaults.

SSH tunnels

Any network connection can prepend an SSH local-port-forward by
adding ssh fields to the params block. The forward is opened
implicitly on :open and torn down when the session closes.

[connection.params]
host     = "db.internal"     # resolved on the bastion side
port     = 5432
username = "alice"
database = "prod"

[connection.params.ssh]
host      = "bastion.example.com"
user      = "alice"
port      = 22               # optional — defaults to 22
# key_path  = "~/.ssh/id_ed25519"  # optional — defaults to ssh-agent / ~/.ssh/config
# jump_host = "jump.example.com"   # optional — maps to `ssh -J`

The spawned ssh subprocess inherits ~/.ssh/config, the agent,
Match blocks, IdentityAgent, and FIDO2 keys for free — narwhal
deliberately shells out to OpenSSH rather than embedding its own
client. URL form: ?ssh_host=bastion&ssh_user=alice on a
:url postgres://... invocation.

TLS defaults changed (v0.2)

Breaking change: ssl_mode = prefer and ssl_mode = require now
perform full CA chain verification instead of accepting any server
certificate. Self-signed certificates will be rejected unless the CA
is explicitly trusted via ssl_root_cert.

If you were relying on the previous insecure behaviour:

  • Self-signed servers: add ssl_root_cert = "/path/to/ca.pem" to
    the connection params, or set ssl_mode = "disable" if TLS is not
    needed.
  • Hostname mismatch: use ssl_mode = "require" or
    ssl_mode = "verify-ca" (chain verified, hostname skipped).
  • Full verification: ssl_mode = "verify-full" (unchanged).

Query-string TLS params (?sslmode=..., ?sslrootcert=..., etc.)
are now parsed into dedicated struct fields instead of being left in
the generic options map.

MCP server: talk to your databases through an AI agent

narwhal ships a built-in Model Context Protocol
server so any MCP-capable AI assistant (Claude Desktop, Cursor, Continue,
Aider, ...) can browse the connections you already configured and inspect
their schema.

narwhal mcp   # runs the JSON-RPC stdio server

Wire it into Claude Desktop:

// ~/.config/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "narwhal": {
      "command": "narwhal",
      "args": ["mcp"]
    }
  }
}

The v0 tool surface:

Tool What it does
list_connections List configured connections — driver, target, SSH flag. No IO, no credentials loaded. Honours the workspace ACL.
describe_schema Schema / table / view tree for one connection.
describe_table Full structure of one table — columns, indexes, foreign keys, unique constraints, engine-native DDL.
run_query Execute a single statement. Read-only by default — syntactic guard + BEGIN/ROLLBACK sandwich + row limit (default 1 000). read_only=false opts out, subject to the workspace ACL.
explain_query Driver-native EXPLAIN with the right dialect prefix. Optional analyze=true runs the statement for real cardinalities (PG / MySQL / DuckDB).

Every database-touching call is audit-logged to
~/.local/share/narwhal/history.jsonl with source: "mcp" so you can
jq 'select(.source == "mcp")' to isolate agent traffic.

Workspace scoping: .narwhal/workspace.toml

A repo-local file (discovered by walking up from pwd, same idiom as
.git) declares what the MCP server may expose when narwhal runs from
inside that directory tree. Commit it next to your code so an agent
launched against your project can only reach the databases you list.

# .narwhal/workspace.toml

# Connection names from connections.toml that the agent may target.
# Empty / omitted = all of them.
allowed_connections = ["staging", "test"]

# When false, run_query rejects read_only=false. Default true.
allow_writes = false

Disallowed connections appear to the agent exactly as a misspelled
name would: the list_connections result hides them, describe_* /
run_query calls answer with the same "unknown connection" tool-level
error, and the agent retries against the visible set automatically.

Keymap

Global

Keys Action
F5 / Alt-Enter / Ctrl-; Run statement under cursor
F6 Run whole buffer
F7 Stream cursor statement
F4 / Ctrl-C Cancel running query
Ctrl-W Cycle pane focus
Ctrl-T New editor tab
Ctrl-Tab / Ctrl-Shift-Tab Cycle tabs
? / F1 Help
:q Quit
:refresh Re-fetch schema tree for active connection

Editor

Keys Action
i / a Enter insert mode
Esc Back to normal mode
Tab / Ctrl-Space Completion
↑ ↓ / Shift-Tab Cycle popup items
Enter / Tab (in popup) Accept completion
h j k l / arrows Move cursor
w / b Word forward / backward
0 / $ Line start / end
v / V Visual / visual-line mode

Sidebar

Keys Action
j / k / ↑ / ↓ Navigate
Enter Describe table
o Preview table data
d Inject DDL into editor

Results

Keys Action
h j k l / arrows Move selection
Enter Open cell popup
e Edit cell value
y / Y Yank cell / row to clipboard
/ Filter rows
n / N Next / prev search match
g / G Jump to first / last row
:next / :prev Page through results

Snippets

Keys Action
:save <name> Save editor buffer as a named snippet
:load <name> Load a snippet into a new tab
:rm-snippet <name> Delete a saved snippet
:snippets Browse saved snippets

Plugins

Plugins are Lua scripts that auto-load from ~/.config/narwhal/plugins/*.lua
(or the platform equivalent under $XDG_CONFIG_HOME). They get a narwhal
global with these entry points:

narwhal.register_command(name, description, handler)
    -- handler(arg : string)
    --   return "..."                 -> status bar message
    --   return { sql = "..." }       -> append to editor buffer
    --   return { sql = "...", append = false }
    --                                -> replace editor buffer
    --   return nil | false           -> silent

narwhal.register_transform(handler)
    -- handler(result : table)
    --   mutate in place; return value ignored

narwhal.sql_run(sql : string) -> result
    -- Run SQL on the active connection synchronously

narwhal.editor_text          : string (read-only)
    -- Current editor buffer content during command dispatch

Sample plugins

Six working samples live in examples/plugins/:

File What it does
uppercase.lua Result transform that uppercases every TEXT cell
format_json.lua Pretty-prints cells that parse as JSON
row_count.lua :rc <table> — count rows via narwhal.sql_run
query_snippet.lua :top <table> — inject SELECT * FROM … LIMIT 10
csv_export.lua :csv-export <table> <path> — dump to CSV
explain_cost.lua :explain-cost / :explain-sqlite — wrap buffer in EXPLAIN

Load on demand: :plug-load /path/to/file.lua. List everything: :plug-list.

For the full API reference, see narwhal-plugin-lua docs
and the plugin examples README.

Security model

Plugins are trusted code that runs with your privileges. They can run
arbitrary SQL, inject into the editor, and read every result row. Only
install scripts from sources you'd trust as a shell script. There is no
sandbox — by design, so auditing a plugin is just reading a short .lua
file.

Built-in command names (run, open, begin, quit, …) are reserved;
a plugin that tries to shadow one is rejected at load time. During a
:begin transaction, narwhal.sql_run is refused entirely.

Headless exec mode: one-shot SQL from the shell

The same binary doubles as a psql -c / mysql -e muadili. Use it in
CI smoke checks, cron jobs, or shell pipelines:

narwhal exec --conn prod 'SELECT count(*) FROM users'

# Choose a format for the consumer (default: table)
narwhal exec -c prod -f json 'SELECT id, email FROM users' | jq '.[].email'
narwhal exec -c prod -f csv  'SELECT * FROM orders' > orders.csv
narwhal exec -c prod -f tsv  'SELECT id, name FROM users' | column -t

# Limit rows to keep large queries snappy
narwhal exec -c prod -l 100 'SELECT * FROM events'

# Writes are sandboxed by default (BEGIN ... ROLLBACK). `--write` opts out:
narwhal exec -c prod --write "UPDATE users SET banned = true WHERE id = 42"

Formats: table (default, ASCII grid), csv (RFC 4180), json
(array-of-objects), tsv (pipe-friendly, no quoting). Connection
resolution is the same as the TUI — keyring first, ~/.pgpass/env
fallback. Every call is audit-logged to history.jsonl with
source: "exec" so it can be grepped alongside MCP traffic.

Transactions

Command Action
:begin [iso] Open a pinned connection; iso accepts ru/rc/rr/s short forms
:commit Commit and close the pinned connection
:rollback Rollback and close the pinned connection
:savepoint NAME Create a named savepoint (drivers that support them)
:release NAME Release a savepoint
:rollback-to NAME Rollback to a savepoint

A TX badge on the status bar reminds you that you're inside a
transaction.

Architecture

              narwhal (bin)
                   │  entry point + CLI
                   ▼
             narwhal-app                       narwhal-mcp
              orchestrator                       MCP server
           │     │      │                          │
           ▼     ▼      ▼                          ▼
   narwhal-tui  narwhal-commands           narwhal-driver-registry
     render     completion / export /             (feature-gated
                wizard / dispatch /                 driver lookup)
                snippets / DDL / …                       │
           │         │                                    ▼
           └─────────┼────────────────┐         narwhal-driver-*
                    ▼                 │          postgres · sqlite
              narwhal-domain          │          mysql · duckdb
              pure model state        │          clickhouse
           (editor buffer, schema     │
            listings, no IO,          │
            no rendering)             │
                   │                  ▼
                   └────────►   narwhal-pool · narwhal-sql ·
                                narwhal-history · narwhal-config ·
                                narwhal-vim
                                       │
                                       ▼
                                  narwhal-core
                            driver trait, value model,
                            schema types, error type

Dependency direction: every arrow points downward. View (narwhal-tui)
and commands (narwhal-commands) read domain state by reference; only
narwhal-app is allowed to mutate it in response to user input.
Concrete drivers are reached exclusively through
narwhal-driver-registry, so cargo features are the only place where
database engines come and go.

For the full layer map see docs/ARCHITECTURE.md;
for the code-style contract see docs/STYLE.md.

Safety

  • Every destructive operation goes through the : command line, never a hotkey.
  • Statements are journaled to ~/.local/share/narwhal/history.jsonl before execution.
  • Passwords prefer the OS keyring; an in-memory fallback is used only when the keyring isn't available. :forget <name> wipes the cached entry.

Building

Nix

nix develop
cargo build --release

The dev shell pulls in cmake, clang, and libcxx for the bundled
DuckDB C++ build, and pre-sets LIBCLANG_PATH for bindgen.

Other systems

Any toolchain at or above the version pinned in rust-toolchain.toml,
plus the usual native build deps for DuckDB (cmake, a C++17 compiler).

cargo build --release

Benchmarks

Criterion harnesses live under crates/*/benches/. Run them all with
cargo bench --workspace or one at a time:

cargo bench -p narwhal-sql --bench splitter
cargo bench -p narwhal-tui --bench sort
cargo bench -p narwhal-tui --bench editor_motion
cargo bench -p narwhal-history --bench append

The pre/post-optimisation numbers are recorded in
docs/perf-after-phase-2.md; current
headline numbers on a Linux box are ~900 MiB/s for the statement
splitter, ~38 µs for a 5 000-line w motion, and ~1.15 ms to sort
2 000 JSON cells.

Contributing

A few ground rules so PRs land smoothly:

  • cargo fmt --all, cargo clippy --workspace --all-targets -- -D warnings,
    cargo test --workspace, and RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps all pass in CI; please run them locally too.
  • New behaviour ships with a regression test under the relevant
    crate's tests/ directory.
  • Commit messages follow Conventional Commits
    (feat:, fix:, refactor:, docs:, chore:).

Licence

Dual-licensed under the MIT and
Apache 2.0 licences. Contributions are accepted under
the same terms.


See docs/RELEASING.md for the release
checklist and CHANGELOG.md for the version history.

Yorumlar (0)

Sonuc bulunamadi