Skip to main content

MCP

Mastra supports the Model Context Protocol (MCP), an open standard for connecting AI agents to external tools and resources.

Use MCPClient to connect to MCP servers. Use MCPServer to expose Mastra agents, tools, workflows, prompts, and resources to other MCP-compatible systems. Both speak the MCP 2026-07-28 revision over stdio and Streamable HTTP.

For hosted MCP servers such as Neon and Canva, Connect on Mastra platform can manage the OAuth flow and credentials for you, so agents get the server's tools through tools() without a hand-configured MCPClient.

note

Upgrading from @mastra/mcp 1.x? See the migration guide.

For credential choices and permission boundaries, see Authentication and authorization with MCP.

Connect to MCP servers
Direct link to Connect to MCP servers

Install the MCP package:

npm install @mastra/mcp@latest

Configure each server with a local command or remote URL:

src/mastra/mcp/client.ts
import { MCPClient } from '@mastra/mcp'

export const mcpClient = new MCPClient({
id: 'my-mcp-client',
servers: {
wikipedia: {
command: 'npx',
args: ['-y', 'wikipedia-mcp'],
},
weather: {
url: new URL('https://weather.example.com/mcp'),
requestInit: {
headers: {
Authorization: `Bearer ${process.env.WEATHER_API_KEY}`,
},
},
},
},
})
Authentication

For OAuth-protected servers, use authenticate() to complete the browser-based authorization flow. Visit OAuth authentication for configuration details.

Pass tools from the configured servers to an agent:

src/mastra/agents/assistant.ts
import { Agent } from '@mastra/core/agent'
import { mcpClient } from '../mcp/client'

export const assistant = new Agent({
id: 'assistant',
name: 'Assistant',
instructions: `
Use the available MCP tools to answer questions.
Include the source of any information you retrieve.
`,
model: 'openai/gpt-6.1-sol',
tools: await mcpClient.listTools(),
})

Generate types for direct tool calls
Direct link to Generate types for direct tool calls

For stable server configurations, generate types from published tool schemas instead of redeclaring tool arguments and results. This optional workflow leaves dynamic discovery unchanged.

Export a client with an output path and pass the generated MCPServers interface to its constructor:

src/mcp/client.ts
import { MCPClient } from '@mastra/mcp'
import type { MCPServers } from './mcp-types.generated'

export const mcp = new MCPClient<MCPServers>({
id: 'weather-client',
servers: { weather: { url: new URL(process.env.WEATHER_MCP_URL!) } },
typegen: { outFile: 'src/mcp/mcp-types.generated.ts' },
})

Set WEATHER_MCP_URL and configure any required authentication before running the command from your project root:

npx @mastra/mcp generate ./src/mcp/client.ts

The generated file can be absent on the first run because its import is type-only. Generation loads TypeScript modules without typechecking and lists schemas without executing tools. Run your project's typecheck afterward.

Call a discovered tool using its generated arguments. The following assumes the server publishes get_weather with a string location and numeric temperature:

src/mcp/call-weather.ts
import { mcp } from './client'

try {
const tools = await mcp.listTools()
const weather = tools.weather_get_weather
if (weather?.execute) {
const result = await weather.execute({ location: 'Paris' })
if (result && 'temperature' in result && typeof result.temperature === 'number') {
console.log(result.temperature)
}
}
} finally {
await mcp.disconnect()
}

Tools can be absent, and execution can return a protocol envelope, validation error, or undefined alongside structured output. Narrow the result before using it. Missing output schemas produce unknown.

Pass multiple explicit client files to generate several outputs. Export clients directly as named or default exports, use different output paths, and give independent clients distinct IDs when their server configurations match. Input and output paths resolve from the command's working directory. Repeated exports of the same client are processed once.

Discovery failures leave existing files unchanged, as does a schema problem generation can't work around, such as a reference cycle, excessive depth, or an unresolved local reference. File replacement is atomic per file, not across the whole set. Regenerate when server schemas change, using equivalent permissions. Before generation, provide credentials and load any environment files yourself. Importing a client executes its module and transitive imports, so choose files without application startup side effects. Interactive authentication isn't part of the command.

See type generation reference for export selection, instance caching, schema limitations, and failure handling.

Static and runtime tools
Direct link to Static and runtime tools

Choose how to load tools based on whether the server configuration changes between requests:

Static toolsRuntime toolsets
Methodawait mcpClient.listTools()await mcpClient.listToolsets()
Use caseShared, fixed configurationPer-user or per-request configuration
CredentialsShared by all requestsCan vary between requests
Agent APItools in the Agent constructortoolsets in generate() or stream()

The preceding agent example uses static tools. For runtime credentials, create a client for the request and pass its toolsets when calling the agent:

src/handle-request.ts
import { MCPClient } from '@mastra/mcp'
import { mastra } from './mastra'

export async function handleRequest(prompt: string, apiKey: string) {
const userMcpClient = new MCPClient({
servers: {
weather: {
url: new URL('https://weather.example.com/mcp'),
requestInit: {
headers: { Authorization: `Bearer ${apiKey}` },
},
},
},
})

const agent = mastra.getAgent('assistant')
const response = await agent.generate(prompt, {
toolsets: await userMcpClient.listToolsets(),
})

await userMcpClient.disconnect()
return response.text
}

Visit listTools() and listToolsets() for their full APIs.

Tool approval
Direct link to Tool approval

Set requireToolApproval on a server to require approval for all its tools:

const mcpClient = new MCPClient({
servers: {
github: {
url: new URL('https://github.example.com/mcp'),
requireToolApproval: true,
},
},
})

You can also provide a function that decides based on the tool name, arguments, or annotations:

requireToolApproval: ({ toolName }) => toolName.startsWith('delete_')

Treat tool annotations from servers you don't control as untrusted hints. Visit tool approval for the callback context and security guidance.

Security
Direct link to Security

MCP servers run code and return content on your agent's behalf, so configure them with the same care as any other external dependency:

  • Stdio subprocess environment: subprocesses inherit only the MCP SDK's curated environment whitelist (for example PATH and HOME on POSIX), not the full parent environment. Set inheritDefaultEnv: false on a server to pass only the variables listed in env.
  • Outbound host restriction: set allowedHosts when HTTP server URLs come from untrusted configuration, to restrict the hosts the client can contact. The default fetch path blocks redirect hops before sending them; a custom fetch is validated against its final response URL only after the request, so it must enforce its own redirect policy when outbound contact must be prevented.
  • Tool response trust: tool results are untrusted model input. Use input and output processors to sanitize content before it reaches the model, and requireToolApproval for sensitive tools.

Visit the MCPClient security reference for enforcement details of each option.

MCP registries
Direct link to MCP registries

Registries provide hosted or packaged MCP servers. The client configuration above works with registry endpoints and commands.

RegistryConnectionNotes
Klavis AIHosted HTTPEnterprise authentication and managed servers
mcp.runSigned hosted URLTreat the profile URL as a secret
ComposioHosted URLURLs are often tied to one user account
SmitheryCLI or hosted URLRun local packages through npx
ApifyHosted HTTPAuthenticate with an Apify API token
AmpersandHosted URL or stdioConnect to configured SaaS integrations

Store signed URLs, API keys, and tokens in environment variables. Follow the registry's documentation to obtain the endpoint, command, and credentials for each server.

Expose a Mastra MCP server
Direct link to Expose a Mastra MCP server

Create an MCPServer to expose Mastra primitives to external MCP clients:

src/mastra/mcp/server.ts
import { MCPServer } from '@mastra/mcp'
import { assistant } from '../agents/assistant'
import { weatherTool } from '../tools/weather'
import { weatherWorkflow } from '../workflows/weather'

export const mcpServer = new MCPServer({
id: 'my-mcp-server',
name: 'My MCP Server',
version: '1.0.0',
agents: { assistant },
tools: { weatherTool },
workflows: { weatherWorkflow },
})

Register the server on the main Mastra instance:

src/mastra/index.ts
import { Mastra } from '@mastra/core/mastra'
import { mcpServer } from './mcp/server'

export const mastra = new Mastra({
mcpServers: { mcpServer },
})
Authentication

Protect HTTP MCP servers with OAuth middleware. Visit OAuth protection for setup instructions.

Registered servers are served at /api/mcp/:serverId/mcp over Streamable HTTP. Because every request is self-contained, the same server runs unchanged on long-lived hosts and serverless platforms.

A tool that needs something from the user before it can finish calls context.suspend() with a payload and declares a resumeSchema. The server turns that into an input_required round and runs the tool again with the answer in context.resumeData. Clients answer those rounds with the inputRequests handler on their server definition. Visit Asking the caller for input and Answering input requests.

Visit the MCPServer reference for prompts, resources, transports, and other server options.

Publish a stdio server package
Direct link to Publish a stdio server package

Package a server that uses the standard input/output (stdio) transport when you want clients to run it locally through a command such as npx. Create a separate executable entry point that starts the server:

src/mastra/mcp/stdio.ts
#!/usr/bin/env node
import { mcpServer } from './server'

mcpServer.startStdio().catch(error => {
console.error('Failed to start MCP server:', error)
process.exit(1)
})

Build the entry point as an executable Node.js file, then point the package's bin field to that output. Include the built file in the published package and keep the shebang as its first line:

package.json
{
"bin": {
"my-mcp-server": "dist/stdio.js"
},
"files": ["dist"]
}

After publishing the package, configure clients to run it by package name:

src/mastra/mcp/client.ts
import { MCPClient } from '@mastra/mcp'

const mcpClient = new MCPClient({
servers: {
myServer: {
command: 'npx',
args: ['-y', '@your-org/my-mcp-server@1.0.0'],
},
},
})

Visit the npm package publishing documentation for package naming, authentication, and publication commands.

Build MCP Apps
Direct link to Build MCP Apps

The MCP Apps extension lets MCP tools serve interactive HTML interfaces through ui:// resources. Mastra Studio renders these apps in sandboxed iframes on tool pages and in agent chat.

Use an MCP App when a tool result benefits from interaction, such as a form, calculator, color picker, or data visualization.

Define an app resource
Direct link to Define an app resource

Return a short content summary for the model and place UI data in structuredContent. Link the tool to its app by setting mcp._meta.ui.resourceUri to the same ui:// URI used in appResources:

src/mastra/mcp/calculator.ts
import { MCPServer } from '@mastra/mcp'
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

const calculatorTool = createTool({
id: 'calculatorWithUI',
description: 'Calculate the sum of two numbers',
inputSchema: z.object({ num1: z.number(), num2: z.number() }),
mcp: { _meta: { ui: { resourceUri: 'ui://calculator/main' } } },
execute: async ({ num1, num2 }) => ({
content: [{ type: 'text', text: 'The result is displayed in the calculator app.' }],
structuredContent: { result: num1 + num2 },
}),
})

const calculatorMcpServer = new MCPServer({
id: 'calculator-app-server',
name: 'Calculator App Server',
version: '1.0.0',
tools: { calculatorTool },
appResources: {
'ui://calculator/main': { name: 'Calculator', htmlPath: './src/mastra/mcp/calculator.html' },
},
})

The model sees content, while the app receives structuredContent. Visit appResources for inline HTML, file paths, metadata, and content security policy options.

Connect the app to Studio
Direct link to Connect the app to Studio

Use the App class from @modelcontextprotocol/ext-apps inside the HTML resource. Register event handlers before calling connect():

src/mastra/mcp/calculator.html
<!doctype html>
<html>
<body>
<p id="result">Waiting for input</p>
<button id="recalculate">Recalculate</button>

<script type="module">
import { App } from 'https://cdn.jsdelivr.net/npm/@modelcontextprotocol/ext-apps/+esm'

const app = new App({ name: 'Calculator', version: '1.0.0' })
let toolInput

app.ontoolinput = params => {
toolInput = params.arguments
}

document.querySelector('#recalculate').addEventListener('click', async () => {
const result = await app.callServerTool({
name: 'calculatorWithUI',
arguments: toolInput,
})
document.querySelector('#result').textContent = JSON.stringify(result)
})

await app.connect()
</script>
</body>
</html>

The guest-side APIs serve different parts of the interaction:

APIPurpose
app.ontoolinputReceive the arguments from the host tool call
app.callServerTool()Call an MCP tool from inside the iframe
app.sendMessage()Add a user message to chat and start a new model turn
app.connect()Connect to the host after registering event handlers

Visit the external App API reference for all guest-side methods and lifecycle hooks.

Register MCP Apps
Direct link to Register MCP Apps

Register the server on Mastra the same way as any other server in Expose a Mastra MCP server, and pass its tool to the agent that should render the app.

For an external MCP server that implements MCP Apps, load its tools with MCPClient.listTools() and register its proxy so Studio can resolve the remote app resources:

src/mastra/remote-apps.ts
import { Agent } from '@mastra/core/agent'
import { Mastra } from '@mastra/core/mastra'
import { mcpClient } from './mcp/client'

const tools = await mcpClient.listTools()
const mcpServers = mcpClient.toMCPServerProxies()

const agent = new Agent({
id: 'remote-app-agent',
name: 'Remote App Agent',
instructions: 'Use the available remote tools.',
model: 'openai/gpt-6-luna',
tools,
})

export const mastra = new Mastra({
agents: { agent },
mcpServers,
})

Tools loaded through listTools() include a serverId in _meta.ui, allowing Studio to resolve each app resource without scanning every server. Visit toMCPServerProxies() for proxy configuration details.

Sandbox security
Direct link to Sandbox security

Mastra Studio uses @mcp-ui/client to load app HTML through a sandbox proxy and communicate over JSON-RPC with postMessage.

App iframes allow scripts, forms, and popups. They can't access the parent page's DOM, cookies, or storage. The host controls all communication with the guest app.

MCP events
Direct link to MCP events

MCP events let a client react when something happens on a server, such as a ticket being opened or a build finishing, instead of calling a tool on a schedule to check. A server declares event types, a client subscribes with arguments, and the server delivers each matching occurrence over the same stdio or Streamable HTTP transport as its tools.

warning

MCP events implement SEP-3415, a draft MCP extension. The wire format and the Mastra API are experimental and may change in any release.

  • Subscription: one client request for one event type with arguments, such as { project: 'web' }. The server delivers only matching occurrences.
  • Occurrence: one delivered event with an eventId, name, timestamp, and data. Delivery is at least once, and the client drops duplicates by eventId within a dedupWindow of recent ids. Handlers should still tolerate a repeat.
  • Cursor: an opaque position in the event sequence. Save it to resume from a later subscription. Replay covers only history the source retains. An emit() source buffers in one process, so a restart or another instance serving the subscription can miss occurrences, and durable replay needs a poll source backed by shared storage.

Publish events
Direct link to Publish events

Declare each type with defineEvent() in the events option of MCPServer, then publish occurrences with server.events.emit():

src/mastra/mcp/tickets.ts
import { createTool } from '@mastra/core/tools'
import { MCPServer, defineEvent } from '@mastra/mcp'
import { z } from 'zod'

const ticket = z.object({ id: z.string(), project: z.string(), title: z.string() })

const server: MCPServer = new MCPServer({
id: 'tickets',
name: 'Tickets',
version: '1.0.0',
tools: {
openTicket: createTool({
id: 'openTicket',
description: 'Opens a support ticket',
inputSchema: ticket,
execute: async input => server.events.emit('ticket.created', input),
}),
},
events: {
'ticket.created': defineEvent({
description: 'A support ticket was opened',
inputSchema: z.object({ project: z.string() }),
payloadSchema: ticket,
match: (args, data) => data.project === args.project,
}),
},
})

The examples serve this server over HTTP at http://localhost:4111/mcp with startHTTP().

match decides which subscriptions receive an occurrence. It filters on arguments the client chooses, so it doesn't control who can subscribe; add authorize to restrict callers, since it runs on every poll and every subscription open and rejects the request when it returns false. An event type takes its occurrences from either server.events.emit(), which serves push and poll from an in-process buffer, or a poll function that reads an external system and supports poll only.

Subscribe to events
Direct link to Subscribe to events

List the types a server offers with mcp.events.list(), then subscribe with mcp.events.subscribe(). The returned subscription is an async iterable of occurrences:

src/mastra/mcp/client.ts
import { MCPClient } from '@mastra/mcp'

const mcp = new MCPClient({ servers: { tickets: { url: new URL('http://localhost:4111/mcp') } } })

const eventTypes = await mcp.events.list('tickets')
const subscription = await mcp.events.subscribe('tickets', 'ticket.created', { project: 'web' })

for await (const event of subscription) {
console.log(event.eventId, event.data)
if (subscription.cursor) await saveCursor('tickets:web', subscription.cursor)
}

subscribe() resolves once the server accepts the subscription. A new subscription starts from now, so pass options.cursor to resume from a saved position. The client picks the first mode the event offers, in the order webhook when the client has webhook settings, then push, then poll, and reopens dropped streams on its own.

Start runs from events
Direct link to Start runs from events

Subscribe in a loop that starts one agent run or workflow run per occurrence. The occurrence comes from the server, so pass event.data as data rather than as instructions, and save subscription.cursor after handling each one. Because the loop handles one occurrence at a time and a slow run delays the next, when order doesn't matter you can start runs without awaiting them and save the cursor only after every earlier run has finished, since a crash otherwise loses the occurrences whose runs were still going.

Under push or webhook delivery the client buffers occurrences the loop hasn't read, and a loop that falls more than maxBufferedEvents behind loses the oldest ones and reports MCP_EVENTS_BUFFER_OVERFLOW. To deliver occurrences to agent threads as notifications instead of writing your own loop, use MCPEventsSignalProvider.

When the server is remote and your application already serves HTTP, use webhook delivery instead: enable webhook in eventsOptions on the server and webhook on the client, and each occurrence arrives as a POST to your endpoint. For delivery modes, error codes, buffers, retries, and the poll source example, visit the MCPServer events reference and the MCPClient events reference. The MCP events example runs the agent flow end to end against a local stdio server.

Next steps
Direct link to Next steps