Back to Mastra

Reference: MCPServer | Tools & MCP

docs/src/content/en/reference/tools/mcp-server.mdx

2025-12-1846.7 KB
Original Source

MCPServer

The MCPServer class provides the functionality to expose your existing Mastra tools and Agents as a Model Context Protocol (MCP) server. This allows any MCP client (like Cursor, Windsurf, or Claude Desktop) to 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 both stdio (subprocess) and SSE (HTTP) MCP transports.

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.

typescript
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.4',
})

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"
  },
})

Configuration Properties

The constructor accepts an MCPServerConfig object with the following properties:

<PropertiesTable content={[ { name: 'id', type: 'string', isOptional: false, description: '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: 'name', type: 'string', isOptional: false, description: "A descriptive name for your server (e.g., 'My Weather and Agent Server').", }, { name: 'version', type: 'string', isOptional: false, description: "The semantic version of your server (e.g., '1.0.0').", }, { name: 'tools', type: 'ToolsInput', isOptional: false, description: '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.', }, { name: 'agents', type: 'Record<string, Agent>', isOptional: true, description: "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.", }, { name: 'workflows', type: 'Record<string, Workflow>', isOptional: true, description: "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.", }, { name: 'description', type: 'string', isOptional: true, description: 'Optional description of what the MCP server does.', }, { name: 'instructions', type: 'string', isOptional: true, description: 'Optional instructions describing how to use the server and its features.', }, { name: 'repository', type: 'Repository', // { url: string; source: string; id: string; } isOptional: true, description: "Optional repository information for the server's source code.", }, { name: 'releaseDate', type: 'string', // ISO 8601 isOptional: true, description: 'Optional release date of this server version (ISO 8601 string). Defaults to the time of instantiation if not provided.', }, { name: 'isLatest', type: 'boolean', isOptional: true, description: 'Optional flag indicating if this is the latest version. Defaults to true if not provided.', }, { name: 'packageCanonical', type: "'npm' | 'docker' | 'pypi' | 'crates' | string", isOptional: true, description: "Optional canonical packaging format if the server is distributed as a package (e.g., 'npm', 'docker').", }, { name: 'packages', type: 'PackageInfo[]', isOptional: true, description: 'Optional list of installable packages for this server.', }, { name: 'remotes', type: 'RemoteInfo[]', isOptional: true, description: 'Optional list of remote access points for this server.', }, { name: 'resources', type: 'MCPServerResources', isOptional: true, description: 'An object defining how the server should handle MCP resources. See Resource Handling section for details.', }, { name: 'prompts', type: 'MCPServerPrompts', isOptional: true, description: 'An object defining how the server should handle MCP prompts. See Prompt Handling section for details.', }, ]} />

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 named 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 named 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 generate() method of the corresponding agent, passing 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., you have a tool named ask_myAgentKey and also an agent with the key 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

When you provide agents in the agents configuration property, MCPServer will automatically create a corresponding tool for each agent. The tool will be named 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>".

Important: 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({ 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.

This allows you to quickly expose the generative capabilities of your agents through the MCP, enabling clients to "ask" your agents questions directly.

Accessing MCP Context in Tools

Tools exposed through MCPServer can access MCP request context (authentication, session IDs, etc.) via two different properties depending on how the tool is invoked:

Call PatternAccess Method
Direct tool callcontext?.mcp?.extra
Agent tool callcontext?.requestContext?.get("mcp.extra")

Universal pattern (works in both contexts):

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

Example: Tool that works in both contexts

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

const fetchUserData = createTool({
  id: 'fetchUserData',
  description: 'Fetches user data using authentication from MCP context',
  inputSchema: z.object({
    userId: z.string().describe('The ID of the user to fetch'),
  }),
  execute: async (inputData, context) => {
    // Access MCP authentication context
    // When called directly via MCP: context.mcp.extra
    // When called via agent: context.requestContext.get('mcp.extra')
    const mcpExtra = context?.mcp?.extra || context?.requestContext?.get('mcp.extra')
    const authInfo = mcpExtra?.authInfo

    if (!authInfo?.token) {
      throw new Error('Authentication required')
    }

    const response = await fetch(`https://api.example.com/users/${inputData.userId}`, {
      headers: {
        Authorization: `Bearer ${authInfo.token}`,
      },
    })

    return response.json()
  },
})

Methods

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

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.

typescript
async startStdio(): Promise<void>

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

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

startSSE()

This method helps you integrate the MCP server with an existing web server to use Server-Sent Events (SSE) for communication. You'll call this from your web server's code when it receives a request for the SSE or message paths.

typescript
async startSSE({
  url,
  ssePath,
  messagePath,
  req,
  res,
}: {
  url: URL;
  ssePath: string;
  messagePath: string;
  req: any;
  res: any;
}): Promise<void>

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

typescript
import http from 'http'

const httpServer = http.createServer(async (req, res) => {
  await server.startSSE({
    url: new URL(req.url || '', `http://localhost:1234`),
    ssePath: '/sse',
    messagePath: '/message',
    req,
    res,
  })
})

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

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

<PropertiesTable content={[ { name: 'url', type: 'URL', description: 'The web address the user is requesting.', }, { name: 'ssePath', type: 'string', description: "The specific part of the URL where clients will connect for SSE (e.g., '/sse').", }, { name: 'messagePath', type: 'string', description: "The specific part of the URL where clients will send messages (e.g., '/message').", }, { name: 'req', type: 'any', description: 'The incoming request object from your web server.', }, { name: 'res', type: 'any', description: 'The response object from your web server, used to send data back.', }, ]} />

startHonoSSE()

This method helps you integrate the MCP server with an existing web server to use Server-Sent Events (SSE) for communication. You'll call this from your web server's code when it receives a request for the SSE or message paths.

typescript
async startHonoSSE({
  url,
  ssePath,
  messagePath,
  req,
  res,
}: {
  url: URL;
  ssePath: string;
  messagePath: string;
  req: any;
  res: any;
}): Promise<void>

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

typescript
import http from 'http'

const httpServer = http.createServer(async (req, res) => {
  await server.startHonoSSE({
    url: new URL(req.url || '', `http://localhost:1234`),
    ssePath: '/hono-sse',
    messagePath: '/message',
    req,
    res,
  })
})

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

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

<PropertiesTable content={[ { name: 'url', type: 'URL', description: 'The web address the user is requesting.', }, { name: 'ssePath', type: 'string', description: "The specific part of the URL where clients will connect for SSE (e.g., '/hono-sse').", }, { name: 'messagePath', type: 'string', description: "The specific part of the URL where clients will send messages (e.g., '/message').", }, { name: 'req', type: 'any', description: 'The incoming request object from your web server.', }, { name: 'res', type: 'any', description: 'The response object from your web server, used to send data back.', }, ]} />

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.

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

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/http:

typescript
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,
    options: {
      sessionIdGenerator: () => randomUUID(),
    },
  })
})

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

For serverless environments (Supabase Edge Functions, Cloudflare Workers, Vercel Edge, etc.), use serverless: true to enable stateless operation:

typescript
// Supabase Edge Function example
import { serve } from 'https://deno.land/[email protected]/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,
      options: {
        serverless: true, // ← Enable stateless mode for serverless
      },
    })

    return toFetchResponse(nodeRes)
  }

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

