"""MCP Resources — reference documents an AI client can attach as context. Tools answer questions; resources ARE context. Two static platform documents ship here (auth still applies — the resource endpoints sit behind the same PAT/OAuth middleware as tools): * ``helix://platform/overview`` — what Helix is, how the AI surface behaves (permission scoping, approval-chain invariant), and which access path fits which audience. The same story the Help Center and ``/llms.txt`` tell, kept deliberately in one voice. * ``helix://platform/tool-catalog`` — the live tool list grouped read/write, generated from the registry at call time so it can never go stale. Lets a client ground "what can you actually do?" without a 400-entry tools/list round-trip in the conversation window. Per-user data stays in tools: resources have no natural audit/rate namespace of their own here, and every "my ..." read already has a purpose-built tool. """ from __future__ import annotations from app.mcp.server import mcp_server _OVERVIEW = """\ # Helix — platform overview for AI agents Helix is PingCAP's unified internal IT platform: people & identity, hardware assets, software seats, API keys, cloud resources (CMDB), permissions, approvals, finance views and security governance for a ~500-person organisation. ## How this MCP surface behaves - Every tool call runs AS the authenticated employee (OAuth or PAT). Results mirror exactly what that person sees in the Helix web UI — same RBAC, same manager-derived scope. An empty result usually means "not visible to you", not "does not exist". - Mutating tools SUBMIT into the normal approval chain (Feishu approval to IT / asset admins). "Success" means *request created*, not *action completed*. Nothing here bypasses review. - Every call is audited (action="mcp.", trace-id, token id). - Access dies at offboarding (Leaver gate blocks writes; token/OAuth grants deactivate with the account). ## Access paths (broadest audience first) 1. In-app AI assistant — zero setup, inside the Helix web UI. 2. Claude connector (OAuth) — claude.ai / Claude Desktop, paste the MCP URL, Individual sign-in. 3. Claude Code plugin `helix@helix` — MCP + skills + slash commands, installed via `helix plugin install` or the Help Center download. 4. helix CLI — scriptable access to every tool (`helix mcp call …`). 5. Direct MCP — any compatible client (Cursor, …), OAuth or PAT. 6. External API (`helix_api_`) — machine-to-machine, no AI involved. Human-facing manual: https://helix.pingcap.net/help (AI 助手集成 tab). """ @mcp_server.resource( "helix://platform/overview", name="helix-platform-overview", title="Helix platform overview", description=( "What Helix is and how this MCP surface behaves: permission " "scoping, the approval-chain invariant for writes, audit, and " "the six access paths. Attach when orienting a new session." ), mime_type="text/markdown", ) def platform_overview() -> str: return _OVERVIEW @mcp_server.resource( "helix://platform/tool-catalog", name="helix-tool-catalog", title="Helix tool catalog (live)", description=( "Generated read/write catalog of every registered Helix tool " "with its first description line. Always current — built from " "the live registry, grouped by mutation class." ), mime_type="text/markdown", ) def tool_catalog() -> str: from app.mcp.annotations import DESTRUCTIVE_TOOLS, mutating_tools_from_source flags = mutating_tools_from_source() tools = mcp_server._tool_manager._tools def first_line(text: str | None) -> str: return (text or "").strip().split(". ")[0][:160] reads = sorted(n for n in tools if not flags.get(n, False)) writes = sorted(n for n in tools if flags.get(n, False)) lines = [ "# Helix tool catalog", "", f"{len(tools)} tools — {len(reads)} read-only, {len(writes)} mutating " "(mutating tools submit into the approval chain; those marked " "DESTRUCTIVE deserve explicit user confirmation).", "", "## Read-only", "", ] lines += [f"- `{n}` — {first_line(tools[n].description)}" for n in reads] lines += ["", "## Mutating", ""] lines += [ f"- `{n}`{' **[DESTRUCTIVE]**' if n in DESTRUCTIVE_TOOLS else ''}" f" — {first_line(tools[n].description)}" for n in writes ] return "\n".join(lines)