This commit is contained in:
Chen, Li
2026-10-09 11:03:24 +08:00
parent dcfdf447e8
commit a9ebe1bf99
11 changed files with 1255 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
"""Mini DB MCP – a minimal database MCP server."""
from importlib.metadata import PackageNotFoundError, version
try:
__version__ = version("mini-db-mcp")
except PackageNotFoundError:
__version__ = "0.0.0"
+4
View File
@@ -0,0 +1,4 @@
from .main import main
if __name__ == "__main__":
main()
+45
View File
@@ -0,0 +1,45 @@
from typing import Literal
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
_ENV_FILE = ".env"
class DatabaseConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="DB_", env_file=_ENV_FILE, extra="ignore")
host: str = "127.0.0.1"
port: int = 3306
user: str = "root"
password: str = ""
database: str = ""
pool_min: int = 1
pool_max: int = 10
connect_timeout: int = 30
class QueryConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="QUERY_", env_file=_ENV_FILE, extra="ignore")
max_rows: int = 1000
timeout_seconds: int = 30
allow_write: bool = False
class ServerConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="MCP_", env_file=_ENV_FILE, extra="ignore")
transport: Literal["stdio", "http"] = "stdio"
host: str = "127.0.0.1"
port: int = 8000
# LOG_LEVEL has no MCP_ prefix so it's declared with an explicit alias
log_level: str = Field(default="INFO", validation_alias="LOG_LEVEL")
class Config(BaseSettings):
model_config = SettingsConfigDict(env_file=_ENV_FILE, extra="ignore")
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
query: QueryConfig = Field(default_factory=QueryConfig)
server: ServerConfig = Field(default_factory=ServerConfig)
+42
View File
@@ -0,0 +1,42 @@
import json
from typing import Annotated, Any, Literal, Self, cast, TypedDict
from pydantic import (
BaseModel,
ConfigDict,
JsonValue
)
JsonObject = dict[str, JsonValue]
class DomainBaseModel(BaseModel):
"""Base class with strict shape and canonical wire serialization."""
model_config = ConfigDict(
extra="forbid",
frozen=True,
str_strip_whitespace=True,
validate_default=True,
)
def to_wire(self) -> JsonObject:
"""Return a JSON-compatible representation with protocol aliases."""
return cast(
JsonObject,
self.model_dump(
by_alias=True,
mode="json",
exclude_none=True,
),
)
def to_canonical_json(self) -> str:
"""Serialize deterministically for manifests, tests, and hashing."""
return json.dumps(
self.to_wire(),
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
)
+49
View File
@@ -0,0 +1,49 @@
import logging
import sys
import asyncio
from contextlib import asynccontextmanager
from typing import Any
from mcp.server import Server
from mcp.types import (
INTERNAL_ERROR,
INVALID_PARAMS,
CallToolResult,
GetPromptResult,
ListPromptsResult,
ListResourcesResult,
ListToolsResult,
ReadResourceResult,
TextContent,
TextResourceContents,
)
from .config import Config
from .prompts import MissingPromptArgumentError, PromptsManager, UnknownPromptError
from .server import MiniServer
from . import __version__
logger = logging.getLogger(__name__)
def _setup_logging(level: str) -> None:
logging.basicConfig(
level=getattr(logging, level, logging.INFO),
format="%(asctime)s %(name)s %(levelname)s %(message)s",
stream=sys.stderr,
)
def main() -> None:
config = Config()
server = MiniServer(config)
_setup_logging(config.server.log_level)
transport = config.server.transport
if transport == "stdio":
asyncio.run(server.start_stdio())
else:
logger.error("Unsupported transport: %s", transport)
if __name__ == "__main__":
main()
+150
View File
@@ -0,0 +1,150 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
from mcp.types import (
GetPromptResult,
Prompt,
PromptArgument,
PromptMessage,
TextContent,
)
class PromptTemplate:
"""Prompt template"""
def __init__(
self,
name: str,
description: str,
template: str,
arguments: list[PromptArgument] | None = None,
category: str = "general",
):
self.name = name
self.description = description
self.template = template
self.arguments = arguments or []
self.category = category
self.created_at = datetime.now()
def render(self, arguments: dict[str, Any]) -> str:
"""Render template content"""
content = self.template
for key, value in arguments.items():
placeholder = f"{{{key}}}"
content = content.replace(placeholder, str(value))
return content
class UnknownPromptError(ValueError):
error_code = "UNKNOWN_PROMPT"
class MissingPromptArgumentError(ValueError):
error_code = "MISSING_REQUIRED_ARGUMENT"
class PromptsManager:
"""Manager for handling prompt templates.
Init the default prompt templates and provide methods to list and get prompts.
"""
def __init__(self):
self.templates = self._init_templates()
def _init_templates(self) -> dict[str, PromptTemplate]:
templates = {}
templates["performance_optimization"] = PromptTemplate(
name="performance_optimization",
description="Database performance optimization analysis template for identifying performance bottlenecks and optimization opportunities",
template="""Please help me with database performance analysis and optimization recommendations:
Focus area: {focus_area}
{table_scope}
Performance metrics: {metrics}
Please generate SQL queries to analyze the following content:
1. Table and query performance statistics
2. Index usage efficiency analysis
3. Slow query identification and analysis
4. Storage space usage
Analysis objectives:
- Identify performance bottlenecks
- Provide optimization recommendations
- Evaluate optimization effects
Please provide specific optimization recommendations and implementation steps.""",
arguments=[
PromptArgument(
name="focus_area",
description="Performance area of focus, such as 'query performance', 'storage optimization'",
required=True,
),
PromptArgument(
name="table_name",
description="Specific table name (optional), if analyzing specific table performance",
required=False,
),
PromptArgument(
name="metrics",
description="Performance metrics of interest, such as 'response time', 'throughput'",
required=False,
),
],
category="performance",
)
return templates
async def list_prompts(self) -> list[Prompt]:
"""List all available prompt templates"""
prompts = []
for template in self.templates.values():
prompt = Prompt(
name=template.name,
description=template.description,
arguments=template.arguments,
)
prompts.append(prompt)
return prompts
async def get_prompt(self, name: str, arguments: dict[str, Any]) -> GetPromptResult:
"""Get content of specific prompt template"""
if name not in self.templates:
raise UnknownPromptError(f"Prompt template named '{name}' not found")
template = self.templates[name]
# Build arguments
processed_args = await self._process_arguments(template, arguments)
# Render template content
rendered_content = template.render(processed_args)
full_content = f"""{rendered_content}
Please generate accurate and efficient SQL queries based on the above requirements and database structure."""
return GetPromptResult(
description=template.description,
messages=[
PromptMessage(
role="user", content=TextContent(type="text", text=full_content)
)
],
)
async def _process_arguments(self, template: PromptTemplate, arguments: dict[str, Any]) -> dict[str, Any]:
"""Process template arguments"""
processed_arguments = {}
for arg in template.arguments:
if arg.name in arguments:
processed_arguments[arg.name] = arguments[arg.name]
elif arg.required:
raise MissingPromptArgumentError(arg.name)
return processed_arguments
+88
View File
@@ -0,0 +1,88 @@
from __future__ import annotations
import logging
from typing import Any, Protocol
from mcp.server import Server
from mcp.types import (
CallToolResult,
GetPromptResult,
ListPromptsResult,
ListResourcesResult,
ListToolsResult,
ReadResourceResult,
TextContent,
TextResourceContents,
Resource,
Tool,
Prompt
)
from .config import Config
from .prompts import PromptsManager
from . import __version__
NAME = "mini-db-mcp"
logger = logging.getLogger(__name__)
class ResourcesProtocol(Protocol):
async def list_resources(self) -> list[Resource]: ...
async def read_resource(self, uri: str) -> str: ...
class ToolsProtocol(Protocol):
async def list_tools(self) -> list[Tool]: ...
async def call_tool(self, name: str, arguments: dict[str, Any]) -> str: ...
class PromptsProtocol(Protocol):
async def list_prompts(self) -> list[Prompt]: ...
async def get_prompt(
self, name: str, arguments: dict[str, Any]
) -> GetPromptResult: ...
class MiniServer():
def __init__(self, config: Config) -> None:
self.config = config
self.prompts_manager = PromptsManager()
self.server = _create_mcp_server(
prompts_manager=self.prompts_manager
)
async def start_stdio(self) -> None:
from mcp.server.stdio import stdio_server
async with stdio_server() as streams:
read_stream, write_stream = streams
await self.server.run(
read_stream,
write_stream,
self.server.create_initialization_options(),
)
def _create_mcp_server(
*,
prompts_manager: PromptsProtocol | None = None,
) -> Server:
async def on_list_prompts(ctx, params):
return ListPromptsResult(prompts=await prompts_manager.list_prompts())
async def on_get_prompt(ctx, params):
return await prompts_manager.get_prompt(params.name, dict(params.arguments or {}))
server = Server(
name=NAME,
version=__version__,
on_list_prompts=on_list_prompts,
on_get_prompt=on_get_prompt,
)
return server