2026-09-26
Why I Forked mcp-remote (And Why You No Longer Need To)
When I first tried connecting Claude to our internal MCP servers through Okta, I hit a wall that I suspect a lot of teams will run into: access tokens don't carry identity claims by default.
The Problem
If your Okta org doesn't have the API Access Management license, you can't create a custom authorization server. You're stuck with the Org Authorization Server, which issues access tokens meant for Okta's own APIs — not yours. These tokens don't include group membership, email, or any custom claims your MCP server needs to make authorization decisions.
The id_token, on the other hand, carries all of that out of the box — it's standard OIDC. Email, groups, profile attributes, and any custom claims you configure on the OIDC app are all there.
What I Built
1. Forking mcp-remote for id_token Support
I forked mcp-remote and added a --token-type flag. When set to id_token, the proxy swaps the id_token into the Bearer header instead of the access_token. From the MCP server's perspective, nothing changes — it just receives a JWT with actual identity claims.
2. AgentGateway as the Auth + AuthZ Layer
The MCP server itself (Okta's official one) only supports local stdio — no HTTPS, no OAuth flow. AgentGateway solves this by translating stdio to HTTPS and acting as both the authentication and authorization middleware.
Here's how the authentication side is configured:
mcpAuthentication:
mode: strict
issuer: https://your-org.okta.com
audiences:
- https://your-org.okta.com
- YOUR_CLIENT_ID
jwks:
url: https://your-org.okta.com/oauth2/v1/keys
resourceMetadata:
resource: https://gateway.internal/mcp/okta
scopesSupported:
- openid
- profile
- groups
bearerMethodsSupported:
- header
This validates every incoming JWT against Okta's JWKS endpoint and enforces the expected issuer and audience.
3. CEL Policies for Tool-Level Authorization
This is where it gets interesting. AgentGateway supports CEL (Common Expression Language) rules for authorization, which lets you gate access to individual MCP tools based on JWT claims.
mcpAuthorization:
rules:
# Users (read-only) — IT + Security only
- 'mcp.tool.name == "list_users" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true)'
- 'mcp.tool.name == "get_user" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true)'
# Groups (read-only) — IT + Security + Engineering
- 'mcp.tool.name == "list_groups" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true || jwt["in_group_engineering"] == true)'
- 'mcp.tool.name == "get_group" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true || jwt["in_group_engineering"] == true)'
- 'mcp.tool.name == "list_group_users" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true || jwt["in_group_engineering"] == true)'
- 'mcp.tool.name == "list_group_apps" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true || jwt["in_group_engineering"] == true)'
# Applications (read-only) — IT + Security + Engineering
- 'mcp.tool.name == "list_applications" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true || jwt["in_group_engineering"] == true)'
- 'mcp.tool.name == "get_application" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true || jwt["in_group_engineering"] == true)'
# Policies (read-only) — IT + Security only
- 'mcp.tool.name == "list_policies" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true)'
- 'mcp.tool.name == "get_policy" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true)'
- 'mcp.tool.name == "list_policy_rules" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true)'
- 'mcp.tool.name == "get_policy_rule" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true)'
# System Logs (read-only) — IT + Security only
- 'mcp.tool.name == "get_logs" && (jwt["in_group_it"] == true || jwt["in_group_security"] == true)'
matches:
- path:
pathPrefix: /mcp/okta
- path:
pathPrefix: /.well-known/oauth-protected-resource/mcp/okta
- path:
pathPrefix: /.well-known/oauth-authorization-server/mcp/okta
Each rule matches an MCP tool name and checks boolean claims in the JWT. If the user's token doesn't satisfy the rule, AgentGateway rejects the tool call before it ever reaches the MCP server.
4. Why Boolean Claims Instead of Group Lists
You'll notice the CEL rules check jwt["in_group_it"] == true instead of something like "IT" in jwt["groups"]. That's intentional.
The default Okta groups claim is a dynamic list, and AgentGateway's CEL evaluator panics on list types. The workaround is to use Okta's Custom Claim Expressions on the OIDC app to flatten group membership into individual boolean claims:
in_group_it→ Expression:isMemberOfGroup("IT")in_group_security→ Expression:isMemberOfGroup("Security")in_group_engineering→ Expression:isMemberOfGroup("Engineering")
Each evaluates to true or false in the id_token — no lists, no panics, and the CEL expressions stay simple.
The Client Config
On the user's machine, the Claude config pointed at the gateway:
{
"mcpServers": {
"okta": {
"command": "npx",
"args": [
"-y",
"mcp-remote-id",
"https://gateway.internal/mcp/okta/sse",
"--token-type", "id_token"
]
}
}
}
The Happy Ending
The upstream mcp-remote project has since added native id_token support via a --use-id-token flag — with additional improvements like JWT expiry handling. My fork is deprecated.
If you're hitting the same problem, just use mcp-remote directly:
{
"mcpServers": {
"okta": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://gateway.internal/mcp/okta/sse",
"--use-id-token"
]
}
}
}
Takeaway
If you're in an Okta org without the API Access Management license and need identity-aware MCP authorization:
- Use
mcp-remotewith--use-id-tokento send OIDC identity claims - Put AgentGateway in front of your MCP servers for HTTPS + auth
- Use Okta Custom Claim Expressions to create boolean group claims (avoids CEL list panics)
- Write CEL rules per tool to enforce least-privilege access
The pattern works for any OIDC provider where access tokens are thin and you need fine-grained tool authorization.
// want to discuss this?
get in touch →