GenerateSaaS

External Agent (MCP)

Expose your app's AI capabilities to an external agent (the user's own Claude Code / Codex / OpenCode) over an authenticated MCP server - it signs in with OAuth and gets the same full capability set the in-app assistant has.

The external agent server turns your app's AI capability layer into an authenticated Model Context Protocol (MCP) server. An always-on external agent (for example Hermes or OpenClaw) connects to it, signs in once with OAuth, and can then call the same capabilities your in-app assistant uses - on that user's behalf, with no app UI open.

It is the third way to drive your app's AI capabilities, alongside the in-app chat assistant and scheduled runs. The capabilities themselves are defined once and shared: the external agent reaches them over MCP exactly as chat reaches them in-process.

The whole server is off by default. It only mounts when config.mcpServer.enabled is true. With it off, the MCP endpoint and its discovery routes do not exist and no OAuth provider is registered.

How an agent connects

The agent needs one thing: the MCP server URL, which is your backend origin plus /api/mcp:

https://your-app.com/api/mcp

Everything else is automatic. The agent discovers how to authenticate, runs an OAuth flow against your app, and from then on calls tools with the access token it receives.

Point the agent at the MCP URL. Add https://your-app.com/api/mcp as a remote MCP server in the agent's configuration.
The user signs in to authorize. On first connect the agent runs an OAuth flow. The user is sent to your app's sign-in page, approves the access, and the agent receives an access token scoped to that user. Your app is the OAuth provider - no third-party service is involved.
The agent calls your capabilities. With a valid token the agent can list the available tools and call them. Each call runs server-side, scoped to the authenticated user, and is audited like every other capability invocation.

Authentication

Access is gated by OAuth 2.1. Your app issues the access token, validates it on every request, and ties each call to a user. An agent that has not authenticated (or whose token is invalid or expired) is rejected before any tool runs.

The agent finds the OAuth endpoints through standard discovery metadata your backend serves automatically:

Discovery documentPathWhat it tells the agent
Protected-resource metadata/.well-known/oauth-protected-resourceThat the MCP endpoint requires a token, and which authorization server issues it
Authorization-server metadata/.well-known/oauth-authorization-serverWhere to run the OAuth flow (the authorize, token, and registration endpoints)

A compliant MCP agent reads these on its own - you never hand-configure client IDs or endpoints. Clients register dynamically, so there is no manual app-registration step either.

In a fullstack project the frontend forwards those two root paths to the co-hosted Hono app, while with a standalone backend the agent resolves them against the backend origin, which serves them directly - so a separate-backend project ships no frontend forwarder routes for them.

What the agent can do

The external agent acts on the user's behalf - it is the user driving their own CLI (Claude Code, Codex, OpenCode), so it gets the SAME full capability set the in-app assistant does, including managing schedules. Capabilities run automatically with no approval prompt. Each call is scoped to the authorized user and audited.

The external agent uses the chat surface - the same one the in-app assistant uses - because both are USER-DRIVEN. A capability restricted to chat (its surfaces) is therefore available to the external agent AND the in-app assistant, but withheld from unattended schedule runs (the loop-guard that stops a scheduled run from managing schedules). There is no per-action approval prompt on any surface.

Concretely, the agent can:

  • Discover the tools available to it (the capability set, with each tool's input schema).
  • Call any exposed capability with arguments, and receive its result as text.

Every call is scoped to the user who authorized the agent, runs your capability's own logic on the backend, and is recorded in the audit log. The agent only ever sees the capabilities you have defined - it has no shell, file, or network access of its own, and no secret is ever returned to it.

Defining what is exposed

You do not write anything MCP-specific. The agent is served the same capability registry the rest of your AI surface uses. Add a capability once and it is reachable from the in-app assistant, from the external agent (both user-driven, the chat surface), and from schedules (unless you restrict it to chat to keep it out of unattended runs).

To shape what runs where:

  • Keep an action out of unattended runs by restricting its capability to the chat surface - it then runs only through user-driven agents (the in-app assistant and the external CLI), never a schedule.
  • Expose a new capability by adding it to the registry - it is available to every surface immediately.

Under the hood

You do not touch any of this to use the server, but it is worth knowing what runs it:

  • Engine: Mastra's MCPServer. It takes your capability toolset (AI SDK tool() objects) as-is and does the tool-to-MCP conversion for you - tools/list schemas and tools/call argument validation, via @mastra/schema-compat. No hand-rolled schema projection.
  • Transport: the official MCP SDK's web-standard streamable HTTP, in stateless JSON-response mode. Mastra's own HTTP serving is Node-bound (node:http), so instead of its native mount the route takes Mastra's underlying MCP server and connects this web-standard transport to it. That keeps @repo/api deployable to every target (Node, serverless, and edge-style runtimes) with no node:http dependency.
  • Client pattern for the other direction. The same @mastra/mcp package gives your agent an MCP client (MCPClient) so it can consume external MCP servers. That inbound direction is documented in Agents; this page is the outbound server.

Two wire details changed with the Mastra engine, both additive and MCP-spec-compliant: the initialize response advertises logging: { enabled: true } in its capabilities, and a successful tools/call returns an explicit isError: false (the previous implementation omitted it). Any compliant MCP client handles both. Mastra's server logger defaults to the ERROR level, so it emits nothing per request under normal operation.

Enabling it

Set config.mcpServer.enabled to true. This is the only switch the server needs: it mounts the /api/mcp endpoint and its OAuth discovery routes, registers your app as the OAuth provider, and shows the MCP settings tab. config.ai.enabled gates none of it.
Apply the database changes for the OAuth provider (the access-token and client tables), then give the agent your MCP URL and let the user authorize it.

The external agent and the in-app assistant share one capability registry. There is no separate set of "agent tools" to maintain - define a capability once, and where it shows up follows from its surface restrictions. Sharing the registry is also why config.ai.enabled still matters for part of the surface: a capability that runs a model on your server - a schedule the agent creates, for example - is never run while the AI product is off. Account capabilities work either way; integration capabilities run too, but integrations are connected from the AI-gated Integrations tab, so an AI-off project has none to manage.

On this page