fix: harden MCP endpoint after code review
Critical: the MCP SDK auto-enables DNS-rebinding protection when its host parameter is left at the 127.0.0.1 default, hard-coding a loopback-only Host allow-list. Every remote client (the only deployment this feature targets) was refused with HTTP 421 before auth or the tool ran. Now driven by a new MCP_ALLOWED_HOSTS setting, with '*' as an explicit, logged opt-out. Also bounds query/top_k to match AskRequest (top_k is amplified 4x downstream, so an unbounded value was a resource-exhaustion vector), decodes the Authorization header as latin-1 per the ASGI spec instead of raising a 500 on malformed bytes, and returns WWW-Authenticate on 401 per RFC 7235. Moves the psycopg2 import guard into backend/tests/conftest.py: duplicated across four test modules, it only worked because of alphabetical collection order, and any earlier-sorting package would have reintroduced a live connection attempt against the production database. Registers the mcp module in the authoritative backend architecture doc. 84 backend tests pass. Verified against a live server: allowed remote Host returns a valid initialize result, unknown Host returns 421, missing token returns 401 with WWW-Authenticate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ correct dict shape out.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -15,6 +16,9 @@ from unittest.mock import MagicMock, patch
|
||||
class _FakeSource:
|
||||
"""Minimal stand-in for a real Source dataclass (only __dict__ is used)."""
|
||||
|
||||
# A dataclass, not a MagicMock: the adapter serializes sources via
|
||||
# source.__dict__, and a MagicMock's __dict__ is full of internal mock
|
||||
# attributes, which would make the assertions meaningless.
|
||||
doc_id: str
|
||||
doc_title: str
|
||||
score: float
|
||||
@@ -75,3 +79,20 @@ def test_search_regulations_default_top_k():
|
||||
search_regulations(query="q")
|
||||
|
||||
assert fake_service.ask.call_args.kwargs["top_k"] == 5
|
||||
|
||||
|
||||
def test_advertised_schema_bounds_top_k_and_query():
|
||||
"""The advertised JSON schema must carry the same bounds as AskRequest.
|
||||
|
||||
Bounds declared via Annotated are what the SDK validates against and what
|
||||
clients see, so asserting on the generated schema is the only way to catch
|
||||
a regression that silently drops them.
|
||||
"""
|
||||
from app.mcp.server import mcp
|
||||
|
||||
schema = asyncio.run(mcp.list_tools())[0].input_schema["properties"]
|
||||
|
||||
assert schema["top_k"]["minimum"] == 1
|
||||
assert schema["top_k"]["maximum"] == 20
|
||||
assert schema["query"]["minLength"] == 1
|
||||
assert schema["query"]["maxLength"] == 2000
|
||||
|
||||
Reference in New Issue
Block a user