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:
| Path | Credential | Use case |
|---|---|---|
| OAuth popup | Bio-ID Bearer JWT (auto-managed) | claude.ai, Cursor, Windsurf, any MCP-OAuth client |
| JCI API key | jci_* static key | Claude Code stdio, CLI, programmatic tools |
Verified 2026-10-02 against DNS and each host's /health and
/.well-known/oauth-protected-resource.
| URL | Status | What it is |
|---|---|---|
https://mcp.askjefferson.com | live — use this | Public entry, served by the JCI plane (Kubernetes, 2 pods, Envoy Gateway; Cloudflare-proxied). Publishes the OAuth discovery documents; POST / and POST /mcp are the MCP transport; /mcp/jci-mcp/<user> is the URL-username path |
https://jci-mcp.tawa.pro | live | Alias of mcp.askjefferson.com: A record straight to the plane load balancer (DNS-only), same pods. Discovery documents name this host when you arrive here |
https://jefferson.daryx.com/mcp/jci-mcp/<user> | live, legacy | Old path-prefix URL, not on the plane. No .well-known documents, so no OAuth discovery |
Since the 2026-09-26 cutover (insureco/jci-plane, docs/cutover-jci-mcp.md), public MCP traffic
runs on the plane, not on jefferson. jefferson keeps a PM2 copy of jci-mcp, and it is still part
of the path:
./deploy.sh → jefferson PM2 jci-mcp (:9002)
→ plane-image-sync timer on jefferson (every 5 min, jci-plane#48)
builds registry.digitalocean.com/insureco/jci-mcp:<sha>, merges the tag into jci-plane
→ Argo CD rolls the plane
deploy.sh proves jefferson only. The change is live for clients when the public
health check shows its commit:
curl -s https://mcp.askjefferson.com/health | grep -o '"commit":"[^"]*"'curl localhost:9002/health on jefferson is the break-glass copy, not what clients hit.jci-mcp on jefferson. It is the commit plane-image-sync ships, and the
standing break-glass if the plane fails (jci-plane docs/runbook-break-glass.md, decision
2026-09-27).Correction (2026-10-02): this table said
jci-mcp.tawa.prowas "an A record straight to jefferson … same nginx proxy to the same process". That stopped being true at the plane cutover. Acting on it, an agent verified a fix on jefferson'slocalhost:9002, called it live while clients still ran the previous commit, and proposed stopping the jefferson process the deploy pipeline depends on.
Correction (2026-09-08): the Claude Code examples below pointed at
https://jci-mcp.tawa.pro/mcpwhile that host did not exist — no DNS record, no ingress. It was brought up the same day as a true alias, so the URL now works, butmcp.askjefferson.comstays the canonical name and the one connectors are registered under. Separately,POST /mcprejectedinitializeuntil insureco/jci-mcp#59 — a client pointed at/mcpbefore that fix would fail its handshake on either host.
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"], ... }
https://mcp.askjefferson.com/.well-known/oauth-protected-resource → discovers Bio-ID is the AS/.well-known/oauth-authorization-serverAuthorization: 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.
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
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 ownerrole — the JCI role (developer, devops, business, admin)pipeline_audiences — the audiences this key can accesspersona_id — which persona (context profile) this key belongs tolast_used — updated on each successful verificationdecoded.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'].
Role and pipeline audiences are stored directly on the key record — no persona lookup
needed. Identity is the key's bio_id.
jci admin means platform admin, and nothing weaker (insureco/jci-brain#396, #400).
Platform admin uses Bio-ID's own definition: the JWT roles claim includes super_admin
(iec-bio lib/is-platform-admin.ts). roles is the user's platform role list. An org
admin's role is in orgRoles and never counts, and a bare admin in roles is a Bio-ID
product role, not an admin.
| Bio-ID token | JCI role | Pipeline audiences |
|---|---|---|
super_admin in roles (platform admin) | admin | business, developer, infra, internal, public |
developer in roles | developer | developer, public |
devops in roles | devops | developer, infra, public |
anything else, including admin in roles and every org role | business | business, public |
The mapping is performed by jci-brain's authMiddleware._verifyBioJwt (bioRolesToJciRole).
Note: for Bio-ID JWT logins, persona_id is set to null, because the middleware does not
look up a persona on JWT authentication.
AuthMiddleware._attach applies capRole after the verifier runs, whatever the credential:
role admin and the internal audience require the owner or a platform admin. Any
other caller carrying them is served as business / business, public. That covers:
role is admin, when its owner is not the owner or a platform
admin (the stored record is not rewritten, and it regains nothing);A person also cannot create an admin persona unless they are a platform admin
(POST /api/identity/me/personas → 403). A personal key copies its persona's role.
Service-identity API keys are an explicit allowlist (src/utils/serviceIdentity.js:
SERVICE-JCI-MCP, SERVICE-ADMIN-CHATBOT). They keep their stored role and audiences, and pass
platform gates only if an operator hand-set platform_admin on the key (jci-brain#407). Only an
allow-listed service may name another person in x-bio-id (jci-brain#403).
/api/intelligence/{tasks,review,schedule,calendar,emails,team,outbound} and the general
/api/intelligence router sit behind trustedCaller. It accepts three kinds of caller:
/tasks only;jci_ credential (loopback, closable with
INTELLIGENCE_ROUTES_ALLOW_LOCAL=false);authenticate() accepts.A person's identity headers and user_id-style parameters are rebuilt from req.jciUser, so a
person cannot name anyone else. The URL-username path (/mcp/jci-brain/:username) was removed in
jci-brain#403. Use a Bio-ID JWT or a JCI API key.
isPlatformAdmin, never role === 'admin'Every gate over other people's data (the roster, identity edits, /api/admin/*, service
keys, dev projects, the unfiltered pipeline view) checks isPlatformAdmin(req.jciUser)
(src/utils/platformAdmin.js): is_owner, or platform_admin from a verified JWT. A role is
a label a caller can arrive with, so it is never the gate. Org admins keep org-scoped powers
only.
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 role | Sees |
|---|---|
admin (platform admin only) | All data (business + developer + infra + internal + public) |
developer | Developer + public data |
devops | Developer + infra + public data |
business | Business + 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.
| Variable | Required by | Purpose |
|---|---|---|
BIO_ID_URL | jci-mcp, jci-brain | Bio-ID base URL. Default: https://bio.tawa.pro |
MCP_SERVER_URL | jci-mcp | Included in oauth-protected-resource response as resource. The plane answers with the host the request arrived on (https://mcp.askjefferson.com or https://jci-mcp.tawa.pro) |
BIO_AUTH_REQUIRED | jci-mcp | Set true to reject requests without a valid JWT (disables legacy fallback) |
OWNER_BIO_ID | jci-brain | bio_id that gets is_owner: true (admin bypass) |
https://mcp.askjefferson.comIn .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.
In ~/.claude/settings.json or the project .claude/settings.json:
{
"mcpServers": {
"jci-mcp": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.askjefferson.com/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 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://mcp.askjefferson.com/mcp"]
}
}
}
jci-brain's authMiddleware checks credentials in this order:
jci_ → JCI API key pathx-api-key header starting with jci_ → JCI API key pathx-bio-id, x-role) → localhost only, development modeIn production with BIO_AUTH_REQUIRED=true, steps 3-5 are rejected with 401.
BIO_ID_URL defaults to https://bio.tawa.pro — no env var needed in standard deployments.orgSlug from the JWT becomes the company_id for data scoping in jci-brain.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.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.x-api-key header → only jci_* keys are accepted thereiecHash (InsureBio Vault entity address) with bioId (JCI user identity)orgSlug in the JWT without the user having an org in Bio-IDBIO_AUTH_REQUIRED=true in dev before you have a JCI API key configuredinternalDependencies: bio-id in catalog-info.yaml to enable OAuth — wrong; use spec.auth: mode: ssoLast updated: October 6, 2026