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.
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 serversDirect link to Connect to MCP servers
Install the MCP package:
- npm
- pnpm
- Yarn
- Bun
npm install @mastra/mcp@latest
pnpm add @mastra/mcp@latest
yarn add @mastra/mcp@latest
bun add @mastra/mcp@latest
Configure each server with a local command or remote URL:
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}`,
},
},
},
},
})
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:
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 callsDirect 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:
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:
- npm
- pnpm
- Yarn
- Bun
npx @mastra/mcp generate ./src/mcp/client.ts
pnpm dlx @mastra/mcp generate ./src/mcp/client.ts
yarn dlx @mastra/mcp generate ./src/mcp/client.ts
bun x @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:
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 toolsDirect link to Static and runtime tools
Choose how to load tools based on whether the server configuration changes between requests:
| Static tools | Runtime toolsets | |
|---|---|---|
| Method | await mcpClient.listTools() | await mcpClient.listToolsets() |
| Use case | Shared, fixed configuration | Per-user or per-request configuration |
| Credentials | Shared by all requests | Can vary between requests |
| Agent API | tools in the Agent constructor | toolsets 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:
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 approvalDirect 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.
SecurityDirect 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
PATHandHOMEon POSIX), not the full parent environment. SetinheritDefaultEnv: falseon a server to pass only the variables listed inenv. - Outbound host restriction: set
allowedHostswhen 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 customfetchis 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
requireToolApprovalfor sensitive tools.
Visit the MCPClient security reference for enforcement details of each option.
MCP registriesDirect link to MCP registries
Registries provide hosted or packaged MCP servers. The client configuration above works with registry endpoints and commands.
| Registry | Connection | Notes |
|---|---|---|
| Klavis AI | Hosted HTTP | Enterprise authentication and managed servers |
| mcp.run | Signed hosted URL | Treat the profile URL as a secret |
| Composio | Hosted URL | URLs are often tied to one user account |
| Smithery | CLI or hosted URL | Run local packages through npx |
| Apify | Hosted HTTP | Authenticate with an Apify API token |
| Ampersand | Hosted URL or stdio | Connect 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 serverDirect link to Expose a Mastra MCP server
Create an MCPServer to expose Mastra primitives to external MCP clients:
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:
import { Mastra } from '@mastra/core/mastra'
import { mcpServer } from './mcp/server'
export const mastra = new Mastra({
mcpServers: { mcpServer },
})
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 packageDirect 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:
#!/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:
{
"bin": {
"my-mcp-server": "dist/stdio.js"
},
"files": ["dist"]
}
After publishing the package, configure clients to run it by package name:
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 AppsDirect 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 resourceDirect 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:
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 StudioDirect 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():
<!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:
| API | Purpose |
|---|---|
app.ontoolinput | Receive 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 AppsDirect 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:
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 securityDirect 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 eventsDirect 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.
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, anddata. Delivery is at least once, and the client drops duplicates byeventIdwithin adedupWindowof 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 apollsource backed by shared storage.
Publish eventsDirect link to Publish events
Declare each type with defineEvent() in the events option of MCPServer, then publish occurrences with server.events.emit():
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 eventsDirect 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:
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 eventsDirect 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.