Skip to content

feat(mcp): read-only MCP server at /api/mcp on the official SDK (#246) - #1070

Merged
cevheri merged 67 commits into
libredb:mainfrom
Roberton003:feat/mcp-server-phase1
Sep 26, 2026
Merged

cevheri merged 67 commits into
libredb:mainfrom
Roberton003:feat/mcp-server-phase1

Conversation

@Roberton003

@Roberton003 Roberton003 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Adds a Model Context Protocol server at /api/mcp, so a coding agent or AI assistant can read the databases an operator opts in, with the same read-only execution path agent mode uses. Closes #246.

The maintainers took this PR over from the first version and rebuilt it on the official TypeScript SDK (2.1.0) after an audit against the current spec (2026-07-28). What it is now:

  • Both protocol eras: 2026-07-28 and the 2025 revisions a client still speaks (2025-11-25, 2025-06-18). JSON-RPC batches are refused.
  • Off by default. LIBREDB_MCP_ENABLED, LIBREDB_MCP_URL and LIBREDB_MCP_TOKEN_LABEL turn it on; the Helm chart has an mcp block.
  • A scoped bearer token per user, minted from a small settings page (/settings/mcp), verified in the proxy and again in the route. The session cookie opens nothing here. Rotating the label revokes every token.
  • Origin is checked on every method, and Host on a loopback bind.
  • Only seed connections marked mcp: true are visible, filtered by the caller's role.
  • Three read-only tools: list_connections, inspect_schema and run_read_query. Structured results, a 32 KiB page with hasMore, and an untrusted-data notice ahead of anything read from a database.
  • Every call and every refusal is audited as mcp_operation, recorded before any provider is touched and failing closed. Every authenticated POST is metered on the query bucket, the budget the editor already spends.

Setup and limits are in docs/MCP.md.

Local checks on this head: format, lint, typecheck, knip, the drift guards, the full suite and the 100% line coverage gate pass, as do build, build:lib, attw and the Windows launcher checks.

Verified live on 2026-09-26 against the standalone build:

  • Clients: the SDK client in both eras, OpenCode 1.18.31, and Claude Code 2.1.283 in both its 2026-07-28 and 2025-11-25 modes, each calling all three tools. Codex CLI was not measured.
  • Engines: PostgreSQL, SQL Server, SQLite, DuckDB and LibreDB. A 500 ms timeout ended the statement in PostgreSQL and SQL Server; a client cancel was recorded as mcp_cancelled; SQLite runs to the end of the statement, as the docs say.
  • The settings page in a browser for an admin and a user, the role filter, and the off state.
  • Edges: a forged Host and a foreign Origin get 403, no token or a cookie alone gets 401 with WWW-Authenticate, batches get 400, and the well-known metadata paths get 404.
  • No regression: agent plan mode on SQLite, PostgreSQL, SQL Server, DuckDB and LibreDB, auto mode and the query editor on SQLite, PostgreSQL and SQL Server.

- Add stdio transport with pure JSON-RPC stdout isolation and graceful shutdown
- Expose list_connections, inspect_schema, and run_read_query MCP tools
- Implement fail-closed execution fence bridging to inspectAgentStatement
- Add safe serializer supporting BigInt, Buffers, Dates, and circular references
- Implement deterministic row ceiling and 64KB byte budget truncation
- Wire McpCancellationManager to native provider query cancellation and timeout
- Add connection versioning to guarantee single-flight provider lifecycle
- Add 24 comprehensive unit and integration tests including real stdio process
@Roberton003
Roberton003 force-pushed the feat/mcp-server-phase1 branch from 0d046ec to b334235 Compare September 22, 2026 02:11
@Roberton003
Roberton003 force-pushed the feat/mcp-server-phase1 branch from b334235 to 4cbb5a0 Compare September 22, 2026 02:14
@codecov

codecov Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cevheri cevheri added Needs Triage ai Artificial intelligence security Supply-chain, auth, or hardening work loop:needs-moderator-action Flagged by the maintainer loop: suspicious content or a decision only a human can make and removed loop:needs-moderator-action Flagged by the maintainer loop: suspicious content or a decision only a human can make labels Sep 22, 2026

@cevheri cevheri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working the design thread before writing code, and for reusing the shipped pagination contract from #816. The JSON-RPC dispatch and the tool schemas, the part I asked you for, are the sound part of this.

Requesting changes, on security and architecture.

Auth is ours, decision (5). route.ts:29 compares a plaintext MCP_TOKEN with ===, not the constant-time secretsMatch we already have, and grants role admin, which route.ts:98 turns into every admin-visible connection with its resolved credentials. It also cannot work: proxy.ts is untouched and /api/mcp is on no public list, so I ran a real MCP client against a build of this branch and the token channel failed while the session channel connected. Your tests import GET and POST and call them directly, so CI never saw that. Please drop the branch, the .env.example block and the auth claim in the GET payload.

inspect-schema.ts:21 catches a refusal by matching "in-memory" in the message and re-acquires with no profile, which is the editor's writable pool. run-read-query.ts:79 is narrower but the same inversion. Let the refusal reach the client.

Please drop bin/libredb-mcp.ts, server.ts and transport/stdio.ts with the SDK: decision (3). The bin cannot run from the published package anyway, files ships no src, and the SDK puts express and hono into production dependencies.

Still needed: an audit event and a rate limit the way agent/drive does it, a cancellation key carrying caller identity, English tool descriptions, a user-facing doc, and the two route-auth.test.ts lines reverted.

@cevheri

cevheri commented Sep 22, 2026

Copy link
Copy Markdown
Member

this is the big one :)
thanks for your hard-work.
this PR : desing, development, test, documents, e2e test, integration(I am using opencode) a lot of things :)

Roberton003 added a commit to Roberton003/libredb-studio that referenced this pull request Sep 23, 2026
@gitguardian

gitguardian Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 3 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
- - Basic Auth String 557d756 tests/unit/mcp/config.test.ts View secret
- - Username Password 979470a tests/api/mcp/token.test.ts View secret
- - Basic Auth String e14f205 tests/api/mcp/token.test.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Comment thread src/lib/mcp/serializer.ts Fixed
Roberton003 added a commit to Roberton003/libredb-studio that referenced this pull request Sep 23, 2026
@Roberton003
Roberton003 force-pushed the feat/mcp-server-phase1 branch from babac69 to 4fe500a Compare September 23, 2026 05:14
Roberton003 added a commit to Roberton003/libredb-studio that referenced this pull request Sep 23, 2026
@Roberton003
Roberton003 force-pushed the feat/mcp-server-phase1 branch from 4fe500a to 222da45 Compare September 23, 2026 05:26
@Roberton003
Roberton003 force-pushed the feat/mcp-server-phase1 branch from 222da45 to 7984266 Compare September 23, 2026 10:16

@cevheri cevheri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that was fast and it landed. I checked this at the head tree, not from the file list: no MCP_TOKEN and no x-libredb-mcp-token anywhere, no re-acquisition fallback left in either tool, the STDIO entry point and the SDK gone with package.json and bun.lock back to the merge base, and tests/security/route-auth.test.ts reverted so /api/mcp goes through the import pin like every other route. Four of my five asks are answered.

I built this branch and drove it. Four things block.

  1. The dispatcher silently discards responses for queries it has already run. I sent a batch of 50 and got 1 response back, HTTP 200, no error and no log line. Controls: 3 pings return 3, 5 small queries return 5, 5 large ones return 1, so it is driven by response size and not by batch length. Reject the oversized batch, or return a per-id error for each response that does not fit.

  2. No audit event when a tool call reaches an engine. I pointed you at /api/agent/drive last round; that route audits only denials, so my citation was wrong and you built what I asked for. The pattern is src/lib/db/operations/execution.ts:169 and :209: tool name, connection id, caller, outcome, correlation id, never the SQL.

  3. src/lib/mcp/context.ts is a second provider cache in front of the factory's own, and it never hits. The key serializes the whole connection and getManagedConnections stamps a fresh createdAt on every call, so I measured a new provider constructed on every single request, twelve in one session for one connection. Call acquireExecutionProfileProvider per request and delete the static maps; the factory already owns provider lifetime and cache keys.

  4. The ReDoS test at tests/unit/mcp/serializer.test.ts:104 cannot fail for the rule it names. The payload is "A".repeat(50000), which has no "://", so it never reaches the [^/\s]+@ quantifier CodeQL flagged at serializer.ts:94. I put a catastrophic variant of the same rule behind that test: 0.06 ms on your payload, well inside the 100 ms budget, and 653 ms on "http://" plus 37 characters. So the test passes a regex that is not safe. Point it at a scheme followed by a long repeat. For what it is worth the shipped rule measures linear here, 0.09 ms at 50 KB and 0.40 ms at 240 KB, so I do not think the alert is exploitable, but that is my measurement and not your test's.

Smaller, same push: bucket "ai" at route.ts:37 should be "query", this route reaches a database and no LLM, and the two membership comments in rate-limit.ts need the same edit. One batch request also executed 20 queries for a single rate-limit slot, so the limit meters requests rather than executions. The catch at inspect-schema.ts:104 turns an engine refusal into an empty column list, which is the shape of the fallbacks you just removed. run_read_query reports an offset cursor its input schema cannot accept. Comments and most new test names are still Portuguese, and everything that lands here is English.

docs/MCP.md says 401 for a missing session. I measured 307 to /login for no credential, for a stale cookie and for an unknown header, so please correct that line. The PR body still advertises the STDIO transport and the token seam you deleted.

One thing is ours, not yours. There is no machine credential yet, so only a browser session cookie reaches this endpoint. I will write the scoped credential and the src/proxy.ts branch for it, the way verifyAgentDriveToken is admitted today, and I will rule on CodeQL alert 536 myself.

cevheri added a commit that referenced this pull request Sep 24, 2026
…actor

CodeQL alert 536 (js/polynomial-redos, high) is open against refs/pull/1070/merge
at src/lib/mcp/serializer.ts:94 and needs a written ruling before it can be
dismissed or fixed. It is deferred rather than settled for two reasons: the
CodeQL check reports SUCCESS because alerts do not fail the job, so a green
rollup hides it; and the code is not on main, it arrives only if #1070 merges,
and a dismissal anchors to a source location.

Records what was measured on 2026-09-24 so the ruling can be made cold: the
shipped expression is linear here, 0.09 ms on a scheme plus 50 KB and 0.40 ms on
a scheme plus 240 KB, which reads as a false positive but is one runtime and not
a ruling.

Opens a "Security scanner triage" section rather than extending one of the
historical security phase sections, which record decisions made during those
phases.
…offset validation

- Map wire budget overflow to per-id JSON-RPC errors instead of dropping responses
- Emit agent_operation audit events for run_read_query and inspect_schema
- Delegate provider caching and lifecycle directly to db factory
- Enforce capability guard rejecting positive offset when pagination is unsupported
- Bubble up schema description errors with redacted messages
- Meter batch queries under the query rate limit bucket
- Translate all internal MCP comments and test titles to English
@socket-security

socket-security Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​modelcontextprotocol/​client@​2.1.0921008490100
Addednpm/​@​modelcontextprotocol/​server@​2.1.01001008595100

View full report

…d live

Both clients connected to a standalone build with a minted token and called the MCP tools on 2026-09-26: Claude Code in both protocol eras, OpenCode over the 2025-11-25 transport. OpenCode's section is written from its official MCP servers page.
@cevheri

cevheri commented Sep 26, 2026

Copy link
Copy Markdown
Member

@Roberton003 the new features are on this branch now. While this was open the spec moved to a stateless revision (2026-07-28), so the transport runs on the official SDK after all; that is why it is back after I asked you to drop it. The three tools still start from your code, and the description has the details. Please don't push to the branch while the review is open, so we don't overwrite each other. Thanks again for the groundwork.

@Roberton003

Copy link
Copy Markdown
Contributor Author

Thanks for the update and for adapting the transport to the 2026-07-28 stateless spec revision, @cevheri! The branch looks great. Leaving it completely untouched on my side so you can finish the review and merge. Glad to have contributed the foundation for MCP in LibreDB Studio.

…_URL is set

The Origin check runs before the token, and the canonical URL's host was in its allowlist, so a probe carrying that Origin got 401 when MCP was configured and 403 when it was not. The Origin list is now the localhost names and the ALLOWED_ORIGINS hosts; the Host list keeps the canonical host. MCP clients send no Origin, so only a browser-based client needs its origin in ALLOWED_ORIGINS.
@cevheri

cevheri commented Sep 26, 2026

Copy link
Copy Markdown
Member

Thanks for the update and for adapting the transport to the 2026-07-28 stateless spec revision, @cevheri! The branch looks great. Leaving it completely untouched on my side so you can finish the review and merge. Glad to have contributed the foundation for MCP in LibreDB Studio.

could I ask you to test the MCP feature locally(it does not matter which one you use: claude, codex, opencode, hermes, deepseek harness, pi, antigravity, kiro, cursor, vscode, copilot etc..)

The screen rendered snippets for five clients but not OpenCode, although
docs/MCP.md carries an OpenCode section verified live. The template now
builds that opencode.json, and the docs guard compares the section with it.
…ay to the MCP screen

A from-scratch run of the MCP guide stalled on facts the page did not carry:
npx resolves a relative SEED_CONFIG_PATH against the directory it is run
from; a Docker container without a volume on /app/data loses its generated
JWT_SECRET and with it every MCP token; a first start prints the generated
admin credentials once and keeps them in auth-bootstrap.json; an admin lands
on the dashboard and reaches MCP through Editor and the user menu; a SQLite
seed's database is a path on the machine running Studio.
…npx runs from

The launcher spawns server.js with its cwd in the payload cache, so
SEED_CONFIG_PATH=./seed-connections.yaml pointed into
~/.libredb-studio/<version>/payload and seed connections were skipped as
not found. Relative values of the filesystem path variables are now
resolved against process.cwd() before the spawn; absolute and empty values
and the URL paths BASE_PATH and NEXT_PUBLIC_MONACO_VS_PATH are left alone.
A guard test fails when .env.example gains an unclassified *_PATH, *_DIR
or *_FILE variable.
…cribes

The resolvePathVariables tests had landed between the docblock for the
spawned launcher tests and the describe it documents. They now sit before
the resolveBindAddress table, so that docblock again follows the table it
calls "above" and precedes "launcher startup URL".

Note for the release: an npx user who set a relative STORAGE_SQLITE_PATH or
WORKFLOW_LOCAL_DATA_DIR used to get that data under the payload cache
(~/.libredb-studio/<version>/payload/...). Since 978316f the path resolves
against the directory the command is run from, so move the old payload/data
there to keep it. Users who leave these variables unset are not affected.
…of failing

The editor opened every file read-write with create and PRAGMA journal_mode = WAL, so a read-only mount or a file owned by another user failed health, inventory and counts with 'attempt to write a readonly database'.
When access(W_OK) refuses the file or its directory, connect() now uses SQLite's read-only open without create or the WAL pragmas, logs the decision once at info with the path, and query() names SQLite's read-only refusal on a write.
A WAL-mode file in an unwritable directory still cannot be read, and connect() now says why and how to fix it.
A writable file keeps the old sequence.
# Conflicts:
#	charts/libredb-studio/Chart.yaml
#	docs/BACKLOG.md
#	operator/helm-charts/libredb-studio/Chart.yaml
#	src/lib/db/connection-fingerprint.ts
#	tests/unit/lib/db/connection-fingerprint.test.ts
#	tests/unit/seed/types.test.ts
…h Kafka

