> Discover all available pages from the documentation index: https://mastra.ai/llms.txt

# 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.

## Constructor

Creates a new instance of the MCPClient class.

```typescript
constructor({
  id?: string;
  servers: Record<string, MastraMCPServerDefinition>;
  timeout?: number;
}: MCPClientOptions)
```

### MCPClientOptions

**id** (`string`): Optional unique identifier for the configuration instance. Use this to prevent memory leaks when creating multiple instances with identical configurations.

**servers** (`Record<string, MastraMCPServerDefinition>`): A map of server configurations, where each key is a unique server identifier and the value is the server configuration.

**timeout** (`number`): Global timeout value in milliseconds for all servers unless overridden in individual server configs. (Default: `60000`)

### `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** (`string`): For Stdio servers: The command to execute.

**args** (`string[]`): For Stdio servers: Arguments to pass to the command.

**env** (`Record<string, string>`): For Stdio servers: Environment variables to set for the command.

**url** (`URL`): For HTTP servers (Streamable HTTP or SSE): The URL of the server.

**requestInit** (`RequestInit`): For HTTP servers: Request configuration for the fetch API.

**eventSourceInit** (`EventSourceInit`): For SSE fallback: Custom fetch configuration for SSE connections. Required when using custom headers with SSE.

**fetch** (`MastraFetchLike`): 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.

**logger** (`LogHandler`): Optional additional handler for logging.

**timeout** (`number`): Server-specific timeout in milliseconds.

**capabilities** (`ClientCapabilities`): Server-specific capabilities configuration.

**authProvider** (`OAuthClientProvider`): For HTTP servers: OAuth authentication provider for automatic token refresh and OAuth flow management. Use MCPOAuthClientProvider for a ready-to-use implementation.

**enableServerLogs** (`boolean`): Whether to enable logging for this server. (Default: `true`)

**forwardInstructions** (`boolean`): Whether to append instructions advertised by this MCP server to an agent's system prompt when the agent uses this server's tools. Disabled by default; enable it only for servers you trust, since the instructions are injected into the agent's system prompt. (Default: `false`)

**instructionsMaxLength** (`number`): Maximum number of server instruction characters to append to an agent's system prompt. (Default: `512`)

**requireToolApproval** (`boolean | (params: RequireToolApprovalContext) => boolean | Promise<boolean>`): Require human approval before executing tools from this server. When set to true, all tools require approval. When set to a function, the function is called with the tool name, arguments, request context, and any tool annotations advertised by the server to dynamically decide whether approval is needed.

## Tool approval

Use `requireToolApproval` on a server definition to require human approval before any tool from that server is executed. This works with the existing [human-in-the-loop](https://mastra.ai/docs/workflows/human-in-the-loop) approval flow.

### Require approval for all tools

Set `requireToolApproval` to `true` to require approval for every tool on the server:

```typescript
const mcp = new MCPClient({
  servers: {
    github: {
      url: new URL('http://localhost:3000/mcp'),
      requireToolApproval: true,
    },
  },
})
```

### Dynamic approval with a function

Pass a function to decide per-call whether approval is needed. The function receives the tool name, the arguments the model passed, any request context from the incoming request, and the tool's MCP `annotations` (when the server advertises them):

```typescript
const mcp = new MCPClient({
  servers: {
    github: {
      url: new URL('http://localhost:3000/mcp'),
      requireToolApproval: ({ toolName, args, requestContext }) => {
        // Read-only tools don't need approval
        if (toolName === 'list_repos') return false
        // Destructive tools with force flag always need approval
        if (toolName === 'delete_repo') return args.force === true
        // Non-admin users need approval for everything else
        return requestContext?.userRole !== 'admin'
      },
    },
  },
})
```

The function can also be async. It receives `requestContext` from the incoming request, which you can use for auth checks or other per-request logic.

### Use tool annotations from a trusted server

If you trust the MCP server, you can use its [tool annotations](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-annotations) (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, `title`) to drive approval decisions:

```typescript
const mcp = new MCPClient({
  servers: {
    github: {
      url: new URL('http://localhost:3000/mcp'),
      requireToolApproval: ({ annotations }) => {
        // Skip approval for tools the server has marked read-only
        if (annotations?.readOnlyHint) return false
        // Always require approval for destructive tools
        if (annotations?.destructiveHint) return true
        return true
      },
    },
  },
})
```

Per the MCP specification, **clients MUST consider tool annotations to be untrusted unless they come from trusted servers**. Annotations are advisory hints, not a security boundary — a malicious or buggy server can claim a tool is read-only when it isn't. Only use annotations to relax approval requirements for servers you trust.

The same annotations are also exposed on the tools returned by `listTools()` and `listToolsets()` under `tool.mcp.annotations`, so you can inspect them when wiring tools into an agent.

## Server instructions

When an MCP server advertises instructions during initialization, `MCPClient` stores them for that server. Forwarding those instructions into an agent's system prompt is **opt-in**: set `forwardInstructions: true` on a server to have agents that use its tools (via `listTools()` or `listToolsets()`) receive its instructions automatically.

The guidance is grouped by server name and truncated to `instructionsMaxLength` characters per server.

```typescript
const mcp = new MCPClient({
  servers: {
    db: {
      url: new URL('http://localhost:3000/mcp'),
      forwardInstructions: true,
      instructionsMaxLength: 512,
    },
  },
})

const agent = new Agent({
  id: 'db-agent',
  name: 'DB Agent',
  instructions: 'Help with database changes.',
  model,
  tools: await mcp.listTools(),
})
```

When `forwardInstructions` is omitted (the default), instructions are still cached and can be inspected via [`getServerInstructions()`](#getserverinstructions), but aren't added to any agent's system prompt.

> **Security note:** server instructions are forwarded verbatim (subject only to length truncation) into the agent's system prompt. A malicious or compromised MCP server can use them to inject instructions the agent will treat as trusted system guidance. Only enable `forwardInstructions` for servers you trust, and prefer reviewing instructions with `getServerInstructions()` before forwarding instructions from third-party servers.

## Methods

### `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.

```ts
new Agent({ id: 'agent', tools: await mcp.listTools() })
```

### `listToolsWithErrors()`

Retrieves all tools from all configured servers, with tool names namespaced by their server name. Also returns per-server errors for servers that failed to connect or list tools.

```typescript
const { tools, errors } = await mcp.listToolsWithErrors()

new Agent({ id: 'agent', tools })
console.log(errors)
```

### `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.

```typescript
const res = await agent.stream(prompt, {
  toolsets: await mcp.listToolsets(),
})
```

### `getServerInstructions()`

Returns the instructions currently known for each configured MCP server. Servers that haven't connected yet, or don't advertise instructions, return `undefined`.

```typescript
getServerInstructions(): Record<string, string | undefined>
```

Example:

```typescript
await mcp.listTools()

const instructionsByServer = mcp.getServerInstructions()
console.log(instructionsByServer.db)
```

### `disconnect()`

Disconnects from all MCP servers and cleans up resources.

```typescript
async disconnect(): Promise<void>
```

### `toMCPServerProxies()`

Returns a map of `MCPClientServerProxy` instances, one per configured server. Each proxy wraps the underlying client connection as an `MCPServerBase` instance, allowing external (non-Mastra) MCP servers to be registered in `mcpServers` and appear in Studio.

```typescript
async toMCPServerProxies(): Promise<Record<string, MCPClientServerProxy>>
```

Spread the result into the `mcpServers` config on `Mastra`:

```typescript
import { Mastra } from '@mastra/core/mastra'
import { MCPClient } from '@mastra/mcp'

const mcpClient = new MCPClient({
  servers: {
    'color-mixer': {
      command: 'node',
      args: ['path/to/color-mixer-server.js'],
    },
  },
})

export const mastra = new Mastra({
  mcpServers: {
    ...(await mcpClient.toMCPServerProxies()),
  },
})
```

This is useful for connecting external MCP servers that implement the MCP Apps extension or other features to Studio without wrapping them in a Mastra `MCPServer`.

### `resources` Property

The `MCPClient` instance has a `resources` property that provides access to resource-related operations.

```typescript
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.

```typescript
async list(): Promise<Record<string, Resource[]>>
```

Example:

```typescript
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.

```typescript
async templates(): Promise<Record<string, ResourceTemplate[]>>
```

Example:

```typescript
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.

```typescript
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:

```typescript
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.

```typescript
async subscribe(serverName: string, uri: string): Promise<object>
```

Example:

```typescript
await mcpClient.resources.subscribe('myWeatherServer', 'weather://current')
```

#### `resources.unsubscribe(serverName: string, uri: string)`

Unsubscribes from updates for a specific resource on a named server.

```typescript
async unsubscribe(serverName: string, uri: string): Promise<object>
```

Example:

```typescript
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.

```typescript
async onUpdated(serverName: string, handler: (params: { uri: string }) => void): Promise<void>
```

Example:

```typescript
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.

```typescript
async onListChanged(serverName: string, handler: () => void): Promise<void>
```

Example:

```typescript
mcpClient.resources.onListChanged('myWeatherServer', () => {
  console.log('Resource list changed on myWeatherServer.')
  // You should re-fetch the list of resources
  // await mcpClient.resources.list();
})
```

### `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.

```typescript
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 needed
- `requestedSchema`: A JSON schema defining the structure of the expected response

The handler must return an `ElicitResult` with:

- `action`: One of `'accept'`, `'decline'`, or `'cancel'`
- `content`: The user's data (only when action is `'accept'`)

**Example:**

```typescript
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 declining the request
  return { action: 'decline' }
})
```

**Complete Interactive Example:**

```typescript
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` Property

The `MCPClient` instance has a `prompts` property that provides access to prompt-related operations.

```typescript
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.

```typescript
async list(): Promise<Record<string, Prompt[]>>
```

Example:

```typescript
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.

```typescript
async get({
  serverName,
  name,
  args?,
  version?,
}: {
  serverName: string;
  name: string;
  args?: Record<string, any>;
  version?: string;
}): Promise<{ prompt: Prompt; messages: PromptMessage[] }>
```

Example:

```typescript
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.

```typescript
async onListChanged(serverName: string, handler: () => void): Promise<void>
```

Example:

```typescript
mcpClient.prompts.onListChanged('myWeatherServer', () => {
  console.log('Prompt list changed on myWeatherServer.')
  // You should re-fetch the list of prompts
  // await mcpClient.prompts.list();
})
```

### `progress` Property

The `MCPClient` instance has a `progress` property for subscribing to progress notifications emitted by MCP servers while tools execute.

```typescript
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.

```typescript
async onUpdate(
  serverName: string,
  handler: (params: {
    progressToken: string;
    progress: number;
    total?: number;
    message?: string;
  }) => void,
): Promise<void>
```

Notes:

- When `enableProgressTracking` is true (default), tool calls include a `progressToken` so you can correlate updates to a specific run.
- If you pass a `runId` when executing a tool, it will be used as the `progressToken`.

To disable progress tracking for a server:

```typescript
const mcpClient = new MCPClient({
  servers: {
    myServer: {
      url: new URL('http://localhost:4111/api/mcp/myServer/mcp'),
      enableProgressTracking: false,
    },
  },
})
```

## 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

1. **Server Request**: An MCP server tool calls `server.elicitation.sendRequest()` with a message and schema
2. **Client Handler**: Your elicitation handler function is called with the request
3. **User Interaction**: Your handler collects user input (via UI, CLI, etc.)
4. **Response**: Your handler returns the user's response (accept/decline/cancel)
5. **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:

```typescript
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

  ```typescript
  return {
    action: 'accept',
    content: { name: 'John Doe', email: 'john@example.com' },
  }
  ```

- **Decline**: User explicitly declined to provide the information

  ```typescript
  return { action: 'decline' }
  ```

- **Cancel**: User dismissed or cancelled the request

  ```typescript
  return { action: 'cancel' }
  ```

### Schema-Based Input Collection

The `requestedSchema` provides structure for the data the server needs:

```typescript
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 decline and cancel responses gracefully
- **Clear UI**: Make it obvious what information is being requested and why
- **Security**: Never auto-accept requests for sensitive information

## OAuth authentication

For connecting to MCP servers that require OAuth authentication per the [MCP Auth Specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization), use the `MCPOAuthClientProvider`:

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

### Quick Token Provider

For testing or when you already have a valid access token:

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

### Custom Token Storage

For persistent token storage across sessions, implement the `OAuthStorage` interface:

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

## Examples

### Static Tool Configuration

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:

```typescript
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.5',
  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()
```

### Dynamic toolsets

When you need a new MCP connection for each user, use `listToolsets()` and add the tools when calling stream or generate:

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

// 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(),
})
```

## Instance management

The `MCPClient` class includes built-in memory leak prevention for managing multiple instances:

1. Creating multiple instances with identical configurations without an `id` will throw an error to prevent memory leaks
2. If you need multiple instances with identical configurations, provide a unique `id` for each instance
3. Call `await configuration.disconnect()` before recreating an instance with the same configuration
4. 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`:

```typescript
// 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:

1. Automatic connection management for multiple servers
2. Graceful server shutdown to prevent error messages during development
3. Proper cleanup of resources when disconnecting

## Using custom fetch for dynamic authentication

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.

```typescript
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({
  id: 'my-agent',
  name: 'My Agent',
  instructions: 'You are a helpful assistant.',
  model: openai('gpt-5.4'),
  tools: await mcpClient.listTools(),
})

await agent.generate('Hello!', {
  requestContext: myRequestContext, // forwarded to the custom fetch
})
```

## Handling auth failures inside custom fetch

A custom `fetch` shouldn't `throw` when authentication is unavailable. The Streamable HTTP transport in the MCP SDK opens a long-lived `GET /mcp` "standalone listener" stream in the background to receive server-pushed notifications. Errors on that stream are retried with exponential backoff, and a thrown `fetch` or a cleanly-closed stream can produce an indefinite reconnect loop at roughly one attempt per second.

Return a synthetic `Response` instead. The [MCP Streamable HTTP specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports) defines `405 Method Not Allowed` as the signal a server returns when it doesn't offer the GET SSE stream, and the SDK honors it as a terminal status that stops the listener cleanly. Use this to disable the listener when your server doesn't push notifications.

The following pattern waits for an auth token on POST requests, attaches it to outgoing headers, and short-circuits the GET listener with a synthetic 405:

```typescript
async function waitForToken(timeoutMs = 5000): Promise<string | null> {
  // Replace with your token lookup. Return null if no token is available.
  return getAuthToken({ timeoutMs })
}

const mcpClient = new MCPClient({
  servers: {
    apiServer: {
      url: new URL('https://api.example.com/mcp'),
      fetch: async (url, init) => {
        const method = (init?.method || 'GET').toUpperCase()

        // The SDK opens a background GET stream for server-pushed notifications.
        // If your server does not use it, short-circuit with 405 to stop reconnect attempts.
        if (method === 'GET') {
          return new Response(null, { status: 405, statusText: 'Method Not Allowed' })
        }

        // POST: wait for the token, then forward the request with an Authorization header.
        const token = await waitForToken()
        if (!token) {
          // Forward the request without a token and let the server reject it.
          // The SDK surfaces non-2xx POST responses as errors to the caller of
          // tools/list, tools/call, etc., which is the desired behavior here.
          return fetch(url, init)
        }

        const headers = new Headers(init?.headers)
        headers.set('authorization', `Bearer ${token}`)
        return fetch(url, { ...init, headers })
      },
    },
  },
})
```

Return `405` for the GET listener only when your server doesn't push notifications back to the client. If your server uses the standalone GET stream, attach the auth token on `GET` requests as well and let the request through.

## 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. Alternatively, you can use a custom `fetch` function which will be automatically used for both POST requests and SSE connections:

```ts
// 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,
        })
      },
    },
  },
})
```

## Related information

- For creating MCP servers, see the [MCPServer documentation](https://mastra.ai/reference/tools/mcp-server).
- For more about the Model Context Protocol, see the [@modelcontextprotocol/sdk documentation](https://github.com/modelcontextprotocol/typescript-sdk).