diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..3b34288 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,354 @@ +# LFX MCP Server — Architecture + +> **Current state** as of April 2026. + +The LFX MCP Server is a [Model Context Protocol](https://modelcontextprotocol.io/) server that +exposes LFX platform capabilities as MCP tools. It supports two transport modes: + +- **stdio** — for local development; the binary reads/writes JSON-RPC 2.0 messages on + stdin/stdout with no authentication required. +- **HTTP (Streamable HTTP)** — for production Kubernetes deployments; the `/mcp` endpoint + accepts JSON-RPC 2.0 over HTTP with OAuth2 bearer token authentication. + +```mermaid +%%{init: {"flowchart": {"defaultRenderer": "elk"}}}%% +flowchart LR + subgraph Clients + U[End user\nClaude / Cursor / Inspector] + M2MC[M2M client] + AK[Static API key\nstop-gap] + end + + subgraph Auth0 + JWKS[JWKS endpoint] + TOKE[Token endpoint\nRFC 8693 CTE] + TOKCC[Token endpoint\nclient_credentials] + end + + subgraph MCP["LFX MCP Server"] + BM[Bearer middleware\nJWT / API-key verify] + NS[newServer factory\ntool registration gate] + CTE[TokenExchangeClient\nCTE → V2 token] + M2MV2[TokenExchangeClient\nclient_credentials → V2 token] + SR[SlugResolver\nslug → UUID] + AC[AccessCheckClient\nOpenFGA] + CCL[ClientCredentialsClient\nper-service M2M token] + end + + subgraph Upstream + V2[LFX V2 API\nnative pass-through] + LENS[LFX Lens API\nM2M-brokered] + ONB[Member Onboarding API\nM2M-brokered] + end + + U -->|"Bearer: MCP JWT"| BM + M2MC -->|"Bearer: M2M JWT"| BM + AK -->|"Bearer: api-key"| BM + BM -->|verifies via| JWKS + BM --> NS + + NS -->|"end-user tools/call"| CTE + NS -->|"M2M / api-key tools/call"| M2MV2 + CTE -->|RFC 8693| TOKE + M2MV2 -->|client_credentials| TOKCC + + CTE -->|V2 token| V2 + M2MV2 -->|V2 token| V2 + + CTE -->|V2 token\nslug + access-check| SR + M2MV2 -->|V2 token\nslug + access-check| SR + SR -->|slug → UUID| V2 + SR --> AC + AC -->|OpenFGA check| V2 + AC -->|authorized| CCL + CCL -->|client_credentials| TOKCC + CCL -->|M2M token| LENS + CCL -->|M2M token| ONB +``` + +--- + +## 1. Client Authentication & Authorization + +All inbound calls in HTTP mode pass through a bearer-token middleware before reaching the MCP +protocol layer. The middleware produces a `TokenInfo` struct (scopes + custom claims) that drives +tool registration: `newServer()` is called once per HTTP request and registers only the tools the +caller is permitted to invoke, so `tools/list` always reflects exactly what the caller can use. + +### Stateless HTTP and per-request tool gating + +The `StreamableHTTPHandler` runs with `Stateless: true`: + +- Each HTTP request gets a fresh `*mcp.Server` from the `newServer()` factory — no MCP-level + session state accumulates across requests. +- Any pod can handle any request; round-robin load balancing works without Kubernetes session + affinity. +- A package-level `schemaCache` is shared across per-request server instances so that + reflection-based JSON schema generation runs once per tool type rather than once per request. +- Client callbacks (`ListRoots`, `CreateMessage`, `Elicit`) are not available in stateless mode + and are not used. + +Tool registration is gated on two boolean values derived from `TokenInfo` at server creation time: + +| Flag | Condition | Grants access to | +|---|---|---| +| `canRead` | token holds `read:all` **or** `manage:all` | All read-only tools | +| `canManage` | token holds `manage:all` | Read + write/delete tools | + +An additional `isStaff` flag (derived from the `http://lfx.dev/claims/lf_staff` custom claim) +gates the `query_lfx_lens` tool independently of scopes. + +### End-user OAuth2 JWT + +The primary authentication path. The user completes an Auth0 authorization code flow in their +MCP client (Claude, Cursor, Inspector, etc.) and receives an MCP JWT. The server verifies the +token signature via JWKS (cached), checks the audience, and extracts scopes and custom claims. + +MCP clients that implement [OAuth 2.0 Protected Resource Metadata (RFC 9728)](https://www.rfc-editor.org/rfc/rfc9728) +first fetch `/.well-known/oauth-protected-resource` from the MCP server to discover the Auth0 +authorization server URL before starting the OAuth flow. + +### M2M client credentials + +A machine caller obtains a bearer token from Auth0 via the client credentials grant and presents +it as a standard bearer token. The server follows the same JWT verification path as for end-user +tokens; the scopes embedded in the M2M JWT determine which tools are registered. + +### Static API key (stop-gap) + +For MCP clients that cannot complete an OAuth2 flow, static API keys can be configured via +`LFXMCP_API_CREDENTIALS_=` environment variables. The `APIKeyVerifier` is checked +before the JWT path; when a key matches it synthesizes a `TokenInfo` with a fixed scope set so +the rest of the tool-gating logic is identical to the JWT path. + +> **This mechanism is a temporary stop-gap and will be retired once all consumers support OAuth2.** + +--- + +## 2. Upstream Authentication & Authorization + +Once a tool handler is invoked, the server authenticates to one or more upstream LFX APIs. There +are two distinct patterns depending on whether the upstream API supports per-user authorization +natively. + +### Custom Token Exchange (CTE) — end-user callers + +For end-user callers, the server exchanges the user's MCP JWT for a V2-scoped token that carries +the user's identity. This is a **Custom Token Exchange** per +[RFC 8693](https://www.rfc-editor.org/rfc/rfc8693): the MCP server's own M2M client +(`LFX MCP Server`) authenticates to Auth0 using a signed JWT client assertion (RS256, RFC 7523) +or client secret, and presents the user's MCP JWT as the `subject_token`. Auth0 issues a +V2-scoped token that carries the user's identity. The exchanged token is cached per user subject +and refreshed automatically on expiry. + +### MCP-server M2M V2 token — M2M and API-key callers + +When the inbound bearer is itself an M2M JWT (Auth0 subjects for M2M tokens end in `@clients`) +or a static API key, there is no user identity to exchange. In this case the server obtains a +V2-scoped token via a standard client credentials grant using the same M2M client — no CTE is +performed. The upstream V2 identity is always the MCP server itself; no user identity is present +in the chain. This token is also cached and shared across all M2M and API-key requests. + +### Native LFX self-service pass-through + +V2 API tools (`search_projects`, `get_committee`, member, meeting, mailing list tools, etc.) pass +the V2 token (CTE token for end-user callers; MCP-server M2M V2 token for M2M callers) directly +to V2 API calls. Authorization is handled natively by V2 and its OpenFGA backend; the MCP server +performs no explicit access-check of its own for these tools. + +### MCP-brokered service APIs (OpenFGA gate + per-service M2M token) + +Service APIs (LFX Lens and Member Onboarding) accept only M2M tokens — they have no per-user +authorization layer. The MCP server acts as the authorization gateway: + +1. Obtain the appropriate V2 token: CTE token (end-user) or MCP-server M2M V2 token (M2M / + API-key caller). +2. Resolve the project slug → UUID via the V2 Query Service, authorized with the V2 token from + step 1. +3. Call the V2 access-check endpoint (`POST /access-check?v=1`, backed by OpenFGA), authorized + with the same V2 token from step 1 — **not** the service-API M2M token. The check format is + `project:{uuid}#auditor` for LFX Lens and `project:{uuid}#writer` for Member Onboarding. +4. Acquire a separate per-service M2M token via a standard client credentials grant (same M2M + client, different `audience`). Each service has its own `ClientCredentialsClient` that caches + the token and refreshes it automatically. +5. Call the service API with the per-service M2M token. The service only ever sees that M2M + token — no user identity is forwarded. + +LFX Lens additionally requires the `lf_staff` claim in the caller's MCP JWT (checked at tool +registration time; the tool is simply not registered for non-staff callers). + +The access-check result format uses `#` for the relation and tab-separates the echoed request +from the boolean result. Multiple checks can be batched; results are not guaranteed to be in +request order and are matched by parsing the request prefix from each result string. + +--- + +## 3. End-to-End Flows + +### Flow 1: End-user → V2 native pass-through + +Representative tool: `get_committee` + +```mermaid +sequenceDiagram + actor User + participant Client as MCP Client + participant MCP as MCP Server + participant Auth0 + participant V2 as LFX V2 API + + User->>Client: open MCP tool + + Client->>MCP: GET /.well-known/oauth-protected-resource + MCP-->>Client: auth server URL (Auth0) + + Client->>Auth0: authorization code flow + Auth0-->>Client: MCP JWT (aud: mcp.lfx.dev) + + Client->>MCP: tools/list\nAuthorization: Bearer {mcp_jwt} + MCP->>Auth0: fetch JWKS (cached) + Auth0-->>MCP: public keys + MCP->>MCP: verify signature, expiry, audience\nextract scopes + lf_staff claim + MCP-->>Client: tools/list (filtered to caller's scopes) + + User->>Client: invoke get_committee (slug="pytorch") + Client->>MCP: tools/call {get_committee}\nAuthorization: Bearer {mcp_jwt} + + MCP->>Auth0: token exchange (RFC 8693)\nM2M client assertion + {mcp_jwt} + Auth0-->>MCP: CTE token (carries user identity, cached) + + MCP->>V2: GET /committees?projectID=...\nAuthorization: Bearer {cte_token} + V2->>V2: verify token + OpenFGA authz\n(natively, no MCP involvement) + V2-->>MCP: committee data + MCP-->>Client: tool result +``` + +### Flow 2: End-user → MCP-brokered service API + +Representative tool: `query_lfx_lens` + +```mermaid +sequenceDiagram + actor User + participant Client as MCP Client + participant MCP as MCP Server + participant Auth0 + participant V2 as LFX V2 API\n(Query Svc + Access Check) + participant Lens as LFX Lens API + + User->>Client: open MCP tool + + Client->>MCP: GET /.well-known/oauth-protected-resource + MCP-->>Client: auth server URL (Auth0) + + Client->>Auth0: authorization code flow + Auth0-->>Client: MCP JWT (aud: mcp.lfx.dev, lf_staff=true) + + Client->>MCP: tools/list\nAuthorization: Bearer {mcp_jwt} + MCP->>Auth0: fetch JWKS (cached) + Auth0-->>MCP: public keys + MCP->>MCP: verify signature, expiry, audience\nextract scopes + lf_staff claim + Note over MCP: query_lfx_lens registered\nonly when lf_staff=true + MCP-->>Client: tools/list (includes query_lfx_lens) + + User->>Client: invoke query_lfx_lens (slug="pytorch") + Client->>MCP: tools/call {query_lfx_lens}\nAuthorization: Bearer {mcp_jwt} + + MCP->>Auth0: token exchange (RFC 8693)\nM2M client assertion + {mcp_jwt} + Auth0-->>MCP: CTE token (carries user identity, cached) + + MCP->>V2: GET /query?filter=slug:pytorch\nAuthorization: Bearer {cte_token} + V2-->>MCP: project UUID (cached) + + MCP->>V2: POST /access-check?v=1\n["project:{uuid}#auditor"]\nAuthorization: Bearer {cte_token} + V2-->>MCP: ["project:{uuid}#auditor@user:...\ttrue"] + + alt access denied + MCP-->>Client: error: access denied + end + + MCP->>Auth0: client_credentials grant\naudience = Lens API resource server + Auth0-->>MCP: Lens M2M token (no user identity, cached) + + MCP->>Lens: POST /workflows/.../runs\nAuthorization: Bearer {lens_m2m_token} + Lens->>Lens: verify JWT via JWKS + Lens-->>MCP: response + MCP-->>Client: tool result +``` + +### Flow 3: M2M client → V2 native pass-through + +Representative tool: `search_projects` + +```mermaid +sequenceDiagram + participant Client as M2M Client + participant Auth0 + participant MCP as MCP Server + participant V2 as LFX V2 API + + Client->>Auth0: client_credentials grant\naudience = MCP API resource server + Auth0-->>Client: M2M JWT (aud: mcp.lfx.dev) + + Client->>MCP: tools/list\nAuthorization: Bearer {m2m_jwt} + MCP->>Auth0: fetch JWKS (cached) + Auth0-->>MCP: public keys + MCP->>MCP: verify signature, expiry, audience\nextract scopes\ndetect M2M (subject ends in @clients) + MCP-->>Client: tools/list (filtered to token scopes) + + Client->>MCP: tools/call {search_projects}\nAuthorization: Bearer {m2m_jwt} + + MCP->>Auth0: client_credentials grant\naudience = V2 API resource server + Auth0-->>MCP: MCP-server M2M V2 token\n(no user identity, cached) + + MCP->>V2: GET /projects?...\nAuthorization: Bearer {mcp_m2m_v2_token} + V2->>V2: verify token + OpenFGA authz\n(natively, MCP server identity) + V2-->>MCP: project data + MCP-->>Client: tool result +``` + +### Flow 4: M2M client → MCP-brokered service API + +Representative tool: `onboarding_list_memberships` + +```mermaid +sequenceDiagram + participant Client as M2M Client + participant Auth0 + participant MCP as MCP Server + participant V2 as LFX V2 API\n(Query Svc + Access Check) + participant ONB as Member Onboarding API + + Client->>Auth0: client_credentials grant\naudience = MCP API resource server + Auth0-->>Client: M2M JWT (aud: mcp.lfx.dev) + + Client->>MCP: tools/list\nAuthorization: Bearer {m2m_jwt} + MCP->>Auth0: fetch JWKS (cached) + Auth0-->>MCP: public keys + MCP->>MCP: verify signature, expiry, audience\nextract scopes\ndetect M2M (subject ends in @clients) + MCP-->>Client: tools/list (filtered to token scopes) + + Client->>MCP: tools/call {onboarding_list_memberships}\nAuthorization: Bearer {m2m_jwt} + + MCP->>Auth0: client_credentials grant\naudience = V2 API resource server + Auth0-->>MCP: MCP-server M2M V2 token\n(no user identity, cached) + + MCP->>V2: GET /query?filter=slug:pytorch\nAuthorization: Bearer {mcp_m2m_v2_token} + V2-->>MCP: project UUID (cached) + + MCP->>V2: POST /access-check?v=1\n["project:{uuid}#writer"]\nAuthorization: Bearer {mcp_m2m_v2_token} + Note over MCP,V2: access-check uses the MCP-server M2M V2 token,\nnot the onboarding service M2M token + V2-->>MCP: ["project:{uuid}#writer@client:...\ttrue"] + + alt access denied + MCP-->>Client: error: access denied + end + + MCP->>Auth0: client_credentials grant\naudience = Onboarding API resource server + Auth0-->>MCP: Onboarding M2M token (cached) + + MCP->>ONB: GET /member-onboarding/{slug}/memberships\nAuthorization: Bearer {onboarding_m2m_token} + ONB->>ONB: verify JWT via JWKS + ONB-->>MCP: memberships response + MCP-->>Client: tool result +``` diff --git a/docs/service-api-architecture.md b/docs/service-api-architecture.md deleted file mode 100644 index 3ba99e2..0000000 --- a/docs/service-api-architecture.md +++ /dev/null @@ -1,259 +0,0 @@ -# Service API Architecture - -> **Date:** 2026-03-24 -> **Authors:** Joan Reyero, Josep Reyero - -## Context - -The LFX MCP Server exposes tools for **Member Onboarding** and **LFX Lens**. These are internal service APIs that have no per-user authorization. The MCP server authenticates to them using OAuth2 client credentials (M2M bearer tokens) issued by Auth0, with each service protected by its own Auth0 resource server. - -The MCP server acts as the authorization gateway: before proxying a request to a service API, it calls the **V2 access-check endpoint** (backed by OpenFGA) to verify the user has the right relationship to the project. - -### Access Rules - -Service tools enforce two layers of authorization: **MCP scopes** (checked at dispatch) and **V2 relations** (checked inside the handler via OpenFGA). Both must pass. - -| Service | MCP Scope | Required V2 Relation | Additional | Rationale | -|-----------------------|--------------|----------------------|------------------|--------------------------------------------------------------------| -| **Member Onboarding** | `manage:all` | `writer` | — | Managing onboarding workflows is a write-level project operation | -| **LFX Lens** | `read:all` | `auditor` | `lf_staff` claim | Analytics/reporting requires auditor-level read access; staff-only | - -MCP scopes act as an upper bound on what a token can do — like a reduced-scope PAT in GitHub. Even if a user has `writer` access to a project in OpenFGA, a `read:all`-only MCP token cannot call onboarding tools. - ---- - -## Three Tokens - -All three tokens are issued by Auth0. The MCP server's M2M client (`LFX MCP Server`) authenticates to Auth0 for both the token exchange and client credentials grants — same client, different grant types. - -| Token | Type | Purpose | Grant type | User identity? | -|---------------|----------------|---------------------------------------------|--------------------------------------------------------------------|-------------------| -| MCP JWT | User | Authenticates the human user | Authorization code (OAuth login) | Yes | -| V2 Token | User-delegated | Slug resolution + access-check | Token exchange (RFC 8693): M2M client credentials + user's MCP JWT | Yes (delegated) | -| Service Token | Machine (M2M) | Authenticates MCP server to Lens/Onboarding | Client credentials: M2M client credentials only | No — pure machine | - ---- - -## Authorization Flow - -```mermaid -sequenceDiagram - actor User - participant Client as MCP Client
(Claude/Cursor) - participant Auth0 - participant MCP as MCP Server - participant V2 as V2 Platform
(OpenFGA) - participant Service as Service API
(Lens/Onboarding) - - Note over User,Auth0: 1. User Authentication (OAuth login) - User->>Client: Use MCP tool - Client->>Auth0: OAuth authorization code flow - Auth0-->>Client: MCP JWT
(aud: mcp.lfx.dev, lf_staff claim) - - Note over Client,MCP: 2. Tool Call - Client->>MCP: tools/call with MCP JWT
(e.g., lfx_lens_query) - - Note over MCP: 3. JWT Verification + MCP Scope Check - MCP->>MCP: Verify JWT signature (JWKS) - MCP->>MCP: Check MCP scope
(read:all or manage:all) - MCP->>MCP: Check lf_staff claim
(Lens only: staff-gated) - - Note over MCP,V2: 4. Authorization via V2 (M2M client exchanges user token) - MCP->>Auth0: Token exchange (RFC 8693)
M2M client authenticates +
sends user's MCP JWT as subject_token - Auth0-->>MCP: V2 token (carries user identity) - - MCP->>V2: Resolve slug → UUID
(GET /query?filter=slug:pytorch) - V2-->>MCP: project UUID - - MCP->>V2: POST /access-check?v=1
project:{uuid}#auditor - V2-->>MCP: allow / deny - - alt denied - MCP-->>Client: Error: access denied - end - - Note over MCP,Service: 5. Service Call (M2M client gets its own token) - MCP->>Auth0: Client credentials grant
M2M client authenticates +
audience = service resource server - Auth0-->>MCP: M2M token (no user identity, cached) - - MCP->>Service: POST /workflows/...
Authorization: Bearer {m2m_token} - Service->>Service: Validate JWT (JWKS + audience) - Service-->>MCP: Response - - MCP-->>Client: Tool result -``` - ---- - -## M2M Client Usage - -The same M2M client (`LFX MCP Server`) authenticates to Auth0 for both grant types. The difference is what it asks for: - -```mermaid -flowchart TD - M2M[M2M Client
LFX MCP Server
client_id + signing key] - - M2M -->|"grant_type=token-exchange
+ user's MCP JWT
audience = V2 API"| V2Token[V2 Token
carries user identity] - M2M -->|"grant_type=client_credentials
audience = Lens API"| LensToken[Lens M2M Token
no user identity] - M2M -->|"grant_type=client_credentials
audience = Onboarding API"| OnbToken[Onboarding M2M Token
no user identity] - - V2Token --> SlugRes[Slug Resolution] - V2Token --> AccessCheck[Access Check] - SlugRes --> Auth{Authorized?} - AccessCheck --> Auth - - Auth -->|yes| LensToken - Auth -->|yes| OnbToken - Auth -->|no| Denied[Access Denied] -``` - ---- - -## Access-Check API - -Per Eric Searcy's clarification, the correct format uses `#` for the relation (not `:` as in the current Swagger docs). - -```http -POST /access-check?v=1 -Authorization: Bearer -Content-Type: application/json - -{ - "requests": ["project:{uuid}#writer"] -} -``` - -Each result is a tab-delimited string containing the request echoed back with the authenticated user appended, followed by `true` or `false`: - -```json -{ - "results": ["project:{uuid}#writer@user:auth0|alice\ttrue"] -} -``` - -Multiple checks can be batched. **Results are NOT guaranteed to be in the same order as requests** — cached entries are populated first. The MCP server matches results back to requests by parsing the request prefix from each result string. - ---- - -## Slug-to-UUID Resolution - -Users provide project **slugs** (e.g., `pytorch`). The access-check endpoint requires **UUIDs**. The MCP server resolves this using the user's exchanged V2 token. - -Results are cached in-memory (slug→UUID mappings are stable). - ---- - -## Tools - -### LFX Lens - -All tools require the `lf_staff` JWT claim and `auditor` relation to the project. - -| MCP Tool | Backend Endpoint | Method | Description | -|------------------|-----------------------------------------|--------|---------------------------------------------------------------------------------------------------------------| -| `lfx_lens_query` | `/workflows/lfx-lens-mcp-workflow/runs` | `POST` | Query LFX Lens analytics for a project. Accepts a natural language question and returns an analytics summary. | - -#### LFX Lens API - -`POST /workflows/lfx-lens-mcp-workflow/runs` accepts `multipart/form-data`: - -| Field | Type | Required | Description | -|-------------------|-------------|----------|-------------------------------------------------| -| `message` | string | Yes | The user's natural language question | -| `additional_data` | JSON string | Yes | `{"foundation": {"slug": ""}}` | -| `stream` | string | No | `"false"` (default) or `"true"` | - -Response: -```json -{ - "content": "CNCF currently has 726 active members\n\n| CURRENT_MEMBER_COUNT |\n| --- |\n| 726 |", - "content_type": "str", - "status": "COMPLETED" -} -``` - -### Member Onboarding - -All tools require `writer` relation to the project. - -The onboarding service exposes two sets of endpoints: - -- **Custom REST endpoints** under `/member-onboarding/` — for memberships and agent configs. -- **AgentOS framework endpoints** under `/agents/{agent_id}/runs` — for running AI agents. - -| MCP Tool | Backend Endpoint | Method | Description | -|-------------------------------|----------------------------------------------|--------|----------------------------------------------------------------------------------------------------------------------------------------| -| `onboarding_list_memberships` | `/member-onboarding/{slug}/memberships` | `GET` | List memberships for a project with per-agent action/todo counts. Accepts `status` filter (`all`, `pending`, `in_progress`, `closed`). | -| `onboarding_get_membership` | `/member-onboarding/{slug}/memberships/{id}` | `GET` | Get a single membership with full agent details. | -| `onboarding_run_agent` | `/agents/{agent_id}/runs` | `POST` | Run a specific onboarding agent for a membership. Supports a `preview` flag to dry-run the agent without executing side effects. | - -#### Agent IDs - -| Agent ID | Description | -|--------------------------------------|--------------------------------------------| -| `member-onboarding-slack` | Adds members to Slack channels | -| `member-onboarding-email` | Sends onboarding emails based on templates | -| `member-onboarding-discord` | Assigns Discord roles to members | -| `member-onboarding-github` | Creates PRs / file changes in GitHub repos | -| `member-onboarding-committees` | Adds members to LFX committees | -| `member-onboarding-hubspot-workflow` | Enrolls contacts in HubSpot workflows | - -#### AgentOS Run Endpoint - -`POST /agents/{agent_id}/runs` accepts `multipart/form-data`: - -| Field | Type | Required | Description | -|--------------|---------|----------|------------------------------------------------| -| `message` | string | Yes | Text input describing what the agent should do | -| `stream` | boolean | No | Enable streaming via Server-Sent Events | -| `session_id` | string | No | Session ID for context continuity | -| `user_id` | string | No | User context identifier | - -The `preview` flag is handled at the MCP tool level by routing to the `member-onboarding-preview` agent ID instead of the requested agent. This returns predicted actions and prerequisite status without executing side effects. - ---- - -## Tool Naming Convention - -Tools follow a `__` pattern: - -- **V2 tools** (no prefix needed — they're the primary domain): `search_projects`, `get_committee`, `create_committee_member` -- **Service tools** (prefixed with service name for disambiguation): `onboarding_list_memberships`, `onboarding_run_agent`, `lfx_lens_query` - -Verbs are consistent with the V2 tools: `search`, `get`, `list`, `create`, `update`, `delete`, `run`, `query`. - ---- - -## Auth0 Resource Servers - -| Resource Server | Audience | Used By | Scopes | -|-----------------------|-------------------------------------------------|---------------------------------|--------------------------| -| LFX MCP API | `mcp.lfx.dev/mcp` | Claude, Cursor, Inspector | `read:all`, `manage:all` | -| LFX V2 API | `lfx-api.v2.cluster.lfx.dev/` | MCP server (token exchange) | `access:api` | -| LFX Lens API | configured via `LFXMCP_LENS_API_AUDIENCE` | MCP server (client credentials) | `access:api` | -| Member Onboarding API | configured via `LFXMCP_ONBOARDING_API_AUDIENCE` | MCP server (client credentials) | `access:api` | - ---- - -## Configuration - -### Environment Variables - -| Variable | Description | -|----------------------------------|--------------------------------------------------------------| -| `LFXMCP_ONBOARDING_API_URL` | Base URL of the member onboarding service | -| `LFXMCP_ONBOARDING_API_AUDIENCE` | Auth0 resource server audience for the member onboarding API | -| `LFXMCP_LENS_API_URL` | Base URL of the LFX Lens service | -| `LFXMCP_LENS_API_AUDIENCE` | Auth0 resource server audience for the LFX Lens API | - -Existing M2M credentials (`LFXMCP_CLIENT_ID`, `LFXMCP_CLIENT_SECRET` or `LFXMCP_CLIENT_ASSERTION_SIGNING_KEY`, `LFXMCP_TOKEN_ENDPOINT`) are reused for both token exchange (V2 access-check) and client credentials grants (service API authentication). - ---- - -## Current Status - -The auth infrastructure is complete but the service API calls are **stubbed with dummy responses**. This is because: - -1. The Auth0 resource servers for Lens and Onboarding need to be created first (via `auth0-terraform`) -2. The service APIs need to be updated to validate Auth0 JWTs instead of API keys -3. Once both are deployed, the dummy responses will be replaced with actual API calls