Skip to main content

MCPServer

The MCPServer class provides the functionality to expose your existing Mastra tools and Agents as a Model Context Protocol (MCP) server. Any MCP client, such as Cursor, Windsurf, or Claude Desktop, can connect to these capabilities and make them available to an agent.

Note that if you only need to use your tools or agents directly within your Mastra application, you don't necessarily need to create an MCP server. This API is specifically for exposing your Mastra tools and agents to external MCP clients.

It supports the stdio (subprocess) and Streamable HTTP MCP transports of the MCP 2026-07-28 revision.

note

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

Constructor
Direct link to Constructor

To create a new MCPServer, you need to provide some basic information about your server, the tools it will offer, and optionally, any agents you want to expose as tools.

import { Agent } from '@mastra/core/agent'
import { createTool } from '@mastra/core/tools'
import { MCPServer } from '@mastra/mcp'
import { z } from 'zod'
import { dataProcessingWorkflow } from '../workflows/dataProcessingWorkflow'

const myAgent = new Agent({
id: 'my-example-agent',
name: 'MyExampleAgent',
description: 'A generalist to help with basic questions.',
instructions: 'You are a helpful assistant.',
model: 'openai/gpt-5.6-sol',
})

const weatherTool = createTool({
id: 'getWeather',
description: 'Gets the current weather for a location.',
inputSchema: z.object({ location: z.string() }),
execute: async inputData => `Weather in ${inputData.location} is sunny.`,
})

const server = new MCPServer({
id: 'my-custom-server',
name: 'My Custom Server',
version: '1.0.0',
description: 'A server that provides weather data and agent capabilities',
instructions:
'Use the available tools to help users with weather information and data processing tasks.',
tools: { weatherTool },
agents: { myAgent }, // this agent will become tool "ask_myAgent"
workflows: {
dataProcessingWorkflow, // this workflow will become tool "run_dataProcessingWorkflow"
},
})

Tool schemas and structured results
Direct link to Tool schemas and structured results

MCP tool input and output schemas are advertised as JSON Schema 2020-12 with the dialect declared in $schema. Advertised Mastra tool schemas preserve supported references, composition keywords, and tuple (prefixItems) shapes.

A tool with an outputSchema can return any JSON value, including an object, array, string, number, boolean, or null. The value is sent as structuredContent without wrapping it in an object. The server validates successful structured output against the tool's output schema before returning it.

Configuration Properties
Direct link to Configuration Properties

The constructor accepts an MCPServerConfig object with the following properties:

id:

string
Unique identifier for the server. This ID is preserved when the server is registered with Mastra and can be used to retrieve the server via getMCPServerById().

name:

string
A descriptive name for your server (e.g., 'My Weather and Agent Server').

version:

string
The semantic version of your server (e.g., '1.0.0').

tools:

ToolsInput
An object where keys are tool names and values are Mastra tool definitions (created with createTool or Vercel AI SDK). These tools will be directly exposed.

agents?:

Record<string, Agent>
An object where keys are agent identifiers and values are Mastra Agent instances. Each agent will be automatically converted into a tool named ask_<agentIdentifier>. The agent **must** have a non-empty description string property defined in its constructor configuration. This description will be used in the tool's description. If an agent's description is missing or empty, an error will be thrown during MCPServer initialization.

workflows?:

Record<string, Workflow>
An object where keys are workflow identifiers and values are Mastra Workflow instances. Each workflow is converted into a tool named run_<workflowKey>. The workflow's inputSchema becomes the tool's input schema. The workflow **must** have a non-empty description string property, which is used for the tool's description. If a workflow's description is missing or empty, an error will be thrown. The tool executes the workflow by calling workflow.createRun() followed by run.start({ inputData: <tool_input> }). If a tool name derived from an agent or workflow (e.g., ask_myAgent or run_myWorkflow) collides with an explicitly defined tool name or another derived name, the explicitly defined tool takes precedence, and a warning is logged. Agents/workflows leading to subsequent collisions are skipped.

description?:

string
Optional description of what the MCP server does. Announced to MCP clients as part of the server's identity and returned in the registry server info.

title?:

string
Optional human-readable display title announced to MCP clients (e.g., 'Weather Server'). Clients fall back to name when it isn't set.

websiteUrl?:

string
Optional URL of the server's website, announced to MCP clients.

icons?:

{ src: string; mimeType?: string; sizes?: string[]; theme?: 'light' | 'dark' }[]
Optional icons clients can display for the server. src is an https: URL or a data: URI; sizes takes values like '48x48' or 'any'.

instructions?:

string
Optional instructions describing how to use the server and its features.

mapAuthInfoToUser?:

({ authInfo, extra, requestContext }) => unknown | null | undefined | Promise<unknown | null | undefined>
Maps MCP transport auth data from extra.authInfo into the user value used by Mastra FGA checks. Use this when an OAuth-protected MCP server is registered on a Mastra instance with an FGA provider.

fga?:

{ resourceMapping?: Partial<Record<'tool' | 'tools', { fgaResourceType: string; deriveId?: ({ user, resourceId, requestContext }) => string | undefined }>>; permissionMapping?: Record<string, string> }
Overrides resource and permission mappings for this MCP server's tools/list and tools/call FGA checks. Use this when MCP authorization should be scoped differently from internal agent or workflow tool execution.

requestState?:

{ key?: string | Uint8Array; ttlSeconds?: number }
Integrity protection for input_required continuation state. key is an HMAC key of at least 32 bytes that every instance able to answer a continuation must share; ttlSeconds (default 600) is how long a suspended round stays answerable. Without a key the server generates one per process. See the Asking the caller for input section.

cacheHints?:

MCPServerCacheHints
Cache hints (ttlMs / cacheScope) advertised on cacheable results, keyed by operation: 'tools/list', 'prompts/list', 'resources/list', 'resources/templates/list', 'resources/read' and 'server/discover'.

jsonSchemaValidator?:

jsonSchemaValidator
Custom JSON Schema validator used for tool input, output and continuation answers, for runtimes where the SDK default is unavailable.

