GenerateSaaS

Agents

Author, rename, and extend the assistant - a Mastra agent. Register in code, add AI SDK tools, give it MCP clients and inline skills, and re-voice its persona. Our seams only; Mastra's own docs cover the rest.

The assistant is a Mastra agent (packages/api/src/mastra/agents/assistant/config.ts). Chat and scheduled runs both call it, so anything you change here changes both. This page covers only the seams specific to this boilerplate; for the agent API itself - streaming, structured output, memory, workflows - follow Mastra's own docs, which are the source of truth for shapes and options.

Author with Mastra's conventions, not a wrapper of ours. If Mastra can do it, do it the Mastra way - we deliberately add no abstraction on top of the agent, its tools, memory, or MCP clients.

Register the agent in code

The agent is constructed and registered in one place - getMastra() (packages/api/src/mastra/index.ts) - via Mastra's agents map:

new Mastra({
  storage,
  server: { auth: serverAuth },
  agents: { assistant: createAssistantAgent(assistantMemory) }
});

Add or rename an agent by editing that map and its folder under packages/api/src/mastra/agents/<name>/.

Register in code - do not rely on file-based discovery. Mastra can auto-discover agents from an agents/ folder, but that path is beta and, by Mastra's own docs, runs only under the Mastra CLI: "When you consume Mastra as a library, register those primitives in code instead." This backend imports its Mastra instance directly (it is a library here, not a CLI-run project), so discovery never applies - the agents map is the registration. The agents/<name>/config.ts folder shape mirrors Mastra's own convention, so it stays recognizable if you later adopt the CLI.

The persona lives in the agent's inline instructions string, not a sibling instructions.md. That is deliberate: the production build rewrites module paths and does not copy adjacent markdown, so a readFileSync loader throws at runtime. Edit the assistantInstructions string to re-voice the agent. The capability nudge and the other injected prompt strings are a separate seam - see AI prompts.

Give it tools

The agent's tools are AI SDK tool() objects. @repo/ai re-exports tool:

import { tool } from "@repo/ai";
import { z } from "zod";

const getForecast = tool({
  description: "Look up the weather forecast for a city.",
  inputSchema: z.object({ city: z.string() }),
  execute: async ({ city }) => fetchForecast(city)
});

Attach it to the agent (new Agent({ ..., tools: { getForecast } })) or hand it in per run as a named toolset - the pattern chat and schedules use for the capability set. You rarely start from scratch: the capabilities that operate the app and the integrations users connect are already turned into tools per run. See Mastra's tools docs for dynamic tools, tool approval, and toolsets.

Give it MCP clients

To let the agent call external MCP servers (a user's connected service, an internal tool server), use Mastra's MCPClient from @mastra/mcp and pass its toolset into a run:

import { MCPClient } from "@mastra/mcp";

const mcp = new MCPClient({
  servers: { docs: { url: new URL("https://example.com/mcp") } }
});
const toolsets = await mcp.listToolsets();
// pass `toolsets` into agent.stream(..., { toolsets }) / agent.generate(..., { toolsets })

@mastra/mcp already ships in the API package (it also powers the optional outward MCP server). If a lean build has dropped it, add it back with pnpm --filter @repo/api add @mastra/mcp before using MCPClient. This is the inbound direction (your agent as an MCP client); the outward server (your app as an MCP server to someone else's agent) is a separate, config-gated option. Follow Mastra's MCP docs for auth, lifecycle, and server config.

Give it skills

Mastra skills attach reusable instruction bundles that the agent loads on demand through three tools it gains automatically: skill, skill_search, and skill_read. Define them inline with createSkill and pass them at construction:

import { Agent } from "@mastra/core/agent";
import { createSkill } from "@mastra/core/skills";

const refundPolicy = createSkill({
  name: "refund-policy",
  description: "Use when a user asks about refunds or cancellations.",
  instructions: "Refunds are prorated within 30 days. Escalate anything older.",
  references: { "policy.md": "# Refund policy\n..." } // served in-memory, no filesystem
});

new Agent({ id: "assistant", model: "openai/gpt-5.5", instructions: "...", skills: [refundPolicy] });

Use inline createSkill, not filesystem-backed skills. A skill referenced by path (skills: ["./skills/refund-policy"]) reads its SKILL.md from disk at runtime, but the production build bundles your code and does not trace a file referenced only by a runtime path string - the SKILL.md and its references never reach the deployed bundle, and skill_read fails there. Verified in a generated production build: an inline skill's skill/skill_read/skill_search tools and its instructions are present in the built server bundle, while a filesystem-backed skill's SKILL.md is not copied in. Inline skills have no filesystem dependency, so they always survive. (This snippet is a recipe - the shipped agent defines no skills.)

See Mastra's skills docs for the full skill format and dynamic (per-request) skill resolution.

Reaching the agent from the Mastra CLI

Mastra's CLI can talk to a running Mastra server (mastra api --url <origin>), but this backend's server.auth is a cookie-session guard - the same Better Auth session the app uses. The CLI's bearer-token style of access is not wired, so mastra api --url will not authenticate out of the box. If you want CLI access to your deployed agent, add your own token mechanism to the server auth. We keep the honest default rather than shipping a half-authenticated path.

On this page