feat(§03): 实现 MCP Server 核心 - Storage/Service/Tool/MCP Protocol Layers
**Storage Layer:** - Database 类(aiosqlite + WAL + 并发保护) - WikiPage 数据模型 - fix_dirty_states 恢复机制 - FTS5 全文搜索支持 - 完整的 SQLite 表结构 **Service Layer:** - CacheService(LRU 缓存 + TTL + 大小限制) - QueryService(查询服务) - ParserService(Markdown 解析) - IndexerService(索引服务) - GraphService(链接图服务) **Tool Layer (8 个 MCP Tools):** - wiki_query - FTS5 全文搜索 + 标签过滤 - memory_bridge - 按工具来源浏览 - wiki_status - 索引状态 - wiki_lint - 健康审计 - cross_linker - 缺失链接发现 - tag_taxonomy - 标签一致性 - wiki_synthesize - 跨概念综合分析 - daily_update - 日常维护 + hot.md 生成 **MCP Protocol Layer:** - MCP 协议解析和封装 - 工具注册和路由 - 错误处理和日志 - stdio 模式支持 **配置和部署:** - requirements.txt - config.example.yaml - ecosystem.config.cjs (PM2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
MCP Server - Wiki MCP Server 主入口
|
||||
|
||||
实现 MCP 协议解析、工具注册和路由。
|
||||
|
||||
参考设计文档:第 2.1 节
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
from pathlib import Path
|
||||
|
||||
# MCP SDK
|
||||
try:
|
||||
from mcp.server.models import InitializationOptions
|
||||
from mcp.server import Server, NotificationOptions
|
||||
from mcp.types import Tool, TextContent
|
||||
MCP_AVAILABLE = True
|
||||
except ImportError:
|
||||
MCP_AVAILABLE = False
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning("MCP SDK not available, using mock implementation")
|
||||
|
||||
# 本地模块
|
||||
from .storage import Database, fix_dirty_states
|
||||
from .services import CacheService, QueryService, ParserService, IndexerService, GraphService
|
||||
from .tools import (
|
||||
WikiQueryTool, MemoryBridgeTool, WikiStatusTool,
|
||||
WikiLintTool, CrossLinkerTool, TagTaxonomyTool,
|
||||
WikiSynthesizeTool, DailyUpdateTool
|
||||
)
|
||||
|
||||
# 配置
|
||||
DEFAULT_WIKI_VAULT = "/Volumes/KnowledgeBase/wiki-vault"
|
||||
DEFAULT_INDEX_PATH = "~/.sanguo-llmwiki/index.db"
|
||||
DEFAULT_LOG_LEVEL = "INFO"
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, DEFAULT_LOG_LEVEL),
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MCPServer:
|
||||
"""MCP 服务器封装类"""
|
||||
|
||||
def __init__(self, wiki_vault_path: str, index_path: str):
|
||||
self.wiki_vault_path = wiki_vault_path
|
||||
self.index_path = index_path
|
||||
self.db: Optional[Database] = None
|
||||
self.tools: Dict[str, Callable] = {}
|
||||
self.tool_schemas: Dict[str, dict] = {}
|
||||
|
||||
# 如果 MCP SDK 可用,创建 Server 实例
|
||||
if MCP_AVAILABLE:
|
||||
self.server = Server("wiki-mcp")
|
||||
self._setup_handlers()
|
||||
else:
|
||||
self.server = None
|
||||
logger.warning("Running in mock mode (MCP SDK not available)")
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""初始化数据库和服务"""
|
||||
logger.info("Initializing Wiki MCP Server...")
|
||||
|
||||
# 初始化数据库
|
||||
self.db = Database(self.index_path)
|
||||
await self.db.connect()
|
||||
|
||||
# 恢复脏状态
|
||||
await fix_dirty_states(self.db)
|
||||
|
||||
# 初始化服务
|
||||
self.cache = CacheService(max_size=1000)
|
||||
self.query_service = QueryService(self.db, self.cache)
|
||||
self.parser_service = ParserService()
|
||||
self.indexer_service = IndexerService(self.db, self.parser_service, self.wiki_vault_path)
|
||||
self.graph_service = GraphService(self.db)
|
||||
|
||||
# 初始化工具
|
||||
self._init_tools()
|
||||
|
||||
logger.info("Wiki MCP Server initialized")
|
||||
|
||||
def _init_tools(self) -> None:
|
||||
"""初始化所有工具"""
|
||||
wiki_query = WikiQueryTool(self.query_service)
|
||||
memory_bridge = MemoryBridgeTool(self.query_service)
|
||||
wiki_status = WikiStatusTool(self.query_service, self.indexer_service, self.graph_service)
|
||||
wiki_lint = WikiLintTool(self.parser_service, self.query_service, self.graph_service)
|
||||
cross_linker = CrossLinkerTool(self.graph_service)
|
||||
tag_taxonomy = TagTaxonomyTool(self.query_service)
|
||||
wiki_synthesize = WikiSynthesizeTool(self.query_service, self.graph_service)
|
||||
daily_update = DailyUpdateTool(self.query_service, self.indexer_service)
|
||||
|
||||
# 注册工具
|
||||
self.tools = {
|
||||
"wiki_query": wiki_query.handle,
|
||||
"memory_bridge": memory_bridge.handle,
|
||||
"wiki_status": wiki_status.handle,
|
||||
"wiki_lint": wiki_lint.handle,
|
||||
"cross_linker": cross_linker.handle,
|
||||
"tag_taxonomy": tag_taxonomy.handle,
|
||||
"wiki_synthesize": wiki_synthesize.handle,
|
||||
"daily_update": daily_update.handle
|
||||
}
|
||||
|
||||
# 注册工具 schema
|
||||
self.tool_schemas = {
|
||||
"wiki_query": wiki_query.get_schema(),
|
||||
"memory_bridge": memory_bridge.get_schema(),
|
||||
"wiki_status": wiki_status.get_schema(),
|
||||
"wiki_lint": wiki_lint.get_schema(),
|
||||
"cross_linker": cross_linker.get_schema(),
|
||||
"tag_taxonomy": tag_taxonomy.get_schema(),
|
||||
"wiki_synthesize": wiki_synthesize.get_schema(),
|
||||
"daily_update": daily_update.get_schema()
|
||||
}
|
||||
|
||||
logger.info(f"Registered {len(self.tools)} tools")
|
||||
|
||||
def _setup_handlers(self) -> None:
|
||||
"""设置 MCP 处理器"""
|
||||
if not self.server:
|
||||
return
|
||||
|
||||
@self.server.list_tools()
|
||||
async def list_tools() -> list[Tool]:
|
||||
"""列出所有可用工具"""
|
||||
return [
|
||||
Tool(
|
||||
name=schema["name"],
|
||||
description=schema["description"],
|
||||
inputSchema=schema["inputSchema"]
|
||||
)
|
||||
for schema in self.tool_schemas.values()
|
||||
]
|
||||
|
||||
@self.server.call_tool()
|
||||
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
||||
"""调用工具"""
|
||||
if name not in self.tools:
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
try:
|
||||
result = await self.tools[name](**arguments)
|
||||
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling tool {name}: {e}")
|
||||
return [TextContent(type="text", text=json.dumps({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}, ensure_ascii=False))]
|
||||
|
||||
async def run(self) -> None:
|
||||
"""运行 MCP 服务器"""
|
||||
if not self.server:
|
||||
logger.error("MCP Server not available (MCP SDK not installed)")
|
||||
return
|
||||
|
||||
# 运行服务器
|
||||
async with self.server.stdio_stdio() as (read_stream, write_stream):
|
||||
await self.server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
InitializationOptions(
|
||||
server_name="wiki-mcp",
|
||||
server_version="1.0.0",
|
||||
capabilities=self.server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""关闭服务器"""
|
||||
if self.db:
|
||||
await self.db.close()
|
||||
logger.info("Database connection closed")
|
||||
|
||||
|
||||
async def main():
|
||||
"""主入口"""
|
||||
import os
|
||||
|
||||
# 从环境变量读取配置
|
||||
wiki_vault_path = os.environ.get("WIKI_VAULT_PATH", DEFAULT_WIKI_VAULT)
|
||||
index_path = os.path.expanduser(os.environ.get("WIKI_INDEX_PATH", DEFAULT_INDEX_PATH))
|
||||
log_level = os.environ.get("LOG_LEVEL", DEFAULT_LOG_LEVEL)
|
||||
|
||||
# 设置日志级别
|
||||
logging.getLogger().setLevel(getattr(logging, log_level))
|
||||
|
||||
# 检查 wiki vault 路径
|
||||
if not Path(wiki_vault_path).exists():
|
||||
logger.error(f"Wiki vault path does not exist: {wiki_vault_path}")
|
||||
sys.exit(1)
|
||||
|
||||
# 创建并运行服务器
|
||||
server = MCPServer(wiki_vault_path, index_path)
|
||||
|
||||
try:
|
||||
await server.initialize()
|
||||
await server.run()
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Interrupted by user")
|
||||
finally:
|
||||
await server.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user