repository?:

Repository
Optional repository information for the server's source code.

releaseDate?:

string
Optional release date of this server version (ISO 8601 string). Defaults to the time of instantiation if not provided.

isLatest?:

boolean
Optional flag indicating if this is the latest version. Defaults to true if not provided.

packageCanonical?:

'npm' | 'docker' | 'pypi' | 'crates' | string
Optional canonical packaging format if the server is distributed as a package (e.g., 'npm', 'docker').

packages?:

PackageInfo[]
Optional list of installable packages for this server.

remotes?:

RemoteInfo[]
Optional list of remote access points for this server.

resources?:

MCPServerResources
An object defining how the server should handle MCP resources. See Resource Handling section for details.

prompts?:

MCPServerPrompts
An object defining how the server should handle MCP prompts. See Prompt Handling section for details.

appResources?:

AppResources
A map of ui:// URIs to app resource configurations. Each entry defines an interactive HTML UI served via the MCP Apps extension (SEP-1865). See the MCP Apps section for details.

Exposing agents as tools
Direct link to Exposing agents as tools

A powerful feature of MCPServer is its ability to automatically expose your Mastra Agents as callable tools. When you provide agents in the agents property of the configuration:

  • Tool Naming: Each agent is converted into a tool with the name ask_<agentKey>, where <agentKey> is the key you used for that agent in the agents object. For instance, if you configure agents: { myAgentKey: myAgentInstance }, a tool with the name ask_myAgentKey will be created.

  • Tool Functionality:

    • Description: The generated tool's description will be in the format: "Ask agent <AgentName> a question. Original agent instructions: <agent description>".
    • Input: The tool expects a single object argument with a message property (string): { message: "Your question for the agent" }.
    • Execution: When this tool is called, it invokes the corresponding agent's generate() method with the provided query.
    • Output: The direct result from the agent's generate() method is returned as the output of the tool.
  • Name collisions. If an explicit tool defined in the tools configuration has the same name as an agent-derived tool (e.g., a tool with the name ask_myAgentKey alongside an agent keyed as myAgentKey), the explicitly defined tool will take precedence. The agent won't be converted into a tool in this conflicting case, and a warning will be logged.

This makes it straightforward to allow MCP clients to interact with your agents using natural language queries, like any other tool.

Agent-to-Tool Conversion
Direct link to Agent-to-Tool Conversion

When you provide agents in the agents configuration property, MCPServer will automatically create a corresponding tool for each agent. The tool will be ask_<agentIdentifier>, where <agentIdentifier> is the key you used in the agents object.

The description for this generated tool will be: "Ask agent <agent.name> a question. Agent description: <agent.description>".

For an agent to be converted into a tool, it must have a non-empty description string property set in its configuration when it was instantiated (e.g., new Agent({ id: 'my-agent', name: 'myAgent', description: 'This agent does X.', ... })). If an agent is passed to MCPServer with a missing or empty description, an error will be thrown when the MCPServer is instantiated, and server setup will fail.

Clients can use MCP to access your agents' generative capabilities and ask them questions directly.

Accessing MCP context in tools
Direct link to Accessing MCP context in tools

Tools exposed through MCPServer receive the protocol context of the current request as context.mcp:

MemberDescription
context.mcp.extra.signalAborts when the client cancels the request or disconnects
context.mcp.extra.requestIdThe JSON-RPC id of the request
context.mcp.extra.authInfoWhatever the transport authenticated (see Authentication context)
context.mcp.extra._metaRequest metadata: trace headers, the log-level opt-in and the progress token
context.mcp.log()Sends a log message to the calling client (see Logging)
context.mcp.progress()Reports progress to the calling client (see Progress notifications)
context.mcp.protocolVersion'2026-07-28'

The same request also populates context.requestContext, the trusted application context, with authInfo, the user returned by mapAuthInfoToUser and the W3C traceContext the client sent. Tools invoked by an agent that an MCP client asked (ask_<agent>) don't receive context.mcp, but the request context is forwarded to the agent, so read auth data from there when a tool can be reached both ways:

const authInfo = context.mcp?.extra.authInfo ?? context.requestContext?.get('authInfo')

Example: Tool that uses caller identity with a separate credential
Direct link to Example: Tool that uses caller identity with a separate credential

import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

import { apiCredentials } from '../lib/api-credentials'

const fetchUserData = createTool({
id: 'fetchUserData',
description: 'Fetches data for the authenticated MCP caller',
inputSchema: z.object({
resourceId: z.string().describe('The ID of the record to fetch'),
}),
execute: async (inputData, context) => {
const authInfo = context.mcp?.extra.authInfo ?? context.requestContext?.get('authInfo')
const user = authInfo?.extra?.user as { id?: string } | undefined

if (!user?.id) {
throw new Error('Authentication required')
}

// Resolve the owner from trusted context, not from model input
const credential = await apiCredentials.getForUser(user.id)

const response = await fetch(
`https://api.example.com/users/${encodeURIComponent(user.id)}/records/${encodeURIComponent(inputData.resourceId)}`,
{
headers: {
Authorization: `Bearer ${credential.token}`,
},
signal: context.mcp?.extra.signal,
},
)

return response.json()
},
})

Use authInfo for caller identity, then select a credential intended for the service the tool calls. The model supplies business input such as resourceId, while trusted code selects the credential. An MCP access token is for your MCP server, so it must not be forwarded to another API. See Access upstream services safely.

Where authInfo comes from
Direct link to where-authinfo-comes-from

When an MCP server is served by a Mastra server, extra.authInfo is populated from the principal resolved by server.auth. Nothing extra is required:

export const mastra = new Mastra({
mcpServers: { myServer },
server: {
auth: new MastraJwtAuth({ secret: process.env.JWT_SECRET! }),
},
})

The authenticated user is mapped to authInfo as:

authInfo fieldValue
tokenThe bearer token or session cookie value used for the request
clientIdThe first present of user.id, user.sub, user.userId, user.email
scopesuser.scopes, user.scope, or user.permissions, normalized to an array
extra.userThe full user object returned by the auth provider

