Skip to content
Closed
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
20 changes: 17 additions & 3 deletions core/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from starlette.types import Scope, Receive, Send
from starlette.requests import Request
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware

from fastmcp import FastMCP
from fastmcp.server.auth.providers.google import GoogleProvider
Expand Down Expand Up @@ -40,6 +41,17 @@

session_middleware = Middleware(MCPSessionMiddleware)

# CORS middleware for OAuth endpoints - allows MCP Inspector and other browser clients
# Note: allow_credentials=True is incompatible with allow_origins=["*"]
# So we either allow credentials with specific origins, or allow all origins without credentials
cors_middleware = Middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all origins for OAuth discovery
allow_credentials=False, # Can't use credentials with wildcard origin
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)


class WellKnownCacheControlMiddleware:
"""Force no-cache headers for OAuth well-known discovery endpoints."""
Expand Down Expand Up @@ -89,15 +101,17 @@ def http_app(self, **kwargs) -> "Starlette":
app = super().http_app(**kwargs)

# Add middleware in order (first added = outermost layer)
app.user_middleware.insert(0, well_known_cache_control_middleware)
# CORS must be outermost to add headers to all responses
app.user_middleware.insert(0, cors_middleware)
app.user_middleware.insert(1, well_known_cache_control_middleware)

# Session Management - extracts session info for MCP context
app.user_middleware.insert(1, session_middleware)
app.user_middleware.insert(2, session_middleware)

# Rebuild middleware stack
app.middleware_stack = app.build_middleware_stack()
logger.info(
"Added middleware stack: WellKnownCacheControl, Session Management"
"Added middleware stack: CORS, WellKnownCacheControl, Session Management"
)
return app

Expand Down
38 changes: 32 additions & 6 deletions gmail/gmail_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@
GMAIL_REQUEST_DELAY = 0.1
HTML_BODY_TRUNCATE_LIMIT = 20000
GMAIL_METADATA_HEADERS = ["Subject", "From", "To", "Cc", "Message-ID", "Date"]
LOW_VALUE_TEXT_PLACEHOLDERS = (
"your client does not support html",
"view this email in your browser",
"open this email in your browser",
)
LOW_VALUE_TEXT_FOOTER_MARKERS = (
"mailing list",
"mailman/listinfo",
"unsubscribe",
"list-unsubscribe",
"manage preferences",
)
LOW_VALUE_TEXT_HTML_DIFF_MIN = 80


class _HTMLTextExtractor(HTMLParser):
Expand Down Expand Up @@ -154,16 +167,29 @@ def _format_body_content(text_body: str, html_body: str) -> str:
"""
text_stripped = text_body.strip()
html_stripped = html_body.strip()
html_text = _html_to_text(html_stripped).strip() if html_stripped else ""

plain_lower = " ".join(text_stripped.split()).lower()
html_lower = " ".join(html_text.split()).lower()
plain_is_low_value = plain_lower and (
any(marker in plain_lower for marker in LOW_VALUE_TEXT_PLACEHOLDERS)
or (
any(marker in plain_lower for marker in LOW_VALUE_TEXT_FOOTER_MARKERS)
and len(html_lower) >= len(plain_lower) + LOW_VALUE_TEXT_HTML_DIFF_MIN
)
or (
len(html_lower) >= len(plain_lower) + LOW_VALUE_TEXT_HTML_DIFF_MIN
and html_lower.endswith(plain_lower)
)
)

# Detect useless fallback: HTML comments in text, or HTML is 50x+ longer
use_html = html_stripped and (
not text_stripped
or "<!--" in text_stripped
or len(html_stripped) > len(text_stripped) * 50
# Prefer plain text, but fall back to HTML when plain text is empty or clearly low-value.
use_html = html_text and (
not text_stripped or "<!--" in text_stripped or plain_is_low_value
)
Comment on lines +174 to 189

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add an explicit “minimum meaningful plain-text length” fallback condition.

Current logic only flags low-value plain text via marker/footer/suffix heuristics. Very short non-empty text/plain (e.g., 1–20 chars) can still win over rich HTML content, which misses the “below minimal meaningful length” requirement.

💡 Proposed adjustment
 LOW_VALUE_TEXT_HTML_DIFF_MIN = 80
+LOW_VALUE_TEXT_MIN_MEANINGFUL_LEN = 40
@@
-    plain_is_low_value = plain_lower and (
-        any(marker in plain_lower for marker in LOW_VALUE_TEXT_PLACEHOLDERS)
+    plain_is_low_value = bool(plain_lower) and (
+        (
+            len(plain_lower) < LOW_VALUE_TEXT_MIN_MEANINGFUL_LEN
+            and len(html_lower) >= len(plain_lower) + LOW_VALUE_TEXT_HTML_DIFF_MIN
+        )
+        or any(marker in plain_lower for marker in LOW_VALUE_TEXT_PLACEHOLDERS)
         or (
             any(marker in plain_lower for marker in LOW_VALUE_TEXT_FOOTER_MARKERS)
             and len(html_lower) >= len(plain_lower) + LOW_VALUE_TEXT_HTML_DIFF_MIN
         )
         or (
             len(html_lower) >= len(plain_lower) + LOW_VALUE_TEXT_HTML_DIFF_MIN
             and html_lower.endswith(plain_lower)
         )
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gmail/gmail_tools.py` around lines 174 - 189, The plain text is considered
non-low-value only via markers/footers/suffix heuristics, so very short plain
text still beats HTML; add a minimum meaningful plain-text length constant
(e.g., MIN_MEANINGFUL_PLAIN_LENGTH) and treat plain text shorter than that as
low-value by incorporating len(plain_lower) < MIN_MEANINGFUL_PLAIN_LENGTH into
the plain_is_low_value determination (or into the use_html decision) so that
use_html becomes true when html_text exists and plain text is present but
beneath the minimum meaningful length; update references to
LOW_VALUE_TEXT_PLACEHOLDERS, LOW_VALUE_TEXT_FOOTER_MARKERS and
LOW_VALUE_TEXT_HTML_DIFF_MIN accordingly when locating plain_is_low_value and
use_html.


if use_html:
content = _html_to_text(html_stripped)
content = html_text
if len(content) > HTML_BODY_TRUNCATE_LIMIT:
content = content[:HTML_BODY_TRUNCATE_LIMIT] + "\n\n[Content truncated...]"
return content
Expand Down
40 changes: 40 additions & 0 deletions test_cors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Quick test to verify CORS middleware is configured correctly."""

