Skip to content

Commit 176f2aa

Browse files
committed
Application layout: Refactor core tool functions to separate module
1 parent 22efd8f commit 176f2aa

File tree

4 files changed

+112
-59
lines changed

4 files changed

+112
-59
lines changed

cratedb_mcp/__main__.py

Lines changed: 49 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,66 @@
1-
import httpx
21
from fastmcp import FastMCP
2+
from fastmcp.tools import Tool
33

44
from . import __appname__
5-
from .knowledge import DocumentationIndex, Queries
6-
from .settings import HTTP_URL, Settings
7-
from .util.sql import sql_is_permitted
8-
9-
# Load CrateDB documentation outline.
10-
documentation_index = DocumentationIndex()
5+
from .tool import (
6+
fetch_cratedb_docs,
7+
get_cluster_health,
8+
get_cratedb_documentation_index,
9+
get_table_metadata,
10+
query_sql,
11+
)
1112

1213
# Create FastMCP application object.
1314
mcp: FastMCP = FastMCP(__appname__)
1415

1516

1617
# ------------------------------------------
17-
# Text-to-SQL
18+
# Health / Status
1819
# ------------------------------------------
19-
20-
21-
def query_cratedb(query: str) -> list[dict]:
22-
"""Sends a `query` to the set `$CRATEDB_CLUSTER_URL`"""
23-
url = HTTP_URL
24-
if url.endswith("/"):
25-
url = url.removesuffix("/")
26-
27-
return httpx.post(f"{url}/_sql", json={"stmt": query}, timeout=Settings.http_timeout()).json()
28-
29-
30-
@mcp.tool(description="Send an SQL query to CrateDB. Only 'SELECT' queries are allowed.")
31-
def query_sql(query: str):
32-
if not sql_is_permitted(query):
33-
raise PermissionError("Only queries that have a SELECT statement are allowed.")
34-
return query_cratedb(query)
35-
36-
37-
@mcp.tool(description="Return an aggregation of all CrateDB's schema, tables and their metadata.")
38-
def get_table_metadata() -> list[dict]:
39-
"""
40-
Return an aggregation of schema:tables, e.g.: {'doc': [{name:'mytable', ...}, ...]}
41-
42-
The tables have metadata datapoints like replicas, shards,
43-
name, version, total_shards, total_records.
44-
"""
45-
return query_cratedb(Queries.TABLES_METADATA)
46-
47-
48-
@mcp.tool(description="Return the health of the CrateDB cluster.")
49-
def get_cluster_health() -> list[dict]:
50-
"""Query sys.health ordered by severity."""
51-
return query_cratedb(Queries.HEALTH)
20+
mcp.add_tool(
21+
Tool.from_function(
22+
fn=get_cluster_health,
23+
description="Return the health of the CrateDB cluster.",
24+
tags={"health", "monitoring", "status"},
25+
)
26+
)
5227

5328