If your own middleware performs the verification, set server.mcpOptions.setRequestAuth to build authInfo yourself. The hook replaces the default mapping:

export const mastra = new Mastra({
mcpServers: { myServer },
server: {
middleware: [verifyBearerToken], // stores the payload on the request context
mcpOptions: {
setRequestAuth: (req, requestContext) => {
const payload = requestContext.get('bearerPayload')
req.auth = {
token: payload.token,
clientId: payload.sub,
scopes: payload.scope.split(' '),
}
},
},
},
})

Leaving req.auth unset inside the hook opts the request out of auth info entirely.

Asking the caller for input
Direct link to Asking the caller for input

A tool that needs something from the user before it can finish calls context.suspend(payload) and returns, exactly as it would inside an agent or a workflow. The server ends the request as an input_required result that carries the form described by the tool's resumeSchema. When the client answers, the server runs the tool again with the answer in context.resumeData and the payload it suspended with in context.suspendPayload. Each round is a separate request: the server never replays earlier rounds, so put the state the next round needs in the payload and branch on it.

import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

export const bookDelivery = createTool({
id: 'bookDelivery',
description: 'Books a delivery for an order after confirming the address.',
inputSchema: z.object({ orderId: z.string() }),
outputSchema: z.object({ confirmed: z.boolean() }),
suspendSchema: z.object({ phase: z.literal('address'), message: z.string() }),
resumeSchema: z.object({ address: z.string() }),
execute: async ({ orderId }, context) => {
if (!context.resumeData) {
await context.suspend?.({ phase: 'address', message: 'Delivery address?' })
return
}
await book(orderId, context.resumeData.address)
return { confirmed: true }
},
})

resumeSchema becomes the form the caller fills in, so it must describe a flat object of primitives (strings, numbers, booleans, enums). A caller that declines or cancels the form ends the call with an error, and the tool doesn't run again. A tool can suspend more than once by changing the phase in its payload.

Continuation state
Direct link to Continuation state

The continuation travels as an opaque requestState string that the client echoes back with its answer. The server signs it with requestState.key and rejects a tampered, expired or foreign state (a different tool, different arguments or a different caller) before your handler runs. The caller is the token subject when the authorization layer provides one, otherwise the id of the user mapAuthInfoToUser returns, otherwise the bearer token itself, so two users behind the same OAuth client can't resume each other's rounds.

On a server without authorization every caller shares one anonymous principal, so a requestState behaves like a bearer credential until its ttlSeconds expire: anyone who obtains it can answer the round. Put tools whose suspensions carry authority (writes, purchases, account changes) behind authorization.

The payload is signed, not encrypted, so keep it small and non-secret (IDs and phase, not confidential data). Set requestState.key from the environment so every instance that may answer a continuation shares the key. Without it the server generates a key per process and continuations only succeed on that process.

const server = new MCPServer({
id: 'booking',
name: 'Booking',
version: '1.0.0',
tools: { bookDelivery },
requestState: { key: process.env.MCP_REQUEST_STATE_KEY!, ttlSeconds: 600 },
})

Resource and prompt callbacks can suspend the same way. See Resource handling and Prompt handling.

Methods
Direct link to Methods

These are the functions you can call on an MCPServer instance to control its behavior and get information.

startStdio()
Direct link to startstdio

Use this method to start the server so it communicates using standard input and output (stdio). This is typical when running the server as a command-line program.

async startStdio(): Promise<void>

Here's how you would start the server using stdio:

const server = new MCPServer({
id: 'my-server',
name: 'My Server',
version: '1.0.0',
tools: {/* ... */},
})
await server.startStdio()

startHTTP()
Direct link to starthttp

This method helps you integrate the MCP server with an existing web server to use Streamable HTTP for communication. You'll call this from your web server's code when it receives HTTP requests.

async startHTTP({
url,
httpPath,
req,
res,
options,
}: {
url: URL;
httpPath: string;
req: http.IncomingMessage;
res: http.ServerResponse<http.IncomingMessage>;
options?: MCPServerHTTPRequestOptions;
}): Promise<void>

Every request is self-contained: there is no session to create or resume, so options only carries request guards.

OptionBehavior
enableDnsRebindingProtectionWhen true, the Host header is checked against allowedHosts and the Origin header against allowedOrigins before the request is handled. A check only runs when its list is non-empty. A rejected request receives a 403 JSON-RPC error.
allowedHostsHostnames accepted in the Host header. Matching is port-agnostic: localhost:3000 in the list allows any port on localhost. Write IPv6 addresses with brackets ([::1]).
allowedOriginsOrigins accepted in the Origin header. Only the hostname is compared, so https://app.example.com:8443 allows every scheme and port on app.example.com. Requests without an Origin header pass because non-browser MCP clients don't send one.

Omit options when you don't need request guards.

Here's an example of how you might use startHTTP within an HTTP server request handler. In this example an MCP client could connect to your MCP server at http://localhost:1234/mcp:

import http from 'http'

const httpServer = http.createServer(async (req, res) => {
await server.startHTTP({
url: new URL(req.url || '', 'http://localhost:1234'),
httpPath: `/mcp`,
req,
res,
})
})

httpServer.listen(PORT, () => {
console.log(`HTTP server listening on port ${PORT}`)
})

Because every request is self-contained, nothing needs to persist between invocations, so startHTTP works in serverless environments (Supabase Edge Functions, Cloudflare Workers, Vercel Edge, AWS Lambda, Deno Deploy). The method takes Node-style http.IncomingMessage and http.ServerResponse objects, so a Fetch-based runtime has to convert its Request with an adapter such as fetch-to-node and turn the result back into a Response:

// Supabase Edge Function example
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { MCPServer } from '@mastra/mcp'
// Note: You will need to convert req/res format from Deno to Node
import { toReqRes, toFetchResponse } from 'fetch-to-node'

const server = new MCPServer({
id: 'my-serverless-mcp',
name: 'My Serverless MCP',
version: '1.0.0',
tools: {/* your tools */},
})

serve(async req => {
const url = new URL(req.url)

if (url.pathname === '/mcp') {
// Convert Deno Request to Node.js-compatible format
const { req: nodeReq, res: nodeRes } = toReqRes(req)

await server.startHTTP({ url, httpPath: '/mcp', req: nodeReq, res: nodeRes })

return toFetchResponse(nodeRes)
}

return new Response('Not found', { status: 404 })
})

Request-scoped features (input_required rounds, progress, per-request logs) stream inside the request that triggered them. Notifications that outlive a request (resources.notifyUpdated(), prompts.notifyListChanged(), toolActions.notifyListChanged()) are delivered on the subscriptions/listen stream a client keeps open, so they only reach clients while that stream is served by a running instance.

Here are the details for the values needed by the startHTTP method:

url:

URL
The web address the user is requesting.

httpPath:

string
The specific part of the URL where the MCP server will handle HTTP requests (e.g., '/mcp').

req:

http.IncomingMessage
The incoming request object from your web server.

res:

http.ServerResponse
The response object from your web server, used to send data back.

options?:

MCPServerHTTPRequestOptions
Optional request guards. See the options table above for more details.

close()
Direct link to close

This method closes the server and releases all resources.

async close(): Promise<void>

getServerInfo()
Direct link to getserverinfo

The method returns the server's basic information.

getServerInfo(): ServerInfo

getServerDetail()
Direct link to getserverdetail

The method returns details about the server's information.

getServerDetail(): ServerDetailInfo

getToolListInfo()
Direct link to gettoollistinfo

The method returns the tools that were set up when you created the server. It's a read-only list, useful for debugging purposes.

getToolListInfo(): { tools: ToolInfo[] }

getToolInfo()
Direct link to gettoolinfo

The method returns details about a specific tool.

getToolInfo(toolId: string): ToolInfo | undefined

executeTool()
Direct link to executetool

Runs a tool without a protocol client, which is how the Mastra REST route POST /api/mcp/:serverId/tools/:toolId/execute and Studio invoke tools.

async executeTool(
toolId: string,
args: unknown,
executionContext?: {
messages?: any[];
toolCallId?: string;
requestContext?: RequestContext;
resumeData?: unknown;
suspendPayload?: unknown;
},
): Promise<MCPToolExecutionResultV2>

The result is { status: 'completed', output } for a finished call, or { status: 'suspended', suspendPayload, resumeSchema } when the tool asked for input. Continue by calling again with the same args plus resumeData and the echoed suspendPayload. A tool that throws, or input or resume data that fails its schema, rejects the promise.

const result = await server.executeTool('bookDelivery', { orderId })
if (result.status === 'suspended') {
const answer = await askUser(result.suspendPayload, result.resumeSchema)
await server.executeTool(
'bookDelivery',
{ orderId },
{ resumeData: answer, suspendPayload: result.suspendPayload },
)
}

toolId:

string
The ID/name of the tool to execute.

args:

unknown
The arguments to pass to the tool's execute function, validated against its input schema.

executionContext?:

object
Optional context for the tool execution. Pass resumeData and suspendPayload to continue a suspended tool.

tools()
Direct link to tools

Returns the registered tool registry, keyed by tool ID.

tools(): Readonly<Record<string, ConvertedTool>>

Resource handling
Direct link to Resource handling

What are MCP Resources?
Direct link to What are MCP Resources?

MCP resources expose server data that clients can read and use as context for LLM interactions. Examples include:

  • File contents
  • Database records
  • API responses
  • Live system data
  • Screenshots and images
  • Log files

