Skip to content

Commit 535548c

Browse files
committed
Prompt: Enable user-defined instructions
- Both options `--instructions` and `--conventions` accept file paths, URLs, or stdin. - Add subcommand `cratedb-mcp show-prompt` to display the system prompt. With both features, you can easily customize the built-in system prompt and iterate on it.
1 parent 25a4cd7 commit 535548c

File tree

11 files changed

+345
-102
lines changed

11 files changed

+345
-102
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

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

69
## v0.0.4 - 2025-07-21
710
- Parameters: Added CLI option `--host` and environment variable `CRATEDB_MCP_HOST`

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,46 @@ All other operations will raise a `PermissionError` exception, unless the
309309
`CRATEDB_MCP_PERMIT_ALL_STATEMENTS` environment variable is set to a
310310
truthy value.
311311

312+
### System prompt customizations
313+
314+
The CrateDB MCP server allows users to adjust the system prompt by either
315+
redefining the baseline instructions or extending them with custom conventions.
316+
Additional conventions can capture domain-specific details—such as information
317+
required for particular ER data models —- or any other guidelines you develop
318+
over time.
319+
320+
If you want to **add** custom conventions to the system prompt,
321+
use the `--conventions` option.
322+
```shell
323+
cratedb-mcp serve --conventions="conventions-custom.md"
324+
```
325+
326+
If you want to **replace** the standard built-in instructions prompt completely,
327+
use the `--instructions` option.
328+
```shell
329+
cratedb-mcp serve --instructions="instructions-custom.md"
330+
```
331+
332+
Alternatively, use the `CRATEDB_MCP_INSTRUCTIONS` and `CRATEDB_MCP_CONVENTIONS`
333+
environment variables instead of the CLI options.
334+
335+
To retrieve the standard system prompt, use the `show-prompt` subcommand. By
336+
redirecting the output to a file, you can subsequently edit its contents and
337+
reuse it with the MCP server using the command outlined above.
338+
```shell
339+
cratedb-mcp show-prompt > instructions-custom.md
340+
```
341+
342+
Instruction and convention fragments can be loaded from the following sources:
343+
344+
- HTTP(S) URLs
345+
- Local file paths
346+
- Standard input (when fragment is "-")
347+
- Direct string content
348+
349+
Because LLMs understand Markdown well, you should also use it for writing
350+
personal instructions or conventions.
351+
312352
### Operate standalone
313353

314354
Start MCP server with `stdio` transport (default).

cratedb_mcp/__main__.py

Lines changed: 3 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,4 @@
1-
import importlib.resources
1+
from cratedb_mcp.core import CrateDbMcp
22

3-
from cratedb_about.instruction import GeneralInstructions
4-
from fastmcp import FastMCP
5-
from fastmcp.tools import Tool
6-
7-
from . import __appname__
8-
from .tool import (
9-
fetch_cratedb_docs,
10-
get_cluster_health,
11-
get_cratedb_documentation_index,
12-
get_table_metadata,
13-
query_sql,
14-
)
15-
16-
instructions_general = GeneralInstructions().render()
17-
instructions_mcp = (importlib.resources.files("cratedb_mcp") / "instructions.md").read_text()
18-
19-
# Create FastMCP application object.
20-
mcp: FastMCP = FastMCP(
21-
name=__appname__,
22-
instructions=instructions_mcp + instructions_general,
23-
)
24-
25-
26-
# ------------------------------------------
27-
# Text-to-SQL
28-
# ------------------------------------------
29-
mcp.add_tool(
30-
Tool.from_function(
31-
fn=get_table_metadata,
32-
description="Return column schema and metadata for all tables stored in CrateDB. "
33-
"Use it to inquire entities you don't know about.",
34-
tags={"text-to-sql"},
35-
)
36-
)
37-
mcp.add_tool(
38-
Tool.from_function(
39-
fn=query_sql,
40-
description="Send an SQL query to CrateDB and return results. "
41-
"Only 'SELECT' queries are allowed.",
42-
tags={"text-to-sql"},
43-
)
44-
)
45-
46-
47-
# ------------------------------------------
48-
# Documentation inquiry
49-
# ------------------------------------------
50-
mcp.add_tool(
51-
Tool.from_function(
52-
fn=get_cratedb_documentation_index,
53-
description="Get an index of CrateDB documentation links for fetching. "
54-
"Should download docs before answering questions. "
55-
"Has documentation title, description, and link.",
56-
tags={"documentation"},
57-
)
58-
)
59-
mcp.add_tool(
60-
Tool.from_function(
61-
fn=fetch_cratedb_docs,
62-
description="Download individual CrateDB documentation pages by link.",
63-
tags={"documentation"},
64-
)
65-
)
66-
67-
68-
# ------------------------------------------
69-
# Health / Status
70-
# ------------------------------------------
71-
mcp.add_tool(
72-
Tool.from_function(
73-
fn=get_cluster_health,
74-
description="Return the health of the CrateDB cluster.",
75-
tags={"health", "monitoring", "status"},
76-
)
77-
)
3+
# Is that a standard entrypoint that should be kept alive?
4+
mcp = CrateDbMcp().mcp