:::info[When to use serverless: true]

Use serverless: true when deploying to environments where each request runs in a fresh, stateless execution context:

  • Supabase Edge Functions
  • Cloudflare Workers
  • Vercel Edge Functions
  • Netlify Edge Functions
  • AWS Lambda
  • Deno Deploy

Use the default session-based mode (without serverless: true) for:

  • Long-lived Node.js servers
  • Docker containers
  • Traditional hosting (VPS, dedicated servers)

The serverless mode disables session management and creates fresh server instances per request, which is necessary for stateless environments where memory doesn't persist between invocations.

Note: The following MCP features require session state or persistent connections and won't work in serverless mode:

  • Elicitation - Interactive user input requests during tool execution require session management to route responses back to the correct client
  • Resource subscriptions - resources/subscribe and resources/unsubscribe need persistent connections to maintain subscription state
  • Resource update notifications - resources.notifyUpdated() requires active subscriptions and persistent connections to notify clients
  • Prompt list change notifications - prompts.notifyListChanged() requires persistent connections to push updates to clients

These features work normally in long-lived server environments (Node.js servers, Docker containers, etc.).

:::

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

<PropertiesTable content={[ { name: 'url', type: 'URL', description: 'The web address the user is requesting.', }, { name: 'httpPath', type: 'string', description: "The specific part of the URL where the MCP server will handle HTTP requests (e.g., '/mcp').", }, { name: 'req', type: 'http.IncomingMessage', description: 'The incoming request object from your web server.', }, { name: 'res', type: 'http.ServerResponse', description: 'The response object from your web server, used to send data back.', }, { name: 'options', type: 'StreamableHTTPServerTransportOptions', description: 'Optional configuration for the HTTP transport. See the options table below for more details.', optional: true, }, ]} />

