2026-09-21 18:08:31 +08:00
|
|
|
from . import __version__
|
2026-08-21 17:26:44 +08:00
|
|
|
|
2026-09-21 18:08:31 +08:00
|
|
|
import json
|
2026-08-21 17:26:44 +08:00
|
|
|
import logging
|
|
|
|
|
from typing import Any, Protocol
|
|
|
|
|
from mcp.server import Server
|
|
|
|
|
from mcp.types import (
|
|
|
|
|
CallToolResult,
|
|
|
|
|
GetPromptResult,
|
|
|
|
|
ListPromptsResult,
|
|
|
|
|
ListToolsResult,
|
2026-09-21 18:08:31 +08:00
|
|
|
PaginatedRequestParams,
|
2026-08-21 17:26:44 +08:00
|
|
|
TextContent,
|
|
|
|
|
Resource,
|
|
|
|
|
Tool,
|
|
|
|
|
Prompt
|
|
|
|
|
)
|
|
|
|
|
|
2026-09-21 18:08:31 +08:00
|
|
|
from mini_db_mcp.config import Config
|
|
|
|
|
from mini_db_mcp.db import Database
|
|
|
|
|
from mini_db_mcp.domain.domain_catalog import DOMAIN_CATALOG
|
|
|
|
|
from mini_db_mcp.domain.domain_handler_factory import create_domain_registry
|
|
|
|
|
from mini_db_mcp.utils.pagination import paginate_list
|
|
|
|
|
from mini_db_mcp.prompts import PromptsManager
|
|
|
|
|
from mini_db_mcp.tools import ToolsManager
|
|
|
|
|
|
2026-08-21 17:26:44 +08:00
|
|
|
|
|
|
|
|
NAME = "mini-db-mcp"
|
|
|
|
|
|
2026-09-21 18:08:31 +08:00
|
|
|
|
2026-08-21 17:26:44 +08:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2026-09-21 18:08:31 +08:00
|
|
|
|
2026-08-21 17:26:44 +08:00
|
|
|
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]: ...
|
|
|
|
|
|
2026-09-21 18:08:31 +08:00
|
|
|
async def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]: ...
|
2026-08-21 17:26:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
2026-09-21 18:08:31 +08:00
|
|
|
self.database = Database(config)
|
|
|
|
|
self.handler_registry = create_domain_registry(
|
|
|
|
|
metadata_store=self.database,
|
|
|
|
|
)
|
|
|
|
|
self.tools_manager = ToolsManager(
|
|
|
|
|
catalog=DOMAIN_CATALOG,
|
|
|
|
|
registry=self.handler_registry,
|
|
|
|
|
)
|
2026-08-21 17:26:44 +08:00
|
|
|
self.server = _create_mcp_server(
|
2026-09-21 18:08:31 +08:00
|
|
|
prompts_manager=self.prompts_manager,
|
|
|
|
|
tools_manager=self.tools_manager,
|
2026-08-21 17:26:44 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def start_stdio(self) -> None:
|
|
|
|
|
from mcp.server.stdio import stdio_server
|
|
|
|
|
|
2026-09-21 18:08:31 +08:00
|
|
|
try:
|
|
|
|
|
async with stdio_server() as streams:
|
|
|
|
|
read_stream, write_stream = streams
|
|
|
|
|
await self.server.run(
|
|
|
|
|
read_stream,
|
|
|
|
|
write_stream,
|
|
|
|
|
self.server.create_initialization_options(),
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
await self.database.close()
|
|
|
|
|
|
|
|
|
|
async def start_http(self) -> None:
|
|
|
|
|
"""Serve MCP over stateless Streamable HTTP at ``/mcp``."""
|
|
|
|
|
import uvicorn
|
|
|
|
|
|
|
|
|
|
app = self.server.streamable_http_app(
|
|
|
|
|
streamable_http_path="/mcp",
|
|
|
|
|
json_response=True,
|
|
|
|
|
stateless_http=True,
|
|
|
|
|
host=self.config.server.host,
|
|
|
|
|
)
|
|
|
|
|
server = uvicorn.Server(
|
|
|
|
|
uvicorn.Config(
|
|
|
|
|
app,
|
|
|
|
|
host=self.config.server.host,
|
|
|
|
|
port=self.config.server.port,
|
|
|
|
|
log_level=self.config.server.log_level.lower(),
|
2026-08-21 17:26:44 +08:00
|
|
|
)
|
2026-09-21 18:08:31 +08:00
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
await server.serve()
|
|
|
|
|
finally:
|
|
|
|
|
await self.database.close()
|
2026-08-21 17:26:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _create_mcp_server(
|
|
|
|
|
*,
|
|
|
|
|
prompts_manager: PromptsProtocol | None = None,
|
2026-09-21 18:08:31 +08:00
|
|
|
tools_manager: ToolsProtocol | None = None,
|
2026-08-21 17:26:44 +08:00
|
|
|
) -> 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 {}))
|
|
|
|
|
|
2026-09-21 18:08:31 +08:00
|
|
|
async def list_tools(
|
|
|
|
|
ctx,
|
|
|
|
|
params: PaginatedRequestParams | None,
|
|
|
|
|
) -> ListToolsResult:
|
|
|
|
|
tools = await tools_manager.list_tools()
|
|
|
|
|
page_items, next_cursor = paginate_list(
|
|
|
|
|
tools,
|
|
|
|
|
cursor=params.cursor if params else None,
|
|
|
|
|
page_size=10,
|
|
|
|
|
identifier=lambda tool: tool.name,
|
|
|
|
|
)
|
|
|
|
|
logger.info("Returning %d of %d tools", len(page_items), len(tools))
|
|
|
|
|
return ListToolsResult(
|
|
|
|
|
tools=page_items,
|
|
|
|
|
next_cursor=next_cursor,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def call_tool(ctx, params):
|
|
|
|
|
# ToolsManager returns either a manifest (discovery) or a child result.
|
|
|
|
|
result = await tools_manager.call_tool(
|
|
|
|
|
params.name,
|
|
|
|
|
dict(params.arguments or {}),
|
|
|
|
|
)
|
|
|
|
|
# Publish the same payload as structured data and backward-compatible text.
|
|
|
|
|
return CallToolResult(
|
|
|
|
|
content=[TextContent(type="text", text=json.dumps(result, ensure_ascii=False))],
|
|
|
|
|
structuredContent=result,
|
|
|
|
|
isError=result.get("mode") == "error",
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-21 17:26:44 +08:00
|
|
|
server = Server(
|
|
|
|
|
name=NAME,
|
|
|
|
|
version=__version__,
|
|
|
|
|
on_list_prompts=on_list_prompts,
|
|
|
|
|
on_get_prompt=on_get_prompt,
|
2026-09-21 18:08:31 +08:00
|
|
|
on_list_tools=list_tools,
|
|
|
|
|
on_call_tool=call_tool,
|
2026-08-21 17:26:44 +08:00
|
|
|
)
|
|
|
|
|
return server
|
|
|
|
|
|