What this enables:
- Tool calls attributed to a specific end user, not just the project
- Member identity (
X-Tengine-Member-Id) injected into every custom tool call- Multi-tenant isolation — each user's session is independent
User-scoped sessions let you attribute each MCP tool call to a specific end user. Instead of a single project API key that authenticates all requests, your backend mints a short-lived member session token for each user, which is then passed to the Anthropic SDK as the authorization_token.
Use this mode when:
Your Backend TengineAI
─────────────────────────────────────────────────────
1. Generate key pair (one-time setup)
2. Register public key ──────────────────────────▶ Store public key (kid)
3. Per request:
Sign member_assertion JWT with private key
4. Exchange for session token ────────────────────▶ Verify signature
◀──────────────────── Return tng_mst_... token
5. Pass token to Anthropic SDK
mcp_servers[authorization_token] ─────────────▶ Authenticate MCP session
as that specific member
The session token carries the member's identity inside TengineAI — tools run in the context of that user for the duration of the session.
Generate an EC key pair using the P-256 curve (ES256). This is a one-time setup per environment.
# Generate private key
openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem
# Extract public key
openssl ec -in private_key.pem -pubout -out public_key.pem
Keep
private_key.pemsecret. It lives on your backend only and should never be committed to source control or sent to TengineAI.
Supported algorithms: ES256 (recommended), ES384, Ed25519, RS256.
Register the public key with TengineAI using your project API key. The kid (key ID) is a string you choose — use something that lets you identify and rotate keys later.
import os
import requests
headers = {"Authorization": f"Bearer {os.environ['TENGINEAI_API_KEY']}"}
with open("public_key.pem") as f:
public_key_pem = f.read()
response = requests.post(
"https://app.tengine.ai/api/v1/assertion-keys",
headers=headers,
json={
"kid": "my-backend-v1",
"public_key_pem": public_key_pem,
"algorithm": "ES256",
"description": "Production signing key",
},
)
response.raise_for_status()
print(response.json())
You only need to do this once per key. Store the kid — you'll reference it in every member_assertion JWT you sign.
member_assertion JWTSecurity requirements — read before continuing:
- The
member_assertionJWT must have a short TTL (≤ 60 seconds). It is a one-time-use proof of identity, not a session credential. If intercepted, a long-lived assertion can be replayed.- The
member_assertionis not the same as the session token. The assertion proves identity to TengineAI. The session token (tng_mst_...) is what you pass to the Anthropic SDK.- Never send your private key to the frontend. Assertion signing must happen server-side only. If a client ever touches the private key, your entire identity system is compromised.
For each user session, your backend signs a short-lived JWT asserting the user's identity. TengineAI verifies this signature against the registered public key before issuing a session token.
Required claims:
| Claim | Type | Description |
|---|---|---|
sub | string | Your internal user ID (any stable identifier) |
aud | string | Must be tengine:project:<your_project_id> |
exp | int | Unix timestamp — keep TTL short (30–120 seconds) |
iat | int | Unix timestamp of issuance |
Optional claims:
| Claim | Type | Description |
|---|---|---|
email | string | User email — forwarded in member context |
roles | list | User roles — forwarded in member context |
JWT header must include kid matching the key registered in Step 2.
import os
import time
import jwt # pip install PyJWT cryptography
with open("private_key.pem") as f:
private_key = f.read()
PROJECT_ID = os.environ["TENGINEAI_PROJECT_ID"]
def mint_member_assertion(user_id: str, email: str = None) -> str:
now = int(time.time())
payload = {
"sub": user_id,
"aud": f"tengine:project:{PROJECT_ID}",
"iat": now,
"exp": now + 60, # 60-second TTL — used only for the exchange
}
if email:
payload["email"] = email
return jwt.encode(
payload,
private_key,
algorithm="ES256",
headers={"kid": "my-backend-v1"},
)
The member_assertion is ephemeral — it exists only to prove identity to TengineAI during the token exchange. It is never stored.
Send the member_assertion to TengineAI's session token endpoint along with your project API key. TengineAI verifies the assertion signature and returns a tng_mst_... session token.
import os
import requests
def get_member_session_token(user_id: str, email: str = None) -> dict:
assertion = mint_member_assertion(user_id, email)
response = requests.post(
"https://app.tengine.ai/api/v1/mcp/session-token",
headers={"Authorization": f"Bearer {os.environ['TENGINEAI_API_KEY']}"},
json={"member_assertion": assertion},
)
response.raise_for_status()
return response.json()
# Returns: {"token": "tng_mst_...", "expires_in_minutes": 15, "token_type": "Bearer"}
The returned token has a 15-minute TTL by default. Cache it and reuse it for the duration of a user's session rather than minting a new one on every request.
Pass token as the authorization_token in mcp_servers:
import json
import os
import anthropic
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
session = get_member_session_token(user_id="user_123", email="alice@example.com")
response = client.beta.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "Look up contact c_001 in the CRM."}],
mcp_servers=[
{
"type": "url",
"url": "https://app.tengine.ai/mcp/",
"name": "tengineai",
"authorization_token": session["token"],
}
],
betas=["mcp-client-2025-04-04"],
)
for block in response.content:
if block.type == "text":
print(block.text)
elif block.type == "mcp_tool_use":
print(
f"[tool_use] server={block.server_name} tool={block.name} id={block.id} "
f"input={json.dumps(block.input, default=str)}"
)
elif block.type == "mcp_tool_result":
print(
f"[tool_result] id={block.tool_use_id} is_error={getattr(block, 'is_error', False)}"
)
print(block.content)
TengineAI reads the session token, resolves the member identity, and executes tools in that user's context.
| Property | Value |
|---|---|
| Prefix | tng_mst_ |
| Default TTL | 15 minutes |
| Algorithm | HS256 (signed by TengineAI) |
| Required claims | iss, aud, sub, exp, iat, jti, kid |
sub format | member:<member_external_id> |
The session token is opaque to your application — treat it as a bearer credential and do not attempt to decode it.
Register a new key with a different kid, update your signing code to use the new key, then deactivate the old key via the API:
# Deactivate old key by ID
requests.delete(
"https://app.tengine.ai/api/v1/assertion-keys/<key_id>",
headers={"Authorization": f"Bearer {os.environ['TENGINEAI_API_KEY']}"},
)
TengineAI validates against all active keys for your project, so you can rotate without downtime by activating the new key before deactivating the old one.