MCPClient
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.
This class replaces the deprecated MastraMCPClient
.
Constructor
Creates a new instance of the MCPClient class.
constructor({
id?: string;
servers: Record<string, MastraMCPServerDefinition>;
timeout?: number;
}: MCPClientOptions)
MCPClientOptions
id?:
servers:
timeout?:
MastraMCPServerDefinition
Each server in the servers
map is configured using the MastraMCPServerDefinition
type. The transport type is detected based on the provided parameters:
- If
command
is provided, it uses the Stdio transport. - If
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.
command?:
args?:
env?:
url?:
requestInit?:
eventSourceInit?:
logger?:
timeout?:
capabilities?:
enableServerLogs?:
Methods
getTools()
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({ tools: await mcp.getTools() });
getToolsets()
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.getToolsets(),
});
disconnect()
Disconnects from all MCP servers and cleans up resources.
async disconnect(): Promise<void>
resources
Property
The 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 theservers
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();
});
prompts
Property
The 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",
});
elicitation
Property
The 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: 'john@example.com' }
};
});
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 Function:
The 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 response
The handler must return an ElicitResult
with:
action
: One of'accept'
,'reject'
, 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: 'alice@example.com'
}
};
}
// Simulate user rejecting the request
return { action: 'reject' };
});
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: 'reject' };
}
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: 'reject' };
}
});
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();
});
Elicitation
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.
How Elicitation Works
- Server Request: An MCP server tool calls
server.elicitation.sendRequest()
with a message and schema - Client Handler: Your elicitation handler function is called with the request
- User Interaction: Your handler collects user input (via UI, CLI, etc.)
- Response: Your handler returns the user’s response (accept/reject/cancel)
- Tool Continuation: The server tool receives the response and continues execution
Setting Up Elicitation
You 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
};
});
Response Types
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: 'john@example.com' } };
-
Reject: User explicitly declined to provide the information
return { action: 'reject' };
-
Cancel: User dismissed or cancelled the request
return { action: 'cancel' };
Schema-Based Input Collection
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 };
});
Best Practices
- Always handle elicitation: Set up your handler before calling tools that might use elicitation
- Validate input: Check that required fields are provided
- Respect user choice: Handle reject and cancel responses gracefully
- Clear UI: Make it obvious what information is being requested and why
- Security: Never auto-accept requests for sensitive information
Examples
Static Tool Configuration
For tools where you have a single connection to the MCP server for you entire app, use getTools()
and pass the tools to your agent:
import { MCPClient } from "@mastra/mcp";
import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";
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({
name: "Multi-tool Agent",
instructions: "You have access to multiple tool servers.",
model: openai("gpt-4"),
tools: await mcp.getTools(),
});
// 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();
Dynamic toolsets
When you need a new MCP connection for each user, use getToolsets()
and add the tools when calling stream or generate:
import { Agent } from "@mastra/core/agent";
import { MCPClient } from "@mastra/mcp";
import { openai } from "@ai-sdk/openai";
// Create the agent first, without any tools
const agent = new Agent({
name: "Multi-tool Agent",
instructions: "You help users check stocks and weather.",
model: openai("gpt-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.getToolsets(),
},
);
Instance Management
The MCPClient
class includes built-in memory leak prevention for managing multiple instances:
- Creating multiple instances with identical configurations without an
id
will throw an error to prevent memory leaks - If you need multiple instances with identical configurations, provide a unique
id
for each instance - Call
await configuration.disconnect()
before recreating an instance with the same configuration - If you only need one instance, consider moving the configuration to a higher scope to avoid recreation
For 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: {
/* ... */
},
});
Server Lifecycle
MCPClient handles server connections gracefully:
- Automatic connection management for multiple servers
- Graceful server shutdown to prevent error messages during development
- Proper cleanup of resources when disconnecting
Using SSE Request Headers
When using the legacy SSE MCP transport, you must configure both requestInit
and eventSourceInit
due to a bug in the MCP SDK:
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,
});
},
},
},
},
});
Related Information
- For creating MCP servers, see the MCPServer documentation.
- For more about the Model Context Protocol, see the @modelcontextprotocol/sdk documentation .