cratedb_mcp/cli.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import click
55
from pueblo.util.cli import boot_click
66

7-
from cratedb_mcp.__main__ import mcp
7+
from cratedb_mcp.core import CrateDbMcp
8+
from cratedb_mcp.prompt import InstructionsPrompt
89

910
logger = logging.getLogger(__name__)
1011

@@ -54,9 +55,29 @@ def cli(ctx: click.Context) -> None:
5455
required=False,
5556
help="The URL path to serve on (for sse, http)",
5657
)
58+
@click.option(
59+
"--instructions",
60+
envvar="CRATEDB_MCP_INSTRUCTIONS",
61+
type=str,
62+
required=False,
63+
help="If you want to change the default instructions prompt, use this option",
64+
)
65+
@click.option(
66+
"--conventions",
67+
envvar="CRATEDB_MCP_CONVENTIONS",
68+
type=str,
69+
required=False,
70+
help="If you want to add custom conventions to the prompt, use this option",
71+
)
5772
@click.pass_context
5873
def serve(
59-
ctx: click.Context, transport: str, host: str, port: int, path: t.Optional[str] = None
74+
ctx: click.Context,
75+
transport: str,
76+
host: str,
77+
port: int,
78+
path: t.Optional[str],
79+
instructions: t.Optional[str],
80+
conventions: t.Optional[str],
6081
) -> None:
6182
"""
6283
Start MCP server.
@@ -69,4 +90,13 @@ def serve(
6990
"port": port,
7091
"path": path,
7192
}
72-
mcp.run(transport=t.cast(transport_types, transport), **transport_kwargs) # type: ignore[arg-type]
93+
mcp_cratedb = CrateDbMcp(instructions=instructions, conventions=conventions)
94+
mcp_cratedb.mcp.run(transport=t.cast(transport_types, transport), **transport_kwargs) # type: ignore[arg-type]
95+
96+
97+
@cli.command()
98+
def show_prompt() -> None:
99+
"""
100+
Display the system prompt.
101+
"""
102+
print(InstructionsPrompt().render()) # noqa: T201

cratedb_mcp/core.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from typing import Optional
2+
3+
from fastmcp import FastMCP
4+
from fastmcp.tools import Tool
5+
6+
from cratedb_mcp.prompt import InstructionsPrompt
7+
8+
from . import __appname__, __version__
9+
from .tool import (
10+
fetch_cratedb_docs,
11+
get_cluster_health,
12+
get_cratedb_documentation_index,
13+
get_table_metadata,
14+
query_sql,
15+
)
16+
17+
18+
class CrateDbMcp:
19+
"""
20+
Small wrapper around the FastMCP API to provide instructions prompt at runtime.
21+
"""
22+
23+
def __init__(
24+
self,
25+
mcp: Optional[FastMCP] = None,
26+
instructions: Optional[str] = None,
27+
conventions: Optional[str] = None,
28+
) -> None:
29+
prompt = InstructionsPrompt(instructions=instructions, conventions=conventions)
30+
self.mcp = mcp or FastMCP(
31+
name=__appname__,
32+
version=__version__,
33+
instructions=prompt.render(),
34+
)
35+
self.add_tools()
36+
37+
def add_tools(self):
38+
"""Register all CrateDB MCP tools with the FastMCP instance."""
39+
# ------------------------------------------
40+
# Text-to-SQL
41+
# ------------------------------------------
42+
self.mcp.add_tool(
43+
Tool.from_function(
44+
fn=get_table_metadata,
45+
description="Return column schema and metadata for all tables stored in CrateDB. "
46+
"Use it to inquire entities you don't know about.",
47+
tags={"text-to-sql"},
48+
)
49+
)
50+
self.mcp.add_tool(
51+
Tool.from_function(
52+
fn=query_sql,
53+
description="Send an SQL query to CrateDB and return results. "
54+
"Only 'SELECT' queries are allowed.",
55+
tags={"text-to-sql"},
56+
)
57+
)
58+
59+
# ------------------------------------------
60+
# Documentation inquiry
61+
# ------------------------------------------
62+
self.mcp.add_tool(
63+
Tool.from_function(
64+
fn=get_cratedb_documentation_index,
65+
description="Get an index of CrateDB documentation links for fetching. "
66+
"Should download docs before answering questions. "
67+
"Has documentation title, description, and link.",
68+
tags={"documentation"},
69+
)
70+
)
71+
self.mcp.add_tool(
72+
Tool.from_function(
73+
fn=fetch_cratedb_docs,
74+
description="Download individual CrateDB documentation pages by link.",
75+
tags={"documentation"},
76+
)
77+
)
78+
79+
# ------------------------------------------
80+
# Health / Status
81+
# ------------------------------------------
82+
self.mcp.add_tool(
83+
Tool.from_function(
84+
fn=get_cluster_health,
85+
description="Return the health of the CrateDB cluster.",
86+
tags={"health", "monitoring", "status"},
87+
)
88+
)
89+
90+
return self

cratedb_mcp/prompt/__init__.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import importlib.resources
2+
import sys
3+
from pathlib import Path
4+
from typing import List, Optional
5+
6+
import httpx
7+
from cratedb_about.prompt import GeneralInstructions
8+
9+
10+
class InstructionsPrompt:
11+
"""
12+
Bundle instructions how to use MCP tools with general instructions how to work with CrateDB.
13+
14+
- 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
15+
- General: https://github.com/crate/about
16+
"""
17+
18+
def __init__(self, instructions: Optional[str] = None, conventions: Optional[str] = None):
19+
fragments: List[str] = []
20+
if instructions:
21+
fragments.append(self.load_fragment(instructions))
22+
else:
23+
instructions_general = GeneralInstructions().render()
24+
mcp_instructions_file = (
25+
importlib.resources.files("cratedb_mcp.prompt") / "instructions.md"
26+
)
27+
if not mcp_instructions_file.is_file(): # pragma: no cover
28+
raise FileNotFoundError(f"MCP instructions file not found: {mcp_instructions_file}")
29+
instructions_mcp = mcp_instructions_file.read_text()
30+
fragments.append(instructions_general)
31+
fragments.append(instructions_mcp)
32+
if conventions:
33+
fragments.append(self.load_fragment(conventions))
34+
self.fragments = fragments
35+
36+
def render(self) -> str:
37+
return "\n\n".join(map(str.strip, self.fragments))
38+
39+
@staticmethod
40+
def load_fragment(fragment: str) -> str:
41+
"""
42+
Load instruction fragment from various sources.
43+
44+
Supports loading from:
45+
- HTTP(S) URLs
46+
- Local file paths
47+
- Standard input (when fragment is "-")
48+
- Direct string content
49+
50+
That's a miniature variant of a "fragment" concept,
51+
adapted from `llm` [1] written by Simon Willison.
52+
53+
[1] https://github.com/simonw/llm
54+
"""
55+
try:
56+
if fragment.startswith("http://") or fragment.startswith("https://"):
57+
with httpx.Client(follow_redirects=True, max_redirects=3, timeout=5.0) as client:
58+
response = client.get(fragment)
59+
response.raise_for_status()
60+
return response.text
61+
if fragment == "-":
62+
return sys.stdin.read()
63+
path = Path(fragment)
64+
if path.exists():
65+
return path.read_text(encoding="utf-8")
66+
return fragment
67+
except (httpx.HTTPError, OSError, UnicodeDecodeError) as e:
68+
raise ValueError(f"Failed to load fragment '{fragment}': {e}") from e
File renamed without changes.

pyproject.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ dependencies = [
7373
"attrs",
7474
"cachetools<7",
7575
"click<9",
76-
"cratedb-about==0.0.6",
76+
"cratedb-about==0.0.7",
7777
"fastmcp>=2.7,<2.11",
7878
"hishel<0.2",
7979
"pueblo==0.0.11",
@@ -94,11 +94,10 @@ optional-dependencies.test = [
9494

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

97-
[tool.setuptools]
98-
include-package-data = true
99-
10097
[tool.setuptools.package-data]
101-
cratedb_mcp = [ "*.md" ]
98+
cratedb_mcp = [
99+
"**/*.md",
100+
]
102101

103102
[tool.setuptools.packages.find]
104103
namespaces = false

0 commit comments

Comments
 (0)