5429
# ------------------------------------------
55-
# Documentation Inquiry
30+
# Text-to-SQL
5631
# ------------------------------------------
57-
58-
59-
@mcp.tool(
60-
description="Get an index of CrateDB documentation links for fetching. "
61-
"Should download docs before answering questions. "
62-
"Has documentation title, description, and link."
32+
mcp.add_tool(
33+
Tool.from_function(
34+
fn=query_sql,
35+
description="Send an SQL query to CrateDB. Only 'SELECT' queries are allowed.",
36+
tags={"text-to-sql"},
37+
)
38+
)
39+
mcp.add_tool(
40+
Tool.from_function(
41+
fn=get_table_metadata,
42+
description="Return an aggregation of all CrateDB's schema, tables and their metadata.",
43+
tags={"text-to-sql"},
44+
)
6345
)
64-
def get_cratedb_documentation_index():
65-
return documentation_index.items()
6646

6747

68-
@mcp.tool(description="Download individual CrateDB documentation pages by link.")
69-
def fetch_cratedb_docs(link: str):
70-
"""Fetch a CrateDB documentation link."""
71-
if not documentation_index.url_permitted(link):
72-
raise ValueError(f"Link is not permitted: {link}")
73-
return documentation_index.client.get(link, timeout=Settings.http_timeout()).text
48+
# ------------------------------------------
49+
# Documentation inquiry
50+
# ------------------------------------------
51+
mcp.add_tool(
52+
Tool.from_function(
53+
fn=get_cratedb_documentation_index,
54+
description="Get an index of CrateDB documentation links for fetching. "
55+
"Should download docs before answering questions. "
56+
"Has documentation title, description, and link.",
57+
tags={"documentation"},
58+
)
59+
)
60+
mcp.add_tool(
61+
Tool.from_function(
62+
fn=fetch_cratedb_docs,
63+
description="Download individual CrateDB documentation pages by link.",
64+
tags={"documentation"},
65+
)
66+
)

cratedb_mcp/tool.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import httpx
2+
3+
from cratedb_mcp.knowledge import DocumentationIndex, Queries
4+
from cratedb_mcp.settings import HTTP_URL, Settings
5+
from cratedb_mcp.util.sql import sql_is_permitted
6+
7+
8+
# ------------------------------------------
9+
# Health / Status
10+
# ------------------------------------------
11+
def get_cluster_health() -> list[dict]:
12+
"""Query sys.health ordered by severity."""
13+
return query_cratedb(Queries.HEALTH)
14+
15+
16+
# ------------------------------------------
17+
# Text-to-SQL
18+
# ------------------------------------------
19+
def query_cratedb(query: str) -> list[dict]:
20+
"""Sends a `query` to the set `$CRATEDB_CLUSTER_URL`"""
21+
url = HTTP_URL
22+
if url.endswith("/"):
23+
url = url.removesuffix("/")
24+
25+
return httpx.post(f"{url}/_sql", json={"stmt": query}, timeout=Settings.http_timeout()).json()
26+
27+
28+
def query_sql(query: str):
29+
if not sql_is_permitted(query):
30+
raise PermissionError("Only queries that have a SELECT statement are allowed.")
31+
return query_cratedb(query)
32+
33+
34+
def get_table_metadata() -> list[dict]:
35+
"""
36+
Return an aggregation of schema:tables, e.g.: {'doc': [{name:'mytable', ...}, ...]}
37+
38+
The tables have metadata datapoints like replicas, shards,
39+
name, version, total_shards, total_records.
40+
"""
41+
return query_cratedb(Queries.TABLES_METADATA)
42+
43+
44+
# ------------------------------------------
45+
# Documentation inquiry
46+
# ------------------------------------------
47+
# Load CrateDB documentation outline.
48+
documentation_index = DocumentationIndex()
49+
50+
51+
def get_cratedb_documentation_index():
52+
"""Get curated CrateDB documentation index."""
53+
return documentation_index.items()
54+
55+
56+
def fetch_cratedb_docs(link: str):
57+
"""Fetch a CrateDB documentation link."""
58+
if not documentation_index.url_permitted(link):
59+
raise ValueError(f"Link is not permitted: {link}")
60+
return documentation_index.client.get(link, timeout=Settings.http_timeout()).text

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ dependencies = [
7474
"cachetools<7",
7575
"click<9",
7676
"cratedb-about==0.0.5",
77-
"fastmcp<2.7",
77+
"fastmcp>=2.7,<2.10",
7878
"hishel<0.2",
7979
"pueblo==0.0.11",
8080
"sqlparse<0.6",

tests/test_mcp.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from cratedb_mcp.__main__ import (
3+
from cratedb_mcp.tool import (
44
fetch_cratedb_docs,
55
get_cluster_health,
66
get_cratedb_documentation_index,
@@ -39,7 +39,7 @@ def test_query_sql_permitted():
3939

4040
def test_query_sql_trailing_slash(mocker):
4141
"""Verify that query_sql works correctly when HTTP_URL has a trailing slash."""
42-
mocker.patch("cratedb_mcp.__main__.HTTP_URL", "http://localhost:4200/")
42+
mocker.patch("cratedb_mcp.tool.HTTP_URL", "http://localhost:4200/")
4343
assert query_sql("SELECT 42")["rows"] == [[42]]
4444

4545

0 commit comments

Comments
 (0)