Resources are identified by unique URIs (e.g., file:///home/user/documents/report.pdf, postgres://database/customers/schema) and can contain either text (UTF-8 encoded) or binary data (base64 encoded).

Clients can discover resources through:

  1. Direct resources: Servers expose a list of concrete resources via a resources/list endpoint.
  2. Resource templates: For runtime-defined resources, servers can expose URI templates (RFC 6570) that clients use to construct resource URIs.

To read a resource, clients make a resources/read request with the URI. Servers can also notify clients about changes to the resource list (notifications/resources/list_changed) or updates to specific resource content (notifications/resources/updated) if a client is listening for that resource.

For more detailed information, refer to the official MCP documentation on Resources.

MCPServerResources Type
Direct link to mcpserverresources-type

The resources option takes an object of type MCPServerResources. This type defines the callbacks your server will use to handle resource requests:

export type MCPServerResources = {
// Callback to list available resources
listResources: (params: {
extra: MCPRequestHandlerExtra
requestContext: RequestContext
}) => Promise<Resource[]>

// Callback to get the content of a specific resource
getResourceContent: (
params: { uri: string } & MCPServerRequest,
) => Promise<MCPServerResourceContent | MCPServerResourceContent[] | void>

// Optional callback to list available resource templates
resourceTemplates?: (params: {
extra: MCPRequestHandlerExtra
requestContext: RequestContext
}) => Promise<ResourceTemplate[]>

// Shape of the answer a suspended getResourceContent expects
resumeSchema?: StandardSchemaWithJSON
}

export type MCPServerResourceContent = { text?: string } | { blob?: string }

Every callback receives extra, the same protocol context tools see as context.mcp.extra (cancellation signal, requestId, authInfo, _meta), and requestContext, the trusted application context that carries authInfo and the user mapped by mapAuthInfoToUser. Use them to scope what a caller can list and read.

getResourceContent also receives suspend, resumeData and suspendPayload (the MCPServerRequest members). Declare resumeSchema to make suspend usable. It works exactly as it does for tools.

Example:

import { MCPServer } from '@mastra/mcp'
import type {
MCPServerResourceContent,
MCPServerResources,
Resource,
ResourceTemplate,
} from '@mastra/mcp'

// Resources/resource templates will generally be dynamically fetched.
const myResources: Resource[] = [
{ uri: 'file://data/123.txt', name: 'Data File', mimeType: 'text/plain' },
]

const myResourceContents: Record<string, MCPServerResourceContent> = {
'file://data.txt/123': { text: 'This is the content of the data file.' },
}

const myResourceTemplates: ResourceTemplate[] = [
{
uriTemplate: 'file://data/{id}',
name: 'Data File',
description: 'A file containing data.',
mimeType: 'text/plain',
},
]

const myResourceHandlers: MCPServerResources = {
listResources: async () => myResources,
getResourceContent: async ({ uri, extra }) => {
if (myResourceContents[uri]) {
return myResourceContents[uri]
}
throw new Error(`Resource content not found for ${uri}`)
},
resourceTemplates: async () => myResourceTemplates,
}

const serverWithResources = new MCPServer({
id: 'resourceful-server',
name: 'Resourceful Server',
version: '1.0.0',
tools: {/* ... your tools ... */},
resources: myResourceHandlers,
})

Notifying Clients of Resource Changes
Direct link to Notifying Clients of Resource Changes

If the available resources or their content change, your server can notify clients that are listening for the specific resource.

server.resources.notifyUpdated({ uri: string })
Direct link to serverresourcesnotifyupdated-uri-string-

Call this method when the content of a specific resource (identified by its uri) has been updated. Clients subscribed to this URI receive a notifications/resources/updated message on their subscriptions/listen stream.

async server.resources.notifyUpdated({ uri: string }): Promise<void>

Example:

// After updating the content of 'file://data.txt'
await serverWithResources.resources.notifyUpdated({ uri: 'file://data.txt' })

server.resources.notifyListChanged()
Direct link to serverresourcesnotifylistchanged

Call this method when the list of available resources has changed (e.g., a resource was added or removed). This will send a notifications/resources/list_changed message to listening clients, prompting them to re-fetch the list of resources.

async server.resources.notifyListChanged(): Promise<void>

Example:

// After adding a new resource to the list managed by 'myResourceHandlers.listResources'
await serverWithResources.resources.notifyListChanged()

Prompt handling
Direct link to Prompt handling

What are MCP Prompts?
Direct link to What are MCP Prompts?

Prompts are reusable templates or workflows that MCP servers expose to clients. They can accept arguments and include resource context. They standardize LLM interactions.

Prompts are identified by a unique name and can be runtime-defined or static.

MCPServerPrompts Type
Direct link to mcpserverprompts-type

The prompts option takes an object of type MCPServerPrompts. This type defines the callbacks your server will use to handle prompt requests:

export type MCPServerPrompts = {
// Callback to list available prompts
listPrompts: (params: {
extra: MCPRequestHandlerExtra
requestContext: RequestContext
}) => Promise<Prompt[]>

// Callback to get the messages for a specific prompt
getPromptMessages?: (
params: { name: string; args?: Record<string, unknown> } & MCPServerRequest,
) => Promise<PromptMessage[] | void>

// Shape of the answer a suspended getPromptMessages expects
resumeSchema?: StandardSchemaWithJSON
}

The callbacks receive the same extra and requestContext as resource callbacks, and getPromptMessages can suspend in the same way when resumeSchema is declared. The server validates required prompt arguments before calling getPromptMessages.

Example:

import { MCPServer } from '@mastra/mcp'
import type { Prompt, PromptMessage, MCPServerPrompts } from '@mastra/mcp'

const prompts: Prompt[] = [
{
name: 'analyze-code',
description: 'Analyze code for improvements',
arguments: [{ name: 'code', description: 'The code to analyze', required: true }],
},
]

const myPromptHandlers: MCPServerPrompts = {
listPrompts: async () => prompts,
getPromptMessages: async ({ name, args }) => {
if (name === 'analyze-code') {
return [
{
role: 'user',
content: {
type: 'text',
text: `Analyze this code: ${args?.code}`,
},
},
]
}
throw new Error('Prompt not found')
},
}

const serverWithPrompts = new MCPServer({
id: 'prompt-server',
name: 'Prompt Server',
version: '1.0.0',
tools: {/* ... your tools ... */},
prompts: myPromptHandlers,
})

Notifying Clients of Prompt Changes
Direct link to Notifying Clients of Prompt Changes

If the available prompts change, your server can notify listening clients.

server.prompts.notifyListChanged()
Direct link to serverpromptsnotifylistchanged

Call this method when the list of available prompts has changed (e.g., a prompt was added or removed). This will send a notifications/prompts/list_changed message to listening clients, prompting them to re-fetch the list of prompts.

await serverWithPrompts.prompts.notifyListChanged()

Best practices for Prompt Handling
Direct link to Best practices for Prompt Handling

  • Use clear, descriptive prompt names and descriptions.
  • Validate all required arguments in getPromptMessages.
  • Return an error for unknown prompts or missing required arguments.
  • Notify clients whenever prompts change.

Dynamic tool management
Direct link to Dynamic tool management

Add and remove tools on a running server. Connected clients are notified with notifications/tools/list_changed.

The property is toolActions because tools() is the method that returns the registered tool registry.

toolActions.add(tools)
Direct link to toolactionsaddtools

Registers new tools on the running server and notifies connected clients. Tools are keyed by their record key, the same as tools passed to the constructor. Adding a tool under an existing key replaces it.

async server.toolActions.add(tools: ToolsInput): Promise<void>

Example:

import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

const searchTool = createTool({
id: 'search',
description: 'Searches the knowledge base.',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => ({ results: [] }),
})

await server.toolActions.add({ searchTool })

toolActions.remove(toolIds)
Direct link to toolactionsremovetoolids

Removes tools from the running server by tool ID and notifies connected clients. Unknown tool IDs are ignored. A notification is sent only when at least one tool is removed.

async server.toolActions.remove(toolIds: string[]): Promise<void>

Example:

await server.toolActions.remove(['searchTool'])

toolActions.notifyListChanged()
Direct link to toolactionsnotifylistchanged

Sends a notifications/tools/list_changed message to connected clients without modifying the tool registry. Call this when tool availability changes through other means (for example, authorization changes).

async server.toolActions.notifyListChanged(): Promise<void>

Mastra registry synchronization
Direct link to Mastra registry synchronization

When the server is registered with a Mastra instance, toolActions.add() and toolActions.remove() also update the Mastra instance's tool registry, matching the automatic tool registration that happens at startup. Added tools become available through mastra.listTools() (keyed by the tool's intrinsic id when present), and removed tools are deleted from the registry.

Logging
Direct link to Logging

Tools send structured log messages to the calling client with notifications/message. Delivery is opted into per request: the client attaches the io.modelcontextprotocol/logLevel metadata key to its request, and the server delivers messages at or above that severity (following RFC 5424 ordering) for that request only. Without the opt-in nothing is delivered. A later round of the same tool call is a new request and must opt in again. The Mastra MCPClient sends the key on every request when enableServerLogs is on.

The Mastra logger and observability are unaffected: context.mcp.log() only controls what the MCP client receives.

context.mcp.log()
Direct link to contextmcplog

Inside a tool's execute function, use context.mcp.log() to send a log message to the client that called the tool.

async context.mcp.log(
level: LoggingLevel,
message: string,
data?: Record<string, unknown>
): Promise<void>

Example:

execute: async ({ location }, context) => {
await context.mcp?.log?.('debug', 'Fetching weather', { location })
const weather = await fetchWeather(location)
await context.mcp?.log?.('info', 'Weather fetched')
return weather
}

Progress notifications
Direct link to Progress notifications

Long-running tools can report progress to the calling client with notifications/progress. Progress is only sent when the caller requested progress tracking by including a progressToken in the request's _meta (the Mastra MCPClient does this when enableProgressTracking is set). When no token was sent, context.mcp.progress() is a no-op.

context.mcp.progress()
Direct link to contextmcpprogress

async context.mcp.progress(params: {
progress: number;
total?: number;
message?: string;
}): Promise<void>

Example:

execute: async ({ items }, context) => {
for (const [index, item] of items.entries()) {
await processItem(item)
await context.mcp?.progress?.({
progress: index + 1,
total: items.length,
message: `Processed ${item.name}`,
})
}
return { done: true }
}

Tracing
Direct link to Tracing

When the server is registered on a Mastra instance that has observability configured, every request it handles produces an MCP_SERVER_REQUEST root span: tools/list, tools/call, resources/*, and prompts/*. executeTool() produces the same span, so the Studio MCP server page is traced like an MCP client.

The span is named after the method and target (for example tools/call lookupOrder). It stores the request params as input and the response as output, and records the server name and version, the negotiated protocol version, and the client name and version when the client reported them. A tools/call that returns isError: true fails the span. Agents and workflows exposed as tools attach their AGENT_RUN and WORKFLOW_RUN spans under it. A served tool doesn't get its own TOOL_CALL span. Tools an agent calls inside the request still do.

A standalone MCPServer with no mastra instance produces no spans.

Notification delivery
Direct link to Notification delivery

Request-scoped notifications (context.mcp.log(), context.mcp.progress()) stream inside the request that triggered them, so they reach exactly the caller. Notifications that outlive a request (resources.notifyListChanged(), prompts.notifyListChanged(), toolActions.notifyListChanged(), resources.notifyUpdated()) are delivered on the subscriptions/listen stream each interested client keeps open. resources.notifyUpdated() only reaches clients whose stream includes that resource URI. List-changed notifications reach every client listening for that list. Over stdio the connection itself carries the stream.

Examples
Direct link to Examples

For a practical example of packaging a stdio server, see Publish a stdio server package.

The example at the beginning of this page also demonstrates how to instantiate MCPServer with both tools and agents.

OAuth protection
Direct link to OAuth protection

To protect your MCP server with OAuth authentication per the MCP Authorization specification, use the createOAuthMiddleware function:

import http from 'node:http'
import { MCPServer, createOAuthMiddleware, createStaticTokenValidator } from '@mastra/mcp'

const mcpServer = new MCPServer({
id: 'protected-server',
name: 'Protected MCP Server',
version: '1.0.0',
tools: {/* your tools */},
})

// Create OAuth middleware
const oauthMiddleware = createOAuthMiddleware({
oauth: {
resource: 'https://mcp.example.com/mcp',
authorizationServers: ['https://auth.example.com'],
scopesSupported: ['mcp:read', 'mcp:write'],
resourceName: 'My Protected MCP Server',
validateToken: createStaticTokenValidator(['allowed-token-1']),
},
mcpPath: '/mcp',
})

// Create HTTP server with OAuth protection
const httpServer = http.createServer(async (req, res) => {
const url = new URL(req.url || '', 'https://mcp.example.com')

// Apply OAuth middleware first
const result = await oauthMiddleware(req, res, url)
if (!result.proceed) return // Middleware handled response (401, metadata, etc.)

// Token is valid, proceed to MCP handler
await mcpServer.startHTTP({ url, httpPath: '/mcp', req, res })
})

httpServer.listen(3000)

The middleware automatically:

  • Serves Protected Resource Metadata at /.well-known/oauth-protected-resource (RFC 9728)
  • Returns 401 Unauthorized with proper WWW-Authenticate headers when authentication is required
  • Validates bearer tokens using your provided validator

Token Validation
Direct link to Token Validation

For production, use proper token validation:

import { createOAuthMiddleware, createIntrospectionValidator } from '@mastra/mcp'

// Option 1: Token introspection (RFC 7662)
const middleware = createOAuthMiddleware({
oauth: {
resource: 'https://mcp.example.com/mcp',
authorizationServers: ['https://auth.example.com'],
validateToken: createIntrospectionValidator('https://auth.example.com/oauth/introspect', {
clientId: 'mcp-server',
clientSecret: 'secret',
}),
},
})

// Option 2: Custom validation (JWT, database lookup, etc.)
const customMiddleware = createOAuthMiddleware({
oauth: {
resource: 'https://mcp.example.com/mcp',
authorizationServers: ['https://auth.example.com'],
validateToken: async (token, resource) => {
const decoded = await verifyJWT(token)
if (!decoded) {
return { valid: false, error: 'invalid_token' }
}
return {
valid: true,
scopes: decoded.scope?.split(' ') || [],
subject: decoded.sub,
}
},
},
})

Return subject from your validator when you can. The server binds input_required continuations to it, so users sharing one OAuth client never share a continuation.

OAuth Middleware Options
Direct link to OAuth Middleware Options

oauth.resource:

string
The canonical URL of your MCP server. This is returned in Protected Resource Metadata.

oauth.authorizationServers:

string[]
URLs of authorization servers that can issue tokens for this resource.

oauth.scopesSupported?:

string[]
= ['mcp:read', 'mcp:write']
Scopes supported by this MCP server.

oauth.resourceName?:

string
Human-readable name for this resource server.

oauth.validateToken?:

(token: string, resource: string) => Promise<TokenValidationResult>
Function to validate access tokens. If not provided, tokens are accepted without validation (NOT recommended for production).

mcpPath?:

string
= '/mcp'
Path where the MCP endpoint is served. Only requests to this path require authentication.

Authentication context
Direct link to Authentication context

Tools can access request metadata via context.mcp.extra when using the HTTP transport. You can pass authentication info and user context, as well as custom data from your HTTP middleware to your MCP tools.

How it works
Direct link to How it works

Whatever you set on req.auth in your HTTP middleware becomes available as context.mcp.extra.authInfo in your tools and as requestContext.get('authInfo') in resource and prompt callbacks:

req.auth = { ... } โ†’ context.mcp.extra.authInfo = { ... }

Map auth data for FGA
Direct link to Map auth data for FGA

When an MCPServer is registered on a Mastra instance with a fine-grained authorization (FGA) provider, Mastra checks requestContext.get('user') before listing or calling tools. The HTTP transport passes authenticated data as extra.authInfo, so use mapAuthInfoToUser to set the user shape expected by your FGA provider.

const server = new MCPServer({
id: 'my-server',
name: 'My Server',
version: '1.0.0',
tools: { getUserData },
mapAuthInfoToUser: ({ authInfo }) => {
const user = authInfo as {
extra?: {
userId?: string
organizationMembershipId?: string
}
}

if (!user.extra?.userId) {
return null
}

return {
id: user.extra.userId,
organizationMembershipId: user.extra.organizationMembershipId,
}
},
})

Scope MCP tool FGA separately
Direct link to Scope MCP tool FGA separately

Use fga.resourceMapping and fga.permissionMapping when MCP clients need a different authorization scope than internal agent or workflow tool execution. The override applies only to tools/list and tools/call checks for this MCP server.

import { MastraFGAPermissions } from '@mastra/core/auth/ee'

const server = new MCPServer({
id: 'my-server',
name: 'My Server',
version: '1.0.0',
tools: { getUserData },
mapAuthInfoToUser: ({ authInfo }) => {
const user = authInfo as {
extra?: {
userId?: string
organizationMembershipId?: string
}
}

if (!user.extra?.userId) {
return null
}

return {
id: user.extra.userId,
organizationMembershipId: user.extra.organizationMembershipId,
}
},
fga: {
resourceMapping: {
tool: {
fgaResourceType: 'user',
deriveId: ({ user }) => (user as { id: string }).id,
},
},
permissionMapping: {
[MastraFGAPermissions.TOOLS_EXECUTE]: 'read',
},
},
})

Setting Up Authentication Middleware
Direct link to Setting Up Authentication Middleware

To pass data to your tools, populate req.auth on the Node.js request object in your HTTP server middleware before calling server.startHTTP().

import express from 'express'

type MCPAuthenticatedRequest = express.Request & {
auth?: {
token: string
clientId: string
scopes: string[]
expiresAt?: number
extra?: Record<string, unknown>
}
}

const app = express()

// Auth middleware - set req.auth before the MCP handler
app.use('/mcp', async (req, res, next) => {
const authorization = req.headers.authorization

if (!authorization?.startsWith('Bearer ')) {
res.status(401).json({ error: 'Missing bearer token' })
return
}

const token = authorization.slice('Bearer '.length)

try {
const user = await verifyToken(token)

// This entire object becomes context.mcp.extra.authInfo
const authenticatedRequest = req as MCPAuthenticatedRequest
authenticatedRequest.auth = {
token,
clientId: user.clientId,
scopes: user.scopes,
expiresAt: user.expiresAt,
extra: {
sub: user.userId,
email: user.email,
},
}
next()
} catch {
res.status(401).json({ error: 'Invalid or expired token' })
}
})

