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
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased
- MCP: Fixed defunct `get_cratedb_documentation_index` tool
- CLI: Added CLI options for user-defined prompts: `--instructions` and `--conventions`,
both accepting file paths or URLs.
- CLI: Added subcommand `cratedb-mcp show-prompt` to display the system prompt.

## v0.0.4 - 2025-07-21
- Parameters: Added CLI option `--host` and environment variable `CRATEDB_MCP_HOST`
Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,46 @@ All other operations will raise a `PermissionError` exception, unless the
`CRATEDB_MCP_PERMIT_ALL_STATEMENTS` environment variable is set to a
truthy value.

### System prompt customizations

The CrateDB MCP server allows users to adjust the system prompt by either
redefining the baseline instructions or extending them with custom conventions.
Additional conventions can capture domain-specific details—such as information
required for particular ER data models —- or any other guidelines you develop
over time.

If you want to **add** custom conventions to the system prompt,
use the `--conventions` option.
```shell
cratedb-mcp serve --conventions="conventions-custom.md"
```

If you want to **replace** the standard built-in instructions prompt completely,
use the `--instructions` option.
```shell
cratedb-mcp serve --instructions="instructions-custom.md"
```

Alternatively, use the `CRATEDB_MCP_INSTRUCTIONS` and `CRATEDB_MCP_CONVENTIONS`
environment variables instead of the CLI options.

To retrieve the standard system prompt, use the `show-prompt` subcommand. By
redirecting the output to a file, you can subsequently edit its contents and
reuse it with the MCP server using the command outlined above.
```shell
cratedb-mcp show-prompt > instructions-custom.md
```

Instruction and convention fragments can be loaded from the following sources:

- HTTP(S) URLs
- Local file paths
- Standard input (when fragment is "-")
- Direct string content

Because LLMs understand Markdown well, you should also use it for writing
personal instructions or conventions.

### Operate standalone

Start MCP server with `stdio` transport (default).
Expand Down
79 changes: 3 additions & 76 deletions cratedb_mcp/__main__.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,4 @@
import importlib.resources
from cratedb_mcp.core import CrateDbMcp

from cratedb_about.instruction import GeneralInstructions
from fastmcp import FastMCP
from fastmcp.tools import Tool

from . import __appname__
from .tool import (
fetch_cratedb_docs,
get_cluster_health,
get_cratedb_documentation_index,
get_table_metadata,
query_sql,
)

instructions_general = GeneralInstructions().render()
instructions_mcp = (importlib.resources.files("cratedb_mcp") / "instructions.md").read_text()

# Create FastMCP application object.
mcp: FastMCP = FastMCP(
name=__appname__,
instructions=instructions_mcp + instructions_general,
)


# ------------------------------------------
# Text-to-SQL
# ------------------------------------------
mcp.add_tool(
Tool.from_function(
fn=get_table_metadata,
description="Return column schema and metadata for all tables stored in CrateDB. "
"Use it to inquire entities you don't know about.",
tags={"text-to-sql"},
)
)
mcp.add_tool(
Tool.from_function(
fn=query_sql,
description="Send an SQL query to CrateDB and return results. "
"Only 'SELECT' queries are allowed.",
tags={"text-to-sql"},
)
)


# ------------------------------------------
# Documentation inquiry
# ------------------------------------------
mcp.add_tool(
Tool.from_function(
fn=get_cratedb_documentation_index,
description="Get an index of CrateDB documentation links for fetching. "
"Should download docs before answering questions. "
"Has documentation title, description, and link.",
tags={"documentation"},
)
)
mcp.add_tool(
Tool.from_function(
fn=fetch_cratedb_docs,
description="Download individual CrateDB documentation pages by link.",
tags={"documentation"},
)
)


# ------------------------------------------
# Health / Status
# ------------------------------------------
mcp.add_tool(
Tool.from_function(
fn=get_cluster_health,
description="Return the health of the CrateDB cluster.",
tags={"health", "monitoring", "status"},
)
)
# Is that a standard entrypoint that should be kept alive?
mcp = CrateDbMcp().mcp
36 changes: 33 additions & 3 deletions cratedb_mcp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import click
from pueblo.util.cli import boot_click

