Gate /mcp with a shared-secret bearer token (QONTO_ASSISTANT_MCP_TOKEN,
mcp_auth.py::BearerTokenAuthMiddleware, constant-time compare, REST
untouched) since no OIDC issuer exists in this fleet yet -- pointing
FastMCP's OAuth Protected Resource flow at a non-existent issuer would be
worse than not having it. This token is a service credential, never a bank
credential; per-actor identity stays the existing X-Actor-* convention.
Add docs/mcp-integration.md: tool catalog, the two-layer auth model (workload
auth today vs. deferred OIDC target), and one shared {"mcpServers": {...}}
client config snippet (url + headers) usable across Claude Code, Claude
Desktop, Cursor, and Codex/Grok-style harnesses.
Verified live using only that snippet: unauthenticated and wrong-token
requests get 401 before reaching any tool; a request built from the
snippet's URL + headers lists tools and calls qonto_org_summary
successfully against the fixture-backed server.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
35 lines
1.1 KiB
Python
35 lines
1.1 KiB
Python
from starlette.applications import Starlette
|
|
from starlette.responses import PlainTextResponse
|
|
from starlette.routing import Route
|
|
from starlette.testclient import TestClient
|
|
|
|
from qonto_assistant.mcp_auth import BearerTokenAuthMiddleware
|
|
|
|
|
|
def _protected_app() -> Starlette:
|
|
async def ok(request):
|
|
return PlainTextResponse("ok")
|
|
|
|
app = Starlette(routes=[Route("/", ok)])
|
|
app.add_middleware(BearerTokenAuthMiddleware, token="secret-token")
|
|
return app
|
|
|
|
|
|
def test_bearer_auth_rejects_missing_header() -> None:
|
|
client = TestClient(_protected_app())
|
|
response = client.get("/")
|
|
assert response.status_code == 401
|
|
assert response.json()["error_code"] == "unauthorized"
|
|
|
|
|
|
def test_bearer_auth_rejects_wrong_token() -> None:
|
|
client = TestClient(_protected_app())
|
|
response = client.get("/", headers={"Authorization": "Bearer wrong-token"})
|
|
assert response.status_code == 401
|
|
|
|
|
|
def test_bearer_auth_accepts_matching_token() -> None:
|
|
client = TestClient(_protected_app())
|
|
response = client.get("/", headers={"Authorization": "Bearer secret-token"})
|
|
assert response.status_code == 200
|
|
assert response.text == "ok"
|