Skip to content

Console identity for MCP servers and connected agents ​

When an agent calls an MCP server or a connected agent, that server often needs to know who the call is for: a CRM that shows each salesperson only their own customers, a ticketing system that records who opened a ticket. With Console identity, Console tells it. Every call carries a short-lived token that Console signs for the person behind the call, valid for that one server only. The server checks it against the workspace's public keys and finds the person on its side.

Console does not forward the person's own sign-in token. A token issued for Console (for example by Google when the person signed in with SSO) is meant for Console only; handing it to another server is what the MCP specification forbids, and that server could not verify it anyway. Console issues a token of its own instead, which states who the person is and is addressed to that server.

When to use it ​

A stored credential (Bearer token, header, basic auth)Console identity
The server sees one caller: whoever owns the keyThe server sees the person behind each call
A secret is stored in Console and must be rotatedNothing secret is stored; tokens expire after five minutes
The server cannot tell two people apartThe server applies its own permissions per person

Use it when the server has users of its own and should apply their access. Keep a stored credential for servers that answer every caller the same way.

How it works ​

text
 person ──► agent (portal, API, playground …)                 your server (MCP / connected agent)
                │                                                      │
                │ 1. Console signs a token for the person,             │
                │    for this server's URL, valid 5 minutes            │
                │                                                      │
                └── Authorization: Bearer <token> ────────────────────►│ 2. verifies it with the key set
                                                                       │    <issuer>/.well-known/jwks.json
                                                                       │ 3. finds the person (sub / email / idp_sub)
                                                                       │    and applies its own permissions

There is no token exchange and nothing to call back: the server only reads the public key set, and caches it.

Turn it on ​

WhereWhat to choose
MCP server: create screen or Settings tab, section Upstream authenticationConsole identity. Available for remote MCP (HTTP / SSE) and OpenAPI servers.
Connected agent: Connect agent or Edit connection, section AuthenticationMethod → Console identity. All three protocols (A2A, OpenAI Chat, OpenAI Responses).
APIMCP server: "upstreamAuth": { "type": "console_identity" } on POST/PATCH of the server. Connected agent: "config": { "kind": "external", "connection": { …, "authType": "console_identity" } }.

Choosing Console identity replaces a stored key: the key, header or password the server had is removed. Static custom headers of a connected agent are still sent, except Authorization, which always carries the token.

Under the option the screen shows two addresses to give to the team that runs the server:

FieldExample
Issuerhttps://console.example.com/api/public/identity/65f0c1d2a3b4c5d6e7f80001
Key set (JWKS)https://console.example.com/api/public/identity/65f0c1d2a3b4c5d6e7f80001/.well-known/jwks.json

The workspace's signing key is created the first time an MCP server or connected agent is saved with Console identity, so the key set is published before the first call.

Discovery runs as you

When you save a remote MCP server, Console lists its tools right away. With Console identity that request carries a token for you, the person saving it (src: console), so the server must accept you to list its tools. Refresh tools does the same.

The token ​

A JWT signed with RS256, header typ: at+jwt (an OAuth access token, RFC 9068) and the kid of the signing key.

ClaimMeaning
issThe issuer above. Built from Console's configured public URL (APP_URL), never from the request.
audThe server's URL as configured in Console, without query string, fragment or credentials, and without a trailing slash (the host in lower case, a default port such as :443 left out): the remote MCP endpoint, the OpenAPI server's base URL (one audience per server, not per operation), or the connected agent's endpoint URL.
subThe person's Console user id.
email, nameThe person's profile, when set.
idp, idp_subFor an account that signs in with SSO (oidc or saml): the identity provider and the person's subject there, for example their Google sub.
tenant_id, project_idThe workspace, and the project of the call when there is one.
client_idAlways cognipeer-console.
srcThe channel: portal, api, mcp, a2a, playground, realtime, trigger or console (saving or refreshing a server).
viaHow Console knew the person, see below.
act{ "sub": "agent:<agent key>" } when an agent makes the call (the actor, RFC 8693). A referenced sub-agent names its own key.
iat, nbf, expIssued now, valid five minutes.
jtiUnique per token; a server can refuse a jti it has already seen.

A token is minted for every call; Console does not reuse one.

Who the call is for ​

The call comes fromsub isvia
The dashboard (playground, Studio, MCP playground) or a portalthe signed-in personsession
The client API, client MCP endpoint or A2A with an API tokenthe token's ownerapi_token
The same, acting for a user (X-Cognipeer-User-Token)the person the statement namesdelegated
A trigger (schedule, webhook …)the trigger's accountable person: whoever last saved ittrigger
A turn that continues after an approval or an answerthe person the turn is for, not whoever approved itas for that turn
A public MCP endpoint or a public A2A endpointnobody: the call is refused

Console refuses, before anything is sent, a call that is not made for a person, a call for an account that is not in the workspace, and a call for a disabled account. The call fails with a message that starts with Console identity: (see Troubleshooting); it does not count as a fault of the server. A disabled account stops getting tokens within 15 seconds on every node.

Verify the token on your server ​

  1. Read the key set from the Key set (JWKS) URL (or from the discovery document at <issuer>/.well-known/openid-configuration). Cache it and read it again when a token names a kid you do not have.
  2. Accept RS256 only, and check the typ header is at+jwt.
  3. Check iss is exactly the issuer, and aud is your URL exactly as configured in Console.
  4. Check exp and nbf, allowing a little clock skew (30 seconds is plenty).
  5. Find the person: by idp_sub when your server signs the same people in with the same identity provider, otherwise by email, or by sub if you keep Console user ids.

Node.js (jose):

js
import { createRemoteJWKSet, jwtVerify } from 'jose';

const ISSUER = 'https://console.example.com/api/public/identity/65f0c1d2a3b4c5d6e7f80001';
const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`));

export async function personOf(authorization) {
  const token = authorization?.replace(/^Bearer /, '');
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: ISSUER,
    audience: 'https://crm.example.com/mcp',
    algorithms: ['RS256'],
    typ: 'at+jwt',
    clockTolerance: 30,
  });
  return payload; // sub, email, idp_sub, act, src, via …
}

Python (PyJWT):

python
import jwt
from jwt import PyJWKClient

ISSUER = "https://console.example.com/api/public/identity/65f0c1d2a3b4c5d6e7f80001"
jwks = PyJWKClient(f"{ISSUER}/.well-known/jwks.json")

def person_of(authorization: str) -> dict:
    token = authorization.removeprefix("Bearer ")
    if jwt.get_unverified_header(token).get("typ") != "at+jwt":
        raise jwt.InvalidTokenError("not a Console identity token")
    key = jwks.get_signing_key_from_jwt(token)
    return jwt.decode(token, key.key, algorithms=["RS256"], issuer=ISSUER,
                      audience="https://crm.example.com/mcp", leeway=30)

An API gateway that validates JWTs from an OpenID issuer (Kong, Envoy, Azure API Management, AWS API Gateway JWT authorizers and others) can be pointed at the issuer or the key set URL instead.

Keys and endpoints ​

  • Each workspace has its own RSA key. The private key never leaves Console and is encrypted at rest; the key set publishes the public half only. A workspace that has not used Console identity publishes an empty set.
  • Select the key by kid: the set can hold more than one key.
  • GET <issuer>/.well-known/jwks.json and GET <issuer>/.well-known/openid-configuration need no token. They answer with Cache-Control: public, max-age=300 (max-age=30 while the set is empty), allow any origin, and accept up to 120 requests a minute from one address (429 with Retry-After above that). An unknown workspace is a 404.
  • The issuer comes from APP_URL. Set it to the public address of Console before you hand the issuer out: changing it changes iss, and servers that check it will refuse the new tokens.

Notes ​

  • Not available for npx / uvx, internal or composite servers: they have no upstream URL of their own. A composite's members that use Console identity mint their own tokens, for their own URLs.
  • Caller-supplied runtime headers cannot replace the token: for these servers any Authorization header a caller sends is dropped.
  • Console masks the token where a server could echo it back into its records: an MCP server's request logs, last error and tool results, and a connected agent's answers (streamed too) and errors.
  • An exported agent that embeds its MCP servers keeps Console identity on them. Importing it asks for no credential: the importing workspace signs its own tokens, with its own issuer.

Troubleshooting ​

What you seeWhat it means
Console identity: this server receives the identity of the person behind each call, and this call is not made for a signed-in person.The call has nobody behind it: a public MCP or A2A endpoint, or an internal actor. Call through an authenticated channel.
Console identity: … the person this call is made for is not an account of this workspace.The account was deleted, or belongs to another workspace.
Console identity: … the account this call is made for is disabled.Enable the account, or call as someone else.
Console identity: Console could not prepare the token for this call. Try again shortly.The workspace's key or the person's account could not be read (for example the database was unreachable). Retry; if it persists, check that the encryption secret of the deployment has not changed.
Your server answers 401Compare aud with your URL character by character (no query string, no trailing slash), check iss against the issuer, and make sure the server's clock is right.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub