Skip to content

Commit 7ac95e8

Browse files
committed
MCP: Add example about PG-MCP
1 parent 572da4a commit 7ac95e8

File tree

7 files changed

+159
-8
lines changed

7 files changed

+159
-8
lines changed

framework/mcp/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
uv.lock
2+
pg-mcp

framework/mcp/backlog.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
https://github.com/crate/crate/issues/17393
1010
- DBHub: Reading resource `tables` does not work,
1111
because `WHERE table_schema = 'public'`
12+
- PG-MCP: Improve installation after packaging has been improved.
13+
https://github.com/stuzero/pg-mcp/issues/10
1214

1315
## Iteration +2
1416
- General: Evaluate all connectors per `stdio` and `sse`, where possible

framework/mcp/example_pg_mcp.py

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# PG-MCP Model Context Protocol Server for CrateDB
2+
# https://github.com/stuzero/pg-mcp
3+
# https://github.com/crate-workbench/pg-mcp
4+
#
5+
# Derived from:
6+
# https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#writing-mcp-clients
7+
from cratedb_toolkit.util import DatabaseAdapter
8+
from mcp import ClientSession, StdioServerParameters
9+
from mcp.client.stdio import stdio_client
10+
import where
11+
12+
from mcp_utils import McpDatabaseConversation
13+
14+
15+
async def run():
16+
# Create server parameters for stdio connection.
17+
server_params = StdioServerParameters(
18+
command=where.first("python"),
19+
args=["example_pg_mcp_server.py"],
20+
env={},
21+
)
22+
23+
async with stdio_client(server_params) as (read, write):
24+
async with ClientSession(
25+
read, write
26+
) as session:
27+
# Initialize the connection.
28+
await session.initialize()
29+
30+
client = McpDatabaseConversation(session)
31+
await client.inquire()
32+
33+
print("## MCP server conversations")
34+
print()
35+
36+
# Provision database content.
37+
db = DatabaseAdapter("crate://crate@localhost:4200/")
38+
db.run_sql("CREATE TABLE IF NOT EXISTS mcp_pg_mcp (id INT, data TEXT)")
39+
db.run_sql("INSERT INTO mcp_pg_mcp (id, data) VALUES (42, 'Hotzenplotz')")
40+
db.refresh_table("mcp_pg_mcp")
41+
42+
# Call a few tools.
43+
connection_string = "postgresql://crate@localhost/doc"
44+
45+
# Connect to the database, receiving the connection UUID.
46+
response = await client.call_tool("connect", arguments={"connection_string": connection_string})
47+
conn_id = client.decode_json_text(response)["conn_id"]
48+
49+
# Query and explain, using the connection id.
50+
await client.call_tool("pg_query", arguments={
51+
"query": "SELECT * FROM sys.summits ORDER BY height DESC LIMIT 3",
52+
"conn_id": conn_id,
53+
})
54+
await client.call_tool("pg_explain", arguments={
55+
"query": "SELECT * FROM mcp_pg_mcp",
56+
"conn_id": conn_id,
57+
})
58+
59+
# Read a few resources.
60+
await client.read_resource(f"pgmcp://{conn_id}/schemas")
61+
await client.read_resource(f"pgmcp://{conn_id}/schemas/sys/tables")
62+
63+
# Disconnect again.
64+
await client.call_tool("disconnect", arguments={"conn_id": conn_id,})
65+
66+
67+
if __name__ == "__main__":
68+
import asyncio
69+
70+
asyncio.run(run())
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/sh
2+
# Acquire MCP server part of `pg-mcp`.
3+
# https://github.com/stuzero/pg-mcp
4+
5+
# FIXME: Improve installation after packaging has been improved.
6+
# https://github.com/stuzero/pg-mcp/issues/10
7+
8+
rm -rf pg-mcp
9+
git clone --depth 1 --no-checkout --filter=blob:none \
10+
https://github.com/crate-workbench/pg-mcp.git
11+
cd pg-mcp
12+
git checkout 16d7f61d5b3197777293ebae33b519f14a9d6e55 -- pyproject.toml uv.lock server test.py
13+
uv pip install .
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
if __name__ == "__main__":
2+
# FIXME: Improve invocation after packaging has been improved.
3+
# https://github.com/stuzero/pg-mcp/issues/10
4+
from server.app import logger, mcp
5+
6+
# TODO: Bring flexible invocation (sse vs. stdio) to mainline.
7+
logger.info("Starting MCP server with STDIO transport")
8+
mcp.run(transport="stdio")

