Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions plane_mcp/__main__.py
Original file line number Diff line number Diff line change
@@ -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")
Comment on lines +37 to +54
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent logging configuration across logger hierarchies.

configure_json_logging() only configures the "fastmcp" logger, but other modules in this codebase use loggers under the "plane_mcp" hierarchy:

  • plane_mcp/server.py uses logging.getLogger(__name__)"plane_mcp.server"
  • plane_mcp/auth/ uses get_logger(__name__)"plane_mcp.auth.*"
  • plane_mcp/client.py uses get_logger(__name__)"plane_mcp.client"

These loggers are not children of "fastmcp", so they won't use the JSON formatter and will output in a different format (or propagate to root with default formatting).

Consider also configuring the "plane_mcp" logger hierarchy for consistent JSON output:

🔧 Proposed fix for consistent logging
 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
+    json_handler = logging.StreamHandler(sys.stderr)
+    json_handler.setFormatter(JSONFormatter())
+
+    for logger_name in ("fastmcp", "plane_mcp"):
+        target_logger = logging.getLogger(logger_name)
+        for h in target_logger.handlers[:]:
+            target_logger.removeHandler(h)
+        target_logger.addHandler(json_handler)
+        target_logger.setLevel(logging.INFO)
+        target_logger.propagate = False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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")
def configure_json_logging():
"""Replace FastMCP's Rich handlers with a JSON formatter on the fastmcp logger."""
json_handler = logging.StreamHandler(sys.stderr)
json_handler.setFormatter(JSONFormatter())
for logger_name in ("fastmcp", "plane_mcp"):
target_logger = logging.getLogger(logger_name)
for h in target_logger.handlers[:]:
target_logger.removeHandler(h)
target_logger.addHandler(json_handler)
target_logger.setLevel(logging.INFO)
target_logger.propagate = False
configure_json_logging()
logger = logging.getLogger("fastmcp.plane_mcp")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plane_mcp/__main__.py` around lines 37 - 54, The configure_json_logging
function only sets up the "fastmcp" logger, leaving "plane_mcp.*" loggers with
different formatting; update configure_json_logging to also configure the
"plane_mcp" logger hierarchy (or accept a list of logger names) by creating the
same StreamHandler/JSONFormatter, removing existing handlers, setting level to
logging.INFO and propagate=False for logging.getLogger("plane_mcp") so that
plane_mcp.server, plane_mcp.client and plane_mcp.auth.* use the same JSON
output; ensure the module-level logger = logging.getLogger("fastmcp.plane_mcp")
remains consistent with this configuration.



class ServerMode(Enum):
Expand Down Expand Up @@ -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


Expand Down
4 changes: 4 additions & 0 deletions plane_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,6 +58,7 @@ def get_oauth_mcp(base_path: str = "/"):
],
),
)
oauth_mcp.add_middleware(StructuredLoggingMiddleware(include_payloads=True))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider security implications of include_payloads=True.

Enabling include_payloads=True logs tool request and response payloads. While tokens are stored in request context (not tool arguments), tool payloads may still contain sensitive workspace data such as issue content, project names, or user information. Verify this aligns with your data handling policies.

If sensitive data exposure in logs is a concern, consider:

  • Setting include_payloads=False or making it configurable via environment variable
  • Ensuring log destinations have appropriate access controls
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plane_mcp/server.py` at line 61, The middleware call
oauth_mcp.add_middleware(StructuredLoggingMiddleware(include_payloads=True))
currently logs full tool request/response payloads which may expose sensitive
workspace data; change this to make include_payloads configurable (eg. read a
boolean from an environment variable or app config) and default it to False, or
set include_payloads=False directly if payload logging is not permitted; update
initialization of StructuredLoggingMiddleware and any related config loading so
the value is controlled via env/config and add a short config comment or
docstring next to the oauth_mcp.add_middleware call mentioning the security
implications.

register_tools(oauth_mcp)
return oauth_mcp

Expand All @@ -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

Expand All @@ -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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down