---
title: "Why mcp-use Has Its Own MCP Client"
description: "How our multi-server client architecture enables AI agents to orchestrate across GitHub, Linear, and custom APIs simultaneously—with automatic OAuth and simplified config management"
date: "2025-10-31"
---

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

```typescript
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: "user@example.com" },
});
```

**For a single integration, this is perfect.** Simple, direct, no abstractions.

**But for AI agents?** You'd need to manage multiple clients manually:

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

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

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

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

```typescript
// Manual configuration
const transport1 = new StreamableHTTPClientTransport(url1, { headers: {...} })
const client1 = new Client({...}, {})
// ... repeat for each server
```

**mcp-use:**

```json
// 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}" }
    }
  }
}
```

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

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

```typescript
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 infrastructure built-in:
// - Token refresh
// - Dynamic Client Registration (DCR)
// - Popup flow (user-initiated)
// - localStorage persistence
await client.createSession("server");
```

Our `BrowserOAuthClientProvider` implements:

- ✅ User-initiated popup auth flow with fallback
- ✅ Token storage in `localStorage`
- ✅ Automatic token refresh before expiration
- ✅ Dynamic Client Registration (DCR)
- ✅ Callback handling via `window.postMessage`

### 4. Transport Abstraction and Fallback

**Official SDK:**

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

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

```typescript
// You check capabilities manually
const caps = client.getServerCapabilities();

if (caps.resources) {
  await client.listResources(); // OK
} else {
  // Skip resources
}
```

**mcp-use:**

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

```typescript
// You cache tools manually
let toolsCache: Tool[] = [];

const listTools = async () => {
  const result = await client.listTools();
  toolsCache = result.tools;
  return toolsCache;
};
```

**mcp-use:**

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

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

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

```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):**

```typescript
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 **built-in OAuth** with DCR and token management
- ✅ 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:

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

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

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

const connect = async () => {
  const transport = new StreamableHTTPClientTransport(url, {
    headers: headers,
    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)

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

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

```typescript
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 infrastructure built-in
client.addServer("server", {
  url: url,
  authProvider: provider,
});
```

**What it handles:**

- ✅ Dynamic Client Registration (DCR)
- ✅ User-initiated popup authorization flow
- ✅ Callback via `window.postMessage`
- ✅ Token storage in `localStorage`
- ✅ Automatic 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:

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

```typescript
// 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();
```

**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 new transports 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, built-in OAuth infrastructure, 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)

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

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

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

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

- [mcp-use Client Documentation](https://mcp-use.com/docs/typescript/client)
- [Server Manager Guide](https://mcp-use.com/docs/typescript/client/server-manager)
- [Official MCP SDK](https://github.com/modelcontextprotocol/typescript-sdk)
- [Python mcp-use Client](https://mcp-use.com/docs/python/client)
