Spanly Docs

Identify your users

Attribute MCP sessions to your end users and accounts with JWT claim mapping or a resolver endpoint. Tokens never leave your infrastructure.

Spanly can tie every captured session to the end user (and account) behind it. Once configured you get:

  • Sessions attributed to users: the sessions list and session detail show who drove each conversation.
  • A Users page listing everyone who used your MCP server, with sessions, requests, errors, and last-seen per user, plus a per-user detail view.
  • Filtering: scope any session view to a single user.

The quickest path: JWT claim mapping

If your MCP server authenticates with JWT bearer tokens, identity is one flag on the Spanly CLI proxy:

spanly run --port 3000 --identity-jwt -- node server.js

The proxy decodes the Authorization: Bearer JWT payload and maps sub to the user id, email to email, and name to name. Override any mapping with --identity-jwt-claim (repeatable, dot-paths supported):

spanly run --port 3000 \
  --identity-jwt \
  --identity-jwt-claim accountId=org_id \
  --identity-jwt-claim id=user.id \
  -- node server.js

The token is decoded only, never signature-verified: your server behind the proxy has already authenticated the request, so the claims are only used to label traffic it accepted.

Using the TypeScript SDK middleware instead of the CLI? The same tiers are available in-process: spanly({ identity: { jwtClaims: true } }), an identity.resolve callback, or identity.resolver. See the SDK API reference for the SDK shapes; the rest of this page uses the CLI flags.

Opaque tokens: the resolver endpoint

If your tokens are opaque (API keys, session tokens), host a small HTTP endpoint that maps a token to a user, and point the proxy at it:

spanly run --port 3000 \
  --identity-resolver-url=https://api.example.com/spanly-identity \
  --identity-resolver-secret=$SPANLY_IDENTITY_SECRET \
  -- node server.js

The endpoint can be one route on the MCP server itself; any URL works. The full request/response contract, caching behavior, and a copy-paste Express handler are in the resolver protocol reference.

When both tiers are configured, a JWT that decodes to a user id wins and the resolver is never called; opaque tokens fall through to the resolver.

Deployment model

Identity is resolved by the producer running inside your infrastructure: the Spanly CLI proxy (spanly run --port or spanly proxy) in front of your HTTP MCP server, or the TypeScript SDK middleware in-process. stdio mode has no auth headers, so identity does not apply there.

Privacy model

Your users' tokens never leave your infrastructure. The JWT decode happens inside the proxy process, and the resolver endpoint is yours: Spanly's backend never sees the token and never calls your resolver. What Spanly receives is only the resolved id, email, name, accountId, and accountName you choose to send, plus a one-way token fingerprint (the first 32 hex characters of the SHA-256 of the bearer credential) that you can disable with --no-auth-fingerprint.

Token rotation

Attribution keys on your stable user id, so rotating tokens do not fragment users: a new token re-resolves to the same id and the sessions line up under one user. The fingerprint-only fallback (no identity configured) counts distinct tokens, so it over-counts users under rotation. That is why the Users page shows "distinct auth tokens" rather than "users" until identity is configured.

What to return

FieldRequiredUse
idyesYour internal user id. Stable across logins and tokens.
emailnoDisplay in the dashboard.
namenoDisplay in the dashboard.
accountIdnoCompany or workspace level attribution.
accountNamenoDisplay for the account.

Do not use the raw token or a rotating claim (jti, exp-derived values) as id: every rotation would then look like a new user, which defeats the point. An email works for display but makes a poor id, since users change emails.

CLI flag reference

FlagDefaultDescription
--identity-jwtoffDecode the bearer JWT with the default mapping: id from sub, email from email, name from name.
--identity-jwt-claimnonefield=claim override, repeatable. Fields: id, email, name, accountId, accountName. Dot-paths OK. Implies --identity-jwt.
--identity-resolver-urlnoneURL of your resolver endpoint (see the protocol reference).
--identity-resolver-secretnoneShared secret sent as X-Spanly-Identity-Secret. Also via SPANLY_IDENTITY_RESOLVER_SECRET; the flag wins.
--no-auth-fingerprintoffDisable the anonymous token fingerprint (on by default in HTTP mode).

Behavior reference

BehaviorValue
Positive cache15 minutes per token
Negative cache60 seconds per token (unknown tokens and resolver failures)
Resolver timeout3 seconds, no retries (natural retry after negative cache expiry)
ConcurrencyOne in-flight resolution per token
Failure modeFail-open: resolution failure never affects MCP serving
Fingerprint schemeBearer only; Basic and other schemes are never fingerprinted

On this page