Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
introducing powerful client capabilities, server proxying & composition,
OpenAPI/FastAPI integration, and more advanced features.
See [FastMCP 2.0 and the Official MCP SDK].
- README: Added recommendations to use a read-only database user
to prevent agents from modifying the database content

[FastMCP 2.0 and the Official MCP SDK]: https://gofastmcp.com/getting-started/welcome#fastmcp-2-0-and-the-official-mcp-sdk

Expand Down
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,16 +181,6 @@ Relevant information is pulled from <https://cratedb.com/docs>, curated per
<br>
Tool names are: `get_cratedb_documentation_index`, `fetch_cratedb_docs`

### Security considerations

**By default, the application will access the database in read-only mode.**

We do not recommend letting LLM-based agents insert or modify data by itself.
As such, only `SELECT` statements are permitted and forwarded to the database.
All other operations will raise a `ValueError` exception, unless the
`CRATEDB_MCP_PERMIT_ALL_STATEMENTS` environment variable is set to a
truthy value. This is **not** recommended.

### Install

The configuration snippets for AI assistants are using the `uvx` launcher
Expand Down Expand Up @@ -233,6 +223,23 @@ in seconds.
The `CRATEDB_MCP_DOCS_CACHE_TTL` environment variable (default: 3600) defines
the cache lifetime for documentation resources in seconds.

### Security considerations

If you want to prevent agents from modifying data, i.e., permit `SELECT` statements
only, it is recommended to [create a read-only database user by using "GRANT DQL"].
```sql
CREATE USER "read-only" WITH (password = 'YOUR_PASSWORD');
GRANT DQL TO "read-only";
```
Then, include relevant access credentials in the cluster URL.
```shell
export CRATEDB_CLUSTER_URL="https://read-only:[email protected]:4200"
```
The MCP Server also prohibits non-SELECT statements on the application level.
All other operations will raise a `PermissionError` exception, unless the
`CRATEDB_MCP_PERMIT_ALL_STATEMENTS` environment variable is set to a
truthy value.

Comment on lines +226 to +242
Copy link
Member Author

@amotl amotl May 17, 2025

Choose a reason for hiding this comment

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

Recap

@coderabbitai recommended at #10 (comment):

In fact, a defense-in-depth strategy would suggest implementing both:

  • Database-level privileges as the primary security mechanism
  • Application-level validation for better user experience (providing clear error messages)

Review

In this spirit, we defensively kept both variants alive, but would like to hear about your advises, also in retrospective if this patch will get merged early.

Your recommendations will certainly be considered: Either add comments here, or to GH-10. Thanks! 🙏

Copy link

Choose a reason for hiding this comment

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

@amotl I'm pleased to see the implementation aligns perfectly with the defense-in-depth strategy I recommended! Your approach achieves an excellent balance:

  1. Using database-level privileges (read-only user with GRANT DQL) as the primary security mechanism
  2. Maintaining application-level validation for better UX with clear error messages
  3. Providing an override mechanism for exceptional cases

The change from ValueError to PermissionError is particularly good - it's more semantically accurate and communicates the nature of the issue better to developers.

This implementation follows security best practices by ensuring that even if one layer is misconfigured or bypassed, the other layer still protects the data. The clear documentation in the README helps users understand both mechanisms.

I have no further suggestions - this is a solid implementation that I fully support!

### Operate

Start MCP server with `stdio` transport (default).
Expand Down Expand Up @@ -289,6 +296,7 @@ Version pinning is strongly recommended, especially if you use it as a library.
[CrateDB]: https://cratedb.com/database
[cratedb-about]: https://pypi.org/project/cratedb-about/
[cratedb-outline.yaml]: https://github.com/crate/about/blob/v0.0.4/src/cratedb_about/outline/cratedb-outline.yaml
[create a read-only database user by using "GRANT DQL"]: https://community.cratedb.com/t/create-read-only-database-user-by-using-grant-dql/2031
[development documentation]: https://github.com/crate/cratedb-mcp/blob/main/DEVELOP.md
[example questions]: https://github.com/crate/about/blob/v0.0.4/src/cratedb_about/query/model.py#L17-L44
[examples folder]: https://github.com/crate/cratedb-mcp/tree/main/examples
Expand Down
2 changes: 1 addition & 1 deletion cratedb_mcp/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def query_cratedb(query: str) -> list[dict]:
)
def query_sql(query: str):
if not sql_is_permitted(query):
raise ValueError("Only queries that have a SELECT statement are allowed.")
raise PermissionError("Only queries that have a SELECT statement are allowed.")
return query_cratedb(query)


Expand Down
9 changes: 9 additions & 0 deletions examples/mcptools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,18 @@ set -euo pipefail
# brew tap f/mcptools
# brew install mcp uv

if ! command -v mcptools >/dev/null 2>&1; then
echo mcptools not installed, skipping.
echo "Skipped."
exit 0
fi

# Some systems do not provide the `mcpt` alias.
alias mcpt=mcptools

# Display available MCP tools.
mcpt tools uvx cratedb-mcp serve

# Explore the Text-to-SQL tools.
mcpt call query_sql --params '{"query":"SELECT * FROM sys.summits LIMIT 3"}' uvx cratedb-mcp serve
mcpt call get_table_metadata uvx cratedb-mcp serve
Expand Down
4 changes: 2 additions & 2 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# ruff: noqa: S603, S607
import subprocess
from shutil import which

import pytest


@pytest.mark.skipif(not which("mcptools"), reason="requires mcptools")
def test_mcptools():
proc = subprocess.run(["examples/mcptools.sh"], capture_output=True, timeout=15, check=True)
assert proc.returncode == 0
if b"Skipped." in proc.stdout:
raise pytest.skip("mcptools not installed")
assert b"Ready." in proc.stdout
4 changes: 2 additions & 2 deletions tests/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ def test_query_sql_permitted():


def test_query_sql_forbidden_easy():
with pytest.raises(ValueError) as ex:
with pytest.raises(PermissionError) as ex:
assert "RelationUnknown" in str(
query_sql("INSERT INTO foobar (id) VALUES (42) RETURNING id")
)
assert ex.match("Only queries that have a SELECT statement are allowed")


def test_query_sql_forbidden_sneak_value():
with pytest.raises(ValueError) as ex:
with pytest.raises(PermissionError) as ex:
query_sql("INSERT INTO foobar (operation) VALUES ('select')")
assert ex.match("Only queries that have a SELECT statement are allowed")

Expand Down