docs/src/content/en/reference/tools/mcp-client.mdx
The MCPClient class provides a way to manage multiple MCP server connections and their tools in a Mastra application. It handles connection lifecycle, tool namespacing, and provides access to tools across all configured servers.
Creates a new instance of the MCPClient class.
constructor({
id?: string;
servers: Record<string, MastraMCPServerDefinition>;
timeout?: number;
}: MCPClientOptions)
<PropertiesTable content={[ { name: 'id', type: 'string', isOptional: true, description: 'Optional unique identifier for the configuration instance. Use this to prevent memory leaks when creating multiple instances with identical configurations.', }, { name: 'servers', type: 'Record<string, MastraMCPServerDefinition>', description: 'A map of server configurations, where each key is a unique server identifier and the value is the server configuration.', }, { name: 'timeout', type: 'number', isOptional: true, defaultValue: '60000', description: 'Global timeout value in milliseconds for all servers unless overridden in individual server configs.', }, ]} />
MastraMCPServerDefinitionEach server in the servers map is configured using the MastraMCPServerDefinition type. The transport type is detected based on the provided parameters:
command is provided, it uses the Stdio transport.url is provided, it first attempts to use the Streamable HTTP transport and falls back to the legacy SSE transport if the initial connection fails.<PropertiesTable
content={[
{
name: 'command',
type: 'string',
isOptional: true,
description: 'For Stdio servers: The command to execute.',
},
{
name: 'args',
type: 'string[]',
isOptional: true,
description: 'For Stdio servers: Arguments to pass to the command.',
},
{
name: 'env',
type: 'Record<string, string>',
isOptional: true,
description: 'For Stdio servers: Environment variables to set for the command.',
},
{
name: 'url',
type: 'URL',
isOptional: true,
description: 'For HTTP servers (Streamable HTTP or SSE): The URL of the server.',
},
{
name: 'requestInit',
type: 'RequestInit',
isOptional: true,
description: 'For HTTP servers: Request configuration for the fetch API.',
},
{
name: 'eventSourceInit',
type: 'EventSourceInit',
isOptional: true,
description:
'For SSE fallback: Custom fetch configuration for SSE connections. Required when using custom headers with SSE.',
},
{
name: 'fetch',
type: 'MastraFetchLike',
isOptional: true,
description:
'For HTTP servers: Custom fetch implementation used for all network requests. Receives an optional third requestContext parameter containing request-scoped data (e.g., authentication cookies, bearer tokens) from the incoming request. When provided, this function will be used for all HTTP requests, allowing you to add dynamic authentication headers, forward request-scoped credentials to the MCP server, customize request behavior per-request, or intercept and modify requests/responses. When fetch is provided, requestInit, eventSourceInit, and authProvider become optional, as you can handle these concerns within your custom fetch function.',
},
{
name: 'logger',
type: 'LogHandler',
isOptional: true,
description: 'Optional additional handler for logging.',
},
{
name: 'timeout',
type: 'number',
isOptional: true,
description: 'Server-specific timeout in milliseconds.',
},
{
name: 'capabilities',
type: 'ClientCapabilities',
isOptional: true,
description: 'Server-specific capabilities configuration.',
},
{
name: 'authProvider',
type: 'OAuthClientProvider',
isOptional: true,
description:
'For HTTP servers: OAuth authentication provider for automatic token refresh and OAuth flow management. Use MCPOAuthClientProvider for a ready-to-use implementation.',
},
{
name: 'enableServerLogs',
type: 'boolean',
isOptional: true,
defaultValue: 'true',
description: 'Whether to enable logging for this server.',
},
]}
/>
listTools()Retrieves all tools from all configured servers, with tool names namespaced by their server name (in the format serverName_toolName) to prevent conflicts.
Intended to be passed onto an Agent definition.
new Agent({ id: 'agent', tools: await mcp.listTools() })
listToolsets()Returns an object mapping namespaced tool names (in the format serverName.toolName) to their tool implementations.
Intended to be passed dynamically into the generate or stream method.
const res = await agent.stream(prompt, {
toolsets: await mcp.listToolsets(),
})
disconnect()Disconnects from all MCP servers and cleans up resources.
async disconnect(): Promise<void>
resources PropertyThe MCPClient instance has a resources property that provides access to resource-related operations.
const mcpClient = new MCPClient({
/* ...servers configuration... */
})
// Access resource methods via mcpClient.resources
const allResourcesByServer = await mcpClient.resources.list()
const templatesByServer = await mcpClient.resources.templates()
// ... and so on for other resource methods.
resources.list()Retrieves all available resources from all connected MCP servers, grouped by server name.
async list(): Promise<Record<string, Resource[]>>
Example:
const resourcesByServer = await mcpClient.resources.list()
for (const serverName in resourcesByServer) {
console.log(`Resources from ${serverName}:`, resourcesByServer[serverName])
}
resources.templates()Retrieves all available resource templates from all connected MCP servers, grouped by server name.
async templates(): Promise<Record<string, ResourceTemplate[]>>
Example:
const templatesByServer = await mcpClient.resources.templates()
for (const serverName in templatesByServer) {
console.log(`Templates from ${serverName}:`, templatesByServer[serverName])
}
resources.read(serverName: string, uri: string)Reads the content of a specific resource from a named server.
async read(serverName: string, uri: string): Promise<ReadResourceResult>
serverName: The identifier of the server (key used in the servers constructor option).uri: The URI of the resource to read.Example:
const content = await mcpClient.resources.read('myWeatherServer', 'weather://current')
console.log('Current weather:', content.contents[0].text)
resources.subscribe(serverName: string, uri: string)Subscribes to updates for a specific resource on a named server.
async subscribe(serverName: string, uri: string): Promise<object>
Example:
await mcpClient.resources.subscribe('myWeatherServer', 'weather://current')
resources.unsubscribe(serverName: string, uri: string)Unsubscribes from updates for a specific resource on a named server.
async unsubscribe(serverName: string, uri: string): Promise<object>
Example:
await mcpClient.resources.unsubscribe('myWeatherServer', 'weather://current')
resources.onUpdated(serverName: string, handler: (params: { uri: string }) => void)Sets a notification handler that will be called when a subscribed resource on a specific server is updated.
async onUpdated(serverName: string, handler: (params: { uri: string }) => void): Promise<void>
Example:
mcpClient.resources.onUpdated('myWeatherServer', params => {
console.log(`Resource updated on myWeatherServer: ${params.uri}`)
// You might want to re-fetch the resource content here
// await mcpClient.resources.read("myWeatherServer", params.uri);
})
resources.onListChanged(serverName: string, handler: () => void)Sets a notification handler that will be called when the overall list of available resources changes on a specific server.
async onListChanged(serverName: string, handler: () => void): Promise<void>
Example:
mcpClient.resources.onListChanged('myWeatherServer', () => {
console.log('Resource list changed on myWeatherServer.')
// You should re-fetch the list of resources
// await mcpClient.resources.list();
})
elicitation PropertyThe MCPClient instance has an elicitation property that provides access to elicitation-related operations. Elicitation allows MCP servers to request structured information from users.
const mcpClient = new MCPClient({
/* ...servers configuration... */
})
// Set up elicitation handler
mcpClient.elicitation.onRequest('serverName', async request => {
// Handle elicitation request from server
console.log('Server requests:', request.message)
console.log('Schema:', request.requestedSchema)
// Return user response
return {
action: 'accept',
content: { name: 'John Doe', email: '[email protected]' },
}
})
elicitation.onRequest(serverName: string, handler: ElicitationHandler)Sets up a handler function that will be called when any connected MCP server sends an elicitation request. The handler receives the request and must return a response.
ElicitationHandler FunctionThe handler function receives a request object with:
message: A human-readable message describing what information is neededrequestedSchema: A JSON schema defining the structure of the expected responseThe handler must return an ElicitResult with:
action: One of 'accept', 'decline', or 'cancel'content: The user's data (only when action is 'accept')Example:
mcpClient.elicitation.onRequest('serverName', async request => {
console.log(`Server requests: ${request.message}`)
// Example: Simple user input collection
if (request.requestedSchema.properties.name) {
// Simulate user accepting and providing data
return {
action: 'accept',
content: {
name: 'Alice Smith',
email: '[email protected]',
},
}
}
// Simulate user declining the request
return { action: 'decline' }
})
Complete Interactive Example:
import { MCPClient } from '@mastra/mcp'
import { createInterface } from 'readline'
const readline = createInterface({
input: process.stdin,
output: process.stdout,
})
function askQuestion(question: string): Promise<string> {
return new Promise(resolve => {
readline.question(question, answer => resolve(answer.trim()))
})
}
const mcpClient = new MCPClient({
servers: {
interactiveServer: {
url: new URL('http://localhost:3000/mcp'),
},
},
})
// Set up interactive elicitation handler
await mcpClient.elicitation.onRequest('interactiveServer', async request => {
console.log(`\nš Server Request: ${request.message}`)
console.log('Required information:')
const schema = request.requestedSchema
const properties = schema.properties || {}
const required = schema.required || []
const content: Record<string, any> = {}
// Collect input for each field
for (const [fieldName, fieldSchema] of Object.entries(properties)) {
const field = fieldSchema as any
const isRequired = required.includes(fieldName)
let prompt = `${field.title || fieldName}`
if (field.description) prompt += ` (${field.description})`
if (isRequired) prompt += ' *required*'
prompt += ': '
const answer = await askQuestion(prompt)
// Handle cancellation
if (answer.toLowerCase() === 'cancel') {
return { action: 'cancel' }
}
// Validate required fields
if (answer === '' && isRequired) {
console.log(`ā ${fieldName} is required`)
return { action: 'decline' }
}
if (answer !== '') {
content[fieldName] = answer
}
}
// Confirm submission
console.log('\nš You provided:')
console.log(JSON.stringify(content, null, 2))
const confirm = await askQuestion('\nSubmit this information? (yes/no/cancel): ')
if (confirm.toLowerCase() === 'yes' || confirm.toLowerCase() === 'y') {
return { action: 'accept', content }
} else if (confirm.toLowerCase() === 'cancel') {
return { action: 'cancel' }
} else {
return { action: 'decline' }
}
})
prompts PropertyThe MCPClient instance has a prompts property that provides access to prompt-related operations.
const mcpClient = new MCPClient({
/* ...servers configuration... */
})
// Access prompt methods via mcpClient.prompts
const allPromptsByServer = await mcpClient.prompts.list()
const { prompt, messages } = await mcpClient.prompts.get({
serverName: 'myWeatherServer',
name: 'current',
})
prompts.list()Retrieves all available prompts from all connected MCP servers, grouped by server name.
async list(): Promise<Record<string, Prompt[]>>
Example:
const promptsByServer = await mcpClient.prompts.list()
for (const serverName in promptsByServer) {
console.log(`Prompts from ${serverName}:`, promptsByServer[serverName])
}
prompts.get({ serverName, name, args?, version? })Retrieves a specific prompt and its messages from a server.
async get({
serverName,
name,
args?,
version?,
}: {
serverName: string;
name: string;
args?: Record<string, any>;
version?: string;
}): Promise<{ prompt: Prompt; messages: PromptMessage[] }>
Example:
const { prompt, messages } = await mcpClient.prompts.get({
serverName: 'myWeatherServer',
name: 'current',
args: { location: 'London' },
})
console.log(prompt)
console.log(messages)
prompts.onListChanged(serverName: string, handler: () => void)Sets a notification handler that will be called when the list of available prompts changes on a specific server.
async onListChanged(serverName: string, handler: () => void): Promise<void>
Example:
mcpClient.prompts.onListChanged('myWeatherServer', () => {
console.log('Prompt list changed on myWeatherServer.')
// You should re-fetch the list of prompts
// await mcpClient.prompts.list();
})
progress PropertyThe MCPClient instance has a progress property for subscribing to progress notifications emitted by MCP servers while tools execute.
const mcpClient = new MCPClient({
servers: {
myServer: {
url: new URL('http://localhost:4111/api/mcp/myServer/mcp'),
// Enabled by default; set to false to disable
enableProgressTracking: true,
},
},
})
// Subscribe to progress updates for a specific server
await mcpClient.progress.onUpdate('myServer', params => {
console.log('š Progress:', params.progress, '/', params.total)
if (params.message) console.log('Message:', params.message)
if (params.progressToken) console.log('Token:', params.progressToken)
})
progress.onUpdate(serverName: string, handler)Registers a handler function to receive progress updates from the specified server.
async onUpdate(
serverName: string,
handler: (params: {
progressToken: string;
progress: number;
total?: number;
message?: string;
}) => void,
): Promise<void>
Notes:
enableProgressTracking is true (default), tool calls include a progressToken so you can correlate updates to a specific run.runId when executing a tool, it will be used as the progressToken.To disable progress tracking for a server:
const mcpClient = new MCPClient({
servers: {
myServer: {
url: new URL('http://localhost:4111/api/mcp/myServer/mcp'),
enableProgressTracking: false,
},
},
})
Elicitation is a feature that allows MCP servers to request structured information from users. When a server needs additional data, it can send an elicitation request that the client handles by prompting the user. A common example is during a tool call.
server.elicitation.sendRequest() with a message and schemaYou must set up an elicitation handler before tools that use elicitation are called:
import { MCPClient } from '@mastra/mcp'
const mcpClient = new MCPClient({
servers: {
interactiveServer: {
url: new URL('http://localhost:3000/mcp'),
},
},
})
// Set up elicitation handler
mcpClient.elicitation.onRequest('interactiveServer', async request => {
// Handle the server's request for user input
console.log(`Server needs: ${request.message}`)
// Your logic to collect user input
const userData = await collectUserInput(request.requestedSchema)
return {
action: 'accept',
content: userData,
}
})
Your elicitation handler must return one of three response types:
Accept: User provided data and confirmed submission
return {
action: 'accept',
content: { name: 'John Doe', email: '[email protected]' },
}
Decline: User explicitly declined to provide the information
return { action: 'decline' }
Cancel: User dismissed or cancelled the request
return { action: 'cancel' }
The requestedSchema provides structure for the data the server needs:
await mcpClient.elicitation.onRequest('interactiveServer', async request => {
const { properties, required = [] } = request.requestedSchema
const content: Record<string, any> = {}
for (const [fieldName, fieldSchema] of Object.entries(properties || {})) {
const field = fieldSchema as any
const isRequired = required.includes(fieldName)
// Collect input based on field type and requirements
const value = await promptUser({
name: fieldName,
title: field.title,
description: field.description,
type: field.type,
required: isRequired,
format: field.format,
enum: field.enum,
})
if (value !== null) {
content[fieldName] = value
}
}
return { action: 'accept', content }
})
For connecting to MCP servers that require OAuth authentication per the MCP Auth Specification, use the MCPOAuthClientProvider:
import { MCPClient, MCPOAuthClientProvider } from '@mastra/mcp'
// Create an OAuth provider
const oauthProvider = new MCPOAuthClientProvider({
redirectUrl: 'http://localhost:3000/oauth/callback',
clientMetadata: {
redirect_uris: ['http://localhost:3000/oauth/callback'],
client_name: 'My MCP Client',
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
},
onRedirectToAuthorization: url => {
// Handle authorization redirect (open browser, redirect response, etc.)
console.log(`Please visit: ${url}`)
},
})
// Use the provider with MCPClient
const client = new MCPClient({
servers: {
protectedServer: {
url: new URL('https://mcp.example.com/mcp'),
authProvider: oauthProvider,
},
},
})
For testing or when you already have a valid access token:
import { MCPClient, createSimpleTokenProvider } from '@mastra/mcp'
const provider = createSimpleTokenProvider('your-access-token', {
redirectUrl: 'http://localhost:3000/callback',
clientMetadata: {
redirect_uris: ['http://localhost:3000/callback'],
client_name: 'Test Client',
},
})
const client = new MCPClient({
servers: {
testServer: {
url: new URL('https://mcp.example.com/mcp'),
authProvider: provider,
},
},
})
For persistent token storage across sessions, implement the OAuthStorage interface:
import { MCPOAuthClientProvider, OAuthStorage } from '@mastra/mcp'
class DatabaseOAuthStorage implements OAuthStorage {
constructor(
private db: Database,
private userId: string,
) {}
async set(key: string, value: string): Promise<void> {
await this.db.query(
'INSERT INTO oauth_tokens (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET value = ?',
[this.userId, key, value, value],
)
}
async get(key: string): Promise<string | undefined> {
const result = await this.db.query(
'SELECT value FROM oauth_tokens WHERE user_id = ? AND key = ?',
[this.userId, key],
)
return result?.[0]?.value
}
async delete(key: string): Promise<void> {
await this.db.query('DELETE FROM oauth_tokens WHERE user_id = ? AND key = ?', [
this.userId,
key,
])
}
}
const provider = new MCPOAuthClientProvider({
redirectUrl: 'http://localhost:3000/callback',
clientMetadata: {
/* ... */
},
storage: new DatabaseOAuthStorage(db, 'user-123'),
})
For tools where you have a single connection to the MCP server for you entire app, use listTools() and pass the tools to your agent:
import { MCPClient } from '@mastra/mcp'
import { Agent } from '@mastra/core/agent'
const mcp = new MCPClient({
servers: {
stockPrice: {
command: 'npx',
args: ['tsx', 'stock-price.ts'],
env: {
API_KEY: 'your-api-key',
},
log: logMessage => {
console.log(`[${logMessage.level}] ${logMessage.message}`)
},
},
weather: {
url: new URL('http://localhost:8080/sse'),
},
},
timeout: 30000, // Global 30s timeout
})
// Create an agent with access to all tools
const agent = new Agent({
id: 'multi-tool-agent',
name: 'Multi-tool Agent',
instructions: 'You have access to multiple tool servers.',
model: 'openai/gpt-5.4',
tools: await mcp.listTools(),
})
// Example of using resource methods
async function checkWeatherResource() {
try {
const weatherResources = await mcp.resources.list()
if (weatherResources.weather && weatherResources.weather.length > 0) {
const currentWeatherURI = weatherResources.weather[0].uri
const weatherData = await mcp.resources.read('weather', currentWeatherURI)
console.log('Weather data:', weatherData.contents[0].text)
}
} catch (error) {
console.error('Error fetching weather resource:', error)
}
}
checkWeatherResource()
// Example of using prompt methods
async function checkWeatherPrompt() {
try {
const weatherPrompts = await mcp.prompts.list()
if (weatherPrompts.weather && weatherPrompts.weather.length > 0) {
const currentWeatherPrompt = weatherPrompts.weather.find(p => p.name === 'current')
if (currentWeatherPrompt) {
console.log('Weather prompt:', currentWeatherPrompt)
} else {
console.log('Current weather prompt not found')
}
}
} catch (error) {
console.error('Error fetching weather prompt:', error)
}
}
checkWeatherPrompt()
When you need a new MCP connection for each user, use listToolsets() and add the tools when calling stream or generate:
import { Agent } from '@mastra/core/agent'
import { MCPClient } from '@mastra/mcp'
// Create the agent first, without any tools
const agent = new Agent({
id: 'multi-tool-agent',
name: 'Multi-tool Agent',
instructions: 'You help users check stocks and weather.',
model: 'openai/gpt-5.4',
})
// Later, configure MCP with user-specific settings
const mcp = new MCPClient({
servers: {
stockPrice: {
command: 'npx',
args: ['tsx', 'stock-price.ts'],
env: {
API_KEY: 'user-123-api-key',
},
timeout: 20000, // Server-specific timeout
},
weather: {
url: new URL('http://localhost:8080/sse'),
requestInit: {
headers: {
Authorization: `Bearer user-123-token`,
},
},
},
},
})
// Pass all toolsets to stream() or generate()
const response = await agent.stream('How is AAPL doing and what is the weather?', {
toolsets: await mcp.listToolsets(),
})
The MCPClient class includes built-in memory leak prevention for managing multiple instances:
id will throw an error to prevent memory leaksid for each instanceawait configuration.disconnect() before recreating an instance with the same configurationFor example, if you try to create multiple instances with the same configuration without an id:
// First instance - OK
const mcp1 = new MCPClient({
servers: {
/* ... */
},
})
// Second instance with same config - Will throw an error
const mcp2 = new MCPClient({
servers: {
/* ... */
},
})
// To fix, either:
// 1. Add unique IDs
const mcp3 = new MCPClient({
id: 'instance-1',
servers: {
/* ... */
},
})
// 2. Or disconnect before recreating
await mcp1.disconnect()
const mcp4 = new MCPClient({
servers: {
/* ... */
},
})
MCPClient handles server connections gracefully:
For HTTP servers, you can provide a custom fetch function to handle dynamic authentication, request interception, or other custom behavior. This is particularly useful when you need to refresh tokens on each request or forward user credentials from the incoming request to the MCP server.
The custom fetch function receives an optional third requestContext parameter, which provides access to request-scoped data (e.g., authentication cookies, bearer tokens) set by middleware or passed during agent/tool execution. The requestContext is null during the initial connection handshake.
When fetch is provided, requestInit, eventSourceInit, and authProvider become optional, as you can handle these concerns within your custom fetch function.
const mcpClient = new MCPClient({
servers: {
apiServer: {
url: new URL('https://api.example.com/mcp'),
fetch: async (url, init, requestContext) => {
const headers = new Headers(init?.headers)
// Forward auth cookie from the incoming request
const cookie = requestContext?.get('cookie')
if (cookie) {
headers.set('cookie', cookie)
}
return fetch(url, { ...init, headers })
},
},
},
})
// Use with an agent ā requestContext is automatically forwarded
const agent = new Agent({
name: 'My Agent',
instructions: 'You are a helpful assistant.',
model: openai('gpt-4o'),
tools: await mcpClient.listTools(),
})
await agent.generate('Hello!', {
requestContext: myRequestContext, // forwarded to the custom fetch
})
When using the legacy SSE MCP transport, you must configure both requestInit and eventSourceInit due to a bug in the MCP SDK. Alternatively, you can use a custom fetch function which will be automatically used for both POST requests and SSE connections:
// Option 1: Using requestInit and eventSourceInit (required for SSE)
const sseClient = new MCPClient({
servers: {
exampleServer: {
url: new URL('https://your-mcp-server.com/sse'),
// Note: requestInit alone isn't enough for SSE
requestInit: {
headers: {
Authorization: 'Bearer your-token',
},
},
// This is also required for SSE connections with custom headers
eventSourceInit: {
fetch(input: Request | URL | string, init?: RequestInit) {
const headers = new Headers(init?.headers || {})
headers.set('Authorization', 'Bearer your-token')
return fetch(input, {
...init,
headers,
})
},
},
},
},
})
// Option 2: Using custom fetch (simpler, works for both Streamable HTTP and SSE)
const sseClientWithFetch = new MCPClient({
servers: {
exampleServer: {
url: new URL('https://your-mcp-server.com/sse'),
fetch: async (url, init) => {
const headers = new Headers(init?.headers || {})
headers.set('Authorization', 'Bearer your-token')
return fetch(url, {
...init,
headers,
})
},
},
},
})