diff --git a/plane_mcp/__main__.py b/plane_mcp/__main__.py index 28afb12..d943d90 100644 --- a/plane_mcp/__main__.py +++ b/plane_mcp/__main__.py @@ -1,19 +1,57 @@ """Main entry point for the Plane MCP Server.""" +import json +import logging import os import sys from contextlib import asynccontextmanager +from datetime import datetime, timezone from enum import Enum import uvicorn -from fastmcp.utilities.logging import get_logger from starlette.applications import Starlette from starlette.middleware.cors import CORSMiddleware from starlette.routing import Mount from plane_mcp.server import get_header_mcp, get_oauth_mcp, get_stdio_mcp -logger = get_logger(__name__) + +class JSONFormatter(logging.Formatter): + """JSON log formatter for structured logging (Datadog, ELK, etc.).""" + + def format(self, record: logging.LogRecord) -> str: + log_entry = { + "timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(), + "level": record.levelname, + "logger": record.name, + "message": record.getMessage(), + } + if record.exc_info and record.exc_info[1]: + log_entry["error"] = { + "type": type(record.exc_info[1]).__name__, + "message": str(record.exc_info[1]), + } + return json.dumps(log_entry) + + +def configure_json_logging(): + """Replace FastMCP's Rich handlers with a JSON formatter on the fastmcp logger.""" + fastmcp_logger = logging.getLogger("fastmcp") + + # Remove all existing handlers (Rich) + for handler in fastmcp_logger.handlers[:]: + fastmcp_logger.removeHandler(handler) + + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(JSONFormatter()) + fastmcp_logger.addHandler(handler) + fastmcp_logger.setLevel(logging.INFO) + fastmcp_logger.propagate = False + + +configure_json_logging() + +logger = logging.getLogger("fastmcp.plane_mcp") class ServerMode(Enum): @@ -80,8 +118,23 @@ def main() -> None: allow_headers=["*"], ) + # Configure uvicorn loggers to use JSON formatting too + for uv_logger_name in ("uvicorn", "uvicorn.error"): + uv_logger = logging.getLogger(uv_logger_name) + for h in uv_logger.handlers[:]: + uv_logger.removeHandler(h) + uv_handler = logging.StreamHandler(sys.stderr) + uv_handler.setFormatter(JSONFormatter()) + uv_logger.addHandler(uv_handler) + logger.info("Starting HTTP server at URLs: /mcp and /header/mcp") - uvicorn.run(app, host="0.0.0.0", port=8211, log_level="info") + uvicorn.run( + app, + host="0.0.0.0", + port=8211, + log_level="info", + access_log=False, + ) return diff --git a/plane_mcp/server.py b/plane_mcp/server.py index fb2ad67..3d6f12c 100644 --- a/plane_mcp/server.py +++ b/plane_mcp/server.py @@ -3,6 +3,7 @@ import os from fastmcp import FastMCP +from fastmcp.server.middleware.logging import StructuredLoggingMiddleware from key_value.aio.stores.memory import MemoryStore from key_value.aio.stores.redis import RedisStore from mcp.types import Icon @@ -57,6 +58,7 @@ def get_oauth_mcp(base_path: str = "/"): ], ), ) + oauth_mcp.add_middleware(StructuredLoggingMiddleware(include_payloads=True)) register_tools(oauth_mcp) return oauth_mcp @@ -68,6 +70,7 @@ def get_header_mcp(): required_scopes=["read", "write"], ), ) + header_mcp.add_middleware(StructuredLoggingMiddleware(include_payloads=True)) register_tools(header_mcp) return header_mcp @@ -76,5 +79,6 @@ def get_stdio_mcp(): stdio_mcp = FastMCP( "Plane MCP Server (stdio)", ) + stdio_mcp.add_middleware(StructuredLoggingMiddleware(include_payloads=True)) register_tools(stdio_mcp) return stdio_mcp diff --git a/pyproject.toml b/pyproject.toml index e61cfe0..f122791 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "plane-mcp-server" -version = "0.2.6" +version = "0.2.7" description = "A Model Context Protocol server for Plane integration" readme = "README.md" requires-python = ">=3.10"