Skip to content

Commit ce015df

Browse files
committed
MCP: Generalize workhorse code. Evaluate available prompts.
1 parent 024b04f commit ce015df

File tree

10 files changed

+167
-126
lines changed

10 files changed

+167
-126
lines changed

framework/mcp/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uv.lock

framework/mcp/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,12 @@ uv run example_jdbc.py
9393

9494
Run integration tests.
9595
```bash
96-
pytest
96+
uv run pytest
97+
```
98+
99+
Run tests selectively.
100+
```bash
101+
uv run pytest -k dbhub
97102
```
98103

99104
## Development

framework/mcp/backlog.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
- Builtin: Submit a CrateDB adapter, getting rid of the
55
`Could not roll back transaction: error`
66
- JDBC: Tool `list_tables` currently blocks.
7+
- JDBC: Tool `describe_table` does not work.
8+
Problem with `SELECT current_database()`.
9+
https://github.com/crate/crate/issues/17393
710
- DBHub: Reading resource `tables` does not work,
811
because `WHERE table_schema = 'public'`
912

1013
## Iteration +2
1114
- General: Evaluate all connectors per `stdio` and `sse`, where possible
12-
- General: Evaluate available prompts, there are no test cases yet
1315
- General: Improve test cases to not just look at stdout/stderr streams,
1416
do regular method-based testing instead
1517

@@ -27,6 +29,7 @@
2729
## Done
2830
- DBHub: https://github.com/bytebase/dbhub/issues/5
2931
Resolved: https://github.com/bytebase/dbhub/pull/6
32+
- General: Evaluate available prompts, there are no test cases yet
3033

3134

3235
[SQLite Explorer]: https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#sqlite-explorer

framework/mcp/example_builtin.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
#
44
# Derived from:
55
# https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#writing-mcp-clients
6+
from cratedb_toolkit.util import DatabaseAdapter
67
from mcp import ClientSession, StdioServerParameters
78
from mcp.client.stdio import stdio_client
8-
from pprint import pprint
99

10+
from mcp_utils import McpDatabaseConversation
1011

