Appa Tools documentation for MCP Studio, including setup, guides, concepts, and API-related reference content.

Skip to main content

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.

AudienceSDK implementers, backend engineers
PrerequisitesA Client ID and Client Secret from the developer portal
Time to integrateAbout 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"}'
ParameterRequiredDescription
clientIdYesYour Client ID from the developer portal
endUserIdNoYour own identifier for the user. Opaque to us, and returned with the server in your usage data

The response:

{
"token": "mcpsat_v1.eyJjaWQiOi...",
"expiresAt": "2026-09-08T16:45:00.000Z",
"expiresInSeconds": 900
}

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.

reasonCauseResolution
expiredMore than 15 minutes passed between creating the token and creating the serverCreate the token when the wizard opens, not when the user's session begins
replayedThe token was already used to create a serverCreate one token per embed session, and do not cache the response
bad_signatureThe token was altered or truncated in transitPass it through exactly as returned, with no trimming or re-encoding
malformedThe value is not a tokenSend the token field, not the whole JSON response
not_configuredSigning is unavailableContact 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.

FilePurpose
src/lib/mcp-studio.tsReads credentials and creates the token, server-side
src/app/api/mcp-studio/session/route.tsThe session endpoint your frontend calls
src/components/McpStudioEmbed.tsxThe browser half, which never sees the secret

Clone it, add your credentials, and it runs. See Sample apps and playground.