MCP Authentication & Bio-ID Identity

Overview

JCI-MCP uses Bio-ID as its authorization server. All identity — bioId, orgSlug, and roles — flows from a Bio-ID JWT. That JWT drives which JCI role a user gets, which pipeline audiences they can access, and how jci-brain scopes returned data.

There are two credential paths:

PathCredentialUse case
OAuth popupBio-ID Bearer JWT (auto-managed)claude.ai, Cursor, Windsurf, any MCP-OAuth client
JCI API keyjci_* static keyClaude Code stdio, CLI, programmatic tools

Path 1 — OAuth (claude.ai, Cursor, Windsurf)

MCP clients that implement the MCP OAuth spec discover the authorization server automatically via two well-known endpoints that jci-mcp exposes:

GET /.well-known/oauth-protected-resource
  → { authorization_servers: ["https://bio.tawa.pro"], bearer_methods_supported: ["header"], ... }

GET /.well-known/oauth-authorization-server
  → { issuer: "https://bio.tawa.pro",
      authorization_endpoint: "https://bio.tawa.pro/oauth/authorize",
      token_endpoint: "https://bio.tawa.pro/api/oauth/token",
      code_challenge_methods_supported: ["S256"], ... }

What the client does automatically

  1. User adds the MCP server URL: https://mcp.askjefferson.com (current: https://jefferson.daryx.com)
  2. Client fetches /.well-known/oauth-protected-resource → discovers Bio-ID is the AS
  3. Client fetches Bio-ID's OAuth metadata from /.well-known/oauth-authorization-server
  4. Client opens the Bio-ID login popup (PKCE authorization code flow)
  5. User authenticates with Bio-ID
  6. Client receives and stores the access token
  7. All subsequent MCP requests carry Authorization: Bearer <bio-id-jwt>

The user does not configure any client ID or secret manually. The MCP client handles the full PKCE flow with Bio-ID directly.

Token verification in jci-mcp

On each request, jci-mcp's bioAuthMiddleware verifies the Bearer token against Bio-ID's JWKS endpoint (https://bio.tawa.pro/.well-known/jwks.json). The JWKS response is cached and refreshed automatically on key rotation. No shared secret is needed — tokens are RS256.

Request arrives with Authorization: Bearer <jwt>
  → bioAuthMiddleware verifies via JWKS
  → extracts { bioId, orgSlug, roles, email } from JWT payload
  → sets req.userContext
  → request proceeds to tool handler

Path 2 — JCI API Key (Claude Code, CLI, programmatic)

JCI API keys are generated from the askJefferson admin portal. They carry a pre-assigned role and pipeline audience set and are stored in jci-brain as SHA-256 hashes (the raw key is shown once at generation time).

Key format: jci_<12-char-prefix>_<36-char-random>

The key is sent in the Authorization header:

Authorization: Bearer jci_abc123456789_<rest-of-key>

Or in the x-api-key header:

x-api-key: jci_abc123456789_<rest-of-key>

jci-brain detects the jci_ prefix and routes to API key verification instead of JWT verification. The key is hashed on arrival and looked up against the jci_api_keys collection. Each key stores:

  • bio_id — the Bio-ID of the key owner
  • role — the JCI role (developer, devops, business, admin)
  • pipeline_audiences — the audiences this key can access
  • persona_id — which persona (context profile) this key belongs to
  • last_used — updated on each successful verification

Identity Flow: Bio-ID JWT → JCI Context

From a Bio-ID JWT

decoded.sub (or decoded.bioId)  →  jciUser.bio_id
decoded.orgSlug                 →  jciUser.org_slug  (company scoping)
decoded.roles[]                 →  mapped to JCI role (see table below)
decoded.email                   →  jciUser.email

After JWT verification, jci-brain looks up the user in jci_users by bio_id to find their active persona. The persona carries the definitive role and pipeline_audiences for this session. If no persona exists yet, defaults to role: business / pipeline_audiences: ['business', 'public'].

From a JCI API key

Role and pipeline audiences are stored directly on the key record — no persona lookup needed. Identity is the key's bio_id.


Role Mapping

Bio-ID rolesJCI rolePipeline audiences
super_admin, adminadminbusiness, developer, infra, internal, public
developerdeveloperdeveloper, public
devops, infradevopsdeveloper, infra, public
(all others / default)businessbusiness, public

The mapping is performed by jci-brain's authMiddleware._verifyBioJwt. Note: for Bio-ID JWT logins, persona_id is set to null — the persona system is defined in jci_users but the middleware does not currently perform a persona lookup on JWT authentication. Persona role overrides are only active for JCI API key sessions (where persona_id is stored on the key record).


Pipeline Audience Scoping

Every data record in jci-brain carries one or more pipeline_audiences tags. Queries are filtered to only return records whose audience overlaps with the authenticated user's pipeline_audiences.

JCI roleSees
adminAll data (business + developer + infra + internal + public)
developerDeveloper + public data
devopsDeveloper + infra + public data
businessBusiness + public data

This means a business user querying pipeline logs will only see records tagged business or public — they cannot see developer build logs or infra metrics.


Environment Variables

VariableRequired byPurpose
BIO_ID_URLjci-mcp, jci-brainBio-ID base URL. Default: https://bio.tawa.pro
MCP_SERVER_URLjci-mcpIncluded in oauth-protected-resource response. Current: https://jefferson.daryx.com. Target: https://mcp.askjefferson.com
BIO_AUTH_REQUIREDjci-mcpSet true to reject requests without a valid JWT (disables legacy fallback)
OWNER_BIO_IDjci-brainbio_id that gets is_owner: true (admin bypass)

Client Configuration Examples

claude.ai (web)

  1. Go to Settings → Integrations → Add MCP Server
  2. Enter the server URL: https://mcp.askjefferson.com (current: https://jefferson.daryx.com)
  3. Claude.ai detects the Bio-ID OAuth requirement and shows a login popup
  4. Authenticate with your Bio-ID account
  5. Done — claude.ai manages token refresh automatically

Cursor / Windsurf

In .cursor/mcp.json or the MCP settings panel:

{
  "mcpServers": {
    "jci-mcp": {
      "url": "https://mcp.askjefferson.com",
      "transport": "http"
    }
  }
}

Cursor will pop the Bio-ID OAuth flow on first connection.

Claude Code (stdio with JCI API key)

In ~/.claude/settings.json or the project .claude/settings.json:

{
  "mcpServers": {
    "jci-mcp": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://jci-mcp.tawa.pro/mcp"],
      "env": {
        "BEARER_TOKEN": "jci_your_api_key_here"
      }
    }
  }
}

Or using environment headers with mcp-remote:

MCP_SERVER_HEADERS='{"Authorization":"Bearer jci_your_key"}' \
  npx mcp-remote https://mcp.askjefferson.com

Claude Code (remote OAuth — claude.ai SDK)

Claude Code in remote mode will follow the same OAuth popup flow as claude.ai when the server URL is used directly without a pre-configured key:

{
  "mcpServers": {
    "jci-mcp": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://jci-mcp.tawa.pro/mcp"]
    }
  }
}

Authentication Priority Order

jci-brain's authMiddleware checks credentials in this order:

  1. Bearer JWT starting with jci_ → JCI API key path
  2. Bearer JWT (other) → Bio-ID RS256 JWT verification
  3. x-api-key header starting with jci_ → JCI API key path
  4. URL-based username (/mcp/jci-brain/:username) → legacy fallback
  5. Dev headers (x-bio-id, x-role) → localhost only, development mode

In production with BIO_AUTH_REQUIRED=true, steps 3-5 are rejected with 401.


Key Facts

  • Bio-ID tokens are RS256-signed. jci-mcp and jci-brain verify via JWKS — no shared secret.
  • BIO_ID_URL defaults to https://bio.tawa.pro — no env var needed in standard deployments.
  • JCI API keys are hashed (SHA-256) in storage — the raw key cannot be recovered.
  • orgSlug from the JWT becomes the company_id for data scoping in jci-brain.
  • The iecHash field on InsureBio entities is the Vault chain address for insurance entities — it is a separate concept from JCI user identity (bioId). Do not conflate the two.
  • Personas allow one bio_id to carry different roles in different contexts (e.g., a user who acts as both developer and business). The default persona is used unless a specific persona_id is passed.

Common Mistakes

  • Putting a Bio-ID access token in the x-api-key header → only jci_* keys are accepted there
  • Confusing iecHash (InsureBio Vault entity address) with bioId (JCI user identity)
  • Expecting orgSlug in the JWT without the user having an org in Bio-ID
  • Using BIO_AUTH_REQUIRED=true in dev before you have a JCI API key configured
  • Adding internalDependencies: bio-id in catalog-info.yaml to enable OAuth — wrong; use spec.auth: mode: sso

Last updated: July 15, 2026