Attribution tokens
An attribution token links an embed session to your integration. Your backend exchanges your Client Secret for a short-lived token, your frontend hands that token to the embed, and Context MCP Studio applies your plan, your analytics tier, and your usage to the server your end user creates.
Your Client ID stays in the browser, in the same way a Stripe publishable key does. The token is what carries your identity.
| Audience | SDK implementers, backend engineers |
|---|---|
| Prerequisites | A Client ID and Client Secret from the developer portal |
| Time to integrate | About 20 minutes |
What attribution controls
A verified session is attributed to you, which means:
- Your plan's limits apply to the end user, rather than MCP Studio's free-tier defaults.
- The analytics tier you have chosen is granted to their server.
- The server and its requests count toward your usage in the portal.
An unverified session still creates a working MCP server. It simply is not yours.
How it works
Your Client Secret never leaves your server. Only the short-lived token reaches the browser.
Step 1: Create a token on your server
curl -X POST https://appatools.com/mcp-studio-sdk/api/attribution/token \
-H "Authorization: Bearer $MCP_STUDIO_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{"clientId":"'"$MCP_STUDIO_CLIENT_ID"'","endUserId":"user_1842"}'
| Parameter | Required | Description |
|---|---|---|
clientId | Yes | Your Client ID from the developer portal |
endUserId | No | Your own identifier for the user, up to 200 characters, not starting with ~. Opaque to us. Sending it also returns a session token for that user |
The response:
{
"token": "mcpsat_v1.eyJjaWQiOi...",
"expiresAt": "2026-09-08T16:45:00.000Z",
"expiresInSeconds": 900,
"sessionToken": "mcpsus_v1.eyJjaWQiOi...",
"sessionExpiresAt": "2026-09-08T17:30:00.000Z",
"sessionExpiresInSeconds": 3600
}
The sessionToken fields appear only when you send endUserId. A session token lets the wizard deploy in place and powers the end-user dashboard. It lasts 60 minutes and can be reused within that time.
Tokens expire after 15 minutes and can be used once. Create one per embed session.
Call this endpoint from your backend only. It rejects requests that carry an Origin header, because a browser making this call means your Client Secret is in frontend code.
Next.js App Router
// app/mcp/page.tsx
async function createAttributionToken(endUserId: string) {
const res = await fetch(
'https://appatools.com/mcp-studio-sdk/api/attribution/token',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MCP_STUDIO_CLIENT_SECRET}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientId: process.env.MCP_STUDIO_CLIENT_ID,
endUserId,
}),
cache: 'no-store',
},
)
if (!res.ok) return null
const { token } = await res.json()
return token as string
}
export default async function Page() {
const session = await getSession()
const attributionToken = await createAttributionToken(session.userId)
return <McpStudioEmbed attributionToken={attributionToken} />
}
Set cache: 'no-store'. A cached response would hand the same token to more than one visitor, and only the first would be accepted.
Express
app.get('/api/mcp-session', requireAuth, async (req, res) => {
const upstream = await fetch(
'https://appatools.com/mcp-studio-sdk/api/attribution/token',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MCP_STUDIO_CLIENT_SECRET}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientId: process.env.MCP_STUDIO_CLIENT_ID,
endUserId: req.user.id,
}),
},
)
if (!upstream.ok) return res.json({ attributionToken: null })
res.set('Cache-Control', 'no-store')
res.json({ attributionToken: (await upstream.json()).token })
})
Step 2: Pass the token to the embed
MCPStudio.init({
clientId: "YOUR_CLIENT_ID",
attributionToken: tokenFromYourBackend,
container: "#mcp-studio-widget",
})
Step 3: Confirm it worked
Create a server through your embed and check that it appears against your integration in the portal.
Errors
A token that is supplied but does not verify returns 403 with an sdk_attribution_invalid error and a reason.
reason | Cause | Resolution |
|---|---|---|
expired | More than 15 minutes passed between creating the token and creating the server | Create the token when the wizard opens, not when the user's session begins |
replayed | The token was already used to create a server | Create one token per embed session, and do not cache the response |
bad_signature | The token was altered or truncated in transit | Pass it through exactly as returned, with no trimming or re-encoding |
malformed | The value is not a token | Send the token field, not the whole JSON response |
not_configured | Signing is unavailable | Contact support. No change is needed on your side |
Rate limits
Successful requests are not rate limited. Create as many tokens as you have sessions, at whatever rate your traffic requires, from a single backend address.
Failed credential attempts are limited to ten per IP address per 15 minutes, after which the endpoint returns 429 with a Retry-After header. A successful request clears the count.
{
"error": "too_many_attempts",
"message": "Too many failed credential attempts. Check your clientId and clientSecret, then try again later."
}
Best practices
- Create the token when the widget opens, not at page render. A page left open for more than 15 minutes would otherwise carry an expired token.
- Create one token per wizard run, not per login. If your product lets a user open the wizard more than once, create a token each time.
- Keep your Client Secret in server-side environment variables. It must never appear in a
NEXT_PUBLIC_*variable, a bundled frontend file, or a mobile binary. - Do not log tokens. They are short-lived and single use, but they identify your integration.
- Rotate from the portal. Regenerating your Client Secret takes effect immediately. Tokens already created remain valid until they expire, so sessions in progress are not interrupted.
Sample implementation
The sample apps repository implements this guide end to end.
| File | Purpose |
|---|---|
src/lib/mcp-studio.ts | Reads credentials and creates the token, server-side |
src/app/api/mcp-studio/session/route.ts | The session endpoint your frontend calls |
src/components/McpStudioEmbed.tsx | The browser half, which never sees the secret |
Clone it, add your credentials, and it runs. See Sample apps and playground.
Related
- Embed the widget for the base integration
- Configuration reference for every
initoption - API reference for the token endpoint's full contract
- Billing for how overage and budgets work