MCP and OAuth
The mcp Edge Function is a stateless Streamable HTTP Model Context Protocol
resource server. It lets OAuth-authorized assistants work with a user's Navaid
companies without sharing the user's browser session or a service-role key.
Components
MCP client
├─ discovers resource/auth metadata
├─ dynamically registers when hosted policy permits
└─ Supabase OAuth 2.1 authorization flow
└─ /oauth/consent in Navaid frontend
Bearer token
└─ /functions/v1/mcp
├─ auth.getUser(token)
├─ require OAuth client_id claim
├─ load active memberships/roles
├─ enforce mcp_enabled (admin bypass)
└─ execute bounded tool with service client
The function does not mint tokens. Supabase Auth is the authorization server.
Deployment Requirements
mcpdeployed with--no-verify-jwt- hosted Supabase OAuth server enabled
- consent URL configured as
/oauth/consent - dynamic client registration enabled/configured if the target client needs it
- application redirect URLs correct
mcp_enabledset as intended
The audited developer-local, Git-ignored supabase/config.toml disables OAuth
server and dynamic registration. That is not a main-branch declaration; hosted
state must be checked separately.
Authentication
Tool requests require:
- syntactically valid Bearer header,
- token validated by
auth.getUser, - OAuth
client_idclaim, - current Navaid user profile,
- feature access.
A normal web-session token without OAuth client identity is not accepted as an MCP connector credential.
Discovery/challenge endpoints are intentionally reachable without bearer auth. They must expose metadata, not tenant data.
Consent Screen
/oauth/consent?authorization_id=...:
- looks up the pending OAuth authorization request,
- identifies the requesting client/scopes,
- redirects through login when necessary,
- validates safe return state,
- lets the user approve or deny,
- is localized and announces progress to assistive technology.
The authorization identifier is not an arbitrary redirect URL.
Feature and Tenant Scope
mcp_enabled defaults off. System administrators bypass the flag.
The function uses a service client, so it constructs an authorization context
from active company_memberships and system-admin state. Every company
argument is resolved against that context.
Admin-equivalent profile writes allow:
- system admin
company_admincompany_consultant
Managers/users cannot update company facts through MCP.
Tools
Current tools:
| Tool | Access |
|---|---|
list_companies | Active memberships |
get_maturity_summary | Member of company |
get_dimension_detail | Member; bounded tasks/detail |
list_tasks | Member; optional status/dimension |
list_sessions | Role/session visibility rules |
get_session_summary | Authorized session visibility |
get_company_knowledge | Company member |
update_company_facts | Admin/consultant |
complete_task | Authorized task/company |
dismiss_task | Authorized task/company |
Results are bounded and structured. Internal error details are converted to MCP tool errors rather than leaking database/provider internals.
Profile Write Concurrency
update_company_facts:
- resolves authorized company,
- loads profile/version,
- applies allowlisted facts update,
- conditionally writes the expected version,
- appends
company_profile_historywithchange_source='mcp', - reports a conflict when concurrent state changed.
It must not overwrite maturity or other pipeline-owned fields.
Scopes
Keep OAuth scopes least privilege. If scope granularity is expanded:
- separate read from write,
- surface human-readable scope descriptions in consent,
- enforce scopes in addition to membership/role,
- preserve compatibility for existing clients,
- add revoke/re-consent guidance.
Do not treat user approval as tenant authorization; both are required.
Security Failure Modes
| Failure | Response |
|---|---|
| No bearer on tool call | OAuth resource challenge/401 |
| Invalid/expired token | 401 |
| Normal web token | reject missing OAuth client identity |
| Feature off | 403 for non-admin |
| Unknown/cross-tenant company | tool error/forbidden |
| Insufficient write role | tool error/forbidden |
| Optimistic write conflict | retryable conflict, no lost update |
| Discovery request | metadata only |
Never log bearer tokens or full authorization callback URLs.
Adding a Tool
- Define a Zod input schema.
- Bound lists and text.
- Resolve company/session/task through authorization context.
- Decide membership and role.
- Enforce OAuth scopes if used.
- Avoid returning secrets, raw tokens, hidden prompts, or unrelated columns.
- Use concurrency/audit patterns for writes.
- Add protocol, auth, tenant and malformed-input tests.
- Update consent copy/scopes and this table.
Troubleshooting
Client cannot discover server
Check public metadata/challenge routes, deployment flag, canonical function URL, and provider expectations.
Consent loops to login
Verify safe redirect preservation and Auth site/redirect URLs.
DCR fails
Hosted dynamic registration may be disabled even though the function is deployed.
Web session works but MCP token fails
Expected if the token lacks an OAuth client_id claim.
Tool sees no companies
Inspect active membership status and role; selected browser-company state is irrelevant to MCP.