Skip to content
Scalekit Docs
Talk to an Engineer Dashboard

Build a Mastra agent with Scalekit AgentKit tools

Give a Mastra agent access to Gmail and 60+ tools through Scalekit AgentKit — zero manual OAuth handling.

A Mastra agent that reads emails needs a Gmail OAuth token. An agent that also posts to Slack needs a second token. Each tool means another OAuth flow, another token store, another refresh cycle. Before you write any agent logic, you are already maintaining parallel credential pipelines.

Scalekit AgentKit eliminates that overhead. It stores one OAuth session per connector per user, handles token refresh automatically, and gives your agent a single API surface for 60+ tools. This recipe shows how to discover AgentKit tools at runtime, wrap them as native Mastra tools, and run them through a Mastra agent — all in TypeScript, with no Python backend.

  • A Mastra agent that fetches Gmail messages through Scalekit AgentKit.
  • Dynamic tool discovery — the agent discovers available tools at runtime from Scalekit, instead of hardcoding tool definitions.
  • Magic link authorization — if the user has not connected their Gmail account, the agent generates an authorization URL.
  • A pattern you can extend to any of Scalekit’s 60+ connectors by changing a single string.

The complete source is available in the mastra-agentkit-example repository.

  • A Scalekit account at app.scalekit.com with API credentials (Settings → API Credentials).
  • A Gmail connection configured under AgentKit → Connections. See Configure a connection.
  • An OpenAI API key.
  • Node.js 18+ and pnpm (or npm).
  1. Terminal
    pnpm add @mastra/core @scalekit-sdk/node @ai-sdk/openai zod dotenv
    pnpm add -D tsx typescript @types/node

    @mastra/core provides the Agent and createTool primitives. @scalekit-sdk/node handles authentication, tool discovery, and tool execution against the Scalekit API. @ai-sdk/openai connects the agent to GPT-4o.

  2. Create a .env file at the project root:

    .env
    # Scalekit — from app.scalekit.com → Settings → API Credentials
    # Threat: leaked credentials grant full API access to your Scalekit environment.
    # Never commit this file to version control; add .env to .gitignore.
    SCALEKIT_ENV_URL=https://your-env.scalekit.dev
    SCALEKIT_CLIENT_ID=skc_your_client_id
    SCALEKIT_CLIENT_SECRET=your_client_secret
    # OpenAI
    # Threat: exposed key allows unauthorized model usage billed to your account.
    OPENAI_API_KEY=sk-your-openai-key
    # User and connection — replace with values from your application
    USER_IDENTIFIER=user_123
    CONNECTION_NAME=gmail
    VariablePurpose
    SCALEKIT_ENV_URLYour Scalekit environment URL (starts with https://)
    SCALEKIT_CLIENT_IDClient ID from API Credentials (starts with skc_)
    SCALEKIT_CLIENT_SECRETClient secret from API Credentials
    OPENAI_API_KEYOpenAI API key for GPT-4o
    USER_IDENTIFIERA unique identifier for the end user in your application
    CONNECTION_NAMEThe connection name configured in your Scalekit dashboard
  3. Initialize Scalekit and ensure the user is connected

    Section titled “Initialize Scalekit and ensure the user is connected”

    Create src/index.ts. Start by initializing the Scalekit client and checking whether the user has an active Gmail connection:

    src/index.ts
    import { Agent } from '@mastra/core/agent';
    import { createTool } from '@mastra/core/tools';
    import { openai } from '@ai-sdk/openai';
    import { ScalekitClient } from '@scalekit-sdk/node';
    import { z } from 'zod';
    import 'dotenv/config';
    const IDENTIFIER = process.env.USER_IDENTIFIER || 'user_123';
    const CONNECTION = process.env.CONNECTION_NAME || 'gmail';
    const scalekit = new ScalekitClient(
    process.env.SCALEKIT_ENV_URL!,
    process.env.SCALEKIT_CLIENT_ID!,
    process.env.SCALEKIT_CLIENT_SECRET!,
    );
    const { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({
    connectionName: CONNECTION,
    identifier: IDENTIFIER,
    });
    if (connectedAccount?.status?.toString() !== '1') {
    const { link } = await scalekit.actions.getAuthorizationLink({
    connectionName: CONNECTION,
    identifier: IDENTIFIER,
    });
    console.log(`\n[${CONNECTION}] Authorization required.`);
    console.log(`Open this link:\n\n ${link}\n`);
    console.log('Press Enter once you have completed the OAuth flow...');
    await new Promise<void>((resolve) => {
    process.stdin.resume();
    process.stdin.once('data', () => { process.stdin.pause(); resolve(); });
    });
    }

    getOrCreateConnectedAccount returns an existing session if one exists or creates a pending one. If the account is not active (status 1), getAuthorizationLink returns a URL you open in a browser. Scalekit handles the full OAuth exchange — your application never sees the provider’s client secret.

  4. Once the user is connected, list the tools available for their account:

    src/index.ts (continued)
    const toolsResponse = await scalekit.tools.listTools({
    filter: { connector: CONNECTION, identifier: IDENTIFIER },
    pageSize: 50,
    });
    const scalekitTools = toolsResponse.tools;
    console.log(
    `Discovered ${scalekitTools.length} tools: ` +
    scalekitTools.map((t) => (t.definition as any)?.name).join(', ')
    );

    listTools returns tool definitions that include a name, description, and input_schema (a JSON Schema object). The filter parameter scopes results to the connector and user — the agent only sees tools the user has authorized.

  5. Mastra agents accept tools created with createTool. Each Scalekit tool needs to be wrapped:

    src/index.ts (continued)
    const mastraTools: Record<string, ReturnType<typeof createTool>> = {};
    for (const tool of scalekitTools) {
    const def = tool.definition as Record<string, any> | undefined;
    if (!def?.name) continue;
    const toolName: string = def.name;
    const description: string = def.description || toolName;
    // Use a permissive Zod schema — Scalekit validates inputs server-side.
    const inputSchema = z.object({}).passthrough();
    mastraTools[toolName] = createTool({
    id: toolName,
    description,
    inputSchema,
    execute: async ({ context }) => {
    const result = await scalekit.tools.executeTool({
    toolName,
    identifier: IDENTIFIER,
    params: context as Record<string, unknown>,
    });
    return result;
    },
    });
    }

    The inputSchema uses z.object({}).passthrough() — a permissive schema that lets the LLM pass any parameters through. Scalekit validates inputs server-side, so client-side validation is optional. If you want stricter types, convert the JSON Schema from def.input_schema into a typed Zod schema.

    The execute function calls scalekit.tools.executeTool(), which sends the tool call to Scalekit. Scalekit injects the user’s OAuth token, calls the third-party API, and returns the structured response.

  6. Create the Mastra agent with the discovered tools and run it:

    src/index.ts (continued)
    const agent = new Agent({
    name: 'gmail-assistant',
    instructions:
    'You are a helpful Gmail assistant. Use the available tools to fulfill requests. ' +
    'Always confirm what you did after completing an action.',
    model: openai('gpt-4o'),
    tools: mastraTools,
    });
    const prompt = process.argv[2] || 'Fetch my last 5 unread emails and summarize them.';
    console.log(`\nPrompt: ${prompt}\n`);
    const result = await agent.generate(prompt);
    console.log(result.text);

    Add a start script to package.json:

    package.json (scripts section)
    {
    "scripts": {
    "start": "tsx src/index.ts"
    }
    }
  7. Terminal
    pnpm start

    On the first run, if the user hasn’t authorized Gmail, you see the authorization flow:

    Terminal
    [gmail] Authorization required.
    Open this link:
    https://auth.scalekit.dev/connect/...
    Press Enter once you have completed the OAuth flow...

    After authorization (or on subsequent runs), the agent runs:

    Terminal
    Connected account for user_123 is active.
    Discovered 8 tools: gmail_fetch_mails, gmail_send_mail, gmail_search_mails, ...
    Created 8 Mastra tools.
    Prompt: Fetch my last 5 unread emails and summarize them.
    Here are your 5 most recent unread emails:
    1. "Q1 roadmap feedback needed" — Sarah Chen (1h ago)
    Requesting feedback on the product roadmap by Friday.
    2. "Deploy failed: production" — GitHub Actions (2h ago)
    CI pipeline failed on the main branch, test suite timeout.
    3. "New PR review requested" — Lin Feng (3h ago)
    Review requested on PR #412: refactor auth middleware.
    ...

    You can also pass a custom prompt:

    Terminal
    pnpm start "Search for emails from GitHub and list the subjects"
Connected account stays in PENDING

You did not complete the OAuth flow in the browser. AgentKit waits for you to authorize through the URL returned by getAuthorizationLink.

Solution: Open the printed URL in a browser, complete the Google OAuth consent, and return to the terminal. The connected account status updates to ACTIVE after a successful callback.

Tool list is empty

The connection name in code does not match the connection name in the Scalekit dashboard, or the connected account is not active.

Solution: Open AgentKit → Connections in the dashboard. Verify the connection name matches exactly (case-sensitive). Then check that the connected account for your identifier shows ACTIVE status.

executeTool fails with identifier error

The identifier you passed to executeTool does not match the identifier you used when creating the connected account.

Solution: Use the same identifier value throughout — getOrCreateConnectedAccount, listTools, and executeTool must all receive the same string.

Agent generates text instead of calling tools

The model did not receive tool definitions with enough detail to trigger a tool call. This happens when every tool has an empty description or when the inputSchema is missing entirely.

Solution: Verify that scalekitTools is not empty after discovery. Print Object.keys(mastraTools) to confirm tools were created. If tools exist but the model still does not call them, check that the tool descriptions are informative — the LLM uses descriptions to decide when a tool is relevant.

Token refresh is automatic. Scalekit stores OAuth tokens per user per connector and refreshes them before expiry. Your agent code never handles refresh tokens directly.

Scope tools per user. The identifier parameter in listTools and executeTool ensures each user only accesses their own connected accounts. Never share an identifier across users.

Add more connectors. Change CONNECTION_NAME to slack, notion, googlecalendar, or any of the 60+ supported connectors. The code is identical — only the connection name changes.

Error handling in production. Wrap executeTool calls in try/catch to handle network errors and expired connections gracefully. Return a clear message to the user when a tool call fails instead of letting the agent retry silently.

MCP alternative. If you prefer Mastra’s built-in MCP client over manual tool wrapping, see the Mastra MCP example. That approach requires a per-user MCP URL generated from the Python SDK.

TopicLink
AgentKit overviewOverview
All connectorsConnectors
Connected accountsManage connected accounts
Mastra MCP exampleMastra
Sample repositorymastra-agentkit-example
Mastra docsmastra.ai/docs