init
This commit is contained in:
@@ -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"
|
||||
@@ -0,0 +1,4 @@
|
||||
from .main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user