feat: add MCP research access and browser key management

This commit is contained in:
yuxuanhui
2026-09-09 16:18:27 +08:00
parent 4debca7dbd
commit 45238280e3
47 changed files with 2642 additions and 44 deletions
+13
View File
@@ -81,6 +81,7 @@ def create_test_app():
_env_file=None,
database_url=f"sqlite+aiosqlite:///{temporary.name}/test.db",
admin_password=TEST_PASSWORD,
mcp_enabled=True,
encryption_key=Fernet.generate_key().decode(),
public_origin=os.environ.get("BROWSER_TEST_ORIGIN", "http://127.0.0.1:5179"),
)
@@ -204,6 +205,18 @@ def create_test_app():
application = create_app(
settings, WqClient(settings, transport=httpx.MockTransport(upstream)), ai_model_factory=fake_model
)
# Test-only fixture, excluded from production images by .dockerignore.
from fastapi import Depends
from app.mcp_api.auth import SCOPES, create_token
from app.security import require_auth
@application.post("/api/v1/test/mcp-token", dependencies=[Depends(require_auth)])
async def synthetic_token():
async with application.state.sessions.begin() as db:
_, token = await create_token(db, "browser-fixture", SCOPES)
return {"token": token}
original_lifespan = application.router.lifespan_context
@asynccontextmanager
+44
View File
@@ -0,0 +1,44 @@
"""Synthetic PostgreSQL-backed server for container proxy acceptance only."""
from contextlib import asynccontextmanager
import httpx
from fastapi import Depends
from app.config import Settings
from app.main import create_app
from app.mcp_api.auth import SCOPES, create_token
from app.models import Account
from app.security import cipher, require_auth
from app.worldquant import WqClient
from tests.backtest_fake import Platform
def create_test_app():
settings = Settings(_env_file=None, enable_runner=False, mcp_enabled=True)
if not settings.database_url.endswith("/wq_mcp_test"):
raise RuntimeError("Disposable test database required")
platform = Platform()
app = create_app(settings, WqClient(settings, transport=httpx.MockTransport(platform)))
original = app.router.lifespan_context
@asynccontextmanager
async def lifespan(application):
async with original(application):
async with app.state.sessions.begin() as db:
account = await db.get(Account, 1)
account.email = "synthetic@example.com"
account.wq_user_id = "TEST_USER"
account.connection_status = "connected"
account.password_encrypted = cipher(settings).encrypt(platform.secret.encode()).decode()
yield
app.router.lifespan_context = lifespan
@app.post("/api/v1/test/mcp-token", dependencies=[Depends(require_auth)])
async def issue_fixture():
async with app.state.sessions.begin() as db:
_, token = await create_token(db, "container-fixture", SCOPES)
return {"token": token}
return app
+97
View File
@@ -0,0 +1,97 @@
"""Disposable PostgreSQL migration/concurrency acceptance; synthetic platform only.
MCP_TEST_DATABASE_URL must identify a local database named wq_mcp_test.
"""
import asyncio
import os
from urllib.parse import urlsplit
import httpx
from alembic import command
from alembic.config import Config
from cryptography.fernet import Fernet
from sqlalchemy import func, select
from app.alphas import upsert_alpha
from app.config import Settings
from app.db import create_database
from app.main import create_app
from app.mcp_api.auth import SCOPES, authenticate, create_token
from app.models import BacktestEvent, BacktestPreview, BacktestRun, Research, ResearchRequest
from app.research_access.contracts import Control, Submit
from app.research_access.service import ResearchAccess, ResearchError
from app.worldquant import WqClient
from tests.backtest_fake import Platform
from tests.test_backtests import candidate, setup
async def seed(settings):
engine, sessions = create_database(settings.database_url)
async with sessions.begin() as db:
await upsert_alpha(db, {"id": "legacy", "regular": {"code": "rank(close)"}})
(await db.get(Research, "legacy")).note = "keep historical note"
await engine.dispose()
async def verify(settings):
app = create_app(settings, WqClient(settings, transport=httpx.MockTransport(Platform())))
async with app.router.lifespan_context(app):
await setup(app)
async with app.state.sessions.begin() as db:
_, secret = await create_token(db, "synthetic", SCOPES)
principal = await authenticate(db, secret)
args = Submit(name="concurrent", candidates=[candidate()], idempotency_key="same")
async def submit(body):
async with app.state.sessions.begin() as db:
return await ResearchAccess(db, principal, app.state.runner.client, settings.public_origin).submit(body)
a, b = await asyncio.gather(submit(args), submit(args))
assert a == b
rid = a["backtest_run_id"]
async with app.state.sessions() as db:
assert await db.scalar(select(func.count()).select_from(BacktestRun)) == 1
assert await db.scalar(select(func.count()).select_from(BacktestPreview)) == 1
assert await db.scalar(select(func.count()).select_from(ResearchRequest)) == 1
assert (await db.get(Research, "legacy")).note == "keep historical note"
try:
await submit(args.model_copy(update={"name": "different"}))
except ResearchError as exc:
assert exc.data["code"] == "IDEMPOTENCY_CONFLICT"
else:
raise AssertionError("different payload accepted")
control = Control(run_id=rid, action="pause", expected_version=1, idempotency_key="pause")
async def pause():
async with app.state.sessions.begin() as db:
return await ResearchAccess(db, principal, app.state.runner.client, settings.public_origin).control(control)
x, y = await asyncio.gather(pause(), pause())
assert x == y
async with app.state.sessions() as db:
assert await db.scalar(select(func.count()).select_from(BacktestEvent).where(BacktestEvent.kind == "control")) == 1
# A fresh process-equivalent application returns the original response after restart.
replacement = create_app(settings, WqClient(settings, transport=httpx.MockTransport(Platform())))
async with replacement.router.lifespan_context(replacement):
async with replacement.state.sessions.begin() as db:
principal = await authenticate(db, secret)
replay = await ResearchAccess(db, principal, replacement.state.runner.client, settings.public_origin).submit(args)
assert replay == a
print("PostgreSQL: migration preserves history; concurrent submit/control and restart replay passed")
if __name__ == "__main__":
url = os.environ["MCP_TEST_DATABASE_URL"]
parsed = urlsplit(url)
if parsed.hostname not in {"127.0.0.1", "localhost"} or parsed.path != "/wq_mcp_test":
raise SystemExit("Refusing a non-local/non-disposable database")
key = Fernet.generate_key().decode()
os.environ.update(DATABASE_URL=url, ADMIN_PASSWORD="synthetic-admin-only", ENCRYPTION_KEY=key,
WQ_EMAIL="", WQ_PASSWORD="")
settings = Settings(_env_file=None, database_url=url, admin_password="synthetic-admin-only",
encryption_key=key, enable_runner=False, mcp_enabled=True, public_origin="http://testserver")
config = Config("alembic.ini")
command.upgrade(config, "0009")
asyncio.run(seed(settings))
command.upgrade(config, "head")
asyncio.run(verify(settings))
command.downgrade(config, "0009")
command.upgrade(config, "head")
print("PostgreSQL: additive migration downgrade/re-upgrade passed")
+280
View File
@@ -0,0 +1,280 @@
"""MCP integration uses real database/transport and a synthetic WorldQuant only."""
import asyncio
from datetime import timedelta
import httpx
import pytest
from sqlalchemy import func, select
from app.mcp_api.auth import SCOPES, authenticate, create_token
from app.models import (
BacktestPreview,
BacktestResult,
BacktestRun,
MCPAudit,
MCPToken,
Pnl,
now,
)
from tests.test_backtests import candidate, execute, setup
ENDPOINT = "/api/v1/mcp/"
@pytest.fixture
async def mcp_app(settings):
from app.main import create_app
from app.models import Base
from app.worldquant import WqClient
settings.mcp_enabled = True
def no_network(request):
raise AssertionError("No real platform")
application = create_app(settings, WqClient(settings, transport=httpx.MockTransport(no_network)))
async with application.state.engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
ready, stop = asyncio.Event(), asyncio.Event()
async def lifespan():
async with application.router.lifespan_context(application):
ready.set()
await stop.wait()
task = asyncio.create_task(lifespan())
await ready.wait()
try:
await setup(application)
yield application
finally:
stop.set()
await task
async def credentials(app, scopes=SCOPES):
async with app.state.sessions.begin() as db:
row, secret = await create_token(db, "synthetic test", scopes)
principal = await authenticate(db, secret)
return principal, secret
def submission(key="batch-1", items=None, **extra):
return {"name": "MCP batch", "candidates": items or [candidate()], "idempotency_key": key, **extra}
async def invoke(app, principal, name, arguments=None):
result = await app.state.mcp.invoke(principal, name, arguments or {})
assert not result.is_error, result.structured_content
return result.structured_content
async def test_http_auth_and_discovery(mcp_app):
principal, secret = await credentials(mcp_app, {"research:read"})
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=mcp_app), base_url="http://testserver") as client:
payload = {"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
assert (await client.post(ENDPOINT, json=payload)).status_code == 401
headers = {"Authorization": f"Bearer {secret}", "Accept": "application/json, text/event-stream"}
response = await client.post(ENDPOINT, json=payload, headers=headers)
assert response.status_code == 200, response.text
names = {t["name"] for t in response.json()["result"]["tools"]}
assert "search_backtests" in names and "submit_backtests" not in names
denied = {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "submit_backtests", "arguments": submission()}}
assert (await client.post(ENDPOINT, json=denied, headers=headers)).status_code == 403
assert (await client.get("/api/v1/account", headers=headers)).status_code == 401
assert (await client.post(ENDPOINT, json=payload, headers=headers | {"Origin": "https://bad.test"})).status_code == 403
assert (await client.post(ENDPOINT, json=payload, headers=headers | {"Host": "bad.test"})).status_code == 403
async with mcp_app.state.sessions.begin() as db:
row = await db.get(MCPToken, principal.token_id)
row.revoked_at = now()
assert (await client.post(ENDPOINT, json=payload, headers=headers)).status_code == 401
async def test_submit_replay_duplicates_and_rotation(mcp_app):
principal, _ = await credentials(mcp_app)
result = await invoke(mcp_app, principal, "submit_backtests", submission())
second, _ = await credentials(mcp_app)
replay = await invoke(mcp_app, second, "submit_backtests", submission())
assert replay == result
conflict = await mcp_app.state.mcp.invoke(second, "submit_backtests", submission(items=[candidate(1)]))
assert conflict.is_error and conflict.structured_content["error"]["code"] == "IDEMPOTENCY_CONFLICT"
duplicate = await mcp_app.state.mcp.invoke(second, "submit_backtests", submission("different"))
assert duplicate.structured_content["error"]["code"] == "DUPLICATE_INPUT"
rerun = await invoke(mcp_app, second, "submit_backtests", submission("different", duplicate_policy="rerun"))
assert rerun["backtest_run_id"] != result["backtest_run_id"]
async with mcp_app.state.sessions() as db:
assert await db.scalar(select(func.count()).select_from(BacktestRun)) == 2
assert await db.scalar(select(func.count()).select_from(BacktestPreview)) == 2
audits = list(await db.scalars(select(MCPAudit)))
assert len(audits) == 5
assert result["source"]["kind"] == "mcp"
assert "#backtests?run_id=" in result["web_url"]
async def test_validation_and_within_batch_atomic(mcp_app):
principal, _ = await credentials(mcp_app)
incomplete = candidate()
incomplete["settings"] = {"region": "USA", "universe": "TOP3000", "delay": 1}
result = await mcp_app.state.mcp.invoke(principal, "submit_backtests", submission(items=[incomplete]))
assert result.structured_content["error"]["code"] == "INVALID_INPUT"
result = await mcp_app.state.mcp.invoke(principal, "submit_backtests", submission(items=[candidate(), candidate() | {"client_item_id": "second"}]))
assert result.structured_content["error"]["code"] == "DUPLICATE_INPUT"
# Failed requests did not consume the key or leave previews.
await invoke(mcp_app, principal, "submit_backtests", submission())
async with mcp_app.state.sessions() as db:
assert await db.scalar(select(func.count()).select_from(BacktestPreview)) == 1
async def test_results_control_and_evidence(mcp_app):
principal, _ = await credentials(mcp_app)
result = await invoke(mcp_app, principal, "submit_backtests", submission(items=[candidate(0), candidate(1)]))
rid = result["backtest_run_id"]
control = {"run_id": rid, "action": "pause", "expected_version": 1, "idempotency_key": "pause"}
paused = await invoke(mcp_app, principal, "control_backtest", control)
assert paused == await invoke(mcp_app, principal, "control_backtest", control)
await invoke(mcp_app, principal, "control_backtest", control | {"action": "resume", "expected_version": 2, "idempotency_key": "resume"})
await execute(mcp_app, mcp_app.state.runner.backtests, rid)
data = await invoke(mcp_app, principal, "get_backtest_results", {"run_id": rid, "limit": 1})
assert data["has_more"] and data["items"][0]["metrics"]["is"]["sharpe"] is None
item = data["items"][0]
async with mcp_app.state.sessions.begin() as db:
saved = await db.get(BacktestResult, item["id"])
saved.snapshot = {**saved.snapshot, "is": {"checks": [{"name": "a", "result": "FAIL"}, {"name": "b", "result": "NEW_STATUS"}]}}
data = await invoke(mcp_app, principal, "get_backtest_results", {"run_id": rid})
assert data["items"][0]["checks"]["counts"]["FAIL"] == 1
assert data["items"][0]["checks"]["counts"]["UNKNOWN"] == 1
pnl = await invoke(mcp_app, principal, "get_backtest_artifact", {"item_id": item["id"], "kind": "pnl"})
assert pnl["status"] == "not_cached"
snapshot = await invoke(mcp_app, principal, "get_backtest_artifact", {"item_id": item["id"], "kind": "snapshot", "limit": 1})
assert snapshot["has_more"]
history = await invoke(mcp_app, principal, "search_backtests", {"candidates": [candidate()], "source": "mcp"})
assert history["total"] == 1 and history["items"][0]["match_type"] == "exact_input"
progress = await invoke(mcp_app, principal, "get_backtest", {"run_id": rid, "after": 0, "event_limit": 1})
assert progress["events"]["has_more"] and progress["submission_counts"]["actual_platform_consumption"] is None
async def test_official_sdk_client_and_error_contract(mcp_app):
import httpx2
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
principal, secret = await credentials(mcp_app)
async with httpx2.AsyncClient(transport=httpx2.ASGITransport(app=mcp_app),
headers={"Authorization": f"Bearer {secret}"}) as http:
async with streamable_http_client("http://testserver/api/v1/mcp/", http_client=http) as streams:
async with ClientSession(streams[0], streams[1]) as client:
await client.initialize()
listed = await client.list_tools()
assert len(listed.tools) == 11
caps = await client.call_tool("get_research_capabilities", {})
assert caps.structured_content["max_candidates"] == 100
result = await client.call_tool("submit_backtests", submission())
assert not result.is_error, result
rid = result.structured_content["backtest_run_id"]
await execute(mcp_app, mcp_app.state.runner.backtests, rid)
results = await client.call_tool("get_backtest_results", {"run_id": rid})
assert results.structured_content["items"][0]["persistence_status"] == "saved"
bad = await client.call_tool("submit_backtests", submission("duplicate"))
assert bad.is_error and bad.structured_content["error"]["code"] == "DUPLICATE_INPUT"
await client.call_tool("get_backtest", {"run_id": rid})
async def test_expiry_binding_disabled_and_audit_redaction(app, mcp_app):
from fastapi import HTTPException
from app.models import Account
principal, secret = await credentials(mcp_app)
await mcp_app.state.mcp.invoke(principal, "get_research_capabilities", {}, request_id=secret)
async with mcp_app.state.sessions.begin() as db:
audit = await db.scalar(select(MCPAudit))
assert secret not in audit.request_id
row = await db.get(MCPToken, principal.token_id)
row.expires_at = now() - timedelta(seconds=1)
async with mcp_app.state.sessions() as db:
with pytest.raises(HTTPException):
await authenticate(db, secret)
_, another = await credentials(mcp_app)
async with mcp_app.state.sessions.begin() as db:
account = await db.get(Account, 1)
account.wq_user_id = "CHANGED"
async with mcp_app.state.sessions() as db:
with pytest.raises(HTTPException):
await authenticate(db, another)
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://testserver") as client:
assert (await client.post(ENDPOINT, json={})).status_code == 404
async def test_metadata_refresh_and_pnl_pagination(mcp_app):
from app.worldquant import WqClient
from tests.research_metadata_fake import response
principal, _ = await credentials(mcp_app)
caps = await invoke(mcp_app, principal, "get_research_metadata", {"query": {"kind": "settings"}})
assert caps["status"] == "not_cached"
await mcp_app.state.runner.client.close()
from tests.backtest_fake import Platform
platform = Platform()
client = WqClient(mcp_app.state.settings, transport=httpx.MockTransport(lambda request: response(request) or platform(request)))
await client.authenticate("synthetic@example.com", "synthetic-password")
mcp_app.state.runner.client = client
mcp_app.state.runner.backtests.client = client
operators = await invoke(mcp_app, principal, "refresh_research_data", {"query": {"kind": "operators"}})
assert operators["status"] == "completed"
page = await invoke(mcp_app, principal, "get_research_metadata", {"query": {"kind": "operators", "limit": 1}})
assert page["total"] == 2 and page["has_more"]
await invoke(mcp_app, principal, "refresh_research_data", {"query": {"kind": "settings"}})
await invoke(mcp_app, principal, "refresh_research_data", {"query": {"kind": "field_availability", "scope": {"region": "USA", "universe": "TOP3000", "delay": 1}, "field_id": "close"}})
invalid = await mcp_app.state.mcp.invoke(principal, "submit_backtests", submission(items=[candidate(region="INVALID")]))
assert invalid.structured_content["error"]["code"] == "UNSUPPORTED_SETTINGS"
run = await invoke(mcp_app, principal, "submit_backtests", submission())
await execute(mcp_app, mcp_app.state.runner.backtests, run["backtest_run_id"])
result = await invoke(mcp_app, principal, "get_backtest_results", {"run_id": run["backtest_run_id"]})
item = result["items"][0]
async with mcp_app.state.sessions.begin() as db:
db.add(Pnl(alpha_id=item["alpha_id"], raw={}, points=[{"date": f"2025-01-0{i}", "value": i} for i in range(1, 4)]))
pnl = await invoke(mcp_app, principal, "get_backtest_artifact", {"item_id": item["id"], "kind": "pnl", "limit": 1, "date_from": "2025-01-02"})
assert pnl["total"] == 2 and pnl["items"][0]["value"] == 2 and pnl["has_more"]
async def test_lost_http_response_replays_without_new_run(mcp_app):
import json
_, secret = await credentials(mcp_app)
class LoseResponse(httpx.ASGITransport):
dropped = False
async def handle_async_request(self, request):
response = await super().handle_async_request(request)
if not self.dropped:
self.dropped = True
await response.aread()
raise httpx.ReadError("synthetic lost response", request=request)
return response
transport = LoseResponse(app=mcp_app)
async with httpx.AsyncClient(transport=transport, base_url="http://testserver",
headers={"Authorization": f"Bearer {secret}", "Accept": "application/json, text/event-stream"}) as client:
payload = {"jsonrpc": "2.0", "id": "lost", "method": "tools/call",
"params": {"name": "submit_backtests", "arguments": submission()}}
with pytest.raises(httpx.ReadError):
await client.post(ENDPOINT, json=payload)
response = await client.post(ENDPOINT, json=payload)
assert response.status_code == 200
assert not response.json()["result"]["isError"]
async with mcp_app.state.sessions() as db:
assert await db.scalar(select(func.count()).select_from(BacktestRun)) == 1
audits = list(await db.scalars(select(MCPAudit)))
assert all("wqmcp_" not in json.dumps(a.__dict__, default=str) for a in audits)
async def test_refresh_job_error_pages_and_kind_isolation(mcp_app):
from app.models import Job, JobItem
principal, _ = await credentials(mcp_app)
async with mcp_app.state.sessions.begin() as db:
db.add(Job(id="refresh", kind="pnl_refresh", failed=2))
db.add(Job(id="auth", kind="connect"))
await db.flush()
db.add_all([JobItem(job_id="refresh", alpha_id=str(i), error="synthetic error") for i in range(2)])
result = await invoke(mcp_app, principal, "get_refresh_job", {"job_id": "refresh", "limit": 1})
assert result["errors"]["total"] == 2 and result["errors"]["has_more"]
result = await invoke(mcp_app, principal, "get_refresh_job", {"job_id": "refresh", "offset": 1})
assert result["errors"]["items"][0]["alpha_id"] == "1"
error = await mcp_app.state.mcp.invoke(principal, "get_refresh_job", {"job_id": "auth"})
assert error.structured_content["error"]["code"] == "NOT_FOUND"
+78
View File
@@ -0,0 +1,78 @@
"""Browser PAT lifecycle and authentication boundaries, with no upstream calls."""
from datetime import timedelta
import httpx
import pytest
from fastapi import HTTPException
from sqlalchemy import select
from app.mcp_api.auth import authenticate
from app.models import Account, MCPToken, now
async def test_token_browser_lifecycle(app, logged_in):
client = logged_in
# Administration remains available when the MCP transport is disabled.
initial = (await client.get("/api/v1/mcp-tokens")).json()
assert initial["enabled"] is False and initial["can_create"] is False
assert (await client.post("/api/v1/mcp-tokens", json={"name": "reader"})).status_code == 422
async with app.state.sessions.begin() as db:
account = await db.get(Account, 1)
account.wq_user_id = "synthetic-user"
issued = await client.post("/api/v1/mcp-tokens", json={"name": " reader "})
assert issued.status_code == 201
assert issued.headers["cache-control"] == "no-store"
token = issued.json()
assert token["name"] == "reader" and token["scopes"] == ["research:read"]
assert token["status"] == "active" and token["token"].startswith("wqmcp_")
async with app.state.sessions() as db:
principal = await authenticate(db, token["token"])
row = await db.get(MCPToken, principal.token_id)
assert row.token_hash != token["token"]
assert 89 < (row.expires_at.replace(tzinfo=now().tzinfo) - now()).total_seconds() / 86400 <= 90
page = await client.get("/api/v1/mcp-tokens?limit=1")
assert token["token"] not in page.text and "token_hash" not in page.text
assert page.json()["items"][0]["id"] == token["id"]
second = await client.post("/api/v1/mcp-tokens", json={"name": "executor", "days": 1, "scopes": ["research:read", "backtests:execute"]})
assert second.status_code == 201
page = (await client.get("/api/v1/mcp-tokens?limit=1")).json()
assert page["total"] == 2 and page["has_more"]
assert (await client.get("/api/v1/mcp-tokens?limit=1&offset=1")).json()["items"][0]["id"] == token["id"]
revoked = await client.post(f'/api/v1/mcp-tokens/{token["id"]}/revoke')
again = await client.post(f'/api/v1/mcp-tokens/{token["id"]}/revoke')
assert revoked.json() == again.json() and revoked.json()["status"] == "revoked"
assert "token" not in revoked.json()
async with app.state.sessions() as db:
with pytest.raises(HTTPException) as exc:
await authenticate(db, token["token"])
assert exc.value.status_code == 401
assert (await client.post("/api/v1/mcp-tokens/missing/revoke")).status_code == 404
async def test_token_browser_security_and_validation(app, logged_in):
client = logged_in
async with app.state.sessions.begin() as db:
(await db.get(Account, 1)).wq_user_id = "synthetic-user"
for body in [
{"name": " "}, {"name": "x", "days": 0}, {"name": "x", "days": 366},
{"name": "x", "days": 1.5}, {"name": "x", "scopes": ["backtests:execute"]},
{"name": "x", "scopes": ["research:read", "admin"]}, {"name": "x", "account_id": 2},
]:
assert (await client.post("/api/v1/mcp-tokens", json=body)).status_code == 422
assert (await client.post("/api/v1/mcp-tokens", json={"name": "x"}, headers={"X-WQ-Request": ""})).status_code == 403
assert (await client.post("/api/v1/mcp-tokens", json={"name": "x"}, headers={"Origin": "https://evil.test"})).status_code == 403
token = (await client.post("/api/v1/mcp-tokens", json={"name": "x"})).json()
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://testserver") as outsider:
headers = {"Authorization": f'Bearer {token["token"]}', "X-WQ-Request": "1"}
assert (await outsider.get("/api/v1/mcp-tokens")).status_code == 401
assert (await outsider.get("/api/v1/mcp-tokens", headers=headers)).status_code == 401
assert (await outsider.post("/api/v1/mcp-tokens", json={"name": "escalation"}, headers=headers)).status_code == 401
assert (await outsider.post(f'/api/v1/mcp-tokens/{token["id"]}/revoke', headers=headers)).status_code == 401
async with app.state.sessions.begin() as db:
(await db.get(Account, 1)).wq_user_id = "different-user"
assert (await client.get("/api/v1/mcp-tokens")).json()["items"][0]["status"] == "invalid_binding"
async with app.state.sessions.begin() as db:
row = await db.scalar(select(MCPToken))
row.expires_at = now() - timedelta(days=1)
assert (await client.get("/api/v1/mcp-tokens")).json()["items"][0]["status"] == "expired"