The mcp block changes chart templates, and 0.1.69 is now a released chart
version (tag libredb-studio-0.1.69), so re-publishing it would mutate the
released index and OCI digest (libredb#167). The Kafka changes line goes back to
its released text; only the MCP line is new in 0.1.70.
…cOS too

On macOS bun:sqlite links Apple's libsqlite3, whose close leaves -wal and
-shm in place, and with the -shm there the read-only open succeeds, so the
refusal test failed on macos-latest. The fixture now checkpoints and
removes both sidecars on every platform. Reproduced on Linux with
SQLITE_FCNTL_PERSIST_WAL: sidecars kept, the file opens; removed, it is
refused. The doc and docblock now say the refusal is for a WAL file with
no -shm beside it.
@Roberton003

Copy link
Copy Markdown
Contributor Author

Pulled the latest commits (including the new OpenCode settings snippet and launcher fixes) and tested the branch locally against a live instance serving /api/mcp with SQLite and DuckDB seed connections.

  • Clients verified:
    • Official SDK / Antigravity client (@modelcontextprotocol/client 2.1.0 over streamable HTTP, 2026-07-28 spec).
    • OpenCode 1.18.4 (opencode mcp list connected to the endpoint and verified cleanly).
    • Codex CLI 0.157.1 configuration verified via codex mcp add.
  • Tools verified:
    • list_connections: exposes opted-in connections (seed:shop and seed:analytics).
    • inspect_schema: inspects table structures with the untrusted-data security banner.
    • run_read_query: runs read statements on both SQLite (2 ms) and DuckDB (14 ms). Attempted mutations (DROP TABLE) are blocked with NON_READ_STATEMENT and audited as mcp_statement_refused.
  • Negative tests: Invalid bearer tokens yield 401 with WWW-Authenticate, and foreign Origin headers yield 403.
  • Automated test suite: Full pass on the latest head (536/536 tests passing).

The MCP feature works smoothly across all clients on my end!

…Lite's words

SQLite words the refusal of an unwritable WAL file by build: the library
bundled on Linux says "attempt to write a readonly database" for the file
alone, while Apple's libsqlite3 says "unable to open database file", and
so does Linux when a -wal is left beside the file with no -shm. The hint
matched only the first wording, so macOS, and Linux with a leftover -wal,
got SQLite's bare refusal. connect() now gives the reason whenever the
file's header says WAL mode (bytes 18 and 19 are 2), and a file this
process cannot read keeps SQLite's words alone.
…1 and 2 both read

opencode.ai now installs OpenCode 2, whose MCP page says servers go under
mcp.servers and that v2 does not place server names directly under mcp;
it also replaced enabled with disabled. The nested snippet connected on
OpenCode 1.18.31 and 1.18.32 and on 2.0.18, on Linux and Windows, with a
bogus-token control failing on each. docs/MCP.md adds that OpenCode 2's
shared service reads the token when it starts (opencode service restart
picks up a later one) and that its first mcp list can report no servers
while it starts.
@cevheri

cevheri commented Sep 26, 2026

Copy link
Copy Markdown
Member

Thank you for testing it, @Roberton003. Three clients, two engines, and the refusal, 401 and 403 paths checked is a thorough pass, and DuckDB was not in my own live runs, so that fills a real gap.

One change landed after your run: the OpenCode snippet now sits under mcp.servers, because OpenCode 2, which opencode.ai now installs, does not read servers placed directly under mcp. The new shape also connected on OpenCode 1.18.31, and your current config keeps working on OpenCode 1.

@cevheri

cevheri commented Sep 26, 2026

Copy link
Copy Markdown
Member

A short way to try MCP with OpenCode. Open the section for your system; each one is complete on its own. The full guide, with the other clients, is docs/MCP.md. npx and the Docker image have the MCP server from the first release that includes this PR.

macOS

Before you start, you need Node.js 24 or newer for npx, or Docker Desktop, and OpenCode with a model you can use. sqlite3 is built into macOS.

Open Terminal, make an empty folder, and run everything below from it.

1. Make a small SQLite file.

sqlite3 shop.db "CREATE TABLE orders (id INTEGER PRIMARY KEY, customer TEXT, total REAL, status TEXT); INSERT INTO orders (customer, total, status) VALUES ('ada', 10, 'paid'), ('bob', 20, 'open'), ('cem', 30, 'shipped');"

2. Opt it in. MCP clients only see seed connections marked mcp: true. Save this as seed-connections.yaml in the same folder, with database set to the full path of shop.db (pwd prints the folder). With Docker, keep /data/shop.db as it is.

version: "1"
connections:
  - id: shop
    name: Shop
    type: sqlite
    database: /Users/you/demo/shop.db
    roles: ["*"]
    mcp: true

3. Start Studio with MCP on, with npx or with Docker, and keep this window open while it runs.

SEED_CONFIG_PATH=./seed-connections.yaml LIBREDB_MCP_ENABLED=true LIBREDB_MCP_TOKEN_LABEL=studio-mcp-1 npx @libredb/studio

With Docker (the data volume keeps your tokens valid when the container is recreated):

docker run -p 3000:3000 -v libredb-data:/app/data \
  -v "$PWD/seed-connections.yaml:/app/config/seed-connections.yaml:ro" \
  -v "$PWD/shop.db:/data/shop.db:ro" \
  -e SEED_CONFIG_PATH=/app/config/seed-connections.yaml \
  -e LIBREDB_MCP_ENABLED=true \
  -e LIBREDB_MCP_URL=http://127.0.0.1:3000/api/mcp \
  -e LIBREDB_MCP_TOKEN_LABEL=studio-mcp-1 \
  ghcr.io/libredb/libredb-studio:latest

4. Get a token. Open http://localhost:3000 and sign in as admin@libredb.org with the password Studio printed in that window on its first start. Choose Editor, open the user menu at the top right, choose MCP, click Create token and copy it. It is shown only once.

5. Connect OpenCode. Open a second Terminal window in the same folder and save this as opencode.json (the MCP screen shows the same snippet):

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "servers": {
      "libredb": {
        "type": "remote",
        "url": "http://127.0.0.1:3000/api/mcp",
        "oauth": false,
        "headers": { "Authorization": "Bearer {env:LIBREDB_MCP_TOKEN}" }
      }
    }
  }
}

Then put the token in that window and check the connection:

export LIBREDB_MCP_TOKEN=<your token>
opencode mcp list

It should show libredb connected. OpenCode 2 reads the token when its background service starts, so set it before your first opencode command, or run opencode service restart after; for its first few seconds, opencode mcp list can say "No MCP servers configured": run it again.

6. Ask it something. If OpenCode has no default model, or its answer does not use libredb, choose one with --model <provider/model>, for example --model opencode/big-pickle:

opencode run "Using libredb, list my connections, show the tables of seed:shop, then count its orders per status."

It answers with list_connections, inspect_schema and run_read_query: one paid, one open and one shipped order. Every call is read-only, capped at 32 KiB and audited.

Linux

Before you start, you need Node.js 24 or newer for npx, or Docker, and OpenCode with a model you can use. For sqlite3, run sudo apt install sqlite3, or install your distribution's package.

Open a terminal, make an empty folder, and run everything below from it.

1. Make a small SQLite file.

sqlite3 shop.db "CREATE TABLE orders (id INTEGER PRIMARY KEY, customer TEXT, total REAL, status TEXT); INSERT INTO orders (customer, total, status) VALUES ('ada', 10, 'paid'), ('bob', 20, 'open'), ('cem', 30, 'shipped');"

2. Opt it in. MCP clients only see seed connections marked mcp: true. Save this as seed-connections.yaml in the same folder, with database set to the full path of shop.db (pwd prints the folder). With Docker, keep /data/shop.db as it is.

version: "1"
connections:
  - id: shop
    name: Shop
    type: sqlite
    database: /home/you/demo/shop.db
    roles: ["*"]
    mcp: true

3. Start Studio with MCP on, with npx or with Docker, and keep this terminal open while it runs.

SEED_CONFIG_PATH=./seed-connections.yaml LIBREDB_MCP_ENABLED=true LIBREDB_MCP_TOKEN_LABEL=studio-mcp-1 npx @libredb/studio

With Docker (the data volume keeps your tokens valid when the container is recreated):

docker run -p 3000:3000 -v libredb-data:/app/data \
  -v "$PWD/seed-connections.yaml:/app/config/seed-connections.yaml:ro" \
  -v "$PWD/shop.db:/data/shop.db:ro" \
  -e SEED_CONFIG_PATH=/app/config/seed-connections.yaml \
  -e LIBREDB_MCP_ENABLED=true \
  -e LIBREDB_MCP_URL=http://127.0.0.1:3000/api/mcp \
  -e LIBREDB_MCP_TOKEN_LABEL=studio-mcp-1 \
  ghcr.io/libredb/libredb-studio:latest

4. Get a token. Open http://localhost:3000 and sign in as admin@libredb.org with the password Studio printed in that terminal on its first start. Choose Editor, open the user menu at the top right, choose MCP, click Create token and copy it. It is shown only once.

5. Connect OpenCode. Open a second terminal in the same folder and save this as opencode.json (the MCP screen shows the same snippet):

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "servers": {
      "libredb": {
        "type": "remote",
        "url": "http://127.0.0.1:3000/api/mcp",
        "oauth": false,
        "headers": { "Authorization": "Bearer {env:LIBREDB_MCP_TOKEN}" }
      }
    }
  }
}

Then put the token in that terminal and check the connection:

export LIBREDB_MCP_TOKEN=<your token>
opencode mcp list

It should show libredb connected. OpenCode 2 reads the token when its background service starts, so set it before your first opencode command, or run opencode service restart after; for its first few seconds, opencode mcp list can say "No MCP servers configured": run it again.

