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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ VYMCP_TRANSPORT=stdio
VYMANAGER_API_TOKEN=vym_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# http only: bind address, port, and the externally reachable URL.
# To accept remote clients, bind all interfaces (VYMCP_HOST=0.0.0.0) AND set
# VYMCP_PUBLIC_URL to the address clients actually use — the server only accepts
# requests whose Host header matches it (DNS-rebinding protection).
VYMCP_HOST=127.0.0.1
VYMCP_PORT=8080
# VYMCP_PUBLIC_URL=https://vymcp.example.com

# Optional: override the allowed Host header values (comma-separated, e.g.
# "192.168.9.103:8080,vymcp.example.com"). Defaults to the VYMCP_PUBLIC_URL host.
# Set to "*" to disable the Host check entirely (only behind a trusted proxy).
# VYMCP_ALLOWED_HOSTS=192.168.9.103:8080
23 changes: 21 additions & 2 deletions src/vymcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from __future__ import annotations

import os
from dataclasses import dataclass
from dataclasses import dataclass, field
from urllib.parse import urlparse

_FALSEY = {"0", "false", "no", "off"}
_TRUTHY = {"1", "true", "yes", "on"}
Expand Down Expand Up @@ -52,6 +53,9 @@ class ServerConfig:
port: int = 8080
public_url: str = "http://127.0.0.1:8080" # externally reachable URL (http mode)
api_token: str | None = None
# Host header values the http server accepts (DNS-rebinding protection). Empty
# tuple means allow any host (protection disabled).
allowed_hosts: tuple[str, ...] = field(default_factory=tuple)

@classmethod
def from_env(cls) -> ServerConfig:
Expand All @@ -77,6 +81,20 @@ def from_env(cls) -> ServerConfig:

host = os.environ.get("VYMCP_HOST", "127.0.0.1")
public_url = os.environ.get("VYMCP_PUBLIC_URL") or f"http://{host}:{port}"
public_url = public_url.rstrip("/")

# Which Host headers the http server accepts. Explicit override wins;
# otherwise default to the public URL's host:port so external clients that
# reach the server at its advertised address are allowed. "*" disables the
# DNS-rebinding check entirely (use only behind a trusted proxy).
allowed_env = os.environ.get("VYMCP_ALLOWED_HOSTS", "").strip()
if allowed_env == "*":
allowed_hosts: tuple[str, ...] = ()
elif allowed_env:
allowed_hosts = tuple(h.strip() for h in allowed_env.split(",") if h.strip())
else:
netloc = urlparse(public_url).netloc
allowed_hosts = (netloc,) if netloc else ()

return cls(
base_url=base_url.rstrip("/"),
Expand All @@ -85,8 +103,9 @@ def from_env(cls) -> ServerConfig:
transport=transport,
host=host,
port=port,
public_url=public_url.rstrip("/"),
public_url=public_url,
api_token=api_token,
allowed_hosts=allowed_hosts,
)

def client_config(self, token: str) -> Config:
Expand Down
18 changes: 18 additions & 0 deletions src/vymcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,29 @@ def main() -> None:
config = server_config()
if config.transport == "http":
from mcp.server.auth.settings import AuthSettings
from mcp.server.transport_security import TransportSecuritySettings

from .auth import VyManagerTokenVerifier

mcp.settings.host = config.host
mcp.settings.port = config.port
# FastMCP auto-configures DNS-rebinding protection for localhost at
# construction time (allowing only 127.0.0.1/localhost). Since we bind and
# advertise a different address in http mode, replace it with an allowlist
# derived from config, or disable it when no hosts are configured.
if config.allowed_hosts:
origins = [
f"{scheme}://{h}" for h in config.allowed_hosts for scheme in ("http", "https")
]
mcp.settings.transport_security = TransportSecuritySettings(
enable_dns_rebinding_protection=True,
allowed_hosts=list(config.allowed_hosts),
allowed_origins=origins,
)
else:
mcp.settings.transport_security = TransportSecuritySettings(
enable_dns_rebinding_protection=False,
)
mcp.settings.auth = AuthSettings(
issuer_url=config.base_url, # type: ignore[arg-type] # pydantic coerces str->AnyHttpUrl
resource_server_url=config.public_url, # type: ignore[arg-type]
Expand Down
Loading