diff --git a/README.md b/README.md index eaba7ea..ad1d5af 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ These go under the `mcp_transport_security` key: - `token`: Token to use for basic authentication. The caller must send this value in request header `Authorization`. For example: `Authorization: Bearer supersecret`. Default `""`. - `enable_dns_rebinding_protection`: Enable DNS rebinding protection") - `allowed_hosts`: List of allowed hosts. Default `["*:*"]` -- `allowed_origins`: list of allowed origins. Default `["http://*:*"]`. +- `allowed_origins`: list of allowed origins. Default `["http://*:*", "https://*:*"]`. With the exception of the `token` field the rest comes from the [Anthropic MCP SDK](https://github.com/modelcontextprotocol/python-sdk). diff --git a/config.yaml.sample b/config.yaml.sample index 29c969a..4e8308c 100644 --- a/config.yaml.sample +++ b/config.yaml.sample @@ -16,6 +16,13 @@ openshift: allow_write: false insecure: false +# Incoming TLS settings for the MCP server +#tls: +# ssl_certfile: ./cert.pem +# ssl_keyfile: ./key.pem +# ssl_keyfile_password: null +# ssl_ca_certs: null + mcp_transport_security: token: supersecret enable_dns_rebinding_protection: false diff --git a/src/rhos_ls_mcps/main.py b/src/rhos_ls_mcps/main.py index 360caa1..602658a 100644 --- a/src/rhos_ls_mcps/main.py +++ b/src/rhos_ls_mcps/main.py @@ -6,6 +6,7 @@ import contextlib import logging +import ssl from mcp.server.fastmcp import FastMCP from starlette.applications import Starlette @@ -108,6 +109,16 @@ def main(): # Pass string instead of an instance to support multiple workers. # Pass the factory=True argument to use a function (create_app) instead of a variable. + ssl_kwargs = {} + if config.tls.ssl_certfile: + ssl_kwargs["ssl_certfile"] = config.tls.ssl_certfile + ssl_kwargs["ssl_keyfile"] = config.tls.ssl_keyfile + if config.tls.ssl_keyfile_password: + ssl_kwargs["ssl_keyfile_password"] = config.tls.ssl_keyfile_password + if config.tls.ssl_ca_certs: + ssl_kwargs["ssl_ca_certs"] = config.tls.ssl_ca_certs + ssl_kwargs["ssl_cert_reqs"] = ssl.CERT_REQUIRED + uvicorn.run( "rhos_ls_mcps.main:create_app", host=config.ip, @@ -118,6 +129,7 @@ def main(): access_log=False, factory=True, log_config=log_config, + **ssl_kwargs, ) diff --git a/src/rhos_ls_mcps/settings.py b/src/rhos_ls_mcps/settings.py index 86eefa3..a237eae 100644 --- a/src/rhos_ls_mcps/settings.py +++ b/src/rhos_ls_mcps/settings.py @@ -3,7 +3,7 @@ import yaml from typing import Optional -from pydantic import Field +from pydantic import Field, model_validator from pydantic_settings import BaseSettings from rhos_ls_mcps import oc_defaults @@ -40,6 +40,31 @@ class OpenShiftSettings(BaseSettings): ) +class TLSSettings(BaseSettings): + ssl_certfile: Optional[str] = Field( + default=None, description="Path to SSL certificate file for incoming TLS" + ) + ssl_keyfile: Optional[str] = Field( + default=None, description="Path to SSL private key file for incoming TLS" + ) + ssl_keyfile_password: Optional[str] = Field( + default=None, description="Password for encrypted SSL key file" + ) + ssl_ca_certs: Optional[str] = Field( + default=None, + description="Path to CA certs file for client certificate verification (mutual TLS)", + ) + + @model_validator(mode="after") + def check_certfile_required(self) -> "TLSSettings": + dependent = [self.ssl_keyfile, self.ssl_keyfile_password, self.ssl_ca_certs] + if any(dependent) and not self.ssl_certfile: + raise ValueError( + "tls.ssl_certfile is required when other tls.ssl_* fields are set" + ) + return self + + class TransportSecuritySettings(BaseSettings): token: Optional[str] = Field( default=os.environ.get("MCP_SECURITY_TOKEN"), @@ -50,7 +75,7 @@ class TransportSecuritySettings(BaseSettings): ) allowed_hosts: list[str] = Field(default=["*:*"], description="Allowed hosts") allowed_origins: list[str] = Field( - default=["http://*:*"], description="Allowed origins" + default=["http://*:*", "https://*:*"], description="Allowed origins" ) @@ -79,6 +104,9 @@ class Settings(BaseSettings): mcp_transport_security: TransportSecuritySettings = Field( default=TransportSecuritySettings(), description="Transport security settings" ) + tls: TLSSettings = Field( + default=TLSSettings(), description="Incoming TLS settings for the MCP server" + ) def load_config():