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

Skip to main content

Verify embed sessions

Your Client ID is public. The embed ships it to the browser, so anyone can read it in your page source. It identifies your integration, but on its own it does not prove that an embed session came from you.

Attribution tokens close that gap. Your backend exchanges your Client Secret for a short-lived token, your frontend passes that token to the embed, and MCP Studio verifies it before attributing a server to you.

AudienceSDK implementers, backend engineers
PrerequisitesA Client ID and Client Secret from the developer portal
Time to integrateAbout 20 minutes

Why this exists

Anything MCP Studio decides from your Client ID alone is decided from a value anyone can copy out of your page source. That includes which plan ceilings apply, which analytics tier your end users receive, and, if you have enabled overage, which payment method is charged.

Attribution tokens are required. A session without one still creates a working MCP server for your end user, but that server is not attributed to you: it does not draw on your plan, your end user receives standard MCP Studio free-tier limits instead of yours, and it will not appear in your usage. Anything you expect your integration to provide depends on the session being verified.

How the pieces fit together

Your Client Secret never leaves your server. Only the short-lived token reaches the browser.

Step 1: Mint a token on your server

Call the token endpoint from your backend. Never from your frontend: that would ship your Client Secret to every visitor, which is the problem this endpoint exists to prevent. The endpoint refuses any request carrying an Origin header for that reason.

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"}'

endUserId is optional and opaque to us. Include it if you want to know later which of your users produced a given server.

The response:

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

A token is valid for 15 minutes and works once. Mint one per embed session, at the point you render the page that hosts the wizard.

Next.js App Router

// app/mcp/page.tsx
async function mintAttributionToken(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 mintAttributionToken(session.userId)
return <McpStudioEmbed attributionToken={attributionToken} />
}

Note cache: 'no-store'. A cached token would be handed to more than one visitor, and the second one to reach the wizard would be refused as a replay.

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) {
// Do not fail your page over this. Without a token the embed still works;
// the session is simply not verified.
return res.json({ attributionToken: null })
}

res.set('Cache-Control', 'no-store')
res.json({ attributionToken: (await upstream.json()).token })
})

Step 2: Pass it to the embed

MCPStudio.init({
clientId: "YOUR_CLIENT_ID",
attributionToken: tokenFromYourBackend,
container: "#mcp-studio-widget",
})

If attributionToken is omitted, the wizard still renders and your end user can still create a server. That server simply is not yours: your plan does not apply to it, and it does not appear in your usage.

Step 3: Confirm it worked

Create a server through your embed and check that it appears against your integration in the portal. If the token was rejected, server creation fails with a 403 and an sdk_attribution_invalid error rather than silently falling back to an unverified session.

Handling rejection

A token that is supplied but does not verify is always refused. It is never ignored in favour of the bare Client ID, because that would let anyone bypass verification by attaching an invalid token.

reasonWhat happenedWhat to do
expiredMore than 15 minutes passed between minting and server creationMint when the wizard is opened, not when the user's session begins
replayedThe token was already used to create a serverMint one token per embed session, and do not cache the response
bad_signatureThe token was altered in transit, or truncatedPass it through exactly as returned, with no trimming or re-encoding
malformedThe value is not a tokenCheck you are sending token, not the whole JSON response
not_configuredSigning is unavailable on our sideContact us. This is not a fault in your integration

Rate limiting

The mint endpoint throttles failed credential attempts. After ten failures from one address within fifteen minutes, further attempts from that address are refused with 429 and a Retry-After header until the window passes. A successful mint clears the count immediately.

Successful mints are not rate limited. You can mint as many tokens as you have sessions, at whatever rate your traffic demands, from a single backend address.

In practice you should only ever see a 429 while wiring the integration up, and it means your credentials are wrong rather than your volume being too high. Check the Client ID and Client Secret in the portal, then retry after the interval given.

{
"error": "too_many_attempts",
"message": "Too many failed credential attempts. Check your clientId and clientSecret, then try again later."
}

Session lifecycle

A token covers one wizard run, not one login. If your product lets a user open the wizard more than once, mint a token each time.

Long-lived pages are the common trap. If a user might leave the page open for longer than 15 minutes before deploying, mint the token when the widget is opened rather than at page render, or re-mint on demand from a small endpoint like the Express example above.

Rotating your secret

Regenerating your Client Secret in the portal invalidates the old one immediately. Tokens already minted stay valid until they expire, so a rotation does not interrupt sessions already in progress.

Security notes

Keep your Client Secret in server-side environment variables only. It must never appear in a NEXT_PUBLIC_* variable, a bundled frontend file, or a mobile app binary.

The token is short-lived and single use, so it is far less sensitive than the secret. It still identifies your integration, so pass it over HTTPS and do not log it.

If you believe your secret has been exposed, regenerate it in the portal. That takes effect immediately.

If your embed is not minting tokens yet

You will notice it as attribution going missing rather than as an error: the wizard renders, your end user creates a server, and none of it counts as yours. Your plan's ceilings will not apply to that user, and your usage will stay flat while people are evidently using the embed.

Add the two steps above and it resolves immediately. There is no migration window to schedule and no flag to request.

A working example

The sample apps repository implements everything on this page. Each app has the three files a real integration needs:

FilePurpose
src/lib/mcp-studio.tsReads credentials and mints 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 own credentials, and it runs. See Sample apps and playground.