docs/embedding/embeddable-mcp.mdx
Every Activepieces project can act as an MCP server — a place an AI can connect to in order to run that project's flows.
When you embed Activepieces, your users log in through your app, not Activepieces. So they can't use the normal "connect your AI" screen (it would ask them to log in to Activepieces, which they can't).
Embeddable MCP fixes that. Your user clicks one Authorize button inside your app, and your backend gets a token it can use to run that user's automations through AI.
Who does what:
If you don't need the full OAuth flow — you just want a token to point an AI at the embedded user's project — call generateMcpToken() from the frontend. It uses the embed session you already configured, so there's no app registration, no PKCE, and no popup.
const { mcpServerUrl, mcpToken } = await activepieces.generateMcpToken();
// Point your MCP client at mcpServerUrl with Authorization: Bearer <mcpToken>
This is the same kind of credential the built-in chat assistant uses internally. The token:
externalProjectId in their embed JWT),generateMcpToken() again to get a fresh one.Embedding should already work for you:
activepieces.configure({ jwtToken, instanceUrl, … }).Below, INSTANCE_URL is your Activepieces address (like https://app.your-company.com).
client_id.authRequestId.code.code for a token.```bash
curl -X POST "$INSTANCE_URL/register" \
-H "Content-Type: application/json" \
-d '{
"client_name": "My App AI Assistant",
"redirect_uris": ["https://app.your-company.com/mcp/callback"]
}'
```
You get back a `client_id`.
<Tip>
`client_name` is the name your user sees in the popup ("**My App AI Assistant** wants to connect"). Skip it and it says "Unknown app".
</Tip>
```ts
import crypto from 'crypto';
const base64url = (b: Buffer) =>
b.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
const codeVerifier = base64url(crypto.randomBytes(45));
const codeChallenge = base64url(crypto.createHash('sha256').update(codeVerifier).digest());
```
```ts
const params = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: 'https://app.your-company.com/mcp/callback',
response_type: 'code',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
const res = await fetch(`${INSTANCE_URL}/authorize?${params}`, { redirect: 'manual' });
const location = res.headers.get('location'); // .../mcp-authorize?authRequestId=eyJ...
const authRequestId = new URL(location).searchParams.get('authRequestId');
```
<Warning>The `authRequestId` only lasts 10 minutes. Get it right before showing the popup.</Warning>
```ts
const result = await activepieces.authorizeMcp({ authRequestId });
if (result.denied) {
// user clicked Deny
} else {
// result.redirectUrl looks like: .../mcp/callback?code=THE_CODE
const code = new URL(result.redirectUrl).searchParams.get('code');
// send `code` to your backend for step 5
}
```
```ts
const res = await fetch(`${INSTANCE_URL}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
code_verifier: codeVerifier,
redirect_uri: 'https://app.your-company.com/mcp/callback',
client_id: CLIENT_ID,
}),
});
const { access_token, refresh_token } = await res.json();
```
Save the `refresh_token` for this user. The `access_token` works for 15 minutes; the `refresh_token` gets you new ones and can be turned off anytime.
```bash
curl -X POST "$INSTANCE_URL/mcp" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```
You'll see the user's tools and flows come back.
Get a fresh token when the 15-minute one expires:
curl -X POST "$INSTANCE_URL/token" -H "Content-Type: application/json" \
-d '{"grant_type":"refresh_token","refresh_token":"<REFRESH_TOKEN>","client_id":"<CLIENT_ID>"}'
Disconnect (turn the AI off):
curl -X POST "$INSTANCE_URL/revoke" -H "Content-Type: application/json" \
-d '{"token":"<REFRESH_TOKEN>","client_id":"<CLIENT_ID>"}'
Add a button so users can see their connection and turn tools on or off — inside your app:
await activepieces.mcpSettings();
redirect_uri everywhere (register, /authorize, /token). If it differs, it's rejected.authRequestId lasts 10 minutes — make it right before the popup.