import sys

sys.path.insert(0, ".")

# Set required env vars before importing
import os

os.environ["MCP_ENABLE_OAUTH21"] = "true"
os.environ["WORKSPACE_MCP_STATELESS_MODE"] = "true"
os.environ["GOOGLE_OAUTH_CLIENT_ID"] = "test"
os.environ["GOOGLE_OAUTH_CLIENT_SECRET"] = "test"

from core.server import server

# Get the HTTP app
app = server.http_app()

# Check middleware stack
print("Middleware stack:")
for i, m in enumerate(app.user_middleware):
if hasattr(m, "cls"):
print(f" {i}: {m.cls.__name__}")
else:
print(f" {i}: {type(m).__name__}")

print("\nChecking for CORSMiddleware...")
has_cors = any(
hasattr(m, "cls") and m.cls.__name__ == "CORSMiddleware"
for m in app.user_middleware
)
print(f"CORS middleware found: {has_cors}")

if has_cors:
print("✅ CORS middleware is configured!")
else:
print("❌ CORS middleware is NOT configured!")
sys.exit(1)
Comment on lines +6 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid import-time execution and sys.exit in test modules.

This file runs assertions at import time and exits the process on failure (Line 40), which can break/abort test collection and leak env mutations into unrelated tests. Please convert it to a real test function with scoped env setup and plain assertions.

✅ Suggested rewrite
-#!/usr/bin/env python3
-"""Quick test to verify CORS middleware is configured correctly."""
-
-import sys
-
-sys.path.insert(0, ".")
-
-# Set required env vars before importing
-import os
-
-os.environ["MCP_ENABLE_OAUTH21"] = "true"
-os.environ["WORKSPACE_MCP_STATELESS_MODE"] = "true"
-os.environ["GOOGLE_OAUTH_CLIENT_ID"] = "test"
-os.environ["GOOGLE_OAUTH_CLIENT_SECRET"] = "test"
-
-from core.server import server
-
-# Get the HTTP app
-app = server.http_app()
-
-# Check middleware stack
-print("Middleware stack:")
-for i, m in enumerate(app.user_middleware):
-    if hasattr(m, "cls"):
-        print(f"  {i}: {m.cls.__name__}")
-    else:
-        print(f"  {i}: {type(m).__name__}")
-
-print("\nChecking for CORSMiddleware...")
-has_cors = any(
-    hasattr(m, "cls") and m.cls.__name__ == "CORSMiddleware"
-    for m in app.user_middleware
-)
-print(f"CORS middleware found: {has_cors}")
-
-if has_cors:
-    print("✅ CORS middleware is configured!")
-else:
-    print("❌ CORS middleware is NOT configured!")
-    sys.exit(1)
+from starlette.middleware.cors import CORSMiddleware
+
+
+def test_http_app_has_cors_middleware(monkeypatch):
+    monkeypatch.setenv("MCP_ENABLE_OAUTH21", "true")
+    monkeypatch.setenv("WORKSPACE_MCP_STATELESS_MODE", "true")
+    monkeypatch.setenv("GOOGLE_OAUTH_CLIENT_ID", "test")
+    monkeypatch.setenv("GOOGLE_OAUTH_CLIENT_SECRET", "test")
+
+    from core.server import server
+
+    app = server.http_app()
+    assert any(
+        getattr(m, "cls", None) is CORSMiddleware for m in app.user_middleware
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test_cors.py` around lines 6 - 40, The test currently performs import-time
execution and calls sys.exit in test_cors.py; refactor it into a proper pytest
test function (e.g., test_cors_middleware_present) that sets required env vars
via a fixture or monkeypatch (instead of global os.environ mutating at import),
imports server and calls server.http_app() inside the test, inspects
app.user_middleware for a middleware with cls.__name__ == "CORSMiddleware", and
replace print/ sys.exit logic with plain assertions (assert any(...)). Ensure
env changes are scoped to the test and not performed at import time so test
collection isn't aborted.

4 changes: 3 additions & 1 deletion tests/core/test_well_known_cache_control_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ async def regular_endpoint(request):
assert "etag" not in extra.headers


def test_configured_server_applies_no_cache_to_served_oauth_discovery_routes(monkeypatch):
def test_configured_server_applies_no_cache_to_served_oauth_discovery_routes(
monkeypatch,
):
monkeypatch.setenv("MCP_ENABLE_OAUTH21", "true")
monkeypatch.setenv("GOOGLE_OAUTH_CLIENT_ID", "dummy-client")
monkeypatch.setenv("GOOGLE_OAUTH_CLIENT_SECRET", "dummy-secret")
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.