app.all('/mcp', async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`)
await server.startHTTP({ url, httpPath: '/mcp', req, res })
})

Set extra.sub (or extra.subject) to the user's identifier. The server uses it to bind input_required continuations to that user.

Accessing Auth Data in Tools
Direct link to Accessing Auth Data in Tools

The req.auth object is available as context.mcp.extra.authInfo in your tool's execute function:

execute: async (inputData, context) => {
// Access the auth data you set in middleware
const authInfo = context.mcp?.extra.authInfo

if (!authInfo?.extra?.sub) {
return { error: 'Authentication required' }
}

// Use the auth data
console.log('User ID:', authInfo.extra.sub)
console.log('Email:', authInfo.extra.email)

// Use a credential intended for the API being called, not the MCP token.
// See https://mastra.ai/docs/guides/mcp-authentication-authorization#access-upstream-services-safely
const credential = await apiCredentials.getForUser(authInfo.extra.sub as string)

const response = await fetch('/api/data', {
headers: { Authorization: `Bearer ${credential.token}` },
signal: context.mcp?.extra.signal,
})

return response.json()
}

Passing RequestContext through to agent
Direct link to passing-requestcontext-through-to-agent

context.requestContext already carries authInfo and the mapped user, so pass it straight through when a tool calls an agent:

execute: async (inputData, context) => {
const authInfo = context.mcp?.extra.authInfo

if (!authInfo?.extra?.sub) {
return { error: 'Authentication required' }
}

const agent = context.mastra?.getAgentById('some-agent-id')

if (!agent) {
return { error: "Agent 'some-agent-id' not found" }
}

const response = await agent.generate(prompt, { requestContext: context.requestContext })

return response.text
}

The extra Object
Direct link to the-extra-object

The full context.mcp.extra object contains:

PropertyDescription
authInfoWhatever you set on req.auth in your middleware
requestIdThe JSON-RPC id of the current request
signalAbortSignal for request cancellation
_metaRequest metadata sent by the client: W3C trace fields, the io.modelcontextprotocol/logLevel log-level opt-in, and progressToken
sendNotification, sendRequestDeprecated. The protocol has no server-initiated requests, so both throw with a message naming the replacement: context.mcp.log(), context.mcp.progress(), or context.suspend().

Complete Example
Direct link to Complete Example

Install jose to verify JSON Web Tokens (JWTs) against your identity provider's JSON Web Key Set (JWKS):

npm install jose

The following example validates the token's signature, issuer, audience, algorithm, expiration, and required claims before passing its user data to the tool:

import express from 'express'
import { createRemoteJWKSet, jwtVerify } from 'jose'
import { MCPServer } from '@mastra/mcp'
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

type MCPAuthenticatedRequest = express.Request & {
auth?: {
token: string
clientId: string
scopes: string[]
expiresAt?: number
extra?: Record<string, unknown>
}
}

const issuer = process.env.JWT_ISSUER
const audience = process.env.JWT_AUDIENCE
const jwksUri = process.env.JWT_JWKS_URI

if (!issuer || !audience || !jwksUri) {
throw new Error('JWT_ISSUER, JWT_AUDIENCE, and JWT_JWKS_URI are required')
}

const jwks = createRemoteJWKSet(new URL(jwksUri))

const verifyToken = async (token: string) => {
const { payload } = await jwtVerify(token, jwks, {
issuer,
audience,
algorithms: ['RS256'],
requiredClaims: ['exp'],
})

const clientId =
typeof payload.client_id === 'string'
? payload.client_id
: typeof payload.azp === 'string'
? payload.azp
: undefined

if (!payload.sub || typeof payload.email !== 'string' || !clientId || !payload.exp) {
throw new Error('Token must contain sub, email, exp, and client_id or azp claims')
}

return {
userId: payload.sub,
clientId,
email: payload.email,
expiresAt: payload.exp,
scopes: typeof payload.scope === 'string' ? payload.scope.split(' ') : [],
}
}

// 1. Define your tool that uses auth context
const getUserData = createTool({
id: 'get-user-data',
description: 'Fetches data for the authenticated user',
inputSchema: z.object({}),
execute: async (inputData, context) => {
const authInfo = context.mcp?.extra.authInfo

if (!authInfo?.extra?.sub) {
return { error: 'Authentication required' }
}

// Access the data you set in middleware
return {
userId: authInfo.extra.sub,
email: authInfo.extra.email,
}
},
})

// 2. Create the MCP server with your tools
const server = new MCPServer({
id: 'my-server',
name: 'My Server',
version: '1.0.0',
tools: { getUserData },
})

// 3. Set up Express with auth middleware
const app = express()

app.use('/mcp', async (req, res, next) => {
const authorization = req.headers.authorization

if (!authorization?.startsWith('Bearer ')) {
res.status(401).json({ error: 'Missing bearer token' })
return
}

const token = authorization.slice('Bearer '.length)

try {
const user = await verifyToken(token)

// This entire object becomes context.mcp.extra.authInfo
const authenticatedRequest = req as MCPAuthenticatedRequest
authenticatedRequest.auth = {
token,
clientId: user.clientId,
scopes: user.scopes,
expiresAt: user.expiresAt,
extra: {
sub: user.userId,
email: user.email,
},
}
next()
} catch {
res.status(401).json({ error: 'Invalid or expired token' })
}
})

app.all('/mcp', async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`)
await server.startHTTP({ url, httpPath: '/mcp', req, res })
})

app.listen(3000)

MCP Apps (appResources)
Direct link to mcp-apps-appresources

The appResources option lets you serve interactive HTML UIs from your MCP server via the MCP Apps extension. Each entry maps a ui:// URI to an HTML app that renders in a sandboxed iframe in Mastra Studio.

AppResources type
Direct link to appresources-type

Key (URI):

string
A ui:// URI that identifies the app resource (e.g., ui://calculator/main).

Each value is an AppResource object:

name:

string
Display name for the UI resource.

description?:

string
Optional description of the UI resource.

html?:

string
Inline HTML content for the UI. Provide either html or htmlPath.

htmlPath?:

string
Path to an HTML file. Resolved at server startup. Provide either html or htmlPath.

meta?:

McpUiResourceMeta
UI resource metadata (CSP, permissions, rendering preferences) from the official ext-apps SDK.

Example
Direct link to Example

import { MCPServer } from '@mastra/mcp'
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

const calculatorTool = createTool({
id: 'calculatorWithUI',
description: 'An interactive calculator',
inputSchema: z.object({
num1: z.number(),
num2: z.number(),
operation: z.enum(['add', 'subtract']),
}),
mcp: {
_meta: { ui: { resourceUri: 'ui://calculator/main' } },
},
execute: async ({ num1, num2, operation }) => {
const result = operation === 'add' ? num1 + num2 : num1 - num2
return {
content: [{ type: 'text', text: 'An interactive calculator is displayed.' }],
structuredContent: { result },
}
},
})

const server = new MCPServer({
id: 'app-server',
name: 'App Server',
version: '1.0.0',
tools: { calculatorTool },
appResources: {
'ui://calculator/main': {
name: 'Interactive Calculator',
html: '<html><body><h2>Calculator</h2>...</body></html>',
},
},
})

Link a tool to its app resource by setting mcp._meta.ui.resourceUri in createTool() to the matching ui:// URI. The server normalizes this metadata for older hosts when listing tools, and returns the same link on successful tool call results. Visit MCP Apps for the full app bridge API and usage patterns.