Announcing CIMD support for MCP Client registration
Learn more

Manage Linear Issues and Projects with a Mastra Agent

Saif Ali Shaik
Founding Developer Advocate

TL;DR

A complete TypeScript script wiring Scalekit's Linear tools into a Mastra agent — create issues, update statuses, pull project data — as individual users, no OAuth code required.

  • User connects Linear once via OAuth (Scalekit vaults the token)
  • listTools returns Linear tool definitions; a createTool loop wraps each for Mastra using z.object({}).passthrough() as the input schema
  • agent.generate(prompt) drives the loop; tool calls hit scalekit.tools.executeTool with the user identifier; Scalekit makes the Linear API call as that specific user

You have a Mastra agent. Now you want it to create Linear issues, update statuses, and pull project data on behalf of your users. This post shows a complete TypeScript script that wires Scalekit's authenticated Linear tools into a Mastra agent, with no OAuth implementation, no token management, no hand-written schemas.

Prerequisites

  • Node.js 18+, TypeScript
  • npm install @mastra/core @ai-sdk/openai @scalekit-sdk/node zod dotenv
  • A Scalekit account with a Linear connection configured (Linear connector setup)
  • A Linear 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 Linear 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 Linear 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 = 'linear' // --- 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 Linear 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> = {} for (const tool of toolsResponse.tools) { const def = tool.definition as Record | 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, }) }, }) } console.log(`Created ${Object.keys(mastraTools).length} Mastra tools.`) // --- Step 4: Build and run the agent ----------------------------------------- const agent = new Agent({ name: 'linear-agent', instructions: 'You are a helpful Linear assistant. Use the available tools to manage issues and projects. Confirm what you did after each action.', model: openai('gpt-4o'), tools: mastraTools, }) const prompt = process.argv[2] || 'Get my user profile and list the open issues assigned to me' 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 "Create a high-priority bug: API returns 500 on /users when email has a plus sign"

Expected output:

Connected account for user_123 is active. Discovered 10 tools Created 10 Mastra tools. Prompt: Get my user profile and list the open issues assigned to me I found your profile: you're logged in as Saif, an Admin on the "Scalekit Agent Kit" team. You have 3 open issues assigned to you: ...

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

How It Works

Step 1: Connect your user

getOrCreateConnectedAccount checks whether this identifier already has an active Linear 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 Linear 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 Linear API schema. If you want strict client-side validation, convert def.input_schema to a Zod schema manually.

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 Linear API request using the stored token for that user. Your code never touches the token.

Available Linear Tools

Tool
Description
linear_user_get
Get details for the authenticated user or another user by ID
linear_team_get
Get a team by ID, including members and key settings
linear_issue_create
Create a new issue with title, team, description, priority, assignee, labels
linear_issue_update
Update status, priority, assignee, title, or description on an existing issue
linear_comment_create
Add a rich-text comment to an issue (supports @mentions and formatting)
linear_attachment_create
Attach a URL, file link, or external resource to an issue
linear_issue_relation_create
Link two issues with a relationship type (blocks, blocked by, duplicate, related)

Note: linear_issue_search is currently deprecated by Linear. Full list in the Linear 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: 'linear-agent', instructions: 'You are a helpful Linear assistant.', model: openai('gpt-4o'), tools, }) const result = await agent.generate('List the open issues assigned to me') console.log(result.text) await mcp.disconnect()

Mastra fetches the tool schemas directly from the Scalekit MCP server, so you skip the discovery and wrapping steps entirely. 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 Linear through the OAuth flow, Scalekit stores their token under that identifier. Every tool call runs with that specific user's permissions.

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 Add Linear Tools to a Claude Agent.

Explore More

Other connectors: HubSpot, Gmail, Slack, 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