From 1183bb1c474badb96acda0d4dc1b256d856b7c5c Mon Sep 17 00:00:00 2001 From: Damien Berezenko Date: Sun, 1 Mar 2026 00:37:39 +0000 Subject: [PATCH] feat(mcp/server): improve MCP server logging and suppress output during tool execution --- codebase_rag/mcp/server.py | 100 +++++++++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 25 deletions(-) diff --git a/codebase_rag/mcp/server.py b/codebase_rag/mcp/server.py index 9e05e36e0..f82380eb5 100644 --- a/codebase_rag/mcp/server.py +++ b/codebase_rag/mcp/server.py @@ -6,7 +6,6 @@ import json import os -import sys from pathlib import Path from loguru import logger @@ -20,14 +19,54 @@ from codebase_rag.services.llm import CypherGenerator -def setup_logging() -> None: - """Configure logging to stderr for MCP stdio transport.""" +def setup_logging(enable_logging: bool = False) -> None: + """Configure logging for MCP stdio transport. + + By default, logging is disabled to prevent token waste in LLM context. + Can be enabled via environment variable MCP_ENABLE_LOGGING=1 for debugging. + + When enabled, logs are written to a file to avoid polluting STDIO transport. + The log file path can be configured via MCP_LOG_FILE environment variable. + + Args: + enable_logging: Whether to enable logging output. Defaults to False. + Can also be controlled via MCP_ENABLE_LOGGING environment variable. + """ logger.remove() # Remove default handler - logger.add( - sys.stderr, - level="INFO", - format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}", + + # Check environment variable to override enable_logging parameter + env_enable = os.environ.get("MCP_ENABLE_LOGGING", "").lower() in ( + "1", + "true", + "yes", ) + should_enable = enable_logging or env_enable + + if should_enable: + # Get log file path from environment or use default + log_file = os.environ.get("MCP_LOG_FILE") + if not log_file: + # Use ~/.cache/code-graph-rag/mcp.log as default + cache_dir = Path.home() / ".cache" / "code-graph-rag" + cache_dir.mkdir(parents=True, exist_ok=True) + log_file = str(cache_dir / "mcp.log") + + # Ensure log file directory exists + log_path = Path(log_file) + log_path.parent.mkdir(parents=True, exist_ok=True) + + # Add file handler - logs go to file, not STDERR/STDOUT + logger.add( + log_file, + level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}", + colorize=False, # Disable ANSI color codes + rotation="10 MB", # Rotate when file reaches 10MB + retention="7 days", # Keep logs for 7 days + ) + else: + # Disable all logging by default for MCP mode + logger.disable("codebase_rag") def get_project_root() -> Path: @@ -143,34 +182,45 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: Tool handlers are dynamically resolved from the MCPToolsRegistry, ensuring consistency with tool definitions. + + Logging is suppressed during tool execution to prevent token waste in LLM context. """ - logger.info(f"[GraphCode MCP] Calling tool: {name}") + import io + from contextlib import redirect_stderr, redirect_stdout try: # Resolve handler from registry handler_info = tools.get_tool_handler(name) if not handler_info: - error_msg = f"Unknown tool: {name}" - logger.error(f"[GraphCode MCP] {error_msg}") + error_msg = "Unknown tool" return [TextContent(type="text", text=f"Error: {error_msg}")] handler, returns_json = handler_info - # Call handler with unpacked arguments - result = await handler(**arguments) - - # Format result based on output type - if returns_json: - result_text = json.dumps(result, indent=2) - else: - result_text = str(result) - - return [TextContent(type="text", text=result_text)] - - except Exception as e: - error_msg = f"Error executing tool '{name}': {str(e)}" - logger.error(f"[GraphCode MCP] {error_msg}", exc_info=True) - return [TextContent(type="text", text=f"Error: {error_msg}")] + # Suppress all logging output during tool execution + with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()): + logger.disable("codebase_rag") + try: + # Call handler with unpacked arguments + result = await handler(**arguments) + + # Format result based on output type + if returns_json: + result_text = json.dumps(result, indent=2) + else: + result_text = str(result) + + return [TextContent(type="text", text=result_text)] + finally: + logger.enable("codebase_rag") + + except Exception: + # Fail silently without logging or printing error details + return [ + TextContent( + type="text", text="Error: There was an error executing the tool" + ) + ] return server, ingestor