Skip to content
Scalekit Docs
Talk to an Engineer Dashboard

Mastra

Connect a Mastra agent to Scalekit tools over MCP, all in Node.js. Create a Virtual MCP server, mint a session token for the user, and run the agent.

Connect a Mastra agent to Scalekit tools using MCP. Mastra has native MCP support via @mastra/mcp. Pass a Scalekit Virtual MCP server URL and a session token, and Mastra handles tool discovery automatically.

The whole example runs in Node.js on your server, in two files. setup.mts creates the Virtual MCP server once. agent.mts connects the user, mints a session token and runs the agent, every run. Run each with npx tsx, for example npx tsx agent.mts.

  • Create a Gmail connection in AgentKit > Connections. The samples use the connection name gmail, so change it if yours differs. See Set up a connection.
  • Have the .env file from the Quickstart, and add your OpenAI API key as OPENAI_API_KEY to it.
  • The samples load .env with import 'dotenv/config'.
Terminal window
npm install @scalekit-sdk/node @mastra/core@1 @mastra/mcp@2 @ai-sdk/openai@4 dotenv

Start both files with the client:

import 'dotenv/config';
import { ScalekitClient } from '@scalekit-sdk/node';
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENVIRONMENT_URL!,
process.env.SCALEKIT_CLIENT_ID!,
process.env.SCALEKIT_CLIENT_SECRET!,
);

Create the server once per agent role, not once per user. Every user and every session reuses its mcpServerUrl. Server names are unique in an environment, so run this file once: a second run fails with config with name 'gmail-user-tools' already exists.

setup.mts
// Run once per agent role, not once per user
const { config } = await scalekit.actions.mcp.createConfig({
name: 'gmail-user-tools',
connectionToolMappings: [{ connectionName: 'gmail', tools: ['gmail_fetch_mails'] }],
});
console.log('Virtual MCP server:', config!.id, config!.mcpServerUrl);

Create a Virtual MCP server covers choosing tools, and listing, updating and deleting servers.

The rest of the code goes in agent.mts and runs before each agent run.

The Virtual MCP server only calls tools for users who have authorized the connection. Before each agent run, make sure the user’s Gmail account is ACTIVE:

import { createInterface } from 'node:readline/promises';
import { ConnectorStatus } from '@scalekit-sdk/node';
// Connect the user's Gmail account, and wait until it's ACTIVE before calling tools
const connectionName = 'gmail';
const identifier = 'user_123'; // your app's unique user ID
let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({
connectionName,
identifier,
});
if (connectedAccount?.status !== ConnectorStatus.ACTIVE) {
const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier });
console.log('Authorize Gmail:', link);
const rl = createInterface({ input: process.stdin, output: process.stdout });
await rl.question('Press Enter after authorizing...');
rl.close();
// Fetch the account again to pick up the new status
({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({
connectionName,
identifier,
}));
}
if (connectedAccount?.status !== ConnectorStatus.ACTIVE) {
throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`);
}

See Authorize a user for production auth handling.

The server URL is the same for everyone. The session token carries the user’s identity. Mint a new token before each agent run, after the connect step:

agent.mts
import { ScalekitNotFoundException, ScalekitUnauthorizedException } from '@scalekit-sdk/node';
// Run before each agent session, after the connect step
let mcpServerUrl: string;
let sessionToken: string;
try {
const { configs } = await scalekit.actions.mcp.listConfigs({ search: 'gmail-user-tools' });
const server = configs.find((c) => c.name === 'gmail-user-tools');
if (!server) throw new Error('Run setup.mts to create the gmail-user-tools server first');
mcpServerUrl = server.mcpServerUrl;
const session = await scalekit.actions.mcp.createSessionToken({
mcpConfigId: server.id,
identifier, // the user who authorized Gmail above
expirySeconds: 60 * 60,
});
sessionToken = session.token;
} catch (err) {
if (err instanceof ScalekitUnauthorizedException) {
// Scalekit client credentials are wrong or expired: fix the environment variables
} else if (err instanceof ScalekitNotFoundException) {
// The server was deleted: create it again, then retry
}
throw err; // don't start the agent without a token
}

Don’t start the agent when minting fails: every tool call would return 401. See Errors and rate limits for what an error response contains. Set expirySeconds above the expected run time; Mint session tokens covers when to mint again.

Pass the server URL to MCPClient, and the user’s session token as a bearer header in requestInit. Mastra fetches the tool list and schemas automatically. It names each tool after the server key, so gmail_fetch_mails appears to the model as scalekit_gmail_fetch_mails.

agent.mts
import { Agent } from '@mastra/core/agent';
import { MCPClient } from '@mastra/mcp';
import { openai } from '@ai-sdk/openai';
const mcp = new MCPClient({
servers: {
scalekit: {
url: new URL(mcpServerUrl),
requestInit: { headers: { Authorization: `Bearer ${sessionToken}` } },
},
},
});
try {
const tools = await mcp.listTools();
const agent = new Agent({
id: 'gmail-assistant',
name: 'Gmail assistant',
instructions: 'You are a helpful Gmail assistant.',
model: openai('gpt-4o'),
tools,
});
const result = await agent.generate('Fetch my last 5 unread emails and summarize them');
console.log(result.text);
} finally {
await mcp.disconnect();
}

API reference

The endpoints this page's code calls, with every field and the SDK method for each: