feat: optional bearer-token auth for streamable-http transport
Pure-ASGI middleware requiring Authorization: Bearer <MCP_API_KEY> on every HTTP request when MCP_API_KEY is set; open/stdio behaviour preserved when unset. Closes unauthenticated-MCP exposure (HOME-119 P1). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6a1b671d79
commit
35c982ef62
1 changed files with 40 additions and 1 deletions
|
|
@ -224,9 +224,48 @@ def delete_paste(paste_id: str) -> dict:
|
||||||
# Entry point
|
# Entry point
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
MCP_API_KEY = os.environ.get("MCP_API_KEY", "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
class _BearerAuthMiddleware:
|
||||||
|
"""Pure-ASGI middleware rejecting HTTP requests without a valid bearer token.
|
||||||
|
|
||||||
|
Enforced only when MCP_API_KEY is set; stdio transport is unaffected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app, token: str) -> None:
|
||||||
|
self.app = app
|
||||||
|
self._expected = f"Bearer {token}".encode()
|
||||||
|
|
||||||
|
async def __call__(self, scope, receive, send) -> None:
|
||||||
|
if scope["type"] != "http":
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
return
|
||||||
|
headers = dict(scope.get("headers") or [])
|
||||||
|
if headers.get(b"authorization") != self._expected:
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
|
await JSONResponse(
|
||||||
|
{"jsonrpc": "2.0",
|
||||||
|
"error": {"code": -32001, "message": "Unauthorized"},
|
||||||
|
"id": None},
|
||||||
|
status_code=401,
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)(scope, receive, send)
|
||||||
|
return
|
||||||
|
await self.app(scope, receive, send)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
if MCP_TRANSPORT not in {"stdio", "sse", "streamable-http"}:
|
if MCP_TRANSPORT not in {"stdio", "sse", "streamable-http"}:
|
||||||
raise ValueError("PASTEBIN_MCP_TRANSPORT must be stdio, sse, or streamable-http.")
|
raise ValueError("PASTEBIN_MCP_TRANSPORT must be stdio, sse, or streamable-http.")
|
||||||
|
if MCP_TRANSPORT == "streamable-http" and MCP_API_KEY:
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
app = mcp.streamable_http_app()
|
||||||
|
app.add_middleware(_BearerAuthMiddleware, token=MCP_API_KEY)
|
||||||
|
uvicorn.run(app, host=MCP_HOST, port=MCP_PORT)
|
||||||
|
else:
|
||||||
mcp.run(transport=MCP_TRANSPORT)
|
mcp.run(transport=MCP_TRANSPORT)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue