Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
399 changes: 399 additions & 0 deletions mcp-server/README.md

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions mcp-server/dotenv.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

# Example environment configuration
# Copy this file to .env and update with your actual values

# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=postgres
DB_USER=postgres
DB_PASSWORD=your_password_here

# Server Configuration
MCP_HOST=localhost
MCP_PORT=8000
MCP_DEBUG=false
70 changes: 70 additions & 0 deletions mcp-server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

[project]
name = "cloudberry-mcp-server"
version = "0.1.0"
description = "MCP server for Apache Cloudberry database interaction"
readme = "README.md"
requires-python = ">=3.10"
authors = [
{name = "Shengwen Yang", email = "[email protected]"},
]
maintainers = [
{name = "Shengwen Yang", email = "[email protected]"},
]
license = {text = "Apache License 2.0"}
keywords = ["mcp", "cloudberry", "postgresql", "database", "server", "ai"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License, Version 2.0",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Database",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Database :: Front-Ends",
]
dependencies = [
"fastmcp>=2.10.6",
"psycopg2-binary==2.9.10",
"asyncpg>=0.29.0",
"pydantic>=2.0.0",
"python-dotenv>=1.0.0",
"aiohttp>=3.12.15",
"starlette>=0.27.0",
]

[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"pytest-cov>=4.0.0",
]

[project.urls]
Homepage = "https://github.com/apache/cloudberry//tree/main/mcp-server"
Repository = "https://github.com/apache/cloudberry"
Documentation = "https://github.com/apache/cloudberry/mcp-server/tree/main/mcp-server/README.md"

[project.scripts]
cloudberry-mcp-server = "cbmcp.server:main"
32 changes: 32 additions & 0 deletions mcp-server/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--tb=short
--strict-markers
--disable-warnings
asyncio_mode = auto
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
unit: marks tests as unit tests
38 changes: 38 additions & 0 deletions mcp-server/run_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.


# Test script for Apache Cloudberry MCP Server

echo "=== Install test dependencies ==="
uv pip install -e ".[dev]"

echo "=== Run all tests ==="
uv run pytest tests/ -v

echo "=== Run specific test patterns ==="
echo "Run stdio mode test:"
uv run pytest tests/test_cbmcp.py::TestCloudberryMCPClient::test_list_capabilities -v

echo "Run http mode test:"
uv run pytest tests/test_cbmcp.py::TestCloudberryMCPClient::test_list_capabilities -v

echo "=== Run coverage tests ==="
uv run pytest tests/ --cov=cbmcp --cov-report=html --cov-report=term

echo "=== Test completed ==="
32 changes: 32 additions & 0 deletions mcp-server/src/cbmcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#

"""
Apache Cloudberry MCP Server Package
"""

from .server import CloudberryMCPServer
from .client import CloudberryMCPClient
from .config import DatabaseConfig, ServerConfig
from .database import DatabaseManager
from .security import SQLValidator

__version__ = "0.1.0"
__all__ = [
"CloudberryMCPServer",
"CloudberryMCPClient",
"DatabaseConfig",
"ServerConfig",
"DatabaseManager",
"SQLValidator",
]
27 changes: 27 additions & 0 deletions mcp-server/src/cbmcp/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""
Main entry point for the cbmcp package
"""

from .server import main

if __name__ == "__main__":
main()
158 changes: 158 additions & 0 deletions mcp-server/src/cbmcp/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""
MCP Client for testing the Apache Cloudberry MCP Server

A client using the fastmcp SDK to interact with the Apache Cloudberry MCP server implementation.
"""

from typing import Any, Dict, Optional
from fastmcp import Client

from .config import DatabaseConfig, ServerConfig
from .server import CloudberryMCPServer

class CloudberryMCPClient:
"""MCP client for testing the Apache Cloudberry server using fastmcp SDK

Usage:
# Method 1: Using async context manager
async with CloudberryMCPClient() as client:
tools = await client.list_tools()
resources = await client.list_resources()

# Method 2: Using create class method
client = await CloudberryMCPClient.create()
tools = await client.list_tools()
await client.close()

# Method 3: Manual initialization
client = CloudberryMCPClient()
await client.initialize()
tools = await client.list_tools()
await client.close()
"""

def __init__(self, mode: str = "stdio", server_url: str = "http://localhost:8000/mcp/"):
self.mode = mode
self.server_url = server_url
self.client: Optional[Client] = None

@classmethod
async def create(cls, mode: str = "stdio", server_url: str = "http://localhost:8000/mcp/") -> "CloudberryMCPClient":
"""Asynchronously create and initialize the client"""
instance = cls(mode, server_url)
await instance.initialize()
return instance

async def initialize(self):
"""Initialize the client connection"""
if self.mode == "stdio":
server_config = ServerConfig.from_env()
db_config = DatabaseConfig.from_env()
server = CloudberryMCPServer(server_config, db_config)
self.client = Client(server.mcp)
else:
self.client = Client(self.server_url)

await self.client.__aenter__()

async def close(self):
"""Close the client connection"""
if self.client:
await self.client.__aexit__(None, None, None)
self.client = None

async def __aenter__(self):
if self.mode == "stdio":
server_config = ServerConfig.from_env()
db_config = DatabaseConfig.from_env()
server = CloudberryMCPServer(server_config, db_config)
self.client = Client(server.mcp)
else:
self.client = Client(self.server_url)

await self.client.__aenter__()
return self

async def __aexit__(self, exc_type, exc_val, exc_tb):
if self.client:
await self.client.__aexit__(exc_type, exc_val, exc_tb)

async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
"""Call a tool on the MCP server"""
if not self.client:
raise RuntimeError("Client not initialized. Use async with statement.")

return await self.client.call_tool(tool_name, arguments)

async def get_resource(self, resource_uri: str):
"""Get a resource from the MCP server"""
if not self.client:
raise RuntimeError("Client not initialized. Use async with statement.")

return await self.client.read_resource(resource_uri)

async def get_prompt(self, prompt_name: str, params: Dict[str, Any]=None):
"""Get a prompt from the MCP server"""
if not self.client:
raise RuntimeError("Client not initialized. Use async with statement.")

return await self.client.get_prompt(prompt_name, params)

async def list_tools(self) -> list:
"""List available tools on the server"""
if not self.client:
raise RuntimeError("Client not initialized. Use async with statement.")

return await self.client.list_tools()

async def list_resources(self) -> list:
"""List available resources on the server"""
if not self.client:
raise RuntimeError("Client not initialized. Use async with statement.")

return await self.client.list_resources()

async def list_prompts(self) -> list:
"""List available prompts on the server"""
if not self.client:
raise RuntimeError("Client not initialized. Use async with statement.")

return await self.client.list_prompts()


if __name__ == "__main__":
import asyncio

async def main():
async with CloudberryMCPClient(mode="http") as client:
results = await client.call_tool("execute_query", {
"query": "SELECT * FROM film LIMIT 5"
})
print("Results:", results)

results = await client.call_tool("list_columns", {
"table": "film",
"schema": "public"
})
print("Columns:", results)

asyncio.run(main())
Loading
Loading