6. Ask it something. If OpenCode has no default model, or its answer does not use libredb, choose one with --model <provider/model>, for example --model opencode/big-pickle:

opencode run "Using libredb, list my connections, show the tables of seed:shop, then count its orders per status."

It answers with list_connections, inspect_schema and run_read_query: one paid, one open and one shipped order. Every call is read-only, capped at 32 KiB and audited.

Windows (PowerShell)

Before you start, you need Node.js 24 or newer for npx, or Docker Desktop, and OpenCode with a model you can use. For sqlite3, run winget install SQLite.SQLite, then close PowerShell and open a new window, so it finds the new command.

Open PowerShell, make an empty folder, and run everything below from it. If npx or opencode stops with "running scripts is disabled on this system", allow local scripts once, then run the command again:

Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

1. Make a small SQLite file.

sqlite3 shop.db "CREATE TABLE orders (id INTEGER PRIMARY KEY, customer TEXT, total REAL, status TEXT); INSERT INTO orders (customer, total, status) VALUES ('ada', 10, 'paid'), ('bob', 20, 'open'), ('cem', 30, 'shipped');"

2. Opt it in. MCP clients only see seed connections marked mcp: true. Save this as seed-connections.yaml in the same folder, for example with Notepad, with database set to the full path of shop.db written with forward slashes (pwd prints the folder). With Docker, keep /data/shop.db as it is.

version: "1"
connections:
  - id: shop
    name: Shop
    type: sqlite
    database: C:/Users/you/demo/shop.db
    roles: ["*"]
    mcp: true

3. Start Studio with MCP on, with npx or with Docker, and keep this window open while it runs.

$env:SEED_CONFIG_PATH = "./seed-connections.yaml"
$env:LIBREDB_MCP_ENABLED = "true"
$env:LIBREDB_MCP_TOKEN_LABEL = "studio-mcp-1"
npx @libredb/studio

With Docker Desktop (the data volume keeps your tokens valid when the container is recreated):

docker run -p 3000:3000 -v libredb-data:/app/data `
  -v "${PWD}/seed-connections.yaml:/app/config/seed-connections.yaml:ro" `
  -v "${PWD}/shop.db:/data/shop.db:ro" `
  -e SEED_CONFIG_PATH=/app/config/seed-connections.yaml `
  -e LIBREDB_MCP_ENABLED=true `
  -e LIBREDB_MCP_URL=http://127.0.0.1:3000/api/mcp `
  -e LIBREDB_MCP_TOKEN_LABEL=studio-mcp-1 `
  ghcr.io/libredb/libredb-studio:latest

4. Get a token. Open http://localhost:3000 and sign in as admin@libredb.org with the password Studio printed in that window on its first start. Choose Editor, open the user menu at the top right, choose MCP, click Create token and copy it. It is shown only once.

5. Connect OpenCode. Open a second PowerShell window in the same folder and save this as opencode.json (the MCP screen shows the same snippet):

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "servers": {
      "libredb": {
        "type": "remote",
        "url": "http://127.0.0.1:3000/api/mcp",
        "oauth": false,
        "headers": { "Authorization": "Bearer {env:LIBREDB_MCP_TOKEN}" }
      }
    }
  }
}

Then put the token in that window and check the connection:

$env:LIBREDB_MCP_TOKEN = "<your token>"
opencode mcp list

It should show libredb connected. OpenCode 2 reads the token when its background service starts, so set it before your first opencode command, or run opencode service restart after; for its first few seconds, opencode mcp list can say "No MCP servers configured": run it again.

6. Ask it something. If OpenCode has no default model, or its answer does not use libredb, choose one with --model <provider/model>, for example --model opencode/big-pickle:

opencode run "Using libredb, list my connections, show the tables of seed:shop, then count its orders per status."

It answers with list_connections, inspect_schema and run_read_query: one paid, one open and one shipped order. Every call is read-only, capped at 32 KiB and audited.

@cevheri
cevheri merged commit b384395 into libredb:main Sep 26, 2026
23 of 24 checks passed
@amirkiarafiei

Copy link
Copy Markdown

This was a big update and IMO the one needed to make this app agent-ready. Good job everyone and thanks !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai Artificial intelligence core-capabilities mcp security Supply-chain, auth, or hardening work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Support Model Context Protocol (MCP) to Integrate with Cursor, Claude Code and Other AI Assistants

4 participants