From 2917c102b7f6b6b8331684d70cd4789aabc5c423 Mon Sep 17 00:00:00 2001 From: wmymz Date: Mon, 10 Nov 2025 10:22:11 +0800 Subject: [PATCH] feat: optimize logging system and add tool debugging interface (v0.1.5) - Redirect FastAPI/Uvicorn logs to loguru to prevent MCP stdio protocol pollution - Implement InterceptHandler class to intercept standard library logging - Configure uvicorn with log_config=None to disable default logging - Remove console log output, only output to file - Add /api/tools endpoint to list available tools - Update README with version 0.1.5 features (Chinese and English) --- .gitignore | 1 + README.md | 18 ++++++++++++- README_EN.md | 18 ++++++++++++- pyproject.toml | 2 +- src/acemcp/logging_config.py | 52 +++++++++++++++++++++++++++++++----- src/acemcp/server.py | 9 ++++--- src/acemcp/web/app.py | 21 +++++++++++++++ uv.lock | 2 +- 8 files changed, 109 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 0a18b2e..caea720 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ venv # ai .cunzhi-memory/ +.kilocode/ diff --git a/README.md b/README.md index 9767b2e..11ccf3d 100644 --- a/README.md +++ b/README.md @@ -331,7 +331,23 @@ Web 管理界面提供: ## 最近更新 -### 版本 0.1.3(最新) +### 版本 0.1.5(最新) + +**新特性:** +- ✨ **日志系统优化**:将 FastAPI/Uvicorn 日志重定向到 loguru,防止污染 MCP stdio 协议 +- ✨ **工具调试接口**:Web 管理界面新增工具列表和调试功能 + +**改进:** +- 🔧 **日志输出控制**:移除控制台日志输出,仅输出到文件,避免干扰 stdio 协议 +- 🔧 **标准库日志拦截**:使用 `InterceptHandler` 拦截所有标准库日志 +- 🔧 **Web API 增强**:新增 `/api/tools` 端点列出可用工具 + +**技术细节:** +- 实现了 `InterceptHandler` 类来拦截标准库 logging +- 配置 uvicorn 使用 `log_config=None` 禁用默认日志 +- 所有日志统一输出到 `~/.acemcp/log/acemcp.log` + +### 版本 0.1.4 **新特性:** - ✨ **多编码支持**:自动检测和处理多种文件编码(UTF-8、GBK、GB2312、Latin-1) diff --git a/README_EN.md b/README_EN.md index 0b26f61..5b7d2d4 100644 --- a/README_EN.md +++ b/README_EN.md @@ -331,7 +331,23 @@ To enable the web interface, use the `--web-port` argument when starting the ser ## Recent Updates -### Version 0.1.3 (Latest) +### Version 0.1.5 (Latest) + +**New Features:** +- ✨ **Logging System Optimization**: Redirect FastAPI/Uvicorn logs to loguru to prevent pollution of MCP stdio protocol +- ✨ **Tool Debugging Interface**: Web management interface now includes tool listing and debugging functionality + +**Improvements:** +- 🔧 **Log Output Control**: Removed console log output, only output to file to avoid interfering with stdio protocol +- 🔧 **Standard Library Log Interception**: Use `InterceptHandler` to intercept all standard library logs +- 🔧 **Web API Enhancement**: New `/api/tools` endpoint to list available tools + +**Technical Details:** +- Implemented `InterceptHandler` class to intercept standard library logging +- Configured uvicorn with `log_config=None` to disable default logging +- All logs unified to output to `~/.acemcp/log/acemcp.log` + +### Version 0.1.4 **New Features:** - ✨ **Multi-Encoding Support**: Automatic detection and handling of multiple file encodings (UTF-8, GBK, GB2312, Latin-1) diff --git a/pyproject.toml b/pyproject.toml index 086440c..a568790 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "acemcp" -version = "0.1.4" +version = "0.1.5" description = "MCP server for codebase indexing and semantic search with multi-encoding support and .gitignore integration" readme = "README.md" authors = [ diff --git a/src/acemcp/logging_config.py b/src/acemcp/logging_config.py index 054ce95..136e027 100644 --- a/src/acemcp/logging_config.py +++ b/src/acemcp/logging_config.py @@ -1,5 +1,6 @@ """Global logging configuration for acemcp.""" +import logging from pathlib import Path from loguru import logger @@ -11,7 +12,31 @@ _console_handler_id: int | None = None _file_handler_id: int | None = None -def setup_logging() -> None: +class InterceptHandler(logging.Handler): + """Intercept standard logging messages and redirect them to loguru.""" + + def emit(self, record: logging.LogRecord) -> None: + """Emit a log record to loguru. + + Args: + record: The log record to emit + """ + # Get corresponding Loguru level if it exists + try: + level = logger.level(record.levelname).name + except ValueError: + level = record.levelno + + # Find caller from where originated the logged message + frame, depth = logging.currentframe(), 2 + while frame.f_code.co_filename == logging.__file__: + frame = frame.f_back + depth += 1 + + logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) + + +def setup_logging(intercept_stdlib: bool = False) -> None: """Setup global logging configuration with file rotation. Configures loguru to write logs to ~/.acemcp/log/acemcp.log with: @@ -21,6 +46,9 @@ def setup_logging() -> None: This function can be called multiple times safely - it will only configure once. Note: This function preserves any existing handlers (e.g., WebSocket log broadcaster). + + Args: + intercept_stdlib: If True, intercept standard library logging (uvicorn, fastapi, etc.) """ global _logging_configured, _console_handler_id, _file_handler_id # noqa: PLW0603 @@ -43,12 +71,13 @@ def setup_logging() -> None: pass # Add console handler with INFO level - _console_handler_id = logger.add( - sink=lambda msg: print(msg, end=""), - format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}", - level="INFO", - colorize=True, - ) + # Only output to file to avoid polluting stdio + # _console_handler_id = logger.add( + # sink=lambda msg: print(msg, end=""), + # format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}", + # level="INFO", + # colorize=True, + # ) # Add file handler with rotation _file_handler_id = logger.add( @@ -62,6 +91,15 @@ def setup_logging() -> None: enqueue=True, # Thread-safe logging ) + # Intercept standard library logging if requested + if intercept_stdlib: + logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True) + # Intercept specific loggers + for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "fastapi"]: + logging_logger = logging.getLogger(logger_name) + logging_logger.handlers = [InterceptHandler()] + logging_logger.propagate = False + _logging_configured = True logger.info(f"Logging configured: log file at {log_file}") diff --git a/src/acemcp/server.py b/src/acemcp/server.py index e252669..98f728e 100644 --- a/src/acemcp/server.py +++ b/src/acemcp/server.py @@ -72,13 +72,15 @@ async def run_web_server(port: int) -> None: port: Port to run the web server on """ web_app = create_app() - # Set log_level to "warning" to reduce WebSocket connection noise + # Configure uvicorn to use loguru through InterceptHandler + # This prevents uvicorn from polluting stdout (which breaks MCP stdio protocol) config_uvicorn = uvicorn.Config( web_app, host="0.0.0.0", port=port, log_level="warning", - access_log=False # Disable access log to reduce noise + access_log=False, # Disable access log to reduce noise + log_config=None, # Disable default logging config to use our interceptor ) server = uvicorn.Server(config_uvicorn) await server.serve() @@ -130,7 +132,8 @@ def run() -> None: get_log_broadcaster() # Initialize the broadcaster # Setup logging after log broadcaster is initialized - setup_logging() + # Intercept stdlib logging (uvicorn, fastapi) to prevent stdout pollution + setup_logging(intercept_stdlib=True) asyncio.run(main(base_url=args.base_url, token=args.token, web_port=args.web_port)) diff --git a/src/acemcp/web/app.py b/src/acemcp/web/app.py index 5dca2ae..8104d13 100644 --- a/src/acemcp/web/app.py +++ b/src/acemcp/web/app.py @@ -136,6 +136,27 @@ def create_app() -> FastAPI: return {"status": "running", "project_count": project_count, "storage_path": str(config.index_storage_path)} + @app.get("/api/tools") + async def list_tools() -> dict: + """List available tools for debugging. + + Returns: + Dictionary containing available tools and their descriptions + """ + return { + "tools": [ + { + "name": "search_context", + "description": "Search for code context in indexed projects", + "status": "stable", + "parameters": { + "project_root_path": "string (required) - Absolute path to project root", + "query": "string (required) - Search query", + }, + }, + ] + } + @app.post("/api/tools/execute") async def execute_tool(tool_request: ToolRequest) -> dict: """Execute a tool for debugging. diff --git a/uv.lock b/uv.lock index 45e130e..928f23a 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.10" [[package]] name = "acemcp" -version = "0.1.4" +version = "0.1.5" source = { editable = "." } dependencies = [ { name = "dynaconf" },