# Basalt MCP server
Basalt's MCP server is a local subprocess. An MCP host starts `basalt mcp`,
writes newline-delimited JSON-RPC messages to its stdin, and reads responses
from stdout. Basalt keeps stdout exclusively for MCP traffic; diagnostics go
to stderr.
The server uses the official [`rmcp`](https://github.com/modelcontextprotocol/rust-sdk)
Rust SDK for protocol framing and lifecycle handling. Its behavior follows the
MCP [stdio transport](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio)
and [tools](https://modelcontextprotocol.io/specification/2026-07-28/server/tools)
specifications, including modern discovery requests and compatibility with the
legacy initialize handshake. Each newline-delimited JSON-RPC frame is capped
at 32 MiB before decoding; tool-specific limits are smaller.
For the current `2026-07-28` protocol, a host can begin with
`server/discover`; subsequent requests carry the protocol version, client
identity, and client capabilities in their `_meta` object. Basalt also accepts
the older `initialize`/`initialized` exchange for legacy hosts. The release
smoke test exercises the modern path against the installed binary, while the
wire tests cover both paths.
## Install
From a Basalt checkout:
```bash
cargo install --path . --locked
```
Confirm the binary is available:
```bash
basalt mcp --help
```
The command requires Rust 1.88 or newer when building from source. If the MCP
host does not inherit the user's `PATH`, use the absolute path printed by
`command -v basalt` as the configuration's `command` value.
Claude Code can register a project-scoped server without hand-editing JSON:
```bash
claude mcp add basalt --scope project -- \
basalt mcp --workspace "$PWD/.basalt-workspace" --init-workspace
```
This writes the project's `.mcp.json`; Claude Code may ask for approval before
using a project server. Use an absolute binary path when the host does not
inherit the shell's `PATH`. Keep `--allow-writes` out of shared configuration
unless the project has an explicit policy for agent imports, applies, and
undos; add it deliberately when that policy is approved. See the [Claude Code
MCP reference](https://docs.anthropic.com/en/docs/claude-code/mcp) for host
scope and registration details.
## Choose a mode
Use workspace mode for the product's agent workflow. It binds the server to a
versioned Basalt workspace and exposes only scoped workspace tools. The path is
the only data location the workspace tools can open:
```json
{
"mcpServers": {
"basalt": {
"command": "/absolute/path/to/.cargo/bin/basalt",
"args": ["mcp", "--workspace", "/absolute/path/to/project-data"]
}
}
}
```
For a new project, add `--init-workspace` to let the server create the
workspace on first start:
```json
{
"mcpServers": {
"basalt": {
"command": "basalt",
"args": [
"mcp",
"--workspace",
"/absolute/path/to/project-data",
"--init-workspace"
]
}
}
}
```
This flag creates only a missing path. It never replaces an existing directory,
manifest, or database; an existing invalid workspace still fails clearly. It
is an explicit bootstrap convenience, not a permission to discover or create
arbitrary paths.
Workspace mode is read-only with respect to data by default. `workspace_preview`
may create a local plan record, but it does not change database state. Add
`--allow-writes` only when the host configuration has an explicit approval
policy for applying plans and undoing changes:
```json
{
"mcpServers": {
"basalt": {
"command": "basalt",
"args": [
"mcp",
"--workspace",
"/absolute/path/to/project-data",
"--allow-writes"
]
}
}
}
```
When a modern `2026-07-28` client advertises MCP form elicitation, Basalt
returns an `input_required` result containing an `elicitation/create` input
request before each `workspace_import`, `workspace_apply`, and
`workspace_undo`. The client retries the original tool call with the opaque
request state and an explicit approval response; no workspace data is changed
before that retry. Legacy initialized clients receive the older
server-initiated `elicitation/create` request directly. Clients that do not
advertise elicitation retain the explicit `--allow-writes` startup policy;
Basalt does not treat tool annotations as approval enforcement.
Direct database mode remains available for compatibility with existing Basalt
users. It is also read-only by default; `execute` and `checkpoint` require
`--allow-writes`. Direct mode is not the scoped workspace workflow and exposes
stateful SQL transactions instead of preview/apply lifecycle tools.
## Host configuration
Use the host's MCP server configuration file. The common local-server shape is:
```json
{
"mcpServers": {
"basalt": {
"command": "/absolute/path/to/.cargo/bin/basalt",
"args": ["mcp", "/absolute/path/to/app.basalt"]
}
}
}
```
For Cursor, save the same shape as a project `.cursor/mcp.json`. Cursor's CLI
also discovers project MCP configuration; its [MCP
documentation](https://docs.cursor.com/context/model-context-protocol) covers
the host's approval and auto-run settings. Basalt's `--allow-writes` flag is
still the server-side boundary and must be enabled separately.
Add `--allow-writes` to a direct database configuration only when unrestricted
SQL writes are intentionally approved:
```json
{
"mcpServers": {
"basalt": {
"command": "basalt",
"args": ["mcp", "/absolute/path/to/app.basalt", "--allow-writes"]
}
}
}
```
Use `:memory:` instead of a file path when the database should disappear when
the MCP process exits. This is useful for a read-only protocol smoke test or a
session whose data is supplied entirely through approved direct SQL:
```json
{
"mcpServers": {
"basalt": {
"command": "basalt",
"args": ["mcp", ":memory:"]
}
}
}
```
When a host must run from a checkout, use an absolute manifest path so its
working directory does not matter:
```json
{
"mcpServers": {
"basalt": {
"command": "cargo",
"args": [
"run",
"--quiet",
"--manifest-path",
"/absolute/path/to/basalt/Cargo.toml",
"--",
"mcp",
"/absolute/path/to/app.basalt"
]
}
}
}
```
Prefer the installed binary: a host may restart an MCP server often, and
`cargo run` adds compile and dependency-resolution work to every startup.
## Tool contract
Common tools in both modes are:
| `query` | `SELECT` and `EXPLAIN SELECT` only | No |
| `list_tables` | List tables and committed column metadata | No |
| `describe_table` | Inspect one table's committed column metadata | No |
| `checkpoint` | Flush a durable snapshot and clear old WAL frames | Filesystem state; requires approval |
Workspace mode also provides:
| `workspace_import` | Import bounded CSV, JSON, or JSON Lines content into a new table and create a recovery point | Yes; requires approval; MRTR or legacy elicitation when advertised |
| `workspace_inspect` | Read workspace metadata, schema, and row counts | No |
| `workspace_preview` | Execute a bounded mutation in isolation and save its exact plan | Plan metadata only |
| `workspace_plan` | Reload one persisted plan by ID, including exact SQL and impact metadata | No |
| `workspace_apply` | Apply one exact plan and create a recovery point | Yes; requires approval; MRTR or legacy elicitation when advertised |
| `workspace_history` | Read the change ledger and recovery statuses | Recovery metadata may be reconciled |
| `workspace_diff` | Compare a change recovery point with current state and report schema plus exact added/removed row counts; bounded to 10,000 rows across each compared database | Recovery metadata may be reconciled |
| `workspace_undo` | Restore the latest committed change's recovery point | Yes; requires approval; MRTR or legacy elicitation when advertised |
| `workspace_export` | Return one table as bounded CSV, JSON Lines, or SQL content | No |
`execute` is available only in direct database mode. It is absent from the
workspace tool list so agents do not see an operation that can bypass the
workspace lifecycle.
In workspace mode, use this sequence:
1. When data is not already present, call `workspace_import` with an explicit
table, format, and content payload. The tool accepts only CSV, JSON, or
JSON Lines, caps content at 16 MiB, 10,000 rows, 256 columns, and 1,000,000
cells, never accepts a filesystem path, and returns a recoverable `change_id`.
2. Call `workspace_inspect` or `query` to understand the current data.
3. Call `workspace_preview` with the mutation. Review the returned exact `sql`
and impact summary, then keep the returned `plan_id`.
4. If the plan response was lost or the server restarted, call
`workspace_plan` with the saved `plan_id` to reload the exact review
context.
5. Have the operator approve that plan, then call `workspace_apply`. A modern
client that advertises form elicitation receives an `input_required` result,
gathers the decision, and retries the same call with the approval; a legacy
initialized client receives `elicitation/create` on the open interaction.
6. Use `workspace_history` and `workspace_diff` to inspect the committed
change.
7. Call `workspace_undo` with the latest change ID if the change should be
reversed; it uses the same modern or legacy approval behavior.
The complete preview report is checked against the MCP response limit before
its plan metadata is persisted. An oversized preview fails without leaving a
plan record that the caller cannot inspect.
Workspace SQL calls open the underlying database for one operation at a time,
and Basalt serializes workspace operations within one MCP server process. This
is required because MCP hosts may dispatch requests concurrently while a
workspace has one file-backed database lock. The workspace itself remains
exclusively owned by the process that opened it, so a separate CLI or MCP
process is rejected until the owner exits. Workspace SQL does not provide a
multi-call SQL transaction; the durable plan and recovery lifecycle is the
transaction boundary. `workspace_apply` rejects stale plans and never silently
applies a mutation against a changed base state.
Workspace previews accept at most 64 statements and 32 mutating statements per
call. MCP workspace previews and applies also accept at most 10,000 affected
rows per plan. This bounds the impact described by one plan; larger jobs should
be split into separately reviewed plans. SQL executed by `query`, direct-mode
`execute`, `workspace_preview`, and `workspace_apply` shares a 1,000,000-unit
engine work budget per request. Workspace imports, diffs, undos, exports, and
checkpoint phases use the same bounded execution model where they inspect or
clone database state. The budget is enforced while
the engine snapshots state, scans, joins, groups, projects, sorts, prepares,
or commits rows, before the full result is handed to MCP. A unit is roughly a
scalar plus one 1 KiB chunk of text payload, with extra units for relational
work; it is a deterministic work bound, not a wall-clock timeout or a
byte-precise memory quota. If it is exceeded, the operation fails without
committing a mutation; narrow the query, add a selective predicate or limit,
split the write into reviewed plans, or use the CLI for a larger intentional
job.
Workspace exports count live rows before materializing them and reject tables
over 10,000 rows or 1 MiB of raw content before the MCP response-size check.
Use the CLI export for larger tables or files.
Workspace diffs inspect the logical contents of a recovery point and the
current database. MCP diffs count live rows before materializing either side,
inspect at most 10,000 rows in each compared database, and reject larger
workspaces before building the comparison; use the CLI diff when a complete
large-workspace comparison is intentional. The report includes schema changes
and exact added/removed row counts from a deterministic row-multiset comparison.
It does not claim keyed row pairing or a row-by-row patch; an update normally
appears as one removed row and one added row. History and diff may reconcile an
interrupted operation's metadata, but they do not apply data changes and do not
require `--allow-writes`. History includes import format, table, byte count,
and summary for every workspace import; `workspace_plan`
reloads the exact persisted SQL and preview impact by plan ID.
`workspace_history` rejects a changes directory larger than 10,000 entries or
1 MiB of change metadata before loading the full response. Non-record files in
that directory count toward the entry limit. Use the CLI history command for a
complete inspection of a larger local ledger.
The import, apply, and undo calls are retry-safe for lost responses. Their
identifiers and persisted request metadata let an exact retry return the
original receipt when the workspace is still at the recorded post-operation
state. A failed import may also be retried when its exact base state is still
present; unresolved records and moved work are never replayed. If later work
moved the workspace, Basalt does not replay or discard that work. Tool
annotations remain hints; the state and identifier checks are the enforcement
boundary.
`workspace_import` is an explicit atomic ingress operation rather than a raw
SQL escape hatch. It requires `--allow-writes`, creates a new table, and stores
the pre-import recovery point in workspace history. Its content is limited to
16 MiB, 10,000 rows, 256 columns, and 1,000,000 cells; use the CLI for larger
imports. An exact retry after a lost response returns the original import
receipt while the workspace remains at the recorded result; it never imports
the table twice. Use `workspace_undo` to reverse it while it is the latest
committed change. SQL dump imports remain a CLI-only operation because they can
contain arbitrary DDL and DML; the CLI records their recovery point as well.
Use the preview/apply lifecycle for SQL changes through MCP.
In direct database mode, all SQL tools operate on one connection for the
lifetime of the MCP process. When `--allow-writes` is enabled, transactions can
span separate calls:
1. Call `execute` with `BEGIN;`.
2. Call `execute` with the writes.
3. Call `execute` with `COMMIT;` or `ROLLBACK;`.
Available tools:
| `execute` | Writes, DDL, transactions, `CHECKPOINT`, and bounded SQL | Yes; requires approval |
`query` and `execute` accept:
```json
{
"sql": "SELECT id, name FROM users ORDER BY id",
"max_rows": 100
}
```
`max_rows` defaults to 100 and cannot exceed 1,000. It caps rows serialized in
the response; it does not replace the engine work budget above. The response
includes statement results in order, the committed generation, transaction state,
execution time, and whether rows were truncated. Each scalar is explicit, for
example `{ "type": "integer", "value": 1 }`, `{ "type": "real", "value": "1.5" }`,
or `{ "type": "null" }`. Real values are strings so non-finite results cannot
break JSON serialization and clients can choose their own numeric precision.
Responses larger than 1 MiB are rejected with an actionable error; narrow the
projection or lower `max_rows` when that happens. SQL input is limited to 1 MiB,
100 statements, and 32 mutating statements per call. A request that exceeds
the engine work budget returns an error naming the operation that crossed the
bound; no partial autocommit mutation is published.
The complete list of safety limits, including parser nesting and durable file
caps, is maintained in the [production-readiness contract](production-readiness.md#fixed-resource-limits).
The `basalt://schema` resource contains the current committed table and column
metadata as `application/json`. It is useful when an agent needs schema context
without spending a tool call on a query.
## Durability and safety
The MCP process has the same local filesystem access as the account that starts
it. A host should only launch Basalt for a database or workspace the user
intends the agent to access. Tool annotations are hints for host UIs; Basalt
also enforces the write flag itself. No host-specific approval behavior is
assumed.
Workspace import and export exchange content rather than accepting filesystem
paths. This keeps the workspace MCP surface from becoming an arbitrary
filesystem reader or writer. Imports are explicit, bounded, and recoverable;
the CLI commands remain available when a user intentionally chooses a source or
destination.
Use a durable path for work that must survive process restarts. Basalt appends
committed writes to its WAL; call `checkpoint` when you want to fold the state
into the snapshot and remove old WAL frames. `checkpoint` is a no-op for
`:memory:`, but still requires `--allow-writes` because it is a write-capable
operation in the protocol contract. A durable path is exclusively owned by one
process; a second CLI or MCP process receives an "already open" error instead
of competing for the same WAL. A workspace is exclusively owned by the
Basalt process that opened it, including an MCP server, until that process
exits. Stop the MCP server before using the workspace from the CLI or another
MCP server. This ownership lasts across the short database close/reopen window
used by undo, so another process cannot write data that an undo operation could
later replace.
## Troubleshooting
- Run `basalt mcp --help` directly to verify the installation.
- Use absolute paths for the database and, when necessary, the binary.
- Do not add CLI output flags such as `--json`; MCP owns stdout while the
server is running.
- If a database is already open by another process, stop that process before
diagnosing a file or WAL error.