framework/mcp/mcp_utils.py

+29-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import io
22
import json
3+
import logging
4+
35
import mcp.types as types
46
from typing import Any
57

@@ -9,13 +11,20 @@
911
from pydantic import AnyUrl
1012

1113

14+
logger = logging.getLogger(__name__)
15+
16+
1217
class McpDatabaseConversation:
1318
"""
1419
Wrap database conversations through MCP servers.
1520
"""
1621
def __init__(self, session: ClientSession):
1722
self.session = session
1823

24+
@staticmethod
25+
def decode_json_text(thing):
26+
return json.loads(thing.content[0].text)
27+
1928
def decode_items(self, items):
2029
return list(map(self.decode_item, json.loads(pydantic_core.to_json(items))))
2130

@@ -36,26 +45,38 @@ def list_items(self, items):
3645
buffer.write("```\n")
3746
return buffer.getvalue()
3847

48+
async def entity_info(self, fun, attribute):
49+
try:
50+
return self.list_items(getattr(await fun(), attribute))
51+
except McpError as e:
52+
logger.error(f"Not implemented on this server: {e}")
53+
54+
@staticmethod
55+
def dump_info(results):
56+
if results:
57+
print(results)
58+
print()
59+
3960
async def inquire(self):
4061
print("# MCP server inquiry")
4162
print()
4263

4364
# List available prompts
4465
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()
66+
self.dump_info(self.entity_info(self.session.list_prompts, "prompts"))
5067

51-
# List available resources
68+
# List available resources and resource templates
5269
print("## Resources")
53-
print(self.list_items((await self.session.list_resources()).resources))
70+
self.dump_info(self.entity_info(self.session.list_resources, "resources"))
71+
print()
72+
73+
print("## Resource templates")
74+
self.dump_info(self.entity_info(self.session.list_resource_templates, "resourceTemplates"))
5475
print()
5576

5677
# List available tools
5778
print("## Tools")
58-
print(self.list_items((await self.session.list_tools()).tools))
79+
self.dump_info(self.entity_info(self.session.list_tools, "tools"))
5980
print()
6081

6182
async def call_tool(

framework/mcp/test.py

+36
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,39 @@ def test_mcp_alchemy():
150150
assert b"Calling tool: schema_definitions" in p.stdout
151151
assert b"id: INTEGER, nullable" in p.stdout
152152
assert b"data: VARCHAR, nullable" in p.stdout
153+
154+
155+
@pytest.mark.skipif(sys.version_info < (3, 13), reason="requires Python 3.13+")
156+
def test_pg_mcp():
157+
"""
158+
Validate the PG-MCP server works well.
159+
160+
It is written in Python and uses pgasync.
161+
https://github.com/crate-workbench/pg-mcp
162+
"""
163+
p = run(f"sh example_pg_mcp_install.sh")
164+
print(p.stdout)
165+
print(p.stderr)
166+
assert p.returncode == 0
167+
p = run(f"{sys.executable} example_pg_mcp.py")
168+
assert p.returncode == 0
169+
170+
# Validate output specific to the MCP server.
171+
assert b"Processing request of type" in p.stderr
172+
assert b"PromptListRequest" in p.stderr
173+
assert b"ListResourcesRequest" in p.stderr
174+
assert b"ListToolsRequest" in p.stderr
175+
assert b"CallToolRequest" in p.stderr
176+
177+
# Validate output specific to CrateDB.
178+
assert b"Calling tool: pg_query" in p.stdout
179+
assert b"mountain: Mont Blanc" in p.stdout
180+
181+
assert b"Calling tool: pg_explain" in p.stdout
182+
183+
assert b"Reading resource: pgmcp://" in p.stdout
184+
assert b"schema_name: blob" in p.stdout
185+
assert b"schema_name: doc" in p.stdout
186+
assert b"schema_name: sys" in p.stdout
187+
assert b"table_name: jobs" in p.stdout
188+
assert b"table_name: shards" in p.stdout

0 commit comments

Comments
 (0)