Connect vMCP to an enterprise identity provider
Your engineering team needs access to GitHub and Kubernetes MCP tools through a single gateway. ToolHive's Virtual MCP Server (vMCP) aggregates multiple backends behind one endpoint, with Cedar policies controlling which tools each group can use.
This guide walks through setting up one VirtualMCPServer (engineering-tools)
with two backends, protected by your corporate identity provider. The pattern
repeats. Adding a second team (e.g., sales-tools) follows the same steps with
a new app registration and a new VirtualMCPServer.
How it works
ToolHive includes an embedded OAuth 2.0 Authorization Server that runs inside the VirtualMCPServer process. MCP clients (VS Code, Claude Desktop) never talk to your IdP directly. They discover the embedded auth server via standard MCP OAuth discovery and authenticate through it.
For a full diagram of the embedded auth server flow, see Authentication.
The flow:
- MCP client discovers the embedded AS via
/.well-known/oauth-protected-resource - Client self-registers via Dynamic Client Registration (DCR)
- User authenticates at Okta or Entra ID through the embedded AS
- Embedded AS issues a ToolHive JWT
- Client uses that JWT for all subsequent MCP requests
- Cedar evaluates group claims per tool call
See Authentication and authorization for deeper background on Cedar and ToolHive's security model.
Per-team isolation
Each team gets its own VirtualMCPServer with its own embedded auth server and
audience. A token for engineering-tools is cryptographically invalid at
sales-tools. This satisfies the MCP specification's requirement that servers
"MUST validate that access tokens were issued specifically for them." See
Adding another team.
Cedar group model
Okta puts group membership in the groups claim. Entra ID puts it in the
roles claim. Both are in ToolHive's default claim extraction list (groups,
roles, cognito:groups), so the same Cedar policies work with either IdP, as
long as you use the same mcp- prefix convention for both Okta group names and
Entra role values.
Prerequisites
- Kubernetes cluster with the ToolHive operator installed
kubectlaccess to your target namespace- Admin access to your identity provider
- A publicly reachable URL for your VirtualMCPServer (the embedded auth server needs a callback URL that your IdP can redirect to)
Choose your identity provider
Follow the guide for your IdP to complete the full setup and deployment:
- Microsoft Entra ID - uses App Roles for group-based
access control, with the
rolesclaim in access tokens - Okta - uses Okta Groups and a custom authorization server,
with the
groupsclaim in access tokens
For other OIDC-compliant providers, see vMCP authentication.
Verify your setup
For instructions on pointing VS Code, Claude Desktop, or other MCP clients at your VirtualMCPServer endpoint, see Connect clients to MCP servers.
Verify group-based access
- Log in as a user in the
developersgroup - should see tools from both backends - Log in as a user in the
platformgroup - should see all tools - Log in as a user in neither group - should be denied all tools (Cedar default deny)
Manual verification with curl
# Port-forward the vMCP service
kubectl port-forward svc/vmcp-engineering-tools 9091:4483 &
# Check the embedded AS is running
curl -s http://localhost:9091/.well-known/oauth-authorization-server | jq .
# Unauthenticated request should return 401
curl -s -o /dev/null -w "%{http_code}" http://localhost:9091/mcp
Cedar quick reference
| Pattern | Cedar expression |
|---|---|
| Allow a group all tools | permit(principal in THVGroup::"mcp-developers", action, resource); |
| Allow a specific user | permit(principal == Client::"user@example.com", action == Action::"call_tool", resource); |
| Allow by claim value | permit(principal, action == Action::"call_tool", resource) when { principal.claim_roles.contains("admin") }; |
| Restrict to one tool | permit(principal in THVGroup::"mcp-platform", action == Action::"call_tool", resource == Tool::"get_pods"); |
| Deny a group one tool | forbid(principal in THVGroup::"mcp-developers", action == Action::"call_tool", resource == Tool::"delete_namespace"); |
| Admin escape hatch | permit(principal in THVGroup::"mcp-admin", action, resource); |
See Cedar policies for full reference.
Adding another team
To add a second team (e.g., sales-tools), repeat the IdP and ToolHive steps
with a new app registration and a new VirtualMCPServer. All teams can share the
same Okta authorization server or Entra tenant.
IdP setup
- Create a new OIDC app registration for the new vMCP. Do not add redirect
URIs to the existing app. Each VirtualMCPServer should have its own app
registration so that:
- Client secrets can be rotated independently
- Access can be revoked per-team without affecting others
- Audit logs attribute events to the correct team
- Configure the new app the same way (Authorization Code and Refresh Token
grants, redirect URI
https://sales-tools.example.com/oauth/callback) - Create an
mcp-salesgroup (matching your prefix convention) and assign users - Assign the group to the new app
ToolHive setup
Copy the VirtualMCPServer manifest, changing:
metadata.name:sales-toolsaudienceandresourceUrl:https://sales-tools.example.comauthServerConfig.issuer:https://sales-tools.example.comupstreamProviders[0].clientId: the new app's client IDupstreamProviders[0].clientSecretRef: a new Secret with the new app's client secretredirectUri:https://sales-tools.example.com/oauth/callback- Cedar policies:
THVGroup::"mcp-sales"
Create the MCPGroup and MCPServer backends for the sales team.
Audience isolation
Each VirtualMCPServer gets its own audience, so tokens for engineering-tools
are cryptographically invalid at sales-tools. This provides per-team isolation
at the OIDC layer. No Cedar misconfiguration can cross this boundary.
The MCP specification requires that each server "MUST validate that access
tokens were issued specifically for them." With the embedded auth server,
per-server audiences are free: the embedded AS sets the aud claim based on the
RFC 8707 resource parameter from the MCP client's request. No extra
authorization server is needed.
A shared app with multiple redirect URIs works for getting started, but couples all teams' lifecycle together. Separate apps give you independent secret rotation, per-team audit attribution, and the ability to revoke one team's access without affecting others.
Next steps
- Connect vMCP to Microsoft Entra ID
- Connect vMCP to Okta
- Authentication and authorization
- Cedar policies
Troubleshooting
401 Unauthorized: Issuer mismatch, audience mismatch, expired token, JWKS
endpoint unreachable, or embedded AS not running. Check that
authServerConfig.issuer matches MCPOIDCConfig.inline.issuer.
403 Forbidden: Cedar policy denied. Check that group or role claim names in
the token match the THVGroup values in your policies.
MCP client can't discover auth: Verify
/.well-known/oauth-protected-resource returns JSON with
authorization_servers pointing to the embedded AS.
DCR fails: Ensure the /oauth/register endpoint is routable from the
client. If using ingress, verify the route exists.
Tools not visible: Backends may not have joined the group yet. Check
kubectl get mcpserver and verify the backends show STATUS: Ready.