Announcing CIMD support for MCP Client registration
Learn more

Add Salesforce Access to a Mastra Agent in Minutes

Saif Ali Shaik
Founding Developer Advocate

TL;DR

A complete TypeScript script wiring Scalekit's Salesforce tools into a Mastra agent — SOQL queries, record updates, activity logging — as individual users, no OAuth code required.

  • User connects Salesforce once via OAuth (Scalekit vaults the token)
  • listTools returns Salesforce tool definitions including SOQL execute; a createTool loop wraps each for Mastra; MCP alternative is especially clean for SOQL
  • agent.generate(prompt) drives the loop; Claude writes SOQL from natural language; tool calls hit scalekit.tools.executeTool with the user identifier

Your Mastra agent can query Salesforce records, update opportunities, and log activities, without writing a single OAuth flow or touching a Salesforce token. This post shows the complete TypeScript script that wires Scalekit's authenticated Salesforce tools into a Mastra agent.

Prerequisites

  • Node.js 18+, TypeScript
  • npm install @mastra/core @ai-sdk/openai @scalekit-sdk/node zod dotenv
  • A Scalekit account with a Salesforce connection configured (Salesforce connector setup)
  • A Salesforce account for testing
  • An OpenAI API key (or swap the model provider. Mastra supports any AI SDK-compatible model)

How It Fits Together

Your code calls Scalekit; Scalekit handles Salesforce OAuth, token storage, and refresh on behalf of each user.

The Complete Script

The full TypeScript. Copy it, set your environment variables, and run it.

/** * Mastra agent with Scalekit-authenticated Salesforce tools. */ 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' // --- Configuration ----------------------------------------------------------- const IDENTIFIER = process.env.USER_IDENTIFIER || 'user_123' const CONNECTION = 'salesforce' // --- Initialize Scalekit ----------------------------------------------------- // Never hard-code credentials — they would be exposed in source control. // Pull them from environment variables at runtime. const scalekit = new ScalekitClient( process.env.SCALEKIT_ENV_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ) // --- Step 1: Ensure the user has a connected account ------------------------- const { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName: CONNECTION, identifier: IDENTIFIER, }) if (connectedAccount?.status?.toString() !== '1') { // Status 1 = ACTIVE in the protobuf enum const { link } = await scalekit.actions.getAuthorizationLink({ connectionName: CONNECTION, identifier: IDENTIFIER, }) console.log(`\nAuthorization required. Open:\n\n ${link}\n`) console.log('Re-run after completing the OAuth flow.') process.exit(0) } console.log(`Connected account for ${IDENTIFIER} is active.`) // --- Step 2: Discover Salesforce tools from Scalekit ------------------------- const toolsResponse = await scalekit.tools.listTools({ filter: { connector: CONNECTION, identifier: IDENTIFIER }, pageSize: 50, }) console.log(`Discovered ${toolsResponse.tools.length} tools`) // --- Step 3: Wrap as Mastra tools -------------------------------------------- const mastraTools: Record<string, ReturnType<typeof createTool>> = {} for (const tool of toolsResponse.tools) { const def = tool.definition as Record<string, any> | undefined if (!def?.name) continue const toolName: string = def.name // Use a permissive Zod schema — Scalekit validates inputs server-side. // For stricter client-side validation, convert def.input_schema to Zod. mastraTools[toolName] = createTool({ id: toolName, description: def.description || toolName, inputSchema: z.object({}).passthrough(), execute: async ({ context }) => { return scalekit.tools.executeTool({ toolName, identifier: IDENTIFIER, params: context as Record<string, unknown>, }) }, }) } console.log(`Created ${Object.keys(mastraTools).length} Mastra tools.`) // --- Step 4: Build and run the agent ----------------------------------------- const agent = new Agent({ name: 'salesforce-agent', instructions: 'You are a helpful Salesforce assistant. You can query records using natural language, update opportunities, and log activities. When asked to find records, write and execute the appropriate SOQL query.', model: openai('gpt-4o'), tools: mastraTools, }) const prompt = process.argv[2] || 'Find all open opportunities closing this quarter with value over $50,000' console.log(`\nPrompt: ${prompt}\n`) const result = await agent.generate(prompt) console.log(result.text)

Run it

Create a .env file:

SCALEKIT_ENV_URL=https://your-env.scalekit.cloud SCALEKIT_CLIENT_ID=skc_... SCALEKIT_CLIENT_SECRET=sks_... USER_IDENTIFIER=user_123 OPENAI_API_KEY=sk-...

Then run:

npx tsx agent.ts

Or pass a custom prompt as a CLI argument:

npx tsx agent.ts "Move the Acme Corp opportunity to the Negotiation stage"

Expected output:

Connected account for user_123 is active. Discovered 8 tools Created 8 Mastra tools. Prompt: Find all open opportunities closing this quarter with value over $50,000 Found 5 open opportunities closing this quarter with value over $50,000: Acme Corp ($120,000, closes Sep 28), ...

If this is the first run for the identifier, you will see an authorization link instead. Open it, complete Salesforce OAuth, then re-run the script.

How It Works

Step 1: Connect your user

getOrCreateConnectedAccount checks whether this identifier already has an active Salesforce connection. Status '1' maps to ACTIVE in the protobuf enum. If the connection is not active, getAuthorizationLink generates a URL the user opens to complete the OAuth flow.

In production, IDENTIFIER comes from your authenticated session: after your app verifies who is making the request via session cookie, JWT, or database lookup. Never accept it from client input.

Step 2: Discover tools

listTools returns Scalekit's tool definitions for the Salesforce connector: names, descriptions, and input schemas. The connector value in the filter must exactly match the Connection name you created in the Scalekit dashboard. It is case-sensitive. An empty result with no error is the most common first-run problem and almost always means a name mismatch.

Step 3: Wrap as Mastra tools

The loop converts each Scalekit tool definition into a Mastra createTool call. Unlike the Anthropic SDK path, Mastra requires explicit tool registration via createTool rather than passing raw schema arrays. z.object({}).passthrough() is the input schema, permissive on purpose. Scalekit validates inputs server-side against the actual Salesforce API schema.

Step 4: Run the agent

agent.generate(prompt) runs the Mastra agent loop. When the model decides to call a tool, it passes the tool name and inputs to the execute function, which calls scalekit.tools.executeTool. Scalekit makes the Salesforce API request using the stored token for that user.

The instructions tell the agent it can write SOQL from natural language, so a prompt like "find all open opps closing this quarter" becomes a SELECT ... FROM Opportunity WHERE ... query executed against the user's Salesforce org. Your code never writes SOQL directly.

Available Salesforce Tools

Category
Capability
Notes
Records
Retrieve accounts, contacts, leads, opportunities, and cases by ID or search
Covers the core CRM objects
SOQL Queries
Execute arbitrary SOQL for custom data retrieval
Agent writes SOQL from natural language
Activities
Create tasks and events linked to any CRM record
Cross-object Search
Find records by name, email, phone, or any field value
Metadata
Inspect and modify Salesforce org metadata via SOAP proxy

Full list in the Salesforce connector docs.

Bonus: The MCP Approach

If you prefer a shorter setup, @mastra/mcp lets Mastra auto-discover tools from the Scalekit MCP server. No createTool loop needed.

Install: npm install @mastra/mcp

import { MCPClient } from '@mastra/mcp' import { Agent } from '@mastra/core/agent' import { openai } from '@ai-sdk/openai' import 'dotenv/config' // Generate this URL per user from your backend using the Scalekit SDK. // Never expose this URL on the client side. const mcpUrl = process.env.SCALEKIT_MCP_URL! const mcp = new MCPClient({ servers: { scalekit: { url: new URL(mcpUrl) }, }, }) const tools = await mcp.getTools() const agent = new Agent({ name: 'salesforce-agent', instructions: 'You are a helpful Salesforce assistant. When asked to find records, write and execute the appropriate SOQL query.', model: openai('gpt-4o'), tools, }) const result = await agent.generate('Find all open opportunities closing this quarter with value over $50,000') console.log(result.text) await mcp.disconnect()

For Salesforce, the MCP path is especially clean: SOQL queries come through as a single natural-language tool call, and the schema is handled automatically. To generate the per-user MCP URL from your backend, see the Scalekit MCP docs.

Why This Already Handles Multiple Users

The USER_IDENTIFIER env var (the IDENTIFIER constant in the script) is what makes this scale from one user to many. In production, swap it with the identifier from your authenticated session. Each user in your system gets their own identifier. When they connect Salesforce through the OAuth flow, Scalekit stores their token under that identifier. Every tool call runs with that specific user's permissions against their own Salesforce org.

The same agent code serves every user without modification. Scalekit stores and refreshes OAuth tokens separately per identifier. Your code never sees a token directly.

For a full walkthrough of the multi-user setup, see The Right Way to GitHub OAuth in LangChain Agents Built for Multiple Users. Prefer Python? See Query Salesforce with Natural Language Using a Claude Agent.

Explore More

Other connectors: HubSpot, Gmail, Slack, Linear, GitHub, Notion, and more: full connector list

Other frameworks: LangChain, CrewAI, Vercel AI SDK, Google ADK: framework 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