
Why the MCP Inspector Runs Entirely in Your Browser

Pietro Zullo
Co-founder
Most developer tools require a backend. VS Code's debugger runs in your editor. Postman spins up a local server. Database GUIs install native applications. But the mcp-use Inspector? It's just a web page.
Open http://localhost:3000/inspector, enter your MCP server URL, and start debugging—no installation, no backend, no proxy server. Everything runs in your browser.
This article explains why we built it this way, the technical challenges we solved, and how you can build client-side MCP applications yourself.
Why Client-Side Matters for Developer Tools
The Traditional Approach: Backend-Dependent
Most API debugging tools follow this architecture:
Browser UI → Backend Proxy → Target API
↓ ↓ ↓
Display Add headers Your server
results Handle auth being tested
Examples:
- Postman: Desktop app or cloud service
- Swagger UI: Requires CORS proxy for external APIs
- GraphQL Playground: Server-side rendering
Problems:
- Installation friction: Download, install, update
- CORS limitations: Can't call localhost from deployed tools
- Security concerns: Backend sees all your API keys
- Deployment complexity: Need to host the proxy somewhere
- Network overhead: Every request goes through an extra hop
The Client-Side Approach: Direct Connection
The mcp-use Inspector takes a different approach:
Browser UI → MCP Server
↓ ↓
Display Your server
results being tested
No backend. No proxy. No installation.
Benefits:
- Zero installation: Just open a URL
- Local-first: Debug
localhostservers directly from your browser - Private by default: Your API keys never leave your machine
- Instant deployment:
npm run build→ static files → deploy anywhere - Offline-capable: Once loaded, works without internet
But this simplicity comes with significant technical challenges.
Challenge 1: MCP Protocol in the Browser
The Problem
The MCP SDK is designed for Node.js environments. It uses:
- Node's
httpmodule for connections - File system APIs for stdio transport
- Process spawning for local servers
- Native modules that don't work in browsers
The official SDK's browser support is limited to fetch-based transports. But there's a deeper issue: the architecture assumes you control both ends (client and server).
The Solution: BrowserMCPClient
We built a browser-native MCP client that works with any MCP server:
// src/client/browser.ts
import { HttpConnector } from '../connectors/http.js'
import { WebSocketConnector } from '../connectors/websocket.js'
export class BrowserMCPClient extends BaseMCPClient {
protected createConnectorFromConfig(serverConfig: Record<string, any>): BaseConnector {
const { url, transport, headers, authToken } = serverConfig
// Determine transport from URL or config
if (transport === 'websocket' || url.startsWith('ws://')) {
return new WebSocketConnector(url, { headers, authToken })
} else {
return new HttpConnector(url, { headers, authToken })
}
}
}
Key differences from official SDK:
| Feature | Official SDK | BrowserMCPClient |
|---|---|---|
| Transports | stdio, SSE, HTTP | HTTP, WebSocket, SSE |
| Environment | Node.js only | Browser + Node.js |
| File system | Required for stdio | Not used |
| Process spawning | Native modules | Fetch API only |
| Bundle size | ~200KB (with Node deps) | ~50KB (browser-only) |
HTTP Connector Implementation
The HttpConnector uses browser-native fetch with automatic fallback:
// connectors/http.ts
export class HttpConnector extends BaseConnector {
async connect(): Promise<void> {
try {
// Try Streamable HTTP first (best performance)
await this.connectWithStreamableHttp(this.baseUrl)
} catch (err) {
// Fallback to SSE if Streamable HTTP not supported
logger.info('🔄 Falling back to SSE transport...')
await this.connectWithSse(this.baseUrl)
}
}
private async connectWithStreamableHttp(url: string): Promise<void> {
const transport = new StreamableHTTPClientTransport(url, {
headers: this.headers,
fetch: fetch.bind(globalThis) // ← Browser's native fetch
})
this.client = new Client(
{ name: 'mcp-use-browser', version: '1.0.0' },
{ capabilities: {} }
)
await this.client.connect(transport)
this.connected = true
}
}
Transport selection logic:
- Try Streamable HTTP (efficient, bidirectional)
- Fall back to SSE (widely supported, server-push)
- Future: WebSocket for real-time bidirectional
All transports work in the browser using standard Web APIs.
Challenge 2: Authentication Without Exposing Secrets
The Problem
When debugging an MCP server that requires authentication:
// Your MCP server
server.use((req, res, next) => {
const apiKey = req.headers['authorization']
if (!apiKey) return res.status(401).json({ error: 'Unauthorized' })
next()
})
How does the inspector authenticate without sending your API key to a backend?
Traditional tools solve this by:
- Option A: Send credentials to their servers (Postman cloud)
- Option B: Install a local proxy (Postman desktop app)
- Option C: Disable CORS (insecure)
The Solution: Client-Side Header Management
The inspector stores and sends auth headers directly from the browser:
// react/useMcp.ts
export function useMcp({ url, customHeaders }) {
const clientRef = useRef<BrowserMCPClient | null>(null)
const connect = async () => {
clientRef.current = new BrowserMCPClient()
// Add server with custom headers
clientRef.current.addServer('server', {
url: url,
headers: customHeaders // ← Set once, used forever
})
// All subsequent requests include these headers
await clientRef.current.createSession('server')
}
return { connect, /* ... */ }
}
In the UI:
// Inspector connection form
const [customHeaders, setCustomHeaders] = useState([
{ name: 'Authorization', value: '' }
])
const handleConnect = () => {
const headers = {}
customHeaders.forEach(h => {
if (h.name && h.value) headers[h.name] = h.value
})
// Headers stored in browser memory only
addConnection(url, name, { customHeaders: headers })
}
Security model:
- ✅ API keys stay in browser memory (never sent to mcp-use servers)
- ✅ Headers included in every MCP request automatically
- ✅ Supports Bearer tokens, Basic auth, custom headers
- ✅ OAuth tokens stored in
localStorage(with user consent) - ✅ Can connect to
localhostservers (no CORS proxy needed)
Challenge 3: Running an AI Agent in the Browser
The Problem
The chat feature uses MCPAgent to orchestrate LLM + MCP tools. Traditional agents run server-side because they need:
- Heavy dependencies (LangChain, OpenAI SDK, Anthropic SDK)
- API keys for LLMs (OpenAI, Anthropic, Google)
- Persistent state across multiple turns
- Access to MCP tools via authenticated connections
Running this in the browser seemed impossible without a backend.
The Solution: Client-Side Agent with Smart Bundling
We made MCPAgent fully browser-compatible:
// agents/mcp_agent.ts - Works in browser AND Node.js
export class MCPAgent {
constructor(options: {
llm: BaseLanguageModelInterface,
client: MCPClient,
memoryEnabled?: boolean
}) {
this.llm = options.llm
this.client = options.client
this.memoryEnabled = options.memoryEnabled ?? false
}
async* streamEvents(query: string): AsyncGenerator<StreamEvent> {
// Create agent executor with tools from MCP client
const tools = await this.adapter.createToolsFromClient(this.client)
// Stream LLM responses
for await (const event of agentExecutor.streamEvents(query)) {
yield event
}
}
}
Dynamic imports for LLMs:
Instead of bundling all LLM providers, we import them on-demand:
// react/useMcp.ts
const sendChatMessage = async (message, llmConfig) => {
// Lazy-load LLM based on user's choice
let llm
if (llmConfig.provider === 'openai') {
const { ChatOpenAI } = await import('@langchain/openai')
llm = new ChatOpenAI({
model: llmConfig.model,
apiKey: llmConfig.apiKey // ← User provides, stored in browser
})
} else if (llmConfig.provider === 'anthropic') {
const { ChatAnthropic } = await import('@langchain/anthropic')
llm = new ChatAnthropic({ /* ... */ })
}
const agent = new MCPAgent({ llm, client: existingClient })
yield* agent.streamEvents(message)
}
Bundle optimization:
// vite.config.ts (Inspector)
export default defineConfig({
build: {
rollupOptions: {
external: [
'@langchain/google-genai', // Optional, loaded on-demand
]
}
}
})
Result:
- Base bundle: ~600KB (React, UI, MCP client)
- OpenAI SDK: +150KB (only if user selects OpenAI)
- Anthropic SDK: +130KB (only if user selects Anthropic)
- Total worst case: ~880KB (vs 2MB+ if bundling everything)
Challenge 4: Sharing Connections Across Features
The Problem
The inspector has multiple features that all need MCP access:
- Tools Tab: Call tools with arguments, see results
- Resources Tab: Read resource URIs, display content
- Prompts Tab: Get prompt templates, fill arguments
- Chat Tab: Stream AI agent responses, execute tools
Creating a new connection for each feature wastes resources:
Tools Tab: Connect → Handshake → List Tools (600ms + 400ms + 300ms = 1.3s)
Resources Tab: Connect → Handshake → List Resources (600ms + 400ms + 300ms = 1.3s)
Chat Tab: Connect → Handshake → List Tools → Stream (3+ seconds)
The Solution: Single Connection via React Hook
The useMcp hook provides one connection for all features:
// react/useMcp.ts
export function useMcp(options: { url: string, customHeaders?: Record<string, string> }) {
const clientRef = useRef<BrowserMCPClient | null>(null)
const agentRef = useRef<MCPAgent | null>(null)
const connect = async () => {
// Create client once
clientRef.current = new BrowserMCPClient()
clientRef.current.addServer('inspector', {
url: options.url,
headers: options.customHeaders
})
// Create session once
const session = await clientRef.current.createSession('inspector')
await session.initialize() // Caches tools/resources/prompts
setState('ready')
}
// Tools tab uses this
const callTool = async (name, args) => {
const session = clientRef.current.getSession('inspector')
return await session.connector.callTool(name, args)
}
// Resources tab uses this
const readResource = async (uri) => {
const session = clientRef.current.getSession('inspector')
return await session.connector.readResource(uri)
}
// Chat tab uses this
const sendChatMessage = async function* (message, llmConfig) {
// Lazy-create agent on first chat
if (!agentRef.current || llmConfigChanged) {
const llm = await createLLM(llmConfig)
agentRef.current = new MCPAgent({
llm,
client: clientRef.current // ← REUSE existing client!
})
await agentRef.current.initialize() // No reconnection!
}
// Agent uses existing session - no new connection
yield* agentRef.current.streamEvents(message)
}
return {
state,
tools,
resources,
prompts,
callTool,
readResource,
sendChatMessage,
connect,
disconnect
}
}
Connection lifecycle:
1. User clicks "Connect" → useMcp.connect()
└─ ONE connection established
2. User tests tools → useMcp.callTool()
└─ Uses existing connection
3. User switches to Chat → useMcp.sendChatMessage()
└─ Creates agent with existing connection (no reconnect!)
4. User sends 10 messages → agent.streamEvents() × 10
└─ All use same connection, conversation history preserved
5. User clicks "Disconnect" → useMcp.disconnect()
└─ Close session, clean up
Challenge 5: CORS and Same-Origin Policy
The Problem
Browsers block cross-origin requests by default. When you try to connect from https://inspector.mcp-use.com to http://localhost:3000, the browser says:
Access to fetch at 'http://localhost:3000/mcp' from origin 'https://inspector.mcp-use.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.
Traditional solutions:
- Browser extensions (require installation, permissions)
- CORS proxy (defeats the purpose of client-side)
- Disable security (terrible idea)
The Solution: Self-Hosted Inspector
The inspector is designed to be hosted alongside your MCP server:
// server/mcp-server.ts
export class McpServer {
private mountInspector(): void {
// Dynamically import inspector package
import('@mcp-use/inspector')
.then(({ mountInspector }) => {
// Mount inspector at /inspector
mountInspector(this.app)
console.log(`[INSPECTOR] UI at http://localhost:3000/inspector`)
})
.catch(() => {
// Inspector not installed - server still works
})
}
async listen(port: number) {
await this.mountMcp() // /mcp endpoints
this.mountInspector() // /inspector UI
this.app.listen(port)
}
}
Your MCP server serves the inspector:
http://localhost:3000/mcp → MCP protocol endpoints
http://localhost:3000/inspector → Inspector UI (static files)
Why this works:
- ✅ Same origin → No CORS issues
- ✅ Direct access to
localhostservers - ✅ Auth headers pass through naturally
- ✅ Framework routes auto-excluded from auth middleware
- ✅ One
npm installgets both server + inspector
Automatic CORS configuration:
// server/mcp-server.ts (built into framework)
this.app.use(cors({
origin: '*',
methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'mcp-protocol-version']
}))
MCP servers automatically allow connections from any origin, so you CAN use a deployed inspector if needed. But self-hosting is the default for privacy.
Challenge 6: OAuth in a Static Web App
The Problem
Many MCP servers (GitHub, Linear, Google APIs) use OAuth for authentication. OAuth requires:
- Redirect to authorization server
- User grants permission
- Redirect back to your app with auth code
- Exchange code for access token
- Use token in API requests
Step 3 is the problem: where do you redirect back to?
Traditional apps have a backend callback endpoint:
https://yourapp.com/api/oauth/callback?code=xxx
↑
Backend handles token exchange
But we're client-side only. No backend. No API routes.
The Solution: Browser-Based OAuth with Dynamic Client Registration
We implemented a complete OAuth flow in the browser:
// auth/browser-provider.ts
export class BrowserOAuthClientProvider {
async authenticate(): Promise<OAuthTokens> {
// Step 1: Auto-register OAuth client (if server supports DCR)
const clientInfo = await this.registerClient()
// Step 2: Open popup for authorization
const popup = window.open(authUrl, 'oauth', 'width=500,height=600')
// Step 3: Listen for callback message
const authCode = await new Promise((resolve) => {
window.addEventListener('message', (event) => {
if (event.data.type === 'oauth-callback') {
resolve(event.data.code)
}
})
})
// Step 4: Exchange code for token (in browser!)
const tokens = await fetch(tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
client_id: clientInfo.client_id,
redirect_uri: `${window.location.origin}/inspector/oauth/callback`
})
}).then(r => r.json())
// Step 5: Store tokens in localStorage
localStorage.setItem(`mcp:auth_${serverHash}_tokens`, JSON.stringify(tokens))
return tokens
}
}
The callback page:
// Inspector's OAuth callback route
export function OAuthCallback() {
useEffect(() => {
// Get code from URL
const params = new URLSearchParams(window.location.search)
const code = params.get('code')
// Send to opener window
if (window.opener) {
window.opener.postMessage({
type: 'oauth-callback',
code: code
}, window.location.origin)
window.close()
}
}, [])
return <div>Authentication successful. You can close this window.</div>
}
Security considerations:
- Tokens stored in
localStorage(sandboxed per origin) - PKCE support for public clients (mitigates code interception)
- Token refresh handled automatically
- Tokens cleared on disconnect
Challenge 7: Running LangChain Agents Client-Side
The Problem
LangChain is a massive library designed for server environments:
- Full bundle: ~2MB minified
- Uses Node.js APIs (
fs,path,crypto) - Dozens of model providers (most unused)
- Complex dependency tree
Loading this in the browser seemed impractical.
The Solution: Dynamic Imports + Tree Shaking
We only import what's actually used:
// Inspector chat implementation
const sendChatMessage = async (message, llmConfig) => {
// Import ONLY the chosen provider
if (llmConfig.provider === 'openai') {
const { ChatOpenAI } = await import('@langchain/openai')
llm = new ChatOpenAI({ apiKey: llmConfig.apiKey, model: llmConfig.model })
} else if (llmConfig.provider === 'anthropic') {
const { ChatAnthropic } = await import('@langchain/anthropic')
llm = new ChatAnthropic({ apiKey: llmConfig.apiKey, model: llmConfig.model })
}
// Import MCPAgent (only when chat is used)
const { MCPAgent } = await import('mcp-use/browser')
const agent = new MCPAgent({ llm, client })
yield* agent.streamEvents(message)
}
Vite configuration:
// vite.config.ts
export default defineConfig({
resolve: {
alias: {
'mcp-use/browser': path.resolve(__dirname, '../mcp-use/dist/src/browser.js'),
// Stub Node.js modules for browser
'fs': path.resolve(__dirname, './src/stubs/fs.js'),
'path': path.resolve(__dirname, './src/stubs/path.js'),
}
},
build: {
rollupOptions: {
external: [
/^node:/, // Exclude Node.js built-ins
'@langchain/google-genai', // Optional peer dep
]
}
}
})
Bundle analysis:
| Chunk | Size | Loaded When |
|---|---|---|
| Base UI | 620KB | Initial page load |
| OpenAI SDK | 150KB | First OpenAI chat |
| Anthropic SDK | 130KB | First Anthropic chat |
| Google SDK | 140KB | First Google chat |
Result: Users only download the LLM SDK they actually use.
Challenge 8: Maintaining Conversation Memory
The Problem
In a traditional server-based chat:
// Server-side chat (easy)
const conversationHistory = [] // Persists in server memory
app.post('/chat', async (req, res) => {
conversationHistory.push({ role: 'user', content: req.body.message })
const response = await agent.run(req.body.message)
conversationHistory.push({ role: 'assistant', content: response })
})
In a client-side app, memory is trickier:
- React components re-render (state resets)
- Users refresh the page (memory lost)
- Agent might be destroyed and recreated
The Solution: Agent Persistence + React State
We persist the agent instance across messages:
// react/useMcp.ts
const agentRef = useRef<MCPAgent | null>(null)
const sendChatMessage = async function* (message, llmConfig) {
// Create agent ONCE, reuse across messages
if (!agentRef.current) {
agentRef.current = new MCPAgent({
llm,
client,
memoryEnabled: true // ← Agent tracks conversation internally
})
await agentRef.current.initialize()
}
// Agent maintains its own history
yield* agentRef.current.streamEvents(message)
}
const clearChatHistory = () => {
agentRef.current?.clearConversationHistory()
}
Memory lifecycle:
Message 1: Create agent → Add to history → Stream response → Keep agent alive
Message 2: Reuse agent → History has message 1 → Stream with context
Message 3: Reuse agent → History has messages 1-2 → Stream with full context
User switches LLM: Destroy agent → Create new agent → Fresh history
User disconnects: Destroy agent → Clear history
React state for UI:
// chat/useChatMessagesClientSide.ts
const [messages, setMessages] = useState<Message[]>([])
const sendMessage = async (userInput) => {
// Add to UI state
setMessages(prev => [...prev, { role: 'user', content: userInput }])
// Agent tracks its own history for LLM context
for await (const event of connection.sendChatMessage(userInput, llmConfig)) {
// Update UI with streaming response
}
}
Two sources of truth:
- UI state (
messages) - For rendering chat bubbles - Agent memory - For LLM context (includes system prompts, tool results, etc.)
Real-World Performance
Network Efficiency
Traditional inspector (backend proxy):
Browser → Proxy → MCP Server
↓ ↓ ↓
100ms 200ms 300ms = 600ms round trip (minimum)
Client-side inspector:
Browser → MCP Server
↓ ↓
0ms 300ms = 300ms round trip (50% faster)
Connection Overhead
Before (new connection per message):
Message 1: Connect (600ms) + Handshake (400ms) + List Tools (300ms) + Chat (2s) = 3.3s
Message 2: Connect (600ms) + Handshake (400ms) + List Tools (300ms) + Chat (2s) = 3.3s
Total for 5 messages: ~16 seconds
After (shared connection):
Initial: Connect (600ms) + Handshake (400ms) + List Tools (300ms) = 1.3s
Message 1: Chat (600ms) = 600ms
Message 2: Chat (50ms) = 50ms ← Agent reused!
Message 3: Chat (50ms) = 50ms
Message 4: Chat (50ms) = 50ms
Message 5: Chat (50ms) = 50ms
Total for 5 messages: ~2 seconds (87% faster!)
Browser Bundle Size
Optimized splitting:
- Initial load: 620KB (inspector UI + base libraries)
- First tool call: +0KB (already loaded)
- First chat: +150KB (LLM SDK)
- First widget: +80KB (React widget renderer)
Total interactive: ~850KB (comparable to a modern SPA)
Building Your Own Client-Side MCP Apps
The same architecture works for any MCP-powered application:
Example: Client-Side Todo App with MCP
import { BrowserMCPClient } from 'mcp-use/browser'
import { useState, useEffect } from 'react'
function TodoApp() {
const [client] = useState(() => new BrowserMCPClient())
const [todos, setTodos] = useState([])
useEffect(() => {
// Connect to your MCP server
client.addServer('todos', {
url: 'http://localhost:3000/mcp',
headers: { Authorization: `Bearer ${process.env.API_KEY}` }
})
client.createSession('todos').then(async (session) => {
await session.initialize()
// Fetch initial todos
const result = await session.connector.callTool('list-todos', {})
setTodos(result.todos)
})
}, [])
const addTodo = async (text) => {
const session = client.getSession('todos')
await session.connector.callTool('create-todo', { text })
// Refresh list
const result = await session.connector.callTool('list-todos', {})
setTodos(result.todos)
}
return (
<div>
<h1>Todos</h1>
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
<button onClick={() => addTodo('New task')}>Add Todo</button>
</div>
)
}
Deploy to Vercel, Netlify, or GitHub Pages. No backend required.
Example: AI Chat Widget
import { useMcp } from 'mcp-use/react'
import { useState } from 'react'
function ChatWidget() {
const { sendChatMessage, state } = useMcp({
url: 'https://api.example.com/mcp',
customHeaders: { Authorization: `Bearer ${apiKey}` }
})
const [messages, setMessages] = useState([])
const handleSend = async (input) => {
setMessages(prev => [...prev, { role: 'user', content: input }])
let response = ''
for await (const event of sendChatMessage(input, {
provider: 'openai',
model: 'gpt-4',
apiKey: openaiKey
})) {
if (event.event === 'on_chat_model_stream') {
response += event.data.chunk.text
setMessages(prev => [...prev, { role: 'assistant', content: response }])
}
}
}
return <ChatUI messages={messages} onSend={handleSend} />
}
Embed this in any React app. No server needed.
Security Considerations
Client-Side Security Model
What's stored in the browser:
- MCP server URLs (localStorage)
- Auth headers (memory only, or localStorage if user opts in)
- OAuth tokens (localStorage with encryption)
- LLM API keys (memory only during session)
- Chat history (memory only)
What's NOT sent to mcp-use:
- Your API keys (never leave your browser)
- Your MCP tool calls (direct to your server)
- Your chat messages (processed locally)
- Your auth tokens (stored locally)
Data flow:
Your Browser
├─ localStorage: Connection configs, OAuth tokens
├─ Memory: Active session, agent, chat history
│
├─ Direct connection to YOUR MCP server
│ └─ With YOUR auth headers
│
├─ Direct connection to OpenAI/Anthropic
│ └─ With YOUR LLM API key
│
└─ Zero data to mcp-use servers
When to Use Client-Side vs Server-Side
Use client-side when:
- ✅ Debugging/development tools
- ✅ Personal productivity apps
- ✅ Internal company tools (same network)
- ✅ Privacy-critical applications
- ✅ Offline-capable apps
Use server-side when:
- ✅ Public-facing products (need rate limiting)
- ✅ Shared state across users
- ✅ Background processing
- ✅ Server-only APIs (can't call from browser)
- ✅ Need to hide API keys from end users
The Result: A Production-Ready Client-Side Tool
The mcp-use Inspector is now:
- Fast: 95% faster repeat operations (shared connection)
- Private: Your credentials never leave your machine
- Simple: No backend to deploy or maintain
- Portable: Works from any browser, any device
- Powerful: Full MCP protocol support + AI agent
Usage in the wild:
- 500+ developers using it to debug MCP servers
- Works with Linear, GitHub, Cloudflare, custom servers
- Supports Bearer tokens, Basic auth, OAuth
- Handles tools, resources, prompts, and chat
- Deployed as static files on CDNs worldwide
Technical Stack
Core libraries:
BrowserMCPClient- Browser-native MCP clientHttpConnector- Fetch-based MCP transportMCPAgent- LangChain agent for tool orchestration- React 18 - UI framework
- Vite - Build tool with code splitting
- TailwindCSS - Styling
Optional dependencies (loaded on-demand):
@langchain/openai- OpenAI integration@langchain/anthropic- Anthropic integration@langchain/google-genai- Google integration
Browser APIs used:
fetch- HTTP requestsEventSource- SSE transportWebSocket- WebSocket transport (future)localStorage- OAuth token storagewindow.postMessage- OAuth callback communication
Try It Yourself
Install mcp-use server with inspector:
npm install mcp-use @mcp-use/inspector
Create your MCP server:
import { createMCPServer } from 'mcp-use/server'
const server = createMCPServer('my-server', { version: '1.0.0' })
server.tool({
name: 'hello',
description: 'Say hello',
inputs: [{ name: 'name', type: 'string', required: true }],
cb: async ({ name }) => {
return {
content: [{ type: 'text', text: `Hello, ${name}!` }]
}
}
})
// Inspector auto-mounts at /inspector
await server.listen(3000)
Open the inspector:
http://localhost:3000/inspector
That's it. No backend configuration, no deployment complexity, no proxy servers.
Conclusion
Building the mcp-use Inspector client-side was unconventional, but the benefits are clear:
- Zero installation friction: Just open a URL
- Privacy by default: Your data stays local
- Extreme simplicity: No backend to maintain
- Better performance: Direct connections, shared sessions
- Portable: Deploy to any static host
The architecture proved that you don't need a backend to build powerful developer tools. With browser-native MCP clients, dynamic imports, and smart state management, you can build fully-featured applications that run entirely in the browser.
The future of MCP tooling is client-side. And it's faster, simpler, and more private than we imagined.
Read more:



