---
title: "MCP Agent Identity: How to Know Which AI Agent Made a Tool Call"
description: "One user, many agents, one MCP server. Why user identity is not agent identity - and how to attribute a tool call to a specific AI agent with a signed passport verified locally via JWKS."
canonical: https://lime.pics/blog/mcp-server-knows-the-user/
locale: en
date: 2026-09-07
updated: 2026-10-14
---

# MCP Agent Identity: How to Know Which AI Agent Made a Tool Call

One user, many agents, one MCP server. Why user identity is not agent identity - and how to attribute a tool call to a specific AI agent with a signed passport verified locally via JWKS.

## Overview {#overview}
Your MCP resource server gets an authenticated request. The token is valid, the user is real, the call succeeds — and you still cannot answer the one question an incident review will ask: **which agent actually did this?**
That is the problem of **MCP agent identity**. A user identity proves that *an account* acted. It does not prove which of that account's autonomous agents made a specific tool call. This article is a practical, testable look at agent identity on the MCP resource boundary — and a live MCP server where you can run the experiment yourself.
By the end you will have: the exact failure mode, why passing an `agent_id` as a tool argument is not a fix, the requirements a real solution must meet, and one minimal implementation built on a signed short-lived passport that the resource server verifies locally against published JWKS.
## The pain: one user, many agents, one label {#the-pain}
Start with the system almost everyone builds first:
```text
                    User
                      |
          +-----------+-----------+
          v           v           v
      Agent A      Agent B      Agent C
          |           |           |
          +-----------+-----------+
                      v
                  MCP Server
```
One user. Three agents. One MCP server. The request is authenticated.
The server knows the account. It does not know the actor. When the tool call is `delete_file`, "user_123" is not the answer anyone needs — "execution-agent" is.
This is not a hypothetical edge case. The moment one human runs more than one assistant, every tool call behind that human collapses onto a single identity.
## Why it matters more than it looks {#why-it-matters}
Multi-agent setups turn a theoretical gap into an operational one:
- **Incidents.** A bad `delete_file` or `send_email` needs an owner. "The user's agent" escalates nobody.
- **Least privilege.** If you cannot name the agent, you cannot grant the browser agent read access to the CRM and the coding agent write access to the repo. Every agent inherits the union of the user's powers.
- **Audit and compliance.** "Who accessed this record?" must resolve to a software entity, not to "the account".
- **Trust and blast radius.** One compromised agent should be revocable without locking the human out of everything else.
- **Quotas and usage accounting.** Per-agent rate limits and usage accounting need a per-agent subject to key on.
User-level attribution answers none of these. Agent-level attribution answers all of them.
## What your logs actually show {#logs}
Skip the abstract "AI security" language and look at a real log today:
```text
user_123 -> search_database
user_123 -> send_email
user_123 -> delete_file
```
Now look at what the account really owns:
```text
user_123
├── research-agent
├── coding-agent
└── execution-agent
```
Every action carries the same label. The resource server can no longer tell which autonomous actor performed which tool call — attribution is lost exactly at the boundary where it matters.
## User identity is not agent identity {#user-vs-agent}
These are two different questions:
- **User identity** answers: who is the human or account?
- **Agent identity** answers: which autonomous software entity is acting right now?
One does not replace the other, and a single user identity can own many agent identities:
```text
user_123
    |
    ├── agent_A
    └── agent_B
```
If your architecture only carries user identity, you have collapsed several actors into one subject and thrown away the information you will need later.
## The tempting bad solution {#bad-solution}
The obvious shortcut is to pass the agent id to the tool:
```json
{
  "agent_id": "agent_A"
}
```
It does not work. If `agent_id` arrives only as a tool argument, the resource server has a claim, not proof: any caller can send any string. You would be trusting the very field you are trying to verify.
```text
agent_id = claim              # a string the caller chose
vs
agent_id = verified identity  # a fact bound to the caller's credential
```
That is attribution theater, not authenticated agent identity. A server might try to bind the claim to a credential afterward — but by then the tool contract is already built around an unverified value.
## What a real solution must satisfy {#requirements}
Derive the requirements before naming any product. Agent identity should be:
- **portable** across services and vendors — not a per-integration lookup table;
- **cryptographically verifiable** — a signature, not a database row the caller can name;
- **independent from user identity** — a separate subject, not a renamed account;
- **verifiable offline by the resource server** — no callback to an identity provider on every request (the "no hot-path callback" rule);
- **not dependent on trusting tool arguments** — the identity must come from the credential, not the payload.
One clean way to meet all five is a **signed, short-lived agent credential** — a passport the resource server verifies locally. That is the shape we tested.
## The hypothesis we tested {#hypothesis}
We wanted to test one specific, falsifiable claim:
An MCP resource server can distinguish two agents acting on behalf of the same user, without trusting any agent id supplied by the caller.
Everything below is one minimal way to make that true, with a live server you can hit right now.
## The experiment {#experiment}
We built the smallest possible public MCP resource server and pointed two different signed agent credentials at the same endpoint.
```text
Passport A -> demo.lime.pics/mcp -> agent_7f31...
Passport B -> demo.lime.pics/mcp -> agent_a921...
```
Same resource server. Same tool. Different verified identities. One server, one endpoint, two passports — and two distinguishable answers.
## who_am_i(): the interesting part {#who-am-i}
The interesting part is not only what `who_am_i()` returns — it is what the tool does **not** receive. There is no `agent_id` argument:
```text
who_am_i()                    # what we want
who_am_i(agent_id="agent_A")  # what we avoid - the caller picking its own identity
```
The server reads identity from an already verified credential, not from client-supplied arguments:
```json
{
  "verified": true,
  "agent_id": "agent_7f31..."
}
```
```json
{
  "verified": true,
  "agent_id": "agent_a921..."
}
```
That is the entire proof shape: one tool, one user, two provably different actors.
## How LIME makes it work {#lime-model}
LIME acts as the identity issuer; the demo server is a minimal public resource server.
```text
Agent
  |
  | authenticate
  v
LIME Core
  |
  | signed short-lived Passport
  v
MCP Resource Server
  |
  | local verification (JWKS)
  v
agent_id (= JWT sub)
```
The important property: LIME does not proxy the MCP request. The resource server does not call LIME on every tool invocation to ask "is this agent valid?". It fetches the published JWKS once, verifies the passport signature locally, and reads the `agent_id` from the `sub` claim. Verification is offline, fast, and needs no shared database on the hot path.
## What the server knows — and what it does not {#boundaries}
After verification, be precise about scope.
The resource server knows:
- the passport is valid, and who issued it;
- the audience and the expiration;
- the `agent_id` (the `sub` claim);
- the domain the passport was minted for.
It does not automatically know:
- what the agent intends to do;
- whether the agent is trustworthy;
- whether the user authorized this specific action;
- what permissions the agent should have.
Identity is not authorization. LIME provides a verified identity primitive; the resource server still decides authorization, trust, and policy.
## When this matters {#when-it-matters}
You may not need this if one user maps to one agent that maps to one backend. It is fine to skip it.
It becomes load-bearing when one user owns many agents across many tools and services:
```text
User
 ├── Research Agent
 ├── Coding Agent
 ├── Browser Agent
 └── Execution Agent
             |
             v
        MCP ecosystem
```
At that point agent identity is a separate architectural concern — not a footnote on user login.
## A builder's checklist {#checklist}
If you run an MCP resource server, answer these honestly:
- Can it distinguish two agents acting for the same user?
- Does the identity come from a verified credential, or from a tool argument?
- Does verification require a network call on the hot path?
- Can you revoke one agent without locking out the human?
- Do your logs and audit records name the agent, or only the account?
You can run all five scenarios against the live demo today.
## FAQ {#faq}
**Is user identity enough?** Only while one user equals one agent. It stops being enough the moment a second autonomous agent shares the account.
**Can I just add an `agent_id` argument?** No — an argument is a claim the caller controls. You need proof bound to a credential.
**Does verification mean calling LIME on every call?** No. The resource server verifies the passport signature locally against published JWKS — no per-request callback.
**Does it authorize the agent?** No. Identity is not authorization; you still own access rules, scopes, and policy.
## The open question {#closing}
Here is an architectural problem you may not have — yet. If you do, one minimal way to solve it is a signed, short-lived passport that the resource server verifies locally.
If you build MCP servers: what does your resource server actually know about the caller today? Can it tell two agents apart for the same user? If yes — how? If not — would agent-level attribution help your architecture? We would like to hear how other MCP developers approach it.
## Try it / dig deeper {#links}
- Live demo MCP: https://demo.lime.pics/mcp
- Public demo repository: https://github.com/Mawyxx/lime-mcp-demo
- lime-mcp-server-sdk: https://github.com/Mawyxx/lime-mcp-server-sdk
- lime-agents-sdk: https://github.com/Mawyxx/lime-agents-sdk
- LIME docs: https://lime.pics/docs/
## Explore the experiment {#cta}
Read the open demo, mint a passport, call `who_am_i` — or tell us why your boundary solves attribution differently.
Open the demo repo
Read the docs

## See also

- Primary catalog: https://lime.pics/.well-known/agent-flows
- Context package: https://lime.pics/.well-known/agent-context/
- Human guides: https://lime.pics/docs/guides/
