feat(mcp): connect card, MCP docs, and an end-to-end roles/audit test over HTTP
MCP-8 #4972 "Connect card, docs, and end-to-end integration test". - AI access page: a "Connect a client" card with the /mcp URL (from Settings → General → Public base URL, else the request host) and recipes for Claude Code, Claude Desktop (mcp-remote) and other Streamable HTTP clients, plus the reverse-proxy note. The token-created page shows the same recipes with the new token filled in, ready to paste. - docs/core/mcp.md: connecting, the reverse-proxy requirements, the security model (token ceiling, default deny, check-mode default, secrets, audit), tools by role, response conventions, adding tools. docs/architecture.md describes the /mcp dispatch outside the blueprint pipeline; docs/reference/code-map.md gains an MCP section. - tests/integration/test_mcp.py: over HTTP, a viewer token lists no write tools and is refused one by name; an operator token reads get_overview and its maintenance-window create/end land in the audit log as "<user> via MCP" with the token id. (Revoked → 401 was already covered.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -139,6 +139,8 @@ def test_routes_create_list_and_revoke(app):
|
||||
assert "You have no tokens yet" in page
|
||||
# An operator can't offer admin.
|
||||
assert 'value="admin"' not in page
|
||||
# The connect card names the endpoint, with a placeholder for the token.
|
||||
assert 'value="http://localhost/mcp"' in page and "claude mcp add" in page
|
||||
|
||||
resp = await client.post("/ai-access/tokens", form={
|
||||
"name": "laptop", "role": "admin", "expires_in_days": ""})
|
||||
@@ -148,7 +150,10 @@ def test_routes_create_list_and_revoke(app):
|
||||
resp = await client.post("/ai-access/tokens", form={
|
||||
"name": "laptop", "role": "operator", "expires_in_days": "30"})
|
||||
assert resp.status_code == 200
|
||||
raw = re.search(r'value="(stw_[^"]+)"', await resp.get_data(as_text=True)).group(1)
|
||||
created = await resp.get_data(as_text=True)
|
||||
raw = re.search(r'value="(stw_[^"]+)"', created).group(1)
|
||||
# The recipes on the created page carry the new token, ready to paste.
|
||||
assert f"Bearer {raw}" in created and "mcp-remote" in created
|
||||
caller = await api_tokens.resolve_token(app, raw)
|
||||
assert caller is not None and caller.role == UserRole.operator
|
||||
|
||||
|
||||
Reference in New Issue
Block a user