The StreamableHTTPServerTransportOptions object allows you to customize the behavior of the HTTP transport. Here are the available options:

<PropertiesTable content={[ { name: 'serverless', type: 'boolean', description: 'If true, runs in stateless mode without session management. Each request is handled independently with a fresh server instance. Essential for serverless environments (Cloudflare Workers, Supabase Edge Functions, Vercel Edge, etc.) where sessions cannot persist between invocations. Defaults to false.', optional: true, }, { name: 'sessionIdGenerator', type: '(() => string) | undefined', description: 'A function that generates a unique session ID. This should be a cryptographically secure, globally unique string. Return undefined to disable session management.', }, { name: 'onsessioninitialized', type: '(sessionId: string) => void', description: 'A callback that is invoked when a new session is initialized. This is useful for tracking active MCP sessions.', optional: true, }, { name: 'enableJsonResponse', type: 'boolean', description: 'If true, the server will return plain JSON responses instead of using Server-Sent Events (SSE) for streaming. Defaults to false.', optional: true, }, { name: 'eventStore', type: 'EventStore', description: 'An event store for message resumability. Providing this enables clients to reconnect and resume message streams.', optional: true, }, ]} />

close()

This method closes the server and releases all resources.

typescript
async close(): Promise<void>

getServerInfo()

This method gives you a look at the server's basic information.

typescript
getServerInfo(): ServerInfo

getServerDetail()

This method gives you a detailed look at the server's information.

typescript
getServerDetail(): ServerDetail

getToolListInfo()

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

typescript
getToolListInfo(): ToolListInfo

getToolInfo()

This method gives you detailed information about a specific tool.

typescript
getToolInfo(toolName: string): ToolInfo

executeTool()

This method executes a specific tool and returns the result.

typescript
executeTool(toolName: string, input: any): Promise<any>

getStdioTransport()

If you started the server with startStdio(), you can use this to get the object that manages the stdio communication. This is mostly for checking things internally or for testing.

typescript
getStdioTransport(): StdioServerTransport | undefined

getSseTransport()

If you started the server with startSSE(), you can use this to get the object that manages the SSE communication. Like getStdioTransport, this is mainly for internal checks or testing.

typescript
getSseTransport(): SSEServerTransport | undefined

getSseHonoTransport()

If you started the server with startHonoSSE(), you can use this to get the object that manages the SSE communication. Like getSseTransport, this is mainly for internal checks or testing.

typescript
getSseHonoTransport(): SSETransport | undefined

getStreamableHTTPTransport()

If you started the server with startHTTP(), you can use this to get the object that manages the HTTP communication. Like getSseTransport, this is mainly for internal checks or testing.

typescript
getStreamableHTTPTransport(): StreamableHTTPServerTransport | undefined

tools()

Executes a specific tool provided by this MCP server.

typescript
async executeTool(
  toolId: string,
  args: any,
  executionContext?: { messages?: any[]; toolCallId?: string },
): Promise<any>

<PropertiesTable content={[ { name: 'toolId', type: 'string', description: 'The ID/name of the tool to execute.', }, { name: 'args', type: 'any', description: "The arguments to pass to the tool's execute function.", }, { name: 'executionContext', type: 'object', isOptional: true, description: 'Optional context for the tool execution, like messages or a toolCallId.', }, ]} />

