Model Context Protocol (MCP): Solving the Identity Challenge in Agentic AI

Introduction
The analogy that stuck with me the most for Model Context Protocol (MCP) is a universal charging station for electric vehicles. Let's revisit the terminology a bit:
The Smart EV (MCP Client): This represents the AI Agent. In the protocol, this is the entity that initiates the connection to your data and tools.
The Engine: This is the LLM’s intelligence—the reasoning power under the hood (like Claude or GPT-4) that decides where the car needs to go.
The Charging Station (MCP Server): This is the specialized host for your data, tools, and business logic.
The Universal Plug: This is the MCP Protocol itself. It’s the standardized interface that lets any "EV" plug into any "Station" without a custom adapter.
Charging Modes: These are the individual Tools, Resources, and Prompts. Just as a station might offer "Eco" or "Supercharge," an MCP server offers specific capabilities like "Read" or "Write" via Tools.
The Common Language
Essentially, MCP acts as the bridge. It lets the MCP client and the server understand each other’s capabilities without a custom integration for every single tool. For this walk through, we’re going to focus strictly on Tools.
Figure 1: MCP as a Universal Charging Station for Electric Vehicles (AI Generated)
Identity: Beyond the Plug
However, in the enterprise, a universal plug is a security risk if it doesn't know who is drawing the power. Just because the plug fits doesn't mean the electricity should be free—or that every driver should have access to the "High-Voltage" fast chargers.
MCP handles the connectivity, but Identity is what ensures we can actually authorize specific actions. To solve this, the MCP Authorization (2025-11-25) framework—which is built on the OAuth 2.1 standard—mandates a specific security stack: CIMD (Client ID Metadata Document), On-Behalf-Of (OBO) workflows, and PKCE (Proof Key for Code Exchange).
We’re going to get into the technical "weeds" of how these work in the upcoming sections.
Scenario Overview: The Four Pillars
Before we get under the hood, let's try to go over the four primary components that make up our scenario:
Users (Resource Owners): Generally in agentic AI scenarios, you might have 100s or 1000s of users that will need to interact with your agentic AI system.
MCP Client: In this setup, our client is MCP Jam, a specialized build of the MCP Inspector suggested by the team at Scalekit. Since an AI Agent effectively hosts one or more MCP clients, I’ll use the terms "Agent," "Client," and "MCP Jam" interchangeably throughout the article—they all refer to the specific instance we’re using to trigger these requests.
MCP Server: In our example, we have a simple To Do List MCP server that I found available on Scalekit's documentation where the objective is to create, list and delete to do tasks. You can choose any other MCP server for that matter.
Oauth Server (Authorization Provider): In our example, we are using ScaleKit as our MCP Authorization Server. For authentication, we are using Google as our Identity Provider (IDP). Thus, the user gets authenticated via Google's IDP and gets authorized via Scalekit's OAuth Server.
The core objective of this architecture is to solve the identity challenge. When a request hits the MCP Server, the server needs to know more than just "An Agent is calling me." It needs the full context of which user the Agent is acting on behalf of.
By using On-Behalf-Of (OBO) workflow (OBO), MCP Jam acts with the delegated authority of the user. This ensures that if User-1 only has read permissions, the Agent cannot accidentally trigger a delete action, even if the LLM "brain" wants to.
Figure 2: Scenario Components
Let's Dive Deeper
Analogies are great for the big picture, but let's get into the weeds. We’re going to walk through the actual handshake, step-by-step, to see how this works in the real world.
Registering MCP Client via CIMD
An AI Agent or MCP Client is acting on behalf of a user and registration basically tells the Authorization server which Agent is acting on behalf of the user. It also prevents Ghost agents - unauthorized clients attempting to impersonate your AI assistant.
In a standard OAuth flow, you typically "pre-register" a client and get a Secret. But in the modern Agentic AI world, and the rate of spinning agents you want a more dynamic way of agent/client registration. CIMD (Client ID Metadata Document), defined in draft-ietf-oauth-client-id-metadata-document, is the mechanism that allows an MCP Client (our MCP Jam) to identify itself dynamically and without a pre-shared secret. It is essentially a document that gets hosted on the client & that gets queried by the Authorization Server where details about the name of the Client, Redirect URI and other details are available.
Figure 3: Client (MCP Jam) asks for the Authorization Server Details
To get the client registered via CIMD, there are two main steps:
Client (MCP Jam) discovers the OAuth Server
Oauth Server connects to MCP Jam to get its CIMD
Client Discovers the OAuth Server
The main steps involved in registering the client via CIMD:
- Client tries to access the MCP Server: When MCP Jam tries to access the MCP Server without a key, the server pushes back with an HTTP 401 Unauthorized. But it's a "smart" rejection: following the RFC 9728 (OAuth 2.0 Protected Resource Metadata) standard, the server includes a link to its resource metadata. This essentially hands MCP Jam a map that says, "I can't let you in yet, but here is exactly where you can find the Authorization Server to get your keys.
{ "header": "Bearer error="invalid_token", error_description="Authentication failed. The provided bearer token is invalid, expired, or no longer recognized by the server. To resolve: clear authentication tokens in your MCP client and reconnect. Your client should automatically re-register and obtain new tokens.",
resource_metadata="https://teena-unquoted-flocculently.ngrok-free.dev/.well-known/oauth-protected-resource/mcp"",
"Received from": "http://localhost:3002/mcp" }
- Client requests resource metadata URL: Now MCP Jam requests the resource metadata URL returned in step 1 to get the authorization server details
{
"Resource": "https://teena-unquoted-flocculently.ngrok-free.dev/mcp",
"Authorization Servers": [
"https://acmecorp-agrzuddyaadqe.scalekit.dev/resources/res_118107431938033410"
]
}
Client requests the Authorization server metadata: MCP Jam requests the authorization server metada and gets the following:
Issuer: When your MCP Server receives a JWT, it checks this URL to make sure the token actually came from the trusted entity it expects.
jwks_uri (JSON Web Key Set): It is the specific URL where your MCP Server goes to fetch the Public Keys needed to verify the token is signed by the Authorization Server (ScaleKit).
Authorization Endpoint: When the user clicks "Connect" in MCP Jam, their browser is sent here to authenticate (e.g., via Google) and give permission to the Agent.
Token Endpoint: After the user authorizes MCP Jam to act on its behalf, the MCP Jam goes to the token endpoint to swap a temporary code for the actual Access Token.
CIMD Support Enabled: With the client_id_metadata_document_supported set to true, it tells MCP Jam that that there is no need for pre-registration.
PKCE Methods: This stands for Proof Key for Code Exchange. It’s a critical security layer that prevents interception attacks. It ensures that even if someone steals the authorization code in transit, they can't use it because they don't have the "secret handshake" (the S256 hash) that the Client (MCP Jam) created at the start.
Grant Types Supported: These are the "Transaction Modes" the server supports. Authorization Code is the gold standard for OBO. It involves the user, the browser, and the agent.
Response Types Supported: This confirms that the server will return a Temporary Code first, rather than the raw token.
The Permissions (The "What")
Scopes: These are the "Charging Modes" or functional boundaries:todo:read: The Agent can see your list of to-do taskstodo:write: The Agent can add or edit your to-do tasks
{ "authorization_endpoint": "https://acmecorp-agrzuddyaadqe.scalekit.dev/resources/res_118107431938033410/oauth/authorize",
"client_id_metadata_document_supported": true, "code_challenge_methods_supported": [ "S256" ],
"grant_types_supported": [ "authorization_code",
"client_credentials", "refresh_token" ],
"introspection_endpoint": "https://acmecorp-agrzuddyaadqe.scalekit.dev/oauth/introspect",
"issuer": "https://acmecorp-agrzuddyaadqe.scalekit.dev",
"jwks_uri": "https://acmecorp-agrzuddyaadqe.scalekit.dev/keys", "registration_endpoint": "https://acmecorp-agrzuddyaadqe.scalekit.dev/api/v1/resources/res_118107431938033410/clients:register", "request_uri_parameter_supported": false,
"response_modes_supported": [ "query" ],
"response_types_supported": [ "code" ],
"revocation_endpoint": "https://acmecorp-agrzuddyaadqe.scalekit.dev/revoke",
"scopes_supported": [ "todo:write", "todo:read" ], "subject_types_supported": [ "public" ],
"token_endpoint": "https://acmecorp-agrzuddyaadqe.scalekit.dev/resources/res_118107431938033410/oauth/token", "token_endpoint_auth_methods_supported": [ "none", "client_secret_basic", "client_secret_post", "private_key_jwt" ], "token_endpoint_auth_signing_alg_values_supported": [ "RS256" ], "userinfo_endpoint": "https://acmecorp-agrzuddyaadqe.scalekit.dev/resources/res_118107431938033410/userinfo" }
Oauth Server connects to the Client to get its CIMD
Figure 4: Authorization Server fetches MCP Jam CIMD details
Next, we see the Authorization server fetching the metadata from client ID URL to get back the JSON metadata document representing MCP Jam.
{
"Client ID": "https://www.mcpjam.com/.well-known/oauth/client-metadata.json",
"Registration Method": "Client ID Metadata Document (CIMD)",
"Client Name": "MCPJam",
"Redirect URIs": [
"mcpjam://oauth/callback",
"mcpjam://authkit/callback",
"http://127.0.0.1:6274/oauth/callback",
"http://127.0.0.1:6274/callback",
"http://127.0.0.1:6274/oauth/callback/debug",
"http://localhost:6274/oauth/callback",
"http://localhost:6274/callback",
"http://localhost:6274/oauth/callback/debug",
"http://127.0.0.1:5173/oauth/callback",
"http://127.0.0.1:5173/oauth/callback/debug",
"http://localhost:5173/oauth/callback",
"http://localhost:5173/oauth/callback/debug",
"https://app.mcpjam.com/oauth/callback",
"https://app.mcpjam.com/oauth/callback/debug"
],
"Token Auth Method": "none",
"Validation": "✓ Metadata fetched and validated",
"Note": "Server fetched and validated client metadata from URL",
"Server Support": "✓ Advertised in metadata (client_id_metadata_document_supported: true)"
}
Significance of the Redirect URL
Think of the Redirect URI as a secure hand-off at a very specific set of coordinates. The Authorization Server is only providing the keys at this specific URI where our MCP Jam is listening.
With this step, we have MCP Jam registered with the Authorization Server. Next, we need the user using MCP Jam to authenticate via Google and authorize our MCP Jam to act on its behalf.
User Login
Authentication (The "Who are you?" Check): In our scenario, the user is redirected to Google, the Identity Provider (IdP). Here, the user enters their credentials to prove their identity. Once Google confirms "Yes, this is Karim," the authentication phase is complete.
Authorization : This is the most critical stage of the On-Behalf-Of (OBO) workflow. Even though the user is logged in, the MCP Jam does not yet have the power to act. The user must explicitly authorize the Client to access our MCP Server named FastMCP Todo Server. By doing so, the user signs a "digital permission slip" that allows MCP Jam to perform specific tasks—like reading or writing to-dos—on their behalf.
Figure 5: User Redirect for Authentication
Figure 6: Authorize MCP Jam to act on user's behalf
Significance of the PKCE Process
PKCE ensures that even if a hacker steals the "Authorization Code" from the browser, they can't use it because they don't know the original secret created by the Agent or MCP Jam our case. The way it works is:
MCP Jam generates a secret (Verifier) and creates a hash of the secret (Challenge). Only the hash gets shared with Oauth Server
When the user logs in and gets a code post successful authentication, MCP Jam needs to swap the code with an access token. At this time, it reaches out to OAuth server with its identity, secret (Verifier) (generated in step 1), and the code asking for an access token.
The OAuth server hashes the code and compares the hash with the Challenge it got from step 1. If they match, then Scalekit knows this is the exact same Agent (MCP Jam) that started the request. It issues the JWT Tokens.
Figure 7: PKCE Process & getting the Authorization Code from Authorization Server
The "Final Swap": Code → Token
Now that the user has successfully authenticated via Google and accepted the authorization request, the MCP Jam holds a temporary Authorization Code. However, a code isn't a key—we need to swap it for a JWT Access Token.
Figure 8: Swapping the Authorization Code for an Access Token
You can see the decoded access token here.
I just want to highlight the significance of the audience field (aud). This ensures the token is being used at the correct station. If a token intended for your To-Do server is intercepted and tried against your Email server, the Email server will see the mismatched aud field and reject it. This prevents "Token Replay" attacks across different parts of your infrastructure. Please note that the output is redacted.
{ "aud": [ "http://localhost:3002/mcp", "res_118107431938033410" ],
"client_id": "https://www.mcpjam.com/.well-known/oauth/client-metadata.json",
"email": "karim.jamali@gmail.com",
"iss": "https://acmecorp-xxxx.scalekit.dev",
"permissions": [ "todo:read", "todo:write" ],
"resid": "res_118107431938033410",
"roles": [ "full_access" ],
"scope": "openid offline_access todo:write todo:read email profile",
"scopes": [ "openid", "offline_access", "todo:write", "todo:read", "email", "profile" ],
"_validation": { "expected_audience": "http://localhost:3002/mcp", "audience_matches": true, "note": "✓ Token audience matches MCP server" } }
The ID Token is a digital "identity card" (typically a JWT) issued by the Identity Provider (Google) that contains verified profile information about the user, such as their name, email, and the time they logged in. Please note that the output is redacted.
"amr": [ "conn_118107394206074626" ],
"at_hash": "eyPmzNnc2UjX1M_M20YDrw",
"aud": [ "http://localhost:3002/",
"res_118107431938033410", "https://www.mcpjam.com/.well-known/oauth/client-metadata.json" ],
"roles": [ "full_access" ]
The Cryptographic "Trust Chain": Why the MCP Server Trusts the Token
It is imperative to ensure that the token the MCP server receives has been signed by the trusted Authorization Server (ScaleKit) and hasn't been altered in transit.
To achieve this, Scalekit does the following:
The Fingerprint: ScaleKit takes the token data and runs it through a hashing algorithm (SHA-256) to create a unique digital fingerprint.
The Lock: It then uses its Private Key to "lock" that fingerprint. This encrypted fingerprint becomes the Signature (the third part of the JWT).
The MCP Server does the following:
The Re-Hash: The server calculates its own fingerprint of the data it just received.
The Unlock: It uses ScaleKit’s Public Key to "unlock" the signature and reveal the original fingerprint ScaleKit created.
The Match: If the fingerprints match exactly, the server knows the token is authentic and hasn't been touched.
read_only role to full_access), the recalculated fingerprint would look 100% different. The MCP Server would instantly detect the "Signature Mismatch" and reject the request.A Quick Note on Roles
If you look back at the access token above you will see:
permissions": [ "todo:read", "todo:write" ]
roles": [ "full_access" ]
In Scalekit, I have defined both roles wfull_access and read_only with their corresponding permissions.
Figure 9: Roles and Permissions on Authorization Server (ScaleKit)
Initially I associated my user with full_access role.
Figure 10: Assigning a Role with specific permissions to a User
I attempt to add a to-do item using the MCP tool. It is successful.
Figure 11: Successful Tool Request
I change the user's role to read_only and try again to get insufficient permissions.
Figure12: Unsuccessful Tool Request (Insufficient Permissions)
It is critical to understand that the MCP Server manages security in a few layers:
Layer 1: Authentication (The Gatekeeper): We use the
ScalekitProviderbecause it acts as the primary firewall. It automatically handles the entire "Trust Chain" verification—fetching public keys via JWKS, checking the cryptographic signature, and ensuring the token isn't expired. If the token is fake or tampered with, the request is rejected before our tool logic even runs.Layer 2: Authorization : The
_require_scopefunction is where we move from identity to permission. Even if a user is authenticated (we know who they are), they might only have "Read-Only" access based on their assigned role in ScaleKit.Layer 3: Scope Enforcement: In the code below, the
create_todotool specifically checks for thetodo:writescope within the verified token. If that permission isn't present, the action is blocked, ensuring the Agent can only perform actions the user is explicitly authorized for.
mcp = FastMCP( "Todo Server", auth=ScalekitProvider( environment_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), client_id=os.getenv("SCALEKIT_CLIENT_ID"), resource_id=os.getenv("SCALEKIT_RESOURCE_ID"), # FastMCP appends /mcp automatically; keep base URL with trailing slash only mcp_url=os.getenv("MCP_URL"), ), )
@dataclass
class TodoItem:
id: str
title: str
description: Optional[str]
completed: bool = False
def to_dict(self) -> dict:
return asdict(self)
_TODO_STORE: dict[str, TodoItem] = {}
def _require_scope(scope: str) -> Optional[str]:
token: AccessToken = get_access_token()
user_permissions = token.claims.get("permissions", [])
if scope not in user_permissions:
return f"Insufficient permissions: {scope} scope required."
return None
@mcp.tool
def create_todo(title: str, description: Optional[str] = None) -> dict:
error = _require_scope("todo:write")
if error:
return {"error": error}
todo = TodoItem(id=str(uuid.uuid4()), title=title, description=description)
_TODO_STORE[todo.id] = todo
return {"todo": todo.to_dict()}
Conclusion
Ultimately, MCP OAuth moves us past the "black box" era of AI. By mandating CIMD for identity and OBO for delegated authority, it replaces blind trust with a verifiable chain of trust that leads straight back to the user.
This moves us toward an identity-first model for Agentic AI. If we’re handing an agent the keys to our enterprise data, we’d better be 100% sure we know exactly who is in the driver’s seat.



