MCP SSE Security

Harden an MCP SSE server with localhost binding, bearer-token auth, and origin validation

SecurityConfig locks down an MCP SSE server with localhost-only binding, bearer-token authentication, and Origin validation.

Diagram

Valid token + origin

Missing / wrong token

🌐 Client

🔒 SecurityConfig

🛠️ MCP Tools

⛔ 401 / 403

Quick Start

  1. Bind to localhost only

    SecurityConfig defaults to bind_localhost_only=True. Pass its recommended bind address to run_sse.

    python
    from praisonaiagents.mcp import ToolsMCPServer
    from praisonaiagents.mcp.mcp_security import SecurityConfig
    
    def search(query: str) -> str:
        """Search the web."""
        return f"Results for {query}"
    
    server = ToolsMCPServer(name="local-tools", tools=[search])
    
    security = SecurityConfig(bind_localhost_only=True)
    server.run_sse(host=security.get_bind_address(), port=8080, security=security)
  2. Require a bearer token

    Set require_auth=True and provide the token via MCP_SSE_AUTH_TOKEN. Requests without a matching Authorization: Bearer … header get a 401.

    python
    import os
    from praisonaiagents.mcp import ToolsMCPServer
    from praisonaiagents.mcp.mcp_security import SecurityConfig
    
    os.environ["MCP_SSE_AUTH_TOKEN"] = "local-dev-token"
    
    def search(query: str) -> str:
        """Search the web."""
        return f"Results for {query}"
    
    server = ToolsMCPServer(name="local-tools", tools=[search])
    security = SecurityConfig(require_auth=True, bind_localhost_only=True)
    
    server.run_sse(host=security.get_bind_address(), port=8080, security=security)

How It Works

run_sse wraps the Starlette app with security middleware when a SecurityConfig requires auth or origin validation.

Diagram
MCP ServerSSE Security MiddlewareClientMCP ServerSSE Security MiddlewareClientalt[Token or origin invalid][Valid]HTTP request (+ Authorization, Origin)Check bearer tokenCheck Origin header401 / 403Forward requestSSE response
StepBehavior
Bind addressget_bind_address() returns 127.0.0.1 when bind_localhost_only=True, else 0.0.0.0
Token checkWhen require_auth=True, a Bearer token matching MCP_SSE_AUTH_TOKEN (or MCP_AUTH_TOKEN) is required, else 401
Origin checkWhen validate_origin=True, the Origin header must be in allowed_origins, else 403
No configWhen security=None, run_sse reads MCP_SSE_* env vars; middleware is skipped if neither auth nor origin validation is active

Configuration Options

Extracted from SecurityConfig in praisonaiagents/mcp/mcp_security.py.

OptionTypeDefaultDescription
validate_originboolTrueValidate the Origin header on each request
allow_missing_originboolFalseAllow requests with no Origin header
allowed_originsList[str]["localhost", "127.0.0.1"]Hosts permitted in the Origin header
require_authboolFalseRequire a bearer token on every request
bind_localhost_onlyboolTrueRecommend binding to 127.0.0.1 via get_bind_address()

Token and origin overrides can also come from the environment:

VariablePurpose
MCP_SSE_AUTH_TOKEN / MCP_AUTH_TOKENExpected bearer token when require_auth=True
MCP_SSE_ALLOWED_ORIGINSComma-separated allowed origins

Common Patterns

Choose a hardening level based on where the server runs.

Diagram

Yes

No

Yes

No

Where does the server run?

Local machine only?

bind_localhost_only=True

Exposed on a network?

require_auth=True + validate_origin=True

Localhost-only, no token — trusted single-machine use:

python
from praisonaiagents.mcp import ToolsMCPServer
from praisonaiagents.mcp.mcp_security import SecurityConfig

def search(query: str) -> str:
    """Search the web."""
    return f"Results for {query}"

server = ToolsMCPServer(name="local-tools", tools=[search])
security = SecurityConfig(bind_localhost_only=True, require_auth=False)
server.run_sse(host=security.get_bind_address(), port=8080, security=security)

Token plus restricted origins — a shared or networked server:

python
import os
from praisonaiagents.mcp import ToolsMCPServer
from praisonaiagents.mcp.mcp_security import SecurityConfig

os.environ["MCP_SSE_AUTH_TOKEN"] = "shared-team-token"

def search(query: str) -> str:
    """Search the web."""
    return f"Results for {query}"

server = ToolsMCPServer(name="team-tools", tools=[search])
security = SecurityConfig(
    require_auth=True,
    validate_origin=True,
    allowed_origins=["localhost", "127.0.0.1", "app.internal"],
    bind_localhost_only=False,
)
server.run_sse(host=security.get_bind_address(), port=8080, security=security)

Best Practices

Keep localhost binding on for local tools

Leave bind_localhost_only=True (the default) for any server that only serves the local machine — it keeps 127.0.0.1 off the network.

Require a token before exposing on a network

Set require_auth=True and provide MCP_SSE_AUTH_TOKEN whenever the server is reachable beyond localhost.

Restrict allowed origins

Narrow allowed_origins to the exact hosts that should call the server instead of leaving the defaults when exposing it more widely.

Store tokens in the environment

Read the token from MCP_SSE_AUTH_TOKEN rather than hard-coding it, so secrets stay out of source.


Related