from cratedb_mcp.__main__ import mcp
from cratedb_mcp.core import CrateDbMcp
from cratedb_mcp.prompt import InstructionsPrompt

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -54,9 +55,29 @@ def cli(ctx: click.Context) -> None:
required=False,
help="The URL path to serve on (for sse, http)",
)
@click.option(
"--instructions",
envvar="CRATEDB_MCP_INSTRUCTIONS",
type=str,
required=False,
help="If you want to change the default instructions prompt, use this option",
)
@click.option(
"--conventions",
envvar="CRATEDB_MCP_CONVENTIONS",
type=str,
required=False,
help="If you want to add custom conventions to the prompt, use this option",
)
@click.pass_context
def serve(
ctx: click.Context, transport: str, host: str, port: int, path: t.Optional[str] = None
ctx: click.Context,
transport: str,
host: str,
port: int,
path: t.Optional[str],
instructions: t.Optional[str],
conventions: t.Optional[str],
) -> None:
"""
Start MCP server.
Expand All @@ -69,4 +90,13 @@ def serve(
"port": port,
"path": path,
}
mcp.run(transport=t.cast(transport_types, transport), **transport_kwargs) # type: ignore[arg-type]
mcp_cratedb = CrateDbMcp(instructions=instructions, conventions=conventions)
mcp_cratedb.mcp.run(transport=t.cast(transport_types, transport), **transport_kwargs) # type: ignore[arg-type]


@cli.command()
def show_prompt() -> None:
"""
Display the system prompt.
"""
print(InstructionsPrompt().render()) # noqa: T201
90 changes: 90 additions & 0 deletions cratedb_mcp/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from typing import Optional

from fastmcp import FastMCP
from fastmcp.tools import Tool

from cratedb_mcp.prompt import InstructionsPrompt

from . import __appname__, __version__
from .tool import (
fetch_cratedb_docs,
get_cluster_health,
get_cratedb_documentation_index,
get_table_metadata,
query_sql,
)


class CrateDbMcp:
"""
Small wrapper around the FastMCP API to provide instructions prompt at runtime.
"""

def __init__(
self,
mcp: Optional[FastMCP] = None,
instructions: Optional[str] = None,
conventions: Optional[str] = None,
) -> None:
prompt = InstructionsPrompt(instructions=instructions, conventions=conventions)
self.mcp = mcp or FastMCP(
name=__appname__,
version=__version__,
instructions=prompt.render(),
)
self.add_tools()

def add_tools(self):
"""Register all CrateDB MCP tools with the FastMCP instance."""
# ------------------------------------------
# Text-to-SQL
# ------------------------------------------
self.mcp.add_tool(
Tool.from_function(
fn=get_table_metadata,
description="Return column schema and metadata for all tables stored in CrateDB. "
"Use it to inquire entities you don't know about.",
tags={"text-to-sql"},
)
)
self.mcp.add_tool(
Tool.from_function(
fn=query_sql,
description="Send an SQL query to CrateDB and return results. "
"Only 'SELECT' queries are allowed.",
tags={"text-to-sql"},
)
)

# ------------------------------------------
# Documentation inquiry
# ------------------------------------------
self.mcp.add_tool(
Tool.from_function(
fn=get_cratedb_documentation_index,
description="Get an index of CrateDB documentation links for fetching. "
"Should download docs before answering questions. "
"Has documentation title, description, and link.",
tags={"documentation"},
)
)
self.mcp.add_tool(
Tool.from_function(
fn=fetch_cratedb_docs,
description="Download individual CrateDB documentation pages by link.",
tags={"documentation"},
)
)

# ------------------------------------------
# Health / Status
# ------------------------------------------
self.mcp.add_tool(
Tool.from_function(
fn=get_cluster_health,
description="Return the health of the CrateDB cluster.",
tags={"health", "monitoring", "status"},
)
)

return self
68 changes: 68 additions & 0 deletions cratedb_mcp/prompt/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import importlib.resources
import sys
from pathlib import Path
from typing import List, Optional

import httpx
from cratedb_about.prompt import GeneralInstructions


class InstructionsPrompt:
"""
Bundle instructions how to use MCP tools with general instructions how to work with CrateDB.

- MCP: https://github.com/crate/cratedb-examples/blob/7f1bc0f94d/topic/chatbot/table-augmented-generation/aws/cratedb_tag_inline_agent.ipynb?short_path=00988ad#L776-L794
- General: https://github.com/crate/about
"""

def __init__(self, instructions: Optional[str] = None, conventions: Optional[str] = None):
fragments: List[str] = []
if instructions:
fragments.append(self.load_fragment(instructions))
else:
instructions_general = GeneralInstructions().render()
mcp_instructions_file = (
importlib.resources.files("cratedb_mcp.prompt") / "instructions.md"
)
if not mcp_instructions_file.is_file(): # pragma: no cover
raise FileNotFoundError(f"MCP instructions file not found: {mcp_instructions_file}")
instructions_mcp = mcp_instructions_file.read_text()
fragments.append(instructions_general)
fragments.append(instructions_mcp)
if conventions:
fragments.append(self.load_fragment(conventions))
self.fragments = fragments

def render(self) -> str:
return "\n\n".join(map(str.strip, self.fragments))

@staticmethod
def load_fragment(fragment: str) -> str:
"""
Load instruction fragment from various sources.

Supports loading from:
- HTTP(S) URLs
- Local file paths
- Standard input (when fragment is "-")
- Direct string content

That's a miniature variant of a "fragment" concept,
adapted from `llm` [1] written by Simon Willison.

[1] https://github.com/simonw/llm
"""
try:
if fragment.startswith("http://") or fragment.startswith("https://"):
with httpx.Client(follow_redirects=True, max_redirects=3, timeout=5.0) as client:
response = client.get(fragment)
response.raise_for_status()
return response.text
if fragment == "-":
return sys.stdin.read()
path = Path(fragment)
if path.exists():
return path.read_text(encoding="utf-8")
return fragment
except (httpx.HTTPError, OSError, UnicodeDecodeError) as e:
raise ValueError(f"Failed to load fragment '{fragment}': {e}") from e
File renamed without changes.
9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ dependencies = [
"attrs",
"cachetools<7",
"click<9",
"cratedb-about==0.0.6",
"cratedb-about==0.0.7",
"fastmcp>=2.7,<2.11",
"hishel<0.2",
"pueblo==0.0.11",
Expand All @@ -94,11 +94,10 @@ optional-dependencies.test = [

scripts.cratedb-mcp = "cratedb_mcp.cli:cli"

[tool.setuptools]
include-package-data = true

[tool.setuptools.package-data]
cratedb_mcp = [ "*.md" ]
cratedb_mcp = [
"**/*.md",
]

[tool.setuptools.packages.find]
namespaces = false
Expand Down
Loading