Resource handling

What are MCP Resources?

Resources are a core primitive in the Model Context Protocol (MCP) that allow servers to expose data and content that can be read by clients and used as context for LLM interactions. They represent any kind of data that an MCP server wants to make available, such as:

  • 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 dynamic 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 has subscribed to that resource.

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

MCPServerResources Type

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

typescript
export type MCPServerResources = {
  // Callback to list available resources
  listResources: () => Promise<Resource[]>

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

  // Optional callback to list available resource templates
  resourceTemplates?: () => Promise<ResourceTemplate[]>
}

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

Example:

typescript
import { MCPServer } from '@mastra/mcp'
import type { MCPServerResourceContent, 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 }) => {
    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

If the available resources or their content change, your server can notify connected clients that are subscribed to the specific resource.

server.resources.notifyUpdated({ uri: string })

Call this method when the content of a specific resource (identified by its uri) has been updated. If any clients are subscribed to this URI, they will receive a notifications/resources/updated message.

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

Example:

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

server.resources.notifyListChanged()

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

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

Example:

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

Prompt handling

What are MCP Prompts?

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

Prompts are identified by a unique name (and optional version) and can be dynamic or static.

MCPServerPrompts Type

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

typescript
export type MCPServerPrompts = {
  // Callback to list available prompts
  listPrompts: () => Promise<Prompt[]>

  // Callback to get the messages/content for a specific prompt
  getPromptMessages?: ({
    name,
    version,
    args,
  }: {
    name: string
    version?: string
    args?: any
  }) => Promise<{ prompt: Prompt; messages: PromptMessage[] }>
}

Example:

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

const prompts: Prompt[] = [
  {
    name: 'analyze-code',
    description: 'Analyze code for improvements',
    version: 'v1',
  },
  {
    name: 'analyze-code',
    description: 'Analyze code for improvements (new logic)',
    version: 'v2',
  },
]

const myPromptHandlers: MCPServerPrompts = {
  listPrompts: async () => prompts,
  getPromptMessages: async ({ name, version, args }) => {
    if (name === 'analyze-code') {
      if (version === 'v2') {
        const prompt = prompts.find(p => p.name === name && p.version === 'v2')
        if (!prompt) throw new Error('Prompt version not found')
        return {
          prompt,
          messages: [
            {
              role: 'user',
              content: {
                type: 'text',
                text: `Analyze this code with the new logic: ${args.code}`,
              },
            },
          ],
        }
      }
      // Default or v1
      const prompt = prompts.find(p => p.name === name && p.version === 'v1')
      if (!prompt) throw new Error('Prompt version not found')
      return {
        prompt,
        messages: [
          {
            role: 'user',
            content: { type: 'text', text: `Analyze this code: ${args.code}` },
          },
        ],
      }
    }
    throw new Error('Prompt not found')
  },
}

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

Notifying Clients of Prompt Changes

If the available prompts change, your server can notify connected clients:

server.prompts.notifyListChanged()

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

typescript
await serverWithPrompts.prompts.notifyListChanged()

Best practices for Prompt Handling

  • Use clear, descriptive prompt names and descriptions.
  • Validate all required arguments in getPromptMessages.
  • Include a version field if you expect to make breaking changes.
  • Use the version parameter to select the correct prompt logic.
  • Notify clients when prompt lists change.
  • Handle errors with informative messages.
  • Document argument expectations and available versions.

Examples

For practical examples of setting up and deploying an MCPServer, see the Publishing an MCP Server guide.

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

Elicitation

What's Elicitation?

Elicitation is a feature in the Model Context Protocol (MCP) that allows servers to request structured information from users. This enables interactive workflows where servers can collect additional data dynamically.

The MCPServer class automatically includes elicitation capabilities. Tools receive a context.mcp object in their execute function that includes an elicitation.sendRequest() method for requesting user input.

Tool Execution Signature

When tools are executed within an MCP server context, they receive MCP-specific capabilities via the context.mcp object:

typescript
execute: async (inputData, context) => {
  // input contains the tool's inputData parameters
  // context.mcp contains server capabilities like elicitation and authentication info

  // Access authentication information (when available)
  if (context.mcp?.extra?.authInfo) {
    console.log('Authenticated request from:', context.mcp.extra.authInfo.clientId)
  }

  // Use elicitation capabilities
  const result = await context.mcp.elicitation.sendRequest({
    message: 'Please provide information',
    requestedSchema: {
      /* schema */
    },
  })

  return result
}

How Elicitation Works

A common use case is during tool execution. When a tool needs user input, it can use the elicitation functionality provided through the context parameter:

  1. The tool calls context.mcp.elicitation.sendRequest() with a message and schema
  2. The request is sent to the connected MCP client
  3. The client presents the request to the user (via UI, command line, etc.)
  4. The user provides input, declines, or cancels the request
  5. The client sends the response back to the server
  6. The tool receives the response and continues execution

Using Elicitation in Tools

Here's an example of a tool that uses elicitation to collect user contact information:

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

const server = new MCPServer({
  id: 'interactive-server',
  name: 'Interactive Server',
  version: '1.0.0',
  tools: {
    collectContactInfo: createTool({
      id: 'collectContactInfo',
      description: 'Collects user contact information through elicitation',
      inputSchema: z.object({
        reason: z.string().optional().describe('Reason for collecting contact info'),
      }),
      execute: async (inputData, context) => {
        const { reason } = inputData

        // Log session info if available
        console.log('Request from session:', context.mcp?.extra?.sessionId)

        try {
          // Request user input via elicitation
          const result = await context.mcp.elicitation.sendRequest({
            message: reason
              ? `Please provide your contact information. ${reason}`
              : 'Please provide your contact information',
            requestedSchema: {
              type: 'object',
              properties: {
                name: {
                  type: 'string',
                  title: 'Full Name',
                  description: 'Your full name',
                },
                email: {
                  type: 'string',
                  title: 'Email Address',
                  description: 'Your email address',
                  format: 'email',
                },
                phone: {
                  type: 'string',
                  title: 'Phone Number',
                  description: 'Your phone number (optional)',
                },
              },
              required: ['name', 'email'],
            },
          })

          // Handle the user's response
          if (result.action === 'accept') {
            return `Contact information collected: ${JSON.stringify(result.content, null, 2)}`
          } else if (result.action === 'decline') {
            return 'Contact information collection was declined by the user.'
          } else {
            return 'Contact information collection was cancelled by the user.'
          }
        } catch (error) {
          return `Error collecting contact information: ${error}`
        }
      },
    }),
  },
})

Elicitation Request Schema

The requestedSchema must be a flat object with primitive properties only. Supported types include:

  • String: { type: 'string', title: 'Display Name', description: 'Help text' }
  • Number: { type: 'number', minimum: 0, maximum: 100 }
  • Boolean: { type: 'boolean', default: false }
  • Enum: { type: 'string', enum: ['option1', 'option2'] }

Example schema:

typescript
{
  type: 'object',
  properties: {
    name: {
      type: 'string',
      title: 'Full Name',
      description: 'Your complete name',
    },
    age: {
      type: 'number',
      title: 'Age',
      minimum: 18,
      maximum: 120,
    },
    newsletter: {
      type: 'boolean',
      title: 'Subscribe to Newsletter',
      default: false,
    },
  },
  required: ['name'],
}

Response Actions

Users can respond to elicitation requests in three ways:

  1. Accept (action: 'accept'): User provided data and confirmed submission
    • Contains content field with the submitted data
  2. Decline (action: 'decline'): User explicitly declined to provide information
    • No content field
  3. Cancel (action: 'cancel'): User dismissed the request without deciding
    • No content field

Tools should handle all three response types appropriately.

Security Considerations

  • Never request sensitive information like passwords, SSNs, or credit card numbers
  • Validate all user input against the provided schema
  • Handle declining and cancellation gracefully
  • Provide clear reasons for data collection
  • Respect user privacy and preferences

Tool Execution API

The elicitation functionality is available through the options parameter in tool execution:

typescript
// Within a tool's execute function
execute: async (inputData, context) => {
  // Use elicitation for user input
  const result = await context.mcp.elicitation.sendRequest({
    message: string,           // Message to display to user
    requestedSchema: object    // JSON schema defining expected response structure
  }): Promise<ElicitResult>

  // Access authentication info if needed
  if (context.mcp?.extra?.authInfo) {
    // Use context.mcp.extra.authInfo.token, etc.
  }
}

Note that elicitation is session-aware when using HTTP-based transports (SSE or HTTP). This means that when multiple clients are connected to the same server, elicitation requests are routed to the correct client session that initiated the tool execution.

The ElicitResult type:

typescript
type ElicitResult = {
  action: 'accept' | 'decline' | 'cancel'
  content?: any // Only present when action is 'accept'
}

OAuth protection

To protect your MCP server with OAuth authentication per the MCP Auth Specification, use the createOAuthMiddleware function:

typescript
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

For production, use proper token validation:

typescript
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,
      }
    },
  },
})

OAuth Middleware Options

<PropertiesTable content={[ { name: 'oauth.resource', type: 'string', description: 'The canonical URL of your MCP server. This is returned in Protected Resource Metadata.', }, { name: 'oauth.authorizationServers', type: 'string[]', description: 'URLs of authorization servers that can issue tokens for this resource.', }, { name: 'oauth.scopesSupported', type: 'string[]', isOptional: true, defaultValue: "['mcp:read', 'mcp:write']", description: 'Scopes supported by this MCP server.', }, { name: 'oauth.resourceName', type: 'string', isOptional: true, description: 'Human-readable name for this resource server.', }, { name: 'oauth.validateToken', type: '(token: string, resource: string) => Promise<TokenValidationResult>', isOptional: true, description: 'Function to validate access tokens. If not provided, tokens are accepted without validation (NOT recommended for production).', }, { name: 'mcpPath', type: 'string', isOptional: true, defaultValue: "'/mcp'", description: 'Path where the MCP endpoint is served. Only requests to this path require authentication.', }, ]} />

Authentication context

Tools can access request metadata via context.mcp.extra when using HTTP-based transports. This allows you to pass authentication info, user context, or any custom data from your HTTP middleware to your MCP tools.

How it works

Whatever you set on req.auth in your HTTP middleware becomes available as context.mcp.extra.authInfo in your tools:

req.auth = { ... }  →  context?.mcp?.extra?.authInfo.extra = { ... }

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().

typescript
import express from 'express'

const app = express()

// Auth middleware - set req.auth before the MCP handler
app.use('/mcp', (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '')
  const user = verifyToken(token)

  // This entire object becomes context.mcp.extra.authInfo
  // @ts-ignore - req.auth is read by the MCP SDK
  req.auth = {
    token,
    userId: user.userId,
    email: user.email,
  }
  next()
})

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

Accessing Auth Data in Tools

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

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

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

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

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

  return response.json()
}

Passing RequestContext through to agent

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

  const requestContext = context.requestContext || new RequestContext().set('someKey', authInfo)

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

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

  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 })

  return response.text
}

The extra Object

The full context.mcp.extra object contains:

PropertyDescription
authInfoWhatever you set on req.auth in your middleware
sessionIdSession identifier for the MCP connection
signalAbortSignal for request cancellation
sendNotificationMCP protocol function for sending notifications
sendRequestMCP protocol function for sending requests

Complete Example

Here's a complete example showing the data flow from middleware to tool:

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

const verifyToken = (token: string) => {
  // TODO: Implement token verification
  return {
    userId: '123',
    email: '[email protected]',
  }
}

// 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?.userId) {
      return { error: 'Authentication required' }
    }

    // Access the data you set in middleware
    return {
      userId: authInfo.extra.userId,
      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', (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '')
  const user = verifyToken(token)

  // This entire object becomes context.mcp.extra.authInfo
  // @ts-ignore - req.auth is read by the MCP SDK
  req.auth = {
    token,
    userId: user.userId,
    email: user.email,
  }
  next()
})

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)