1112
# Create server parameters for stdio connection.
1213
server_params = StdioServerParameters(
@@ -28,27 +29,25 @@ async def run():
2829
# Initialize the connection
2930
await session.initialize()
3031

31-
# List available prompts
32-
# TODO: Not available on this server.
33-
#print("Prompts:")
34-
#pprint(await session.list_prompts())
35-
#print()
32+
client = McpDatabaseConversation(session)
33+
await client.inquire()
3634

37-
# List available resources
38-
print("Resources:")
39-
pprint(await session.list_resources())
35+
print("## MCP server conversations")
4036
print()
4137

42-
# List available tools
43-
print("Tools:")
44-
pprint(await session.list_tools())
45-
print()
38+
# Call a few tools.
39+
await client.call_tool("query", arguments={"sql": "SELECT * FROM sys.summits ORDER BY height DESC LIMIT 3"})
4640

47-
print("Calling tool: read_query")
48-
result = await session.call_tool("query", arguments={"sql": "SELECT * FROM sys.summits ORDER BY height DESC LIMIT 3"})
49-
pprint(result)
50-
print()
41+
# Validate database content.
42+
db = DatabaseAdapter("crate://crate@localhost:4200/")
43+
db.run_sql("CREATE TABLE IF NOT EXISTS public.testdrive (id INT, data TEXT)")
44+
db.run_sql("INSERT INTO public.testdrive (id, data) VALUES (42, 'Hotzenplotz')")
45+
db.refresh_table("public.testdrive")
5146

47+
# Read a few resources.
48+
# FIXME: Only works on schema=public, because the PostgreSQL adapter hard-codes `WHERE table_schema = 'public'`.
49+
# https://github.com/bytebase/dbhub/blob/09424c8513c8c7bef7f66377b46a2b93a69a57d2/src/connectors/postgres/index.ts#L89-L107
50+
await client.read_resource("postgres://crate@localhost:5432/testdrive/schema")
5251

5352

5453
if __name__ == "__main__":

framework/mcp/example_dbhub.py

Lines changed: 20 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
#
44
# Derived from:
55
# https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#writing-mcp-clients
6-
import shlex
7-
6+
from cratedb_toolkit.util import DatabaseAdapter
87
from mcp import ClientSession, StdioServerParameters
98
from mcp.client.stdio import stdio_client
10-
from pprint import pprint
119

10+
from mcp_utils import McpDatabaseConversation
1211

1312
# Create server parameters for stdio connection.
1413
server_params_npx = StdioServerParameters(
@@ -24,22 +23,6 @@
2423
env=None,
2524
)
2625

27-
docker_command = """
28-
docker run --rm --init \
29-
--name dbhub \
30-
--publish 8080:8080 \
31-
bytebase/dbhub:latest \
32-
--transport sse \
33-
--port 8080 \
34-
--dsn="postgres://crate@host.docker.internal:5432/doc?sslmode=disable"
35-
"""
36-
server_params_docker = StdioServerParameters(
37-
command="npx",
38-
args=shlex.split(docker_command),
39-
env=None,
40-
)
41-
42-
4326
async def run():
4427
async with stdio_client(server_params_npx) as (read, write):
4528
async with ClientSession(
@@ -48,40 +31,30 @@ async def run():
4831
# Initialize the connection
4932
await session.initialize()
5033

51-
# List available prompts
52-
print("Prompts:")
53-
pprint(await session.list_prompts())
54-
print()
55-
56-
# List available resources
57-
print("Resources:")
58-
pprint(await session.list_resources())
59-
print()
34+
client = McpDatabaseConversation(session)
35+
await client.inquire()
6036

61-
# List available tools
62-
print("Tools:")
63-
pprint(await session.list_tools())
37+
print("## MCP server conversations")
6438
print()
6539

66-
print("Calling tool: run_query")
67-
result = await session.call_tool("run_query", arguments={"query": "SELECT * FROM sys.summits ORDER BY height DESC LIMIT 3"})
68-
pprint(result)
69-
print()
40+
# Call a few tools.
41+
await client.call_tool("run_query", arguments={"query": "SELECT * FROM sys.summits ORDER BY height DESC LIMIT 3"})
42+
await client.call_tool("list_connectors", arguments={})
7043

71-
print("Calling tool: list_connectors")
72-
result = await session.call_tool("list_connectors", arguments={})
73-
pprint(result)
74-
print()
44+
# Validate database content.
45+
db = DatabaseAdapter("crate://crate@localhost:4200/")
46+
db.run_sql("CREATE TABLE IF NOT EXISTS public.testdrive (id INT, data TEXT)")
47+
db.run_sql("INSERT INTO public.testdrive (id, data) VALUES (42, 'Hotzenplotz')")
48+
db.refresh_table("public.testdrive")
7549

76-
# Read a resource
77-
# FIXME: Does not work, because the PostgreSQL adapters hard-codes `WHERE table_schema = 'public'`.
50+
# Read a few resources.
51+
# FIXME: Only works on schema=public, because the PostgreSQL adapter hard-codes `WHERE table_schema = 'public'`.
7852
# https://github.com/bytebase/dbhub/blob/09424c8513c8c7bef7f66377b46a2b93a69a57d2/src/connectors/postgres/index.ts#L89-L107
79-
"""
80-
print("Reading resource: tables")
81-
content, mime_type = await session.read_resource("db://tables")
82-
print("MIME type:", mime_type)
83-
pprint(content)
84-
"""
53+
await client.read_resource("db://tables")
54+
55+
# Get a few prompts.
56+
await client.get_prompt("generate_sql", arguments={"description": "Please enumerate the highest five mountains.", "dialect": "postgres"})
57+
await client.get_prompt("explain_db", arguments={"target": "testdrive"})
8558

8659

8760
if __name__ == "__main__":

framework/mcp/example_jdbc.py

Lines changed: 15 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
# https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#writing-mcp-clients
66
from mcp import ClientSession, StdioServerParameters
77
from mcp.client.stdio import stdio_client
8-
from pprint import pprint
98

9+
from mcp_utils import McpDatabaseConversation
1010

1111
# Create server parameters for stdio connection.
1212
server_params = StdioServerParameters(
@@ -30,61 +30,24 @@ async def run():
3030
# Initialize the connection
3131
await session.initialize()
3232

33-
# List available prompts
34-
print("Prompts:")
35-
pprint(await session.list_prompts())
36-
print()
37-
38-
# List available resources
39-
print("Resources:")
40-
pprint(await session.list_resources())
41-
print()
42-
43-
# List available tools
44-
print("Tools:")
45-
pprint(await session.list_tools())
46-
print()
33+
client = McpDatabaseConversation(session)
34+
await client.inquire()
4735

48-
# Call a tool
49-
print("Calling tool: database_info")
50-
result = await session.call_tool("database_info")
51-
pprint(result)
36+
print("## MCP server conversations")
5237
print()
5338

39+
# Call a few tools.
40+
await client.call_tool("database_info")
5441
# FIXME: This operation currently blocks.
55-
#print("Calling tool: list_tables")
56-
#result = await session.call_tool("list_tables")
57-
#pprint(result)
58-
#print()
59-
60-
print("Calling tool: describe_table")
61-
result = await session.call_tool("describe_table", arguments={"schema": "sys", "table": "summits"})
62-
pprint(result)
63-
print()
64-
65-
print("Calling tool: read_query")
66-
result = await session.call_tool("read_query", arguments={"query": "SELECT * FROM sys.summits ORDER BY height DESC LIMIT 3"})
67-
pprint(result)
68-
print()
69-
70-
print("Calling tool: create_table")
71-
result = await session.call_tool("create_table", arguments={"query": "CREATE TABLE IF NOT EXISTS testdrive (id INT, data TEXT)"})
72-
pprint(result)
73-
print()
74-
75-
print("Calling tool: write_query")
76-
result = await session.call_tool("write_query", arguments={"query": "INSERT INTO testdrive (id, data) VALUES (42, 'foobar')"})
77-
pprint(result)
78-
print()
79-
80-
# Read a resource
81-
#content, mime_type = await session.read_resource("file://some/path")
82-
83-
# Get a prompt
84-
#prompt = await session.get_prompt(
85-
# "example-prompt", arguments={"arg1": "value"}
86-
#)
87-
42+
# await client.call_tool("list_tables", arguments={})
43+
await client.call_tool("describe_table", arguments={"schema": "sys", "table": "summits"})
44+
await client.call_tool("read_query", arguments={"query": "SELECT * FROM sys.summits ORDER BY height DESC LIMIT 3"})
45+
await client.call_tool("create_table", arguments={"query": "CREATE TABLE IF NOT EXISTS testdrive (id INT, data TEXT)"})
46+
await client.call_tool("write_query", arguments={"query": "INSERT INTO testdrive (id, data) VALUES (42, 'foobar')"})
47+
48+
# Get a few prompts.
49+
await client.get_prompt("er_diagram")
50+
await client.get_prompt("sample_data", arguments={"topic": "wind energy"})
8851

8952

9053
if __name__ == "__main__":

framework/mcp/mcp_utils.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import io
2+
import json
3+
import mcp.types as types
4+
from typing import Any
5+
6+
import pydantic_core
7+
import yaml
8+
from mcp import ClientSession, McpError
9+
from pydantic import AnyUrl
10+
11+
12+
class McpDatabaseConversation:
13+
"""
14+
Wrap database conversations through MCP servers.
15+
"""
16+
def __init__(self, session: ClientSession):
17+
self.session = session
18+
19+
def decode_items(self, items):
20+
return list(map(self.decode_item, json.loads(pydantic_core.to_json(items))))
21+
22+
@staticmethod
23+
def decode_item(item):
24+
try:
25+
item["text"] = json.loads(item["text"])
26+
except Exception:
27+
pass
28+
return item
29+
30+
def list_items(self, items):
31+
buffer = io.StringIO()
32+
if items:
33+
data = self.decode_items(items)
34+
buffer.write("```yaml\n")
35+
buffer.write(yaml.dump(data, sort_keys=False, width=100))
36+
buffer.write("```\n")
37+
return buffer.getvalue()
38+
39+
async def inquire(self):
40+
print("# MCP server inquiry")
41+
print()
42+
43+
# List available prompts
44+
print("## Prompts")
45+
try:
46+
print(self.list_items((await self.session.list_prompts()).prompts))
47+
except McpError as e:
48+
print(f"Not implemented on this server: {e}")
49+
print()
50+
51+
# List available resources
52+
print("## Resources")
53+
print(self.list_items((await self.session.list_resources()).resources))
54+
print()
55+
56+
# List available tools
57+
print("## Tools")
58+
print(self.list_items((await self.session.list_tools()).tools))
59+
print()
60+
61+
async def call_tool(
62+
self, name: str, arguments: dict[str, Any] | None = None
63+
) -> types.CallToolResult:
64+
print(f"Calling tool: {name} with arguments: {arguments}")
65+
result = await self.session.call_tool(name, arguments)
66+
print(self.list_items(result.content))
67+
print()
68+
return result
69+
70+
async def get_prompt(
71+
self, name: str, arguments: dict[str, str] | None = None
72+
) -> types.GetPromptResult:
73+
print(f"Getting prompt: {name} with arguments: {arguments}")
74+
result = await self.session.get_prompt(name, arguments)
75+
print(self.list_items(result.messages))
76+
print()
77+
return result
78+
79+
async def read_resource(self, uri: AnyUrl) -> types.ReadResourceResult:
80+
print(f"Reading resource: {uri}")
81+
result = await self.session.read_resource(uri)
82+
print(self.list_items(result.contents))
83+
print()
84+
return result

framework/mcp/requirements-test.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
cratedb-toolkit
21
pytest<9

framework/mcp/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
cratedb-toolkit
12
mcp<1.5

0 commit comments

Comments
 (0)