GenerateSaaS

External Agent (MCP)

Expose your app's AI capabilities to an external agent (the user's own Claude Code or Codex) over an MCP server authenticated by OAuth or an API key, with the same capability set the in-app assistant has.

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

The server is off by default: it mounts only 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.

Connect an agent

The agent needs one thing - your backend origin plus /api/mcp:

https://your-app.com/api/mcp
Point the agent at the MCP URL. Add it 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 approves on your app's sign-in page and the agent receives an access token scoped to them. Your app is the OAuth provider - no third party is involved.
The agent calls your capabilities. Each call runs server-side, scoped to the authenticated user, and is audited like every other capability invocation.

Authentication

Every request carries one of two credentials, and both tie each call to exactly one user. Anything else is rejected with the same 401 before any tool runs.

CredentialHeaderFor
OAuth 2.1 access tokenAuthorization: Bearer <token>Interactive agents: the user signs in once in the browser
API key with MCP accessx-api-key: <key>Headless callers (CI, a server) that cannot open a browser. Needs config.apiKeys.enabled and a key created with Allow MCP access - see API keys
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 (authorize, token, and registration endpoints)

The consent screen ships with the project at /auth/consent: the first time a client asks for a scope the user has not granted, the provider sends them there to approve or refuse it by name.

A compliant agent reads these itself and registers dynamically, so you hand-configure no client IDs, endpoints, or app registrations. A fullstack project forwards those two root paths to the co-hosted Hono app; with a standalone backend the agent resolves them against the backend origin directly, so no forwarder routes ship.

What the agent can do

The agent acts on the user's behalf, so it gets the same full capability set the in-app assistant does, automations included.

  • Discover and call. It lists the tools available to it with each input schema, calls any of them, and receives the result as text. It has no shell, file, or network access of its own, and no secret is returned to it.
  • Hints, so reads run unprompted. Each capability declares MCP tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) through its annotations field, and they appear in tools/list. A client treats a tool that declares none as destructive and asks before every call, so declare them truthfully on every capability you add.
  • Nothing MCP-specific to write. The agent is served the same capability registry the rest of your AI surface uses, so adding a capability once reaches the in-app assistant, the external agent, and automations at the same time.
  • Web research too. web_search and web_extract appear whenever a vendor key is set and config.ai.webSearch is not false. There is no per-request toggle here, so those two are the whole gate.
  • Your skills, both ways. Every skill in packages/ai/skills reaches the agent as the skills_list and skill_get tools, and each is also offered as an MCP prompt a person can pick by name in their client. Both are served on every build, so the surface does not change shape when you add your first skill.

Those calls cost money, billed to the credential's user. Each search or page read is a credits line item under the ai.mcp feature tag, priced through your pricingConfig.credits markup like every other metered call. An empty balance refuses the call before the vendor is reached, and a project with credits disabled meters nothing. An identical call repeated within 15 minutes bills once, so an agent that retries a call it timed out on cannot charge the user twice. The endpoint's AI rate limiter bounds how fast an agent can drive any of this.

The external agent runs at the chat surface, like the in-app assistant, because both are USER-DRIVEN. Restricting a capability to chat (via surfaces) therefore keeps it available to both while withholding it from unattended automation runs - the loop-guard that stops an automated run from managing automations.

Under the hood

  • Engine: Mastra's MCPServer. It takes your capability toolset (AI SDK tool() objects) as-is and does the tool-to-MCP conversion - tools/list schemas and tools/call argument validation - via @mastra/schema-compat.
  • Transport: the MCP SDK's web-standard streamable HTTP, stateless JSON-response mode. Mastra's own HTTP serving is Node-bound, so the route connects this transport to Mastra's underlying MCP server instead, keeping @repo/api deployable to Node, serverless, and edge-style runtimes alike.
  • The other direction - your agent consuming external MCP servers - uses MCPClient from the same @mastra/mcp package; see Agents.

With PostHog enabled, every tool call an agent makes here is reported to PostHog's MCP Analytics - see Analytics.

Enabling it

Set config.mcpServer.enabled to true. That mounts /api/mcp and its OAuth discovery routes, registers your app as the OAuth provider, and shows the MCP server entry in the settings rail. 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.

config.ai.enabled still matters for part of the surface, because the registry is shared: a capability that runs a model on your server - an automation the agent creates, say - never runs 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.

On this page