Skip to main content

Why mcp-use Has Its Own MCP Client

The official MCP SDK provides an excellent Client class for connecting to MCP servers. So why does mcp-use include its own MCPClient and BrowserMCPClient? The answer: multi-server orchestration. Our client is designed for AI agents that need to coordinate across multiple MCP servers simultaneously—GitHub for code, Linear for tasks, and custom APIs for business logic—all in one conversation.

The Official SDK: Single-Server Design

The official SDK’s Client is designed for one server at a time:
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'

// Connect to ONE server
const transport = new StreamableHTTPClientTransport('http://localhost:3000/mcp')
const client = new Client({ name: 'my-app', version: '1.0.0' }, { capabilities: {} })

await client.connect(transport)

// Call tools on THIS server
await client.callTool({ name: 'send-email', arguments: { to: '[email protected]' } })
For a single integration, this is perfect. Simple, direct, no abstractions. But for AI agents? You’d need to manage multiple clients manually:
// Managing multiple servers with official SDK (manual)
const githubTransport = new StreamableHTTPClientTransport('https://api.github.com/mcp')
const githubClient = new Client({ name: 'agent', version: '1.0.0' }, {})
await githubClient.connect(githubTransport)

const linearTransport = new StreamableHTTPClientTransport('https://mcp.linear.app/mcp')
const linearClient = new Client({ name: 'agent', version: '1.0.0' }, {})
await linearClient.connect(linearTransport)

const customTransport = new StreamableHTTPClientTransport('http://localhost:3000/mcp')
const customClient = new Client({ name: 'agent', version: '1.0.0' }, {})
await customClient.connect(customTransport)

// Now manually route tool calls to the right client
if (toolName === 'create_issue') {
  await linearClient.callTool({ name: toolName, arguments: args })
} else if (toolName === 'search_code') {
  await githubClient.callTool({ name: toolName, arguments: args })
} else {
  await customClient.callTool({ name: toolName, arguments: args })
}
This becomes unmaintainable with 5+ servers.

mcp-use Client: Multi-Server by Design

Our client provides a unified interface for multiple servers:
import { MCPClient } from 'mcp-use'

const client = new MCPClient()

// Add multiple servers
client.addServer('github', {
  url: 'https://api.github.com/mcp',
  headers: { Authorization: `Bearer ${githubToken}` }
})

client.addServer('linear', {
  url: 'https://mcp.linear.app/mcp',
  headers: { Authorization: `Bearer ${linearToken}` }
})

client.addServer('custom', {
  url: 'http://localhost:3000/mcp',
  headers: { Authorization: `Bearer ${customKey}` }
})

// Create sessions for all servers
await client.createSession('github')
await client.createSession('linear')
await client.createSession('custom')

// Get a session and call tools
const githubSession = client.getSession('github')
await githubSession.connector.callTool('search_code', { query: 'async function' })

const linearSession = client.getSession('linear')
await linearSession.connector.callTool('create_issue', { title: 'Bug fix' })
Key difference: The client knows about all your servers and provides a unified API.

What mcp-use Adds: The Value Proposition

1. Session Management

Official SDK:
// You manage transport lifecycle
const transport = new StreamableHTTPClientTransport(url)
const client = new Client(info, {})
await client.connect(transport)
// ... use client
await client.close()  // Don't forget cleanup!
mcp-use:
// Client manages sessions
const session = await client.createSession('server-name')
// Session auto-connects, caches tools, handles lifecycle
await client.closeSession('server-name')
Sessions encapsulate:
  • ✅ Transport creation and lifecycle
  • ✅ Connection state management
  • ✅ Tool/resource/prompt caching
  • ✅ Automatic reconnection
  • ✅ Cleanup on errors

2. Config File Support

Official SDK:
// Manual configuration
const transport1 = new StreamableHTTPClientTransport(url1, { headers: {...} })
const client1 = new Client({...}, {})
// ... repeat for each server
mcp-use:
// mcp-config.json
{
  "mcpServers": {
    "github": {
      "url": "https://api.github.com/mcp",
      "headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" }
    },
    "linear": {
      "url": "https://mcp.linear.app/mcp",
      "headers": { "Authorization": "Bearer ${LINEAR_TOKEN}" }
    }
  }
}
// One line to connect to all servers
const client = MCPClient.fromConfigFile('./mcp-config.json')
await client.createAllSessions()
This matches the Python API (Python SDK uses config files) and makes multi-server setups declarative.

3. Automatic OAuth Integration

The official SDK supports OAuth but requires manual wiring: Official SDK:
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'

// You implement OAuthClientProvider interface
class MyOAuthProvider implements OAuthClientProvider {
  async saveClientInformation(info) { /* ... */ }
  async tokens() { /* ... */ }
  async redirectToAuthorization() { /* ... */ }
}

