Announcing CIMD support for MCP Client registration
Learn more

Build a Per-User Intercom Agent with Mastra and Scalekit

Saif Ali Shaik
Founding Developer Advocate

TL;DR

  • A working Mastra agent that replies to Intercom conversations and searches contacts for individual users. Scalekit handles OAuth, token storage, and refresh. Your code resolves the identifier and calls scoped tools wrapped as Mastra tools.
  • User connects their Intercom account once (you verify ownership)
  • Call listScopedTools with the identifier to get Intercom tools, then wrap them with createTool
  • Plug into a Mastra Agent. It automatically acts with the connected user's Intercom data.

Getting a Mastra agent to call Intercom is straightforward at the start. The moment it needs to work for different people, things get messy fast.

A teammate on your support team wants replies going out from their own workspace. A customer inside your product expects the agent to only touch their conversations and data. One shared token can't handle that, and writing the auth yourself is the last thing you want to own.

This post shows how to add per-user Intercom access to a Mastra agent without any of the OAuth boilerplate. Scalekit manages the connections. Your agent just gets the right tools for whoever triggered it.

Two common agentic use cases for per-user Intercom in Mastra agents are internal team agents and customer-facing product agents. An internal agent used by multiple teammates (e.g. support or customer success) lets each person ask the agent to reply to conversations or update customer records, executed with that specific user's Intercom permissions and identity. A customer-facing agent (for example, in a SaaS support product) can manage conversations inside the end customer's own Intercom workspace, but only after the customer has connected their Intercom account through your app.

If you're adding Intercom to a multi-user Mastra agent, the schemas are rarely the hard part. The real work is making sure the agent always uses the correct user's credentials and permissions without you writing and maintaining all the OAuth plumbing.

By the end of this tutorial you will have:

  • A way for users to connect their own Intercom account.
  • A secure OAuth flow handled by Scalekit.
  • Per-user Intercom connections stored safely via identifiers.
  • A Mastra agent that calls Intercom tools scoped to the current user.
  • Complete working code for per-user Intercom tool calling in Mastra agents that you can extend to reply to conversations, search contacts, and other workflows.

Architecture overview

When you're building a Mastra agent for multiple users, the challenge is letting each person bring their own Intercom account without sharing credentials.

You control the mapping from your users to Scalekit identifiers. Scalekit handles the Intercom OAuth, token vault, and refresh. Your agent only ever uses the identifier for the current user when calling tools.

The two key flows are:

  • Connection time: Your user connects their Intercom through Scalekit (with proper verification that the real user completed the OAuth).
  • Runtime: Your agent resolves the identifier for the current user (after your own authorization check) and gets properly scoped Intercom tools via Scalekit.

Here's the architecture:

Key point: The identifier is the secure key that lets your code act as one of your users. Store it securely in your own system and always authorize before using it.

Who is this for

This is for developers building Mastra agents that multiple people will use.

It could be an internal tool your own support or success folks rely on, or a capability you're adding for customers of your product. In either case, the people using it need their own Intercom context to show up correctly.

You'd rather not also become responsible for managing Intercom OAuth and tokens. This approach keeps the focus on the agent work itself.

One shared token breaks down quickly

When you add Intercom to a Mastra agent for a single user or a shared service account, everything feels simple at first. You hard-code a token, the agent can reply to conversations, and you move on.

The moment the agent needs to act for different people, problems appear:

  • Your support team wants the agent to reply in their Intercom workspaces with their permissions.
  • A customer-facing agent needs to manage conversations inside the customer's own Intercom.
  • Different users have different workspaces, teams, and access levels.

If you try to solve this yourself you quickly own a small auth product: OAuth flows, token storage per user, refresh logic, and mapping your internal IDs to Intercom identities. This is exactly the kind of hidden cost of building OAuth internally that slows teams down.

Let Scalekit handle the identity

Scalekit AgentKit lets you map your users (or customers) to a stable identifier. It handles the Intercom OAuth, token vault, and refresh for you.

Your Mastra agent receives clean, already-scoped tools for that specific person. When the agent decides to call a tool, you execute it with the identifier. No OAuth code lives in the agent logic itself.

The pattern is the same whether the agent is an internal team tool or embedded in a product for external customers. For more on how tool calling authentication works for AI agents, see our deep dive.

Prerequisites

  • Node.js 18+
  • A Scalekit account with an Intercom connection created in the dashboard (connection name must match exactly, e.g. intercom).
  • @mastra/core, @scalekit-sdk/node, zod
  • An LLM key (OpenAI in the examples below).
  • Basic familiarity with Mastra agents and tools.

Workflows that feel useful once the right account is connected

Once the agent is acting as the actual person, a few things start to click:

Reply to the latest conversation from someone specific. Search for a contact by email and see what they've been talking about lately. Drop a note on a contact after the agent has context.

All of it happens with that user's own permissions and view of the data. No one else's conversations leaking in.

Step 1: Install dependencies

npm install @scalekit-sdk/node @mastra/core zod dotenv

