Skip to content

Commit 1d9a1f2

Browse files
author
Shengwen Yang
authored
Feature: add MCP server for Cloudberry
Add a comprehensive Model Context Protocol (MCP) server for Apache Cloudberry enabling seamless integration with LLM applications. This implementation provides secure database interaction capabilities through AI-ready interfaces, supporting both stdio and HTTP transport modes for maximum compatibility with various LLM clients. **What:** - Complete MCP server implementation with async database operations - Safe SQL query execution with parameterized queries - Administrative tools for performance monitoring and optimization - Predefined prompts for common database tasks - Security-first design with SQL injection prevention **Why:** - Enable AI assistants to interact with Cloudberry databases safely - Provide standardized interface for LLM applications - Reduce manual database management overhead - Support modern AI development workflows **How:** - Built with asyncpg for high-performance PostgreSQL operations - Implements MCP protocol for universal LLM compatibility - Uses environment-based configuration for flexible deployment - Includes comprehensive test suite and documentation **Compatibility:** - Requires Python 3.8+ with asyncpg support - Compatible with Claude Desktop, Cursor, Windsurf, VS Code, Trae, Qwen Desktop, etc. - Supports both stdio and HTTP transport modes - Works with standard PostgreSQL connection strings **Breaking Changes:** None - this is a new addition.
1 parent 0d40460 commit 1d9a1f2

16 files changed

Lines changed: 3098 additions & 0 deletions

mcp-server/README.md

Lines changed: 399 additions & 0 deletions
Large diffs are not rendered by default.

mcp-server/dotenv.example

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
# Example environment configuration
19+
# Copy this file to .env and update with your actual values
20+
21+
# Database Configuration
22+
DB_HOST=localhost
23+
DB_PORT=5432
24+
DB_NAME=postgres
25+
DB_USER=postgres
26+
DB_PASSWORD=your_password_here
27+
28+
# Server Configuration
29+
MCP_HOST=localhost
30+
MCP_PORT=8000
31+
MCP_DEBUG=false

mcp-server/pyproject.toml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
[project]
19+
name = "cloudberry-mcp-server"
20+
version = "0.1.0"
21+
description = "MCP server for Apache Cloudberry database interaction"
22+
readme = "README.md"
23+
requires-python = ">=3.10"
24+
authors = [
25+
{name = "Shengwen Yang", email = "yangshengwen@gmail.com"},
26+
]
27+
maintainers = [
28+
{name = "Shengwen Yang", email = "yangshengwen@gmail.com"},
29+
]
30+
license = {text = "Apache License 2.0"}
31+
keywords = ["mcp", "cloudberry", "postgresql", "database", "server", "ai"]
32+
classifiers = [
33+
"Development Status :: 3 - Alpha",
34+
"Intended Audience :: Developers",
35+
"License :: OSI Approved :: Apache Software License, Version 2.0",
36+
"Programming Language :: Python :: 3",
37+
"Programming Language :: Python :: 3.8",
38+
"Programming Language :: Python :: 3.9",
39+
"Programming Language :: Python :: 3.10",
40+
"Programming Language :: Python :: 3.11",
41+
"Programming Language :: Python :: 3.12",
42+
"Programming Language :: Python :: 3.13",
43+
"Topic :: Database",
44+
"Topic :: Software Development :: Libraries :: Python Modules",
45+
"Topic :: Database :: Front-Ends",
46+
]
47+
dependencies = [
48+
"fastmcp>=2.10.6",
49+
"psycopg2-binary==2.9.10",
50+
"asyncpg>=0.29.0",
51+
"pydantic>=2.0.0",
52+
"python-dotenv>=1.0.0",
53+
"aiohttp>=3.12.15",
54+
"starlette>=0.27.0",
55+
]
56+
57+
[project.optional-dependencies]
58+
dev = [
59+
"pytest>=7.0.0",
60+
"pytest-asyncio>=0.21.0",
61+
"pytest-cov>=4.0.0",
62+
]
63+
64+
[project.urls]
65+
Homepage = "https://github.com/apache/cloudberry//tree/main/mcp-server"
66+
Repository = "https://github.com/apache/cloudberry"
67+
Documentation = "https://github.com/apache/cloudberry/mcp-server/tree/main/mcp-server/README.md"
68+
69+
[project.scripts]
70+
cloudberry-mcp-server = "cbmcp.server:main"

mcp-server/pytest.ini

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
[tool:pytest]
19+
testpaths = tests
20+
python_files = test_*.py
21+
python_classes = Test*
22+
python_functions = test_*
23+
addopts =
24+
-v
25+
--tb=short
26+
--strict-markers
27+
--disable-warnings
28+
asyncio_mode = auto
29+
markers =
30+
slow: marks tests as slow (deselect with '-m "not slow"')
31+
integration: marks tests as integration tests
32+
unit: marks tests as unit tests

mcp-server/run_tests.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
19+
20+
# Test script for Apache Cloudberry MCP Server
21+
22+
echo "=== Install test dependencies ==="
23+
uv pip install -e ".[dev]"
24+
25+
echo "=== Run all tests ==="
26+
uv run pytest tests/ -v
27+
28+
echo "=== Run specific test patterns ==="
29+
echo "Run stdio mode test:"
30+
uv run pytest tests/test_cbmcp.py::TestCloudberryMCPClient::test_list_capabilities -v
31+
32+
echo "Run http mode test:"
33+
uv run pytest tests/test_cbmcp.py::TestCloudberryMCPClient::test_list_capabilities -v
34+
35+
echo "=== Run coverage tests ==="
36+
uv run pytest tests/ --cov=cbmcp --cov-report=html --cov-report=term
37+
38+
echo "=== Test completed ==="

mcp-server/src/cbmcp/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
14+
"""
15+
Apache Cloudberry MCP Server Package
16+
"""
17+
18+
from .server import CloudberryMCPServer
19+
from .client import CloudberryMCPClient
20+
from .config import DatabaseConfig, ServerConfig
21+
from .database import DatabaseManager
22+
from .security import SQLValidator
23+
24+
__version__ = "0.1.0"
25+
__all__ = [
26+
"CloudberryMCPServer",
27+
"CloudberryMCPClient",
28+
"DatabaseConfig",
29+
"ServerConfig",
30+
"DatabaseManager",
31+
"SQLValidator",
32+
]

mcp-server/src/cbmcp/__main__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
"""
21+
Main entry point for the cbmcp package
22+
"""
23+
24+
from .server import main
25+
26+
if __name__ == "__main__":
27+
main()

mcp-server/src/cbmcp/client.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
"""
21+
MCP Client for testing the Apache Cloudberry MCP Server
22+
23+
A client using the fastmcp SDK to interact with the Apache Cloudberry MCP server implementation.
24+
"""
25+
26+
from typing import Any, Dict, Optional
27+
from fastmcp import Client
28+
29+
from .config import DatabaseConfig, ServerConfig
30+
from .server import CloudberryMCPServer
31+
32+
class CloudberryMCPClient:
33+
"""MCP client for testing the Apache Cloudberry server using fastmcp SDK
34+
35+
Usage:
36+
# Method 1: Using async context manager
37+
async with CloudberryMCPClient() as client:
38+
tools = await client.list_tools()
39+
resources = await client.list_resources()
40+
41+
# Method 2: Using create class method
42+
client = await CloudberryMCPClient.create()
43+
tools = await client.list_tools()
44+
await client.close()
45+
46+
# Method 3: Manual initialization
47+
client = CloudberryMCPClient()
48+
await client.initialize()
49+
tools = await client.list_tools()
50+
await client.close()
51+
"""
52+
53+
def __init__(self, mode: str = "stdio", server_url: str = "http://localhost:8000/mcp/"):
54+
self.mode = mode
55+
self.server_url = server_url
56+
self.client: Optional[Client] = None
57+
58+
@classmethod
59+
async def create(cls, mode: str = "stdio", server_url: str = "http://localhost:8000/mcp/") -> "CloudberryMCPClient":
60+
"""Asynchronously create and initialize the client"""
61+
instance = cls(mode, server_url)
62+
await instance.initialize()
63+
return instance
64+
65+
async def initialize(self):
66+
"""Initialize the client connection"""
67+
if self.mode == "stdio":
68+
server_config = ServerConfig.from_env()
69+
db_config = DatabaseConfig.from_env()
70+
server = CloudberryMCPServer(server_config, db_config)
71+
self.client = Client(server.mcp)
72+
else:
73+
self.client = Client(self.server_url)
74+
75+
await self.client.__aenter__()
76+
77+
async def close(self):
78+
"""Close the client connection"""
79+
if self.client:
80+
await self.client.__aexit__(None, None, None)
81+
self.client = None
82+
83+
async def __aenter__(self):
84+
if self.mode == "stdio":
85+
server_config = ServerConfig.from_env()
86+
db_config = DatabaseConfig.from_env()
87+
server = CloudberryMCPServer(server_config, db_config)
88+
self.client = Client(server.mcp)
89+
else:
90+
self.client = Client(self.server_url)
91+
92+
await self.client.__aenter__()
93+
return self
94+
95+
async def __aexit__(self, exc_type, exc_val, exc_tb):
96+
if self.client:
97+
await self.client.__aexit__(exc_type, exc_val, exc_tb)
98+
99+
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
100+
"""Call a tool on the MCP server"""
101+
if not self.client:
102+
raise RuntimeError("Client not initialized. Use async with statement.")
103+
104+
return await self.client.call_tool(tool_name, arguments)
105+
106+
async def get_resource(self, resource_uri: str):
107+
"""Get a resource from the MCP server"""
108+
if not self.client:
109+
raise RuntimeError("Client not initialized. Use async with statement.")
110+
111+
return await self.client.read_resource(resource_uri)
112+
113+
async def get_prompt(self, prompt_name: str, params: Dict[str, Any]=None):
114+
"""Get a prompt from the MCP server"""
115+
if not self.client:
116+
raise RuntimeError("Client not initialized. Use async with statement.")
117+
118+
return await self.client.get_prompt(prompt_name, params)
119+
120+
async def list_tools(self) -> list:
121+
"""List available tools on the server"""
122+
if not self.client:
123+
raise RuntimeError("Client not initialized. Use async with statement.")
124+
125+
return await self.client.list_tools()
126+
127+
async def list_resources(self) -> list:
128+
"""List available resources on the server"""
129+
if not self.client:
130+
raise RuntimeError("Client not initialized. Use async with statement.")
131+
132+
return await self.client.list_resources()
133+
134+
async def list_prompts(self) -> list:
135+
"""List available prompts on the server"""
136+
if not self.client:
137+
raise RuntimeError("Client not initialized. Use async with statement.")
138+
139+
return await self.client.list_prompts()
140+
141+
142+
if __name__ == "__main__":
143+
import asyncio
144+
145+
async def main():
146+
async with CloudberryMCPClient(mode="http") as client:
147+
results = await client.call_tool("execute_query", {
148+
"query": "SELECT * FROM film LIMIT 5"
149+
})
150+
print("Results:", results)
151+
152+
results = await client.call_tool("list_columns", {
153+
"table": "film",
154+
"schema": "public"
155+
})
156+
print("Columns:", results)
157+
158+
asyncio.run(main())

0 commit comments

Comments
 (0)