const authProvider = new MyOAuthProvider()
const transport = new StreamableHTTPClientTransport(url, {
  authProvider  // ← Must pass to EVERY transport
})
mcp-use:
import { BrowserOAuthClientProvider } from 'mcp-use/browser'

// Built-in browser OAuth with automatic DCR
const authProvider = new BrowserOAuthClientProvider(url, {
  clientName: 'My App',
  callbackUrl: window.location.origin + '/oauth/callback',
  storageKeyPrefix: 'mcp:auth'
})

// Add to client once - works for all sessions
client.addServer('server', {
  url: url,
  authProvider  // ← Client passes to transport automatically
})

// OAuth handled automatically:
// - Token refresh
// - Dynamic Client Registration (DCR)
// - Popup flow
// - localStorage persistence
await client.createSession('server')
Our BrowserOAuthClientProvider implements:
  • ✅ Automatic popup-based auth flow
  • ✅ Token storage in localStorage
  • ✅ Token refresh before expiration
  • ✅ Dynamic Client Registration (DCR)
  • ✅ Callback handling via window.postMessage

4. Transport Abstraction and Fallback

Official SDK:
// You choose transport manually
const transport = new StreamableHTTPClientTransport(url)
// If server doesn't support it, you get an error

// To support fallback:
try {
  const httpTransport = new StreamableHTTPClientTransport(url)
  await client.connect(httpTransport)
} catch (err) {
  const sseTransport = new SSEClientTransport(url)
  await client.connect(sseTransport)
}
mcp-use:
// HttpConnector tries transports automatically
const connector = new HttpConnector(url, { headers })
await connector.connect()

// Internally:
// 1. Try Streamable HTTP (best performance)
// 2. If 404/405 → Fallback to SSE
// 3. If ECONNREFUSED → Clear error message
Graceful degradation without manual logic.

5. Capability-Aware Methods

Official SDK:
// You check capabilities manually
const caps = client.getServerCapabilities()

if (caps.resources) {
  await client.listResources()  // OK
} else {
  // Skip resources
}
mcp-use:
// Connector checks capabilities automatically
const resources = await session.connector.listAllResources()
// Returns [] if not supported, instead of throwing
During initialize(), our connector caches capabilities and checks them before each method call. This prevents “Method not found” errors when a server doesn’t implement resources/prompts.

6. Tool Caching

Official SDK:
// You cache tools manually
let toolsCache: Tool[] = []

const listTools = async () => {
  const result = await client.listTools()
  toolsCache = result.tools
  return toolsCache
}
mcp-use:
// Connector caches on initialize()
await session.initialize()  // Fetches and caches tools once

// Subsequent access is instant (no network call)
const tools = session.connector.tools  // ← From cache
Especially useful for AI agents that query tool lists frequently.

7. Unified Error Handling

Official SDK:
// Different error types from different transports
try {
  await client.callTool({ name: 'my-tool', arguments: {} })
} catch (err) {
  if (err instanceof StreamableHTTPError) { /* ... */ }
  else if (err instanceof SSEError) { /* ... */ }
  else if (err.code === -32601) { /* ... */ }
  // Different error shapes!
}
mcp-use:
// Consistent error interface
try {
  await session.connector.callTool('my-tool', {})
} catch (err) {
  // Always has err.code, err.message
  if (err.code === -32601) {
    // Method not found
  } else if (err.code === 401) {
    // Unauthorized
  }
}

8. Python API Parity

For teams using both Python and TypeScript, our client provides API consistency: Python:
from mcp_use import MCPClient

client = MCPClient.from_config_file('mcp-config.json')
await client.create_all_sessions()

session = client.get_session('github')
result = await session.connector.call_tool('search_code', {'query': 'bug'})
TypeScript (mcp-use):
import { MCPClient } from 'mcp-use'

const client = MCPClient.fromConfigFile('mcp-config.json')
await client.createAllSessions()

const session = client.getSession('github')
const result = await session.connector.callTool('search_code', { query: 'bug' })
Nearly identical APIs reduce cognitive load when switching languages.

When to Use Each Client

Use Official SDK When:

  • ✅ Connecting to one server only
  • ✅ Building a simple MCP client app
  • ✅ Need minimal bundle size (no abstractions)
  • ✅ Direct transport control required
  • ✅ Following official examples/tutorials
Example: A single-purpose tool that sends emails via one MCP server.

Use mcp-use Client When:

  • ✅ Building AI agents that use multiple MCP servers
  • ✅ Need config file driven setup
  • ✅ Want automatic OAuth with DCR
  • ✅ Prefer session management over manual transport lifecycle
  • ✅ Building apps that match the Python mcp-use API
  • ✅ Need automatic capability checking
Example: An AI assistant that searches GitHub, creates Linear issues, sends emails, and queries your database—all in one conversation.

Real-World Example: MCPAgent

Here’s why the multi-server client matters for AI agents:
import { MCPAgent, MCPClient } from 'mcp-use'

// One client, three servers
const client = new MCPClient({
  mcpServers: {
    github: {
      url: 'https://api.github.com/mcp',
      headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }
    },
    linear: {
      url: 'https://mcp.linear.app/mcp',
      headers: { Authorization: `Bearer ${process.env.LINEAR_TOKEN}` }
    },
    database: {
      url: 'http://localhost:3000/mcp',
      headers: { Authorization: `Bearer ${process.env.DB_API_KEY}` }
    }
  }
})

// Connect to all servers
await client.createAllSessions()

// Create agent with ALL servers
const agent = new MCPAgent({
  llm: new ChatOpenAI({ model: 'gpt-4' }),
  client  // ← Agent has access to tools from all 3 servers
})

await agent.initialize()

// Agent can use tools from ANY server
const result = await agent.run(
  "Search GitHub for bugs related to authentication, " +
  "create a Linear issue for each one, " +
  "and log them to our database"
)

// Agent automatically:
// - Calls search_code on github
// - Calls create_issue on linear (for each bug)
// - Calls log_entry on database
// All coordinated in one execution!
With the official SDK, you’d need to manually route each tool call to the correct client.

The Architecture: Sessions and Connectors

Official SDK

Client → Transport → MCP Server
One client, one transport, one server.

mcp-use Client

MCPClient
  ├─ Session('github') → Connector → Transport → GitHub MCP
  ├─ Session('linear') → Connector → Transport → Linear MCP
  └─ Session('database') → Connector → Transport → Custom MCP
One client, multiple sessions, multiple servers.

Why Sessions?

A session represents a connection lifecycle to one server:
// Session API
const session = await client.createSession('server-name')

// Provides:
session.connector.tools           // Cached tool list
session.connector.callTool()      // Call a tool
session.connector.readResource()  // Read a resource
session.isConnected               // Connection state

await client.closeSession('server-name')  // Cleanup
Sessions give you:
  • Isolation: Each server’s state is separate
  • Caching: Tools/resources cached per server
  • Lifecycle: Clear connect/disconnect
  • Error handling: Errors scoped to one server

Inspector: Why We Switched to mcp-use Client

The inspector started with the official SDK but hit limitations:

Before (Official SDK)

// react/useMcp.ts (OLD)
const clientRef = useRef<Client | null>(null)

const connect = async () => {
  const transport = new StreamableHTTPClientTransport(url, {
    headers: customHeaders,
    authProvider: oauthProvider
  })

  clientRef.current = new Client({ name: 'inspector', version: '1.0.0' }, {})
  await clientRef.current.connect(transport)

  // Manually list and cache tools
  const toolsResult = await clientRef.current.listTools()
  setTools(toolsResult.tools)
}

const callTool = async (name, args) => {
  return await clientRef.current.request({
    method: 'tools/call',
    params: { name, arguments: args }
  }, CallToolResultSchema)
}
Problems:
  • ❌ Can’t add second server without major refactoring
  • ❌ Manual tool caching
  • ❌ Manual capability checking
  • ❌ No graceful fallback for unsupported features

After (mcp-use Client)

// react/useMcp.ts (NEW)
const clientRef = useRef<BrowserMCPClient | null>(null)

const connect = async () => {
  clientRef.current = new BrowserMCPClient()

  client.current.addServer('inspector-server', {
    url: url,
    headers: customHeaders,
    authProvider: oauthProvider
  })

  // Session handles everything
  const session = await clientRef.current.createSession('inspector-server')
  await session.initialize()  // Caches tools automatically

  // Tools already available
  setTools(session.connector.tools)
}

const callTool = async (name, args) => {
  const session = clientRef.current.getSession('inspector-server')
  return await session.connector.callTool(name, args)
}
Benefits:
  • ✅ Can add second server by calling addServer() again
  • ✅ Automatic tool caching
  • ✅ Automatic capability checking
  • ✅ Graceful handling of unsupported features
  • ✅ Future-proof for multi-server debugging

OAuth: Delegating to the SDK

Both clients support OAuth, but the approach differs:

Official SDK: You Implement OAuthClientProvider

import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'

class MyOAuthProvider implements OAuthClientProvider {
  async saveClientInformation(info: OAuthClientInformation) {
    // You implement storage
    localStorage.setItem('oauth_client', JSON.stringify(info))
  }

  async tokens(): Promise<OAuthTokens | undefined> {
    // You implement token retrieval
    const data = localStorage.getItem('oauth_tokens')
    return data ? JSON.parse(data) : undefined
  }

  async redirectToAuthorization() {
    // You implement popup/redirect logic
    const authUrl = buildAuthUrl()
    window.open(authUrl, 'oauth', 'width=500,height=600')
  }

  // ... more methods
}

const provider = new MyOAuthProvider()
const transport = new StreamableHTTPClientTransport(url, { authProvider: provider })

mcp-use: BrowserOAuthClientProvider Built-In

import { BrowserOAuthClientProvider } from 'mcp-use/browser'

// Pre-built provider for browser environments
const provider = new BrowserOAuthClientProvider(url, {
  clientName: 'My App',
  callbackUrl: '/oauth/callback',
  storageKeyPrefix: 'mcp:auth'
})

// Pass to client - OAuth handled automatically
client.addServer('server', {
  url: url,
  authProvider: provider
})
What it handles:
  • ✅ Dynamic Client Registration (DCR)
  • ✅ Popup-based authorization flow
  • ✅ Callback via window.postMessage
  • ✅ Token storage in localStorage
  • ✅ Token refresh before expiration
  • ✅ Multiple servers (different tokens per server)
The official SDK provides the interface. We provide the implementation.

Connector Layer: Transport Independence

Official SDK exposes specific transport classes:
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
mcp-use abstracts this:
// HttpConnector auto-selects transport
const connector = new HttpConnector(url, { headers, authProvider })
await connector.connect()  // Tries HTTP, falls back to SSE

// StdioConnector for local processes
const connector = new StdioConnector({ command: 'python', args: ['server.py'] })
await connector.connect()

// WebSocketConnector for WS servers
const connector = new WebSocketConnector('ws://localhost:8080', { headers })
await connector.connect()
Why this matters:
  1. Automatic fallback: User doesn’t care if server uses HTTP or SSE
  2. Consistent API: All connectors have .callTool(), .readResource()
  3. Easy switching: Change transport by changing one line
  4. Future transports: Add WebRTC, WebSocket, etc. without breaking API

Bundle Size Comparison

Official SDK (minimal):
@modelcontextprotocol/sdk: ~120KB
Your transport choice: ~30KB
Total: ~150KB
mcp-use Client:
mcp-use (base): ~180KB
Includes: Multi-server, sessions, all transports, OAuth provider
Extra 30KB gets you multi-server support, automatic OAuth, and simplified API. For browser apps: Use mcp-use/browser (no Node.js deps) at ~50KB total.

Migration Path

Already using the official SDK? Migration is straightforward:

Single Server (Keep Official SDK)

// If you only need one server, official SDK is simpler
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'

const transport = new StreamableHTTPClientTransport(url)
const client = new Client(info, {})
await client.connect(transport)

Multiple Servers (Migrate to mcp-use)

// BEFORE (Official SDK)
const githubClient = new Client(...)
await githubClient.connect(githubTransport)

const linearClient = new Client(...)
await linearClient.connect(linearTransport)

// AFTER (mcp-use)
const client = new MCPClient()
client.addServer('github', { url: githubUrl, headers: {...} })
client.addServer('linear', { url: linearUrl, headers: {...} })

await client.createAllSessions()

AI Agents (Use mcp-use)

import { MCPAgent, MCPClient } from 'mcp-use'

const client = MCPClient.fromConfigFile('./mcp-config.json')
const agent = new MCPAgent({ llm, client })

await agent.initialize()  // Connects to ALL servers
const result = await agent.run('Your multi-server query')

Compatibility: We Don’t Fork the SDK

Important: mcp-use wraps the official SDK, it doesn’t replace it.
// connectors/http.ts (our code)
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'

export class HttpConnector extends BaseConnector {
  async connect() {
    const transport = new StreamableHTTPClientTransport(this.baseUrl, {
      authProvider: this.opts.authProvider  // ← Official SDK feature
    })

    this.client = new Client(  // ← Official SDK client
      { name: 'mcp-use', version: '1.0.0' },
      { capabilities: {} }
    )

    await this.client.connect(transport)  // ← Official SDK method
  }

  async callTool(name, args) {
    return await this.client.callTool({ name, arguments: args })  // ← Official SDK
  }
}
This means:
  • ✅ We benefit from SDK updates automatically
  • ✅ No compatibility issues
  • ✅ Can use official SDK features directly
  • ✅ Reduces maintenance burden

Conclusion

The official MCP SDK provides excellent low-level primitives. mcp-use builds on top to provide:
  1. Multi-server management - Essential for AI agents
  2. Config file support - Python API parity
  3. Browser OAuth - Ready-to-use authentication
  4. Transport abstraction - Automatic fallback
  5. Capability checking - Graceful degradation
  6. Session lifecycle - Simplified connection management
For simple integrations: Use the official SDK directly. For AI agents, multi-server apps, or Python/TS consistency: Use mcp-use’s client. Both are valid choices. We’re not replacing the SDK—we’re extending it for complex use cases.
Learn more: