Skip to main content

useMcp Hook

The useMcp hook provides a complete React interface for MCP server connections. It handles connection management, authentication, tool execution, and AI chat—all with automatic reconnection and OAuth support.

Basic Usage

import { useMcp } from 'mcp-use/react'

function MyComponent() {
  const mcp = useMcp({
    url: 'http://localhost:3000/mcp',
    customHeaders: {
      Authorization: 'Bearer YOUR_API_KEY'
    }
  })

  // Wait for connection
  if (mcp.state !== 'ready') {
    return <div>Connecting...</div>
  }

  // Show available tools
  return (
    <div>
      <h2>Available Tools</h2>
      <ul>
        {mcp.tools.map(tool => (
          <li key={tool.name}>{tool.name}: {tool.description}</li>
        ))}
      </ul>
    </div>
  )
}

Connection States

The hook manages connection state automatically:
const mcp = useMcp({ url })

// State progression:
// 'discovering' → Initial state, attempting connection
// 'authenticating' → OAuth flow in progress
// 'pending_auth' → Waiting for user to approve OAuth
// 'ready' → Connected and ready to use
// 'failed' → Connection failed (check mcp.error)

Handling Connection States

function ConnectionStatus() {
  const mcp = useMcp({ url: 'http://localhost:3000/mcp' })

  switch (mcp.state) {
    case 'discovering':
      return <Spinner>Connecting...</Spinner>

    case 'authenticating':
      return <div>Authenticating... Check for popup window</div>

    case 'pending_auth':
      return (
        <button onClick={mcp.authenticate}>
          Click to Authenticate
        </button>
      )

    case 'ready':
      return <div>✅ Connected ({mcp.tools.length} tools available)</div>

    case 'failed':
      return (
        <div>
Connection failed: {mcp.error}
          <button onClick={mcp.retry}>Retry</button>
        </div>
      )
  }
}

Configuration Options

interface UseMcpOptions {
  // Required
  url: string                              // MCP server URL

  // Connection
  enabled?: boolean                        // Enable/disable connection (default: true)
  customHeaders?: Record<string, string>   // Custom headers (auth, etc.)
  transportType?: 'auto' | 'http' | 'sse'  // Transport preference
  timeout?: number                         // Connection timeout (ms, default: 30000)
  sseReadTimeout?: number                  // SSE read timeout (ms, default: 300000)

  // OAuth
  clientName?: string                      // OAuth client name
  clientUri?: string                       // OAuth client URI
  callbackUrl?: string                     // OAuth callback URL
  preventAutoAuth?: boolean                // Prevent automatic OAuth popup
  onPopupWindow?: (win: Window) => void    // OAuth popup window handler
  storageKeyPrefix?: string                // localStorage key prefix

  // Reconnection
  autoRetry?: boolean | number             // Auto-retry on failure
  autoReconnect?: number                   // Auto-reconnect delay (ms)

  // Client info
  clientConfig?: {
    name?: string                          // Client name
    version?: string                       // Client version
  }
}

Authentication

Bearer Token Authentication

const mcp = useMcp({
  url: 'http://localhost:3000/mcp',
  customHeaders: {
    Authorization: 'Bearer YOUR_API_KEY'
  }
})

OAuth Authentication

const mcp = useMcp({
  url: 'https://mcp.linear.app/mcp',
  clientName: 'My App',
  callbackUrl: window.location.origin + '/oauth/callback',
  onPopupWindow: (popup) => {
    // Track popup for UX
    console.log('OAuth popup opened:', popup)
  }
})

// OAuth handled automatically:
// 1. Server returns 401
// 2. SDK triggers OAuth popup
// 3. User authorizes
// 4. Popup sends callback
// 5. Connection retries with token

OAuth Callback Page

Create an OAuth callback route to handle redirects:
// /oauth/callback route
import { useEffect } from 'react'

export function OAuthCallback() {
  useEffect(() => {
    const params = new URLSearchParams(window.location.search)
    const code = params.get('code')
    const error = params.get('error')

    if (window.opener) {
      window.opener.postMessage({
        type: 'mcp_auth_callback',
        success: !error,
        code: code,
        error: error
      }, window.location.origin)

      window.close()
    }
  }, [])

  return <div>Authentication successful. You can close this window.</div>
}

Calling Tools

function ToolExecutor() {
  const mcp = useMcp({ url: 'http://localhost:3000/mcp' })
  const [result, setResult] = useState(null)

  const handleSendEmail = async () => {
    try {
      const result = await mcp.callTool('send-email', {
        to: '[email protected]',
        subject: 'Hello',
        body: 'Test message'
      })
      setResult(result)
    } catch (error) {
      console.error('Tool call failed:', error)
    }
  }

  return (
    <div>
      <button onClick={handleSendEmail} disabled={mcp.state !== 'ready'}>
        Send Email
      </button>
      {result && <pre>{JSON.stringify(result, null, 2)}</pre>}
    </div>
  )
}

Reading Resources

function ResourceViewer({ uri }: { uri: string }) {
  const mcp = useMcp({ url: 'http://localhost:3000/mcp' })
  const [content, setContent] = useState('')

  useEffect(() => {
    if (mcp.state === 'ready') {
      mcp.readResource(uri).then(resource => {
        setContent(resource.contents[0].text)
      })
    }
  }, [mcp.state, uri])

  return (
    <div>
      <h3>Resource: {uri}</h3>
      <pre>{content}</pre>
    </div>
  )
}

Listing Resources and Prompts

function ServerExplorer() {
  const mcp = useMcp({ url: 'http://localhost:3000/mcp' })

  const handleListResources = async () => {
    await mcp.listResources()
    // Updates mcp.resources state
  }

  const handleListPrompts = async () => {
    await mcp.listPrompts()
    // Updates mcp.prompts state
  }

  return (
    <div>
      <button onClick={handleListResources}>Refresh Resources</button>
      <button onClick={handleListPrompts}>Refresh Prompts</button>

      <h3>Resources ({mcp.resources.length})</h3>
      <ul>
        {mcp.resources.map(r => (
          <li key={r.uri}>{r.name || r.uri}</li>
        ))}
      </ul>

      <h3>Prompts ({mcp.prompts.length})</h3>
      <ul>
        {mcp.prompts.map(p => (
          <li key={p.name}>{p.name}: {p.description}</li>
        ))}
      </ul>
    </div>
  )
}

AI Chat with MCP Tools

The useMcp hook exposes the underlying client which you can use to create an AI agent with access to all MCP tools.
For browser apps: Use dynamic import of mcp-use/browser (shown below) to avoid bundling server-side code. For Node.js apps: You can import directly from mcp-use.
import { useMcp } from 'mcp-use/react'
import { ChatOpenAI } from '@langchain/openai'
import { useState, useMemo, useEffect } from 'react'

function ChatInterface() {
  const mcp = useMcp({
    url: 'http://localhost:3000/mcp',
    customHeaders: { Authorization: 'Bearer YOUR_API_KEY' }
  })

  const [agent, setAgent] = useState(null)
  const [messages, setMessages] = useState<Array<{role: string, content: string}>>([])
  const [input, setInput] = useState('')
  const [isLoading, setIsLoading] = useState(false)

  // Create LLM instance
  const llm = useMemo(() => new ChatOpenAI({
    model: 'gpt-4',
    apiKey: process.env.OPENAI_API_KEY,
    temperature: 0.7
  }), [])

  // Create agent when connection is ready
  useEffect(() => {
    if (mcp.state === 'ready' && mcp.client && !agent) {
      // Use dynamic import for browser environments to avoid bundling server-side code
      import('mcp-use/browser').then(({ MCPAgent }) => {
        const newAgent = new MCPAgent({
          llm,
          client: mcp.client,
          memoryEnabled: true,
          systemPrompt: 'You are a helpful assistant.'
        })
        newAgent.initialize().then(() => setAgent(newAgent))
      })
    }
  }, [mcp.state, mcp.client, llm])

  const handleSend = async () => {
    if (!input.trim() || !agent) return

    const userMessage = { role: 'user', content: input }
    setMessages(prev => [...prev, userMessage])
    setInput('')
    setIsLoading(true)

    try {
      let assistantContent = ''

      // Stream response from AI agent
      for await (const event of agent.streamEvents(input)) {
        // Handle streaming text
        if (event.event === 'on_chat_model_stream' && event.data?.chunk?.text) {
          assistantContent += event.data.chunk.text

          // Update UI with streaming text
          setMessages(prev => {
            const last = prev[prev.length - 1]
            if (last?.role === 'assistant') {
              return [
                ...prev.slice(0, -1),
                { role: 'assistant', content: assistantContent }
              ]
            }
            return [...prev, { role: 'assistant', content: assistantContent }]
          })
        }

        // Handle tool calls
        if (event.event === 'on_tool_start') {
          console.log('Using tool:', event.name)
        }
      }
    } catch (error) {
      console.error('Chat error:', error)
      setMessages(prev => [...prev, {
        role: 'assistant',
        content: `Error: ${error.message}`
      }])
    } finally {
      setIsLoading(false)
    }
  }

  const handleClearHistory = () => {
    if (agent) {
      agent.clearConversationHistory()
      setMessages([])
    }
  }

  return (
    <div>
      <div className="messages">
        {messages.map((msg, i) => (
          <div key={i} className={msg.role}>
            <strong>{msg.role}:</strong> {msg.content}
          </div>
        ))}
      </div>

      <input
        value={input}
        onChange={e => setInput(e.target.value)}
        onKeyDown={e => e.key === 'Enter' && handleSend()}
        disabled={isLoading || !agent}
      />

      <button onClick={handleSend} disabled={isLoading || !agent}>
        {isLoading ? 'Sending...' : 'Send'}
      </button>

      <button onClick={handleClearHistory} disabled={!agent}>
        Clear History
      </button>
    </div>
  )
}
Key features:
  • 🧠 Conversation memory: The agent remembers previous messages (set memoryEnabled: true)
  • 🛠️ Automatic tool access: Agent can call any MCP tool from the connected server
  • Streaming responses: Real-time token streaming for better UX
  • 🔄 Agent persistence: Reuse the same agent across messages for memory

Supported LLM Providers

MCPAgent works with any LangChain chat model:
// OpenAI
import { ChatOpenAI } from '@langchain/openai'
const llm = new ChatOpenAI({
  model: 'gpt-4',
  apiKey: process.env.OPENAI_API_KEY
})

// Anthropic
import { ChatAnthropic } from '@langchain/anthropic'
const llm = new ChatAnthropic({
  model: 'claude-3-5-sonnet-20241022',
  apiKey: process.env.ANTHROPIC_API_KEY
})

// Google
import { ChatGoogleGenerativeAI } from '@langchain/google-genai'
const llm = new ChatGoogleGenerativeAI({
  model: 'gemini-pro',
  apiKey: process.env.GOOGLE_API_KEY
})

// Or any other LangChain-compatible model
import { ChatOllama } from '@langchain/community/chat_models/ollama'
const llm = new ChatOllama({ model: 'llama2' })

Error Handling

function RobustConnection() {
  const mcp = useMcp({
    url: 'http://localhost:3000/mcp',
    autoRetry: 5000,  // Auto-retry every 5 seconds on failure
  })

  // Monitor errors
  useEffect(() => {
    if (mcp.state === 'failed') {
      console.error('Connection failed:', mcp.error)

      // Common errors:
      if (mcp.error?.includes('401')) {
        alert('Add your Authorization header in settings')
      } else if (mcp.error?.includes('ECONNREFUSED')) {
        alert('Server is not running on ' + url)
      }
    }
  }, [mcp.state, mcp.error])

  return (
    <div>
      {mcp.state === 'failed' && (
        <div className="error">
          {mcp.error}
          <button onClick={mcp.retry}>Retry Connection</button>
        </div>
      )}
    </div>
  )
}

Automatic Reconnection

const mcp = useMcp({
  url: 'http://localhost:3000/mcp',
  autoReconnect: 3000,  // Reconnect after 3 seconds if connection drops
  autoRetry: 5000,      // Retry every 5 seconds on failure
})

// The hook automatically:
// - Detects connection drops
// - Waits specified delay
// - Attempts reconnection
// - Maintains state across retries

Debugging and Logs

function ConnectionDebugger() {
  const mcp = useMcp({ url: 'http://localhost:3000/mcp' })

  return (
    <div>
      <h3>Connection Logs</h3>
      <div className="logs">
        {mcp.log.map((entry, i) => (
          <div key={i} className={entry.level}>
            [{new Date(entry.timestamp).toLocaleTimeString()}]
            [{entry.level}] {entry.message}
          </div>
        ))}
      </div>

      <button onClick={mcp.clearStorage}>
        Clear OAuth Tokens
      </button>
    </div>
  )
}

Transport Selection

The hook supports multiple transports with automatic fallback:
// Automatic (default): Try HTTP, fallback to SSE
const mcp1 = useMcp({
  url: 'http://localhost:3000/mcp',
  transportType: 'auto'  // Default
})

// Force HTTP only
const mcp2 = useMcp({
  url: 'http://localhost:3000/mcp',
  transportType: 'http'
})

// Force SSE only
const mcp3 = useMcp({
  url: 'http://localhost:3000/mcp',
  transportType: 'sse'
})

Complete Example: Todo App

import { useMcp } from 'mcp-use/react'
import { useState, useEffect } from 'react'

function TodoApp() {
  const mcp = useMcp({
    url: 'http://localhost:3000/mcp',
    customHeaders: {
      Authorization: `Bearer ${process.env.API_KEY}`
    },
    autoReconnect: 3000
  })

  const [todos, setTodos] = useState([])
  const [newTodo, setNewTodo] = useState('')

  // Load todos when connected
  useEffect(() => {
    if (mcp.state === 'ready') {
      loadTodos()
    }
  }, [mcp.state])

  const loadTodos = async () => {
    try {
      const result = await mcp.callTool('list-todos', {})
      setTodos(result.todos || [])
    } catch (error) {
      console.error('Failed to load todos:', error)
    }
  }

  const addTodo = async () => {
    if (!newTodo.trim()) return

    try {
      await mcp.callTool('create-todo', {
        text: newTodo,
        completed: false
      })
      setNewTodo('')
      await loadTodos()
    } catch (error) {
      console.error('Failed to create todo:', error)
    }
  }

  const toggleTodo = async (id: string) => {
    try {
      await mcp.callTool('toggle-todo', { id })
      await loadTodos()
    } catch (error) {
      console.error('Failed to toggle todo:', error)
    }
  }

  if (mcp.state === 'discovering') {
    return <div>Connecting to server...</div>
  }

  if (mcp.state === 'failed') {
    return (
      <div>
        <p>Connection failed: {mcp.error}</p>
        <button onClick={mcp.retry}>Retry</button>
      </div>
    )
  }

  if (mcp.state !== 'ready') {
    return <div>Authenticating...</div>
  }

  return (
    <div>
      <h1>Todos</h1>

      <div>
        <input
          value={newTodo}
          onChange={e => setNewTodo(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && addTodo()}
          placeholder="Add a todo..."
        />
        <button onClick={addTodo}>Add</button>
      </div>

      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
          </li>
        ))}
      </ul>

      <div className="status">
        Connected to {mcp.tools.length} tools
      </div>
    </div>
  )
}

API Reference

State Properties

PropertyTypeDescription
state'discovering' | 'authenticating' | 'pending_auth' | 'ready' | 'failed'Current connection state
toolsTool[]Available tools from the server
resourcesResource[]Available resources
promptsPrompt[]Available prompt templates
errorstring | undefinedError message if connection failed
logArray<{level, message, timestamp}>Connection log entries
authUrlstring | undefinedOAuth authorization URL (if manual auth needed)

Methods

callTool(name, args)

Execute a tool on the MCP server.
const result = await mcp.callTool('tool-name', {
  param1: 'value1',
  param2: 'value2'
})

readResource(uri)

Read a resource by URI.
const resource = await mcp.readResource('file:///path/to/file')
console.log(resource.contents[0].text)

listResources()

Refresh the list of available resources.
await mcp.listResources()
// mcp.resources is now updated

listPrompts()

Refresh the list of available prompts.
await mcp.listPrompts()
// mcp.prompts is now updated

getPrompt(name, args)

Get a prompt template with arguments.
const prompt = await mcp.getPrompt('code-review', {
  language: 'typescript',
  focus: 'security'
})
console.log(prompt.messages)

client

The underlying BrowserMCPClient instance. Use this to create an MCPAgent for AI chat functionality.
import { ChatOpenAI } from '@langchain/openai'

const mcp = useMcp({ url: 'http://localhost:3000/mcp' })

// For browser environments, use dynamic import
const { MCPAgent } = await import('mcp-use/browser')

// Create agent with the exposed client
const agent = new MCPAgent({
  llm: new ChatOpenAI({ model: 'gpt-4' }),
  client: mcp.client,
  memoryEnabled: true
})

await agent.initialize()

// Use agent for chat
for await (const event of agent.streamEvents('Send an email')) {
  console.log(event)
}

// Clear conversation history
agent.clearConversationHistory()
Note: Use import('mcp-use/browser') for browser apps to avoid bundling server-side connectors (stdio, etc.).

retry()

Manually retry connection after failure.
if (mcp.state === 'failed') {
  mcp.retry()
}

disconnect()

Disconnect from the server.
await mcp.disconnect()

authenticate()

Trigger manual OAuth authentication flow.
if (mcp.state === 'pending_auth') {
  mcp.authenticate()  // Opens OAuth popup
}

clearStorage()

Clear OAuth tokens and disconnect.
mcp.clearStorage()  // Removes tokens from localStorage

Advanced Patterns

Conditional Connection

function ConditionalMcp({ enabled }: { enabled: boolean }) {
  const mcp = useMcp({
    url: 'http://localhost:3000/mcp',
    enabled: enabled  // Only connect when enabled is true
  })

  return (
    <div>
      {enabled ? (
        <div>Connection state: {mcp.state}</div>
      ) : (
        <div>Connection disabled</div>
      )}
    </div>
  )
}

Multiple Independent Connections

function MultiServerApp() {
  const github = useMcp({ url: 'https://api.github.com/mcp' })
  const linear = useMcp({ url: 'https://mcp.linear.app/mcp' })
  const custom = useMcp({ url: 'http://localhost:3000/mcp' })

  // Each connection is independent
  return (
    <div>
      <div>GitHub: {github.state} ({github.tools.length} tools)</div>
      <div>Linear: {linear.state} ({linear.tools.length} tools)</div>
      <div>Custom: {custom.state} ({custom.tools.length} tools)</div>
    </div>
  )
}

Custom Connection Flow

function CustomConnectionFlow() {
  const [url, setUrl] = useState('')
  const [shouldConnect, setShouldConnect] = useState(false)

  const mcp = useMcp({
    url: url,
    enabled: shouldConnect && url !== ''
  })

  const handleConnect = () => {
    if (url) {
      setShouldConnect(true)
    }
  }

  const handleDisconnect = () => {
    setShouldConnect(false)
    mcp.disconnect()
  }

  return (
    <div>
      <input
        value={url}
        onChange={e => setUrl(e.target.value)}
        placeholder="MCP Server URL"
        disabled={shouldConnect}
      />

      {!shouldConnect ? (
        <button onClick={handleConnect}>Connect</button>
      ) : (
        <button onClick={handleDisconnect}>Disconnect</button>
      )}

      {mcp.state === 'ready' && (
        <div>✅ Connected with {mcp.tools.length} tools</div>
      )}
    </div>
  )
}

Best Practices

1. Handle All Connection States

Always handle all possible states to provide good UX:
const renderConnectionState = (mcp: UseMcpResult) => {
  switch (mcp.state) {
    case 'discovering':
      return <Spinner />
    case 'authenticating':
      return <div>Check popup for authentication...</div>
    case 'pending_auth':
      return <button onClick={mcp.authenticate}>Authenticate</button>
    case 'ready':
      return <div>✅ Connected</div>
    case 'failed':
      return <div>❌ {mcp.error} <button onClick={mcp.retry}>Retry</button></div>
  }
}

2. Wait for ‘ready’ State

Don’t call methods until connected:
const handleAction = async () => {
  if (mcp.state !== 'ready') {
    alert('Not connected yet')
    return
  }

  await mcp.callTool('my-tool', {})
}

3. Clean Up on Unmount

The hook automatically disconnects on unmount, but you can force cleanup:
useEffect(() => {
  return () => {
    mcp.disconnect()
  }
}, [])

4. Secure API Keys

Never hardcode API keys:
// ❌ Bad
const mcp = useMcp({
  customHeaders: { Authorization: 'Bearer sk_live_12345' }
})

// ✅ Good
const mcp = useMcp({
  customHeaders: {
    Authorization: `Bearer ${process.env.REACT_APP_API_KEY}`
  }
})

TypeScript Types

import type { UseMcpOptions, UseMcpResult } from 'mcp-use/react'

// Full type definitions available
const options: UseMcpOptions = {
  url: 'http://localhost:3000/mcp',
  customHeaders: { Authorization: 'Bearer xxx' },
  autoRetry: true,
  transportType: 'auto'
}

const mcp: UseMcpResult = useMcp(options)

Comparison with MCPClient

FeatureuseMcp HookMCPClient Class
Use caseReact appsAny TypeScript app
Multi-serverSingle serverMultiple servers
ReconnectionAutomaticManual
React stateBuilt-inManual useState
Chat supportBuilt-inVia MCPAgent
Best forUI componentsBackground services, agents
For multi-server AI agents, use MCPClient + MCPAgent instead.

Next Steps