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
12 changes: 12 additions & 0 deletions docs/environment-variables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2108,6 +2108,18 @@
- ``CodeKeeper``
- ``CodeKeeper``
- MCP
* - ``MCP_ALLOWED_HOSTS``
- רשימת Host מותרים לשרת ה-MCP (CSV; תומך wildcard כמו ``*.onrender.com``). ריק = הגנת DNS-rebinding כבויה (מתאים לשרת ציבורי מוגן-טוקן).
- לא
- "" (כבוי)
- ``codekeeper-mcp.onrender.com``
- MCP
* - ``MCP_ALLOWED_ORIGINS``
- רשימת Origin מותרים לשרת ה-MCP (CSV). רלוונטי רק כשמפעילים הגנה דרך ``MCP_ALLOWED_HOSTS``.
- לא
- ""
- ``https://claude.ai``
- MCP

דגלי בדיקות ופיתוח
-------------------
Expand Down
29 changes: 28 additions & 1 deletion mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@

from __future__ import annotations

import os
from typing import Any

from mcp.server.fastmcp import Context, FastMCP
from mcp.server.transport_security import TransportSecuritySettings
from starlette.responses import JSONResponse
from starlette.routing import Route

Expand All @@ -28,8 +30,33 @@
)


def _transport_security() -> TransportSecuritySettings:
"""DNS-rebinding protection config for the Streamable-HTTP transport.

That protection targets *localhost* servers (a malicious web page tricking a
browser into calling 127.0.0.1). This server is public and Bearer-token
gated, so the default host check only blocks legitimate access behind a real
domain (HTTP 421 "Invalid Host header"). Default: OFF. Set MCP_ALLOWED_HOSTS
(comma-separated; wildcards like ``*.onrender.com`` allowed) to lock it down.
"""
hosts = [h.strip() for h in os.getenv("MCP_ALLOWED_HOSTS", "").split(",") if h.strip()]
origins = [o.strip() for o in os.getenv("MCP_ALLOWED_ORIGINS", "").split(",") if o.strip()]
if hosts or origins:
return TransportSecuritySettings(
enable_dns_rebinding_protection=True,
allowed_hosts=hosts,
allowed_origins=origins,
)
return TransportSecuritySettings(enable_dns_rebinding_protection=False)


def build_mcp(backend: Any, *, name: str = "CodeKeeper") -> FastMCP:
mcp: FastMCP = FastMCP(name, instructions=_INSTRUCTIONS, stateless_http=True)
mcp: FastMCP = FastMCP(
name,
instructions=_INSTRUCTIONS,
stateless_http=True,
transport_security=_transport_security(),
)

@mcp.tool(description="List the user's saved code files (metadata only, no code).")
def list_files(ctx: Context, page: int = 1, per_page: int = 50) -> dict:
Expand Down
12 changes: 12 additions & 0 deletions services/config_inspector_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,18 @@ class ConfigService:
description="שם התצוגה של שרת ה-MCP (שם ה-Connector שמוצג ללקוח).",
category="mcp",
),
"MCP_ALLOWED_HOSTS": ConfigDefinition(
key="MCP_ALLOWED_HOSTS",
default="",
description="Host מותרים לשרת ה-MCP (CSV, תומך wildcard). ריק = הגנת DNS-rebinding כבויה (מתאים לשרת ציבורי מוגן-טוקן).",
category="mcp",
),
"MCP_ALLOWED_ORIGINS": ConfigDefinition(
key="MCP_ALLOWED_ORIGINS",
default="",
description="Origin מותרים לשרת ה-MCP (CSV). רלוונטי רק כשמפעילים הגנה דרך MCP_ALLOWED_HOSTS.",
category="mcp",
),

# --- Repo Sync Engine (Git Mirror) ---
"REPO_NAME": ConfigDefinition(
Expand Down
20 changes: 20 additions & 0 deletions tests/test_mcp_server_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,23 @@ def test_build_app_exposes_healthz_route():
app = build_app(_FakeBackend(), _FakeStore())
paths = {getattr(r, "path", None) for r in app.routes}
assert "/healthz" in paths


def test_transport_security_off_by_default(monkeypatch):
monkeypatch.delenv("MCP_ALLOWED_HOSTS", raising=False)
monkeypatch.delenv("MCP_ALLOWED_ORIGINS", raising=False)
from mcp_server.server import _transport_security

ts = _transport_security()
# Public token-gated server: DNS-rebinding host check must be off so a real
# domain (e.g. *.onrender.com) is not rejected with HTTP 421.
assert ts.enable_dns_rebinding_protection is False


def test_transport_security_locks_down_via_env(monkeypatch):
monkeypatch.setenv("MCP_ALLOWED_HOSTS", "a.com, *.b.com")
from mcp_server.server import _transport_security

ts = _transport_security()
assert ts.enable_dns_rebinding_protection is True
assert ts.allowed_hosts == ["a.com", "*.b.com"]
Loading