Connect the user (the secure way)

const scalekit = new ScalekitClient(envUrl, clientId, clientSecret); const { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName: 'intercom', identifier, }); if (connectedAccount.status !== 'ACTIVE') { // getAuthorizationLink + userVerifyUrl + later verifyConnectedAccountUser }

Turn scoped tools into something Mastra can use

const { tools: scopedTools } = await scalekit.tools.listScopedTools(identifier, { filter: { connectionNames: ['intercom'] }, }); const mastraTools = scopedTools.map((scoped: any) => { const tool = scoped.tool || scoped; const def = tool.definition || tool; return createTool({ id: def.name, description: def.description, inputSchema: z.object({}).passthrough(), execute: async ({ context }) => { return scalekit.actions.executeTool({ toolName: def.name, identifier, toolInput: context, }); }, }); });

Wire it into the agent and run it

const agent = new Agent({ name: 'Intercom Support Agent', instructions: 'You are a helpful support agent...', model: openai('gpt-4o'), tools: mastraTools, }); const result = await agent.generate(prompt);

See the full runnable example below.

Complete Code

import { Agent, createTool } from '@mastra/core'; import { openai } from '@ai-sdk/openai'; import { ScalekitClient } from '@scalekit-sdk/node'; import { z } from 'zod'; import 'dotenv/config'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENV_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET! ); async function ensureConnected(identifier: string) { const { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName: 'intercom', identifier, }); if (connectedAccount.status !== 'ACTIVE') { const { authorizationUrl } = await scalekit.actions.getAuthorizationLink({ connectionName: 'intercom', identifier, userVerifyUrl: `https://your-app.com/callback?identifier=${encodeURIComponent(identifier)}`, }); throw new Error(`Authorize at: ${authorizationUrl}`); } } async function getMastraIntercomTools(identifier: string) { const { tools: scopedTools } = await scalekit.tools.listScopedTools(identifier, { filter: { connectionNames: ['intercom'] }, }); return scopedTools.map((scoped: any) => { const t = scoped.tool || scoped; const def = t.definition || t; return createTool({ id: def.name, description: def.description, inputSchema: z.object({}).passthrough(), execute: async ({ context }) => scalekit.actions.executeTool({ toolName: def.name, identifier, toolInput: context, }), }); }); } export async function runIntercomMastraAgent(identifier: string, prompt: string) { await ensureConnected(identifier); const tools = await getMastraIntercomTools(identifier); const agent = new Agent({ name: 'intercom-support', instructions: 'Help the user with their Intercom conversations using the tools.', model: openai('gpt-4o'), tools, }); const result = await agent.generate(prompt); return result.text; } // Example: runIntercomMastraAgent('user-456', 'Reply to the conversation with john@acme.com')

Run it:

SCALEKIT_ENV_URL=... SCALEKIT_CLIENT_ID=... SCALEKIT_CLIENT_SECRET=... \ OPENAI_API_KEY=... tsx 16-mastra-intercom.ts user-456

Supported Intercom Tools

Typical tools for a connected account:

  • intercom_conversations_reply
  • intercom_contacts_search
  • intercom_notes_create
  • intercom_conversations_search

Full list lives in the Scalekit Intercom connector docs.

The identifier does the heavy lifting

Change the identifier and the agent is suddenly working inside someone else's Intercom. Mastra and the model stay far away from the actual tokens.

Your code resolves the identifier after it has done its own authentication check on the request. Never accept it from the client. This aligns with the broader principle of secure token management for AI agents at scale.

The Claude version of this same Intercom integration uses the identical approach. Different wrapper, same core rule.

Troubleshooting

Connection never ACTIVE?

Missing or incorrect verifyConnectedAccountUser call after the OAuth redirect.

Permission errors from tools?

Insufficient scopes on the connection or token revoked. Re-authorize the same identifier. For a deeper look at handling this, see how to handle token refresh for AI agents.

No tools or wrong user data?

Using an identifier with no ACTIVE connection, or (dangerously) accepting the identifier from the client instead of your server-side auth.

Tradeoffs & Limitations

Approach
Pros
Cons
When to use
Shared bot / service account
Simple
No per-user permissions, audit, or revocation
Prototypes only
You roll your own OAuth + vault
Full control
You now own a security product
Rarely worth it
Scalekit Connected Accounts
Fast, secure, automatic refresh, one integration for many providers
Another dependency
Production multi-user agents

What's Next

Get more like this in your inbox

Subscribe to the Scalekit developer newsletter for weekly implementation guides, new connectors, and agent auth patterns.

For production multi-turn handling and proper callback server, see the Mastra + Scalekit cookbook examples.

No items found.
Agent Auth Quickstart
On this page
Share this article
Agent Auth Quickstart

Acquire enterprise customers with zero upfront cost

Every feature unlocked. No hidden fees.
Start Free
$0
/ month
1 million Monthly Active Users
100 Monthly Active Organizations
1 SSO connection
1 SCIM connection
10K Connected Accounts
Unlimited Dev & Prod environments