Skip to content

Access mcp server#443

Open
eguerrant wants to merge 7 commits into
mainfrom
access_mcp
Open

Access mcp server#443
eguerrant wants to merge 7 commits into
mainfrom
access_mcp

Conversation

@eguerrant
Copy link
Copy Markdown
Contributor

@eguerrant eguerrant commented May 12, 2026

Add embedded MCP server (off by default)

Adds an optional MCP server at /mcp, gated behind ENABLE_MCP=False. Lets any MCP-compatible LLM client (Claude Code, Cursor, Zed, OpenAI clients, MCP Inspector, self-hosted models) read Access data and submit access/role/group requests on behalf of the authenticated user.

What's added

  • ENABLE_MCP config flag. Off by default; operators not running LLM tooling pay nothing.
  • FastMCP server (mcp[cli]==1.27.0) mounted at /mcp via a Starlette Route (works without a trailing slash).
  • Two built-in MCP auth providers, mutually exclusive for /mcp:
    • Cloudflare Access (verifies the CF JWT; activates when CLOUDFLARE_TEAM_DOMAIN is set).
    • OIDC bearer-token (verifies a JWT against your IdP's JWKS; activates when OIDC_SERVER_METADATA_URL is set; requires OIDC_MCP_AUDIENCE so the aud claim is validated, fails closed at startup if missing).
    • Plus a dev provider for local development (gates on ENV=development/test).
  • source field on AuditLogSchema and RequestContext. Auto-injected from context so existing operations tag correctly with zero per-operation code changes.
  • MCP_FALLBACK_SCOPES config (default read_all,create_requests). Tokens carrying an explicit scope claim override it.
  • MCP_ALLOWED_HOSTS config. Empty by default (DNS-rebinding protection off; structural defenses cover the threat). Set to your public host for defense-in-depth, or to localhost:* for dev work with a browser open.
  • @requires_scope decorator on every tool; scope errors are returned as a standard {"error": "..."} JSON envelope.
  • 30 MCP tests; full suite is 451 green. Ruff and mypy clean.

Tool surface (21 tools, 1 prompt)

  • Reads (read_all scope): list/get for groups, roles, apps, users, tags, access requests, role requests, group requests; plus list_audit_entries and list_group_memberships.
  • Writes (create_requests scope): create_access_request, create_role_request, create_group_request. Each runs the same authorization predicate as the matching REST endpoint (e.g. create_role_request gates on can_manage_group(role)).
  • Prompt: request_access guides users toward role-based requests, time-bounded grants, and surfacing tag constraints up front.
  • Approvals, rejections, and direct membership edits are intentionally NOT exposed.

Three-layer authorization

  1. @requires_scope(...) decorator: the token attenuates which tools can be called.
  2. Bare predicates from api/auth/permissions.py: gate what the resolved user can do.
  3. Operation constraints: tag constraints, is_managed, reason requirements.

Scopes never grant permissions. MCP tools can do strictly less than what the same user can do via REST.

Open-source posture

  • LLM-agnostic: standard MCP Streamable HTTP, any compliant client connects by URL.
  • IdP-agnostic: CF Access and OIDC ship as the two built-in providers. Access was originally Okta-only and the Okta prefix on some model names (OktaGroup, OktaUser, etc.) is legacy; the MCP server's instructions tell the LLM to treat them as a vestigial label, not a deployment requirement.
  • Off by default: no overhead for operators not running LLM tooling.

Operator notes

  • ENABLE_MCP=true to activate.
  • For Cloudflare deployments: set CLOUDFLARE_TEAM_DOMAIN + CLOUDFLARE_APPLICATION_AUDIENCE.
  • For OIDC deployments: set OIDC_SERVER_METADATA_URL + OIDC_MCP_AUDIENCE. The MCP path is a bearer-token resource server, not the REST OIDC session-cookie integration.
  • Default fallback scope set is read_all,create_requests. Set MCP_FALLBACK_SCOPES=read_all for read-only sessions, or MCP_FALLBACK_SCOPES="" to fail closed for tokens that don't carry a scope claim.

TODO

  • Review code
  • Add README + deployment notes
  • Disallow client-side auto-approval for write tools (elicitations on create_access_request / create_role_request / create_group_request; follow-up)
  • Test locally
  • Test in staging
  • Add rate limiting for /mcp (follow-up PR)

Comment thread api/plugins/mcp_auth.py Outdated
Comment thread api/config.py
# When CF (or your provider) starts emitting scope claims, this
# fallback never fires and the token controls scope per session;
# the value here becomes inert.
MCP_FALLBACK_SCOPES: str = "read_all,create_requests"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this default to read_all instead of both scopes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could go either way on this but given that tool use is still gated on whether or not the user has the permissions to do an action (eg. creating a role request requires them to own a role) I was leaning towards leaving both scopes as the default

Comment thread requirements.txt Outdated
Comment thread api/mcp/server.py Outdated
Comment thread api/mcp/server.py Outdated
Comment thread requirements.txt
Comment thread api/mcp/auth/cloudflare.py Outdated
Comment thread api/mcp/prompts.py Outdated
Comment thread api/mcp/server.py Outdated
Comment thread api/mcp/server.py Outdated
Comment thread api/mcp/server.py Outdated
Comment thread api/mcp/tools.py
Comment thread api/mcp/tools.py Outdated
Comment thread api/mcp/tools.py Outdated
Comment thread api/mcp/tools.py
if err:
return _error(err)
db = _db_shim.session
query = (
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since most of this code is copied from the api/routers I wonder if we can abstract it where both the router and MCP have a shared method to call maybe defined in api/views or something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be possible to create a top level layer for query construction that would remove some duplicate code and reduce the possibility of drift but since that would touch the routers as well I'm inclined to say that should be left as a follow up. This PR is already massive

@eguerrant eguerrant requested a review from somethingnew2-0 May 27, 2026 00:10
@eguerrant eguerrant marked this pull request as ready for review May 27, 2026 00:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants