A React-based AI agent system that implements the Model Context Protocol (MCP) architecture entirely within the browser. This system demonstrates how to run MCP Host ↔ Client ↔ Server components in a browser environment while maintaining protocol compliance with the MCP specification (2025-06-18).
This project implements a complete MCP Host ↔ Client ↔ Server architecture running in the browser. The system uses in-memory JSON-RPC communication between MCP components, allowing for MCP protocol compliance without requiring separate processes.
- Browser-Native MCP: Implements MCP protocol components within a single browser environment
- Protocol Compliance: Follows the official MCP specification (2025-06-18) for component interactions
- In-Memory Transport: Uses optimized JSON-RPC communication between MCP components
- Multi-Agent Support: Supports multiple AI agents sharing MCP infrastructure
- Tool Management: Comprehensive tool registration and execution system
- Node.js 18+
- OpenAI API key (required for AI functionality)
-
Clone and navigate to the project:
git clone <repository-url> cd mcp-browser-agent
-
Install dependencies:
# Install backend dependencies cd backend npm install # Install frontend dependencies cd ../frontend npm install cd ..
-
Configure environment:
# Copy environment template cp backend/.env.example backend/.env # Edit backend/.env and add your OpenAI API key: # OPENAI_API_KEY=your_openai_api_key_here # PORT=3001
-
Start the application:
# Terminal 1: Start backend server cd backend npm run dev # Terminal 2: Start frontend (in a new terminal) cd frontend npm run dev
-
Open your browser: Navigate to http://localhost:3000
That's it! The MCP system is ready. Click "Start Agent" to begin interacting with the AI assistant.
This implementation follows the official MCP specification (2025-06-18) exactly, implementing the required Host ↔ Client ↔ Server architecture with proper JSON-RPC communication.
┌─────────────────────────────────────────────────────────────┐
│ MCP HOST │
│ ApplicationHost.ts + MCPHost.ts │
│ │
│ • Creates and manages MCP client instances │
│ • Controls connection permissions and lifecycle │
│ • Enforces security policies and user consent │
│ • Handles user authorization decisions │
│ • Coordinates AI/LLM integration and sampling │
│ • Manages context aggregation across clients │
│ │
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
│ │ MCP CLIENT │◄──►│ MCP SERVER │ │
│ │ MCPClient.ts │ │ MCPServer.ts │ │
│ │ │ │ │ │
│ │ • Maintains │ │ • Exposes tools via │ │
│ │ stateful session │ │ MCP primitives │ │
│ │ • Protocol │ │ • Handles JSON-RPC │ │
│ │ negotiation │ │ requests │ │
│ │ • Message routing │ │ • Capability │ │
│ │ • Subscriptions │ │ negotiation │ │
│ │ • Security │ │ • Security boundaries │ │
│ │ boundaries │ │ • Tool execution │ │
│ └─────────────────────┘ └─────────────────────────┘ │
│ │ │ │
│ └─────────┬───────────────────┘ │
│ │ │
│ ┌─────────────────┐ │
│ │ JSON-RPC TRANSPORT │
│ │ MCPTransport.ts │ │
│ │ │ │
│ │ • Request/Response correlation │
│ │ • Serialization/Deserialization │
│ │ • Error handling │
│ │ • Message routing │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Browser Process
According to the official MCP specification, MCP follows a Host ↔ Client ↔ Server architecture. Here's how our browser implementation maps perfectly to these roles:
Official Role: Container and coordinator for the entire MCP ecosystem
Our Implementation:
- ApplicationHost: Application-level orchestration, conversation management, LLM integration
- MCPHost: Pure MCP protocol coordination, multi-client management, context aggregation
- Responsibilities:
- Creates and manages multiple MCP client instances
- Controls client connection permissions and lifecycle
- Enforces security policies and user consent requirements
- Handles user authorization decisions
- Coordinates AI/LLM integration and sampling requests
- Manages context aggregation across all connected servers
Official Role: Protocol client maintaining 1:1 connection with MCP server
Our Implementation:
- Establishes stateful session with the MCP server using JSON-RPC
- Handles protocol negotiation and capability exchange
- Routes protocol messages bidirectionally with proper correlation
- Manages subscriptions and notifications (future feature)
- Maintains security boundaries between servers
- Provides health monitoring and connection management
Official Role: Provides specialized context and capabilities
Our Implementation:
- Exposes resources, tools, and prompts via MCP primitives
- Operates independently with focused responsibilities
- Handles JSON-RPC requests with proper error handling
- Manages capability negotiation during initialization
- Maintains proper security constraints and validation
- Provides comprehensive health monitoring and statistics
The system implements true JSON-RPC 2.0 communication between MCP components:
// JSON-RPC initialization request
const initRequest: InitializeRequest = {
protocolVersion: "2025-06-18",
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true, listChanged: true },
sampling: {}
},
clientInfo: {
name: "mcp-browser-client",
version: "1.0.0"
}
};
const response = await transport.sendRequest(MCPMethods.INITIALIZE, initRequest);// JSON-RPC tools list request
const toolsResponse = await transport.sendRequest(MCPMethods.TOOLS_LIST);
// Convert MCP tools to OpenAI function format
const tools = toolsResponse.tools.map(tool => ({
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.inputSchema
}
}));// JSON-RPC tool call request
const mcpResult = await transport.sendRequest(MCPMethods.TOOLS_CALL, {
name: toolCall.function.name,
arguments: arguments_
});
// MCP response format
interface ToolCallResponse {
content: Array<{
type: string;
text?: string;
data?: any;
}>;
isError?: boolean;
}Our implementation adheres to all four core MCP design principles:
// Simple tool registration
const calculatorTool = createTool(
'calculator',
'Calculate mathematical expressions safely',
schema,
async (args) => {
return { success: true, data: { result: evaluate(args.expression) } };
}
);
mcpServer.registerTool(calculatorTool);- Each tool operates independently with focused functionality
- Multiple tools combine seamlessly through shared protocol
- Modular design supports dynamic tool addition/removal
- Shared MCP infrastructure enables interoperability
- Server receives only tool execution requests via JSON-RPC
- Full conversation history stays with the Host/Client
- Each tool call is isolated with minimal context
- Cross-tool interactions controlled by the Host
- Core protocol provides minimal required functionality
- Additional capabilities negotiated during initialization
- Tools can be added/removed dynamically at runtime
- Backwards compatibility maintained through capability negotiation
When a user clicks "Start Agent", here's the complete initialization sequence:
🚀 User Action: Click "Start Agent"
│
├── 1. UI Component (MCPHostInterface.tsx)
│ ├── Button click handler triggered
│ ├── Set loading state: "Starting agent..."
│ └── Call: agentManager.createAgent()
│
├── 2. Agent Manager (MultiAgentManager.ts)
│ ├── Generate unique agent ID
│ ├── Create new ApplicationHost instance
│ ├── Store agent in registry
│ └── Call: applicationHost.initialize()
│
├── 3. Application Host (ApplicationHost.ts)
│ ├── Set startTime = Date.now()
│ ├── Log: "🚀 Starting MCP Browser Agent Application..."
│ ├── Call: initializeMCPInfrastructure()
│ ├── Call: initializeConversation()
│ ├── Set isActive = true
│ └── Call: logSystemSummary()
│
├── 4. MCP Infrastructure Setup (ApplicationHost.initializeMCPInfrastructure)
│ ├── Get MCP server instance: getMCPServer()
│ ├── Call: mcpServer.initialize()
│ ├── Create server config with capabilities
│ ├── Call: mcpHost.addServer(serverConfig)
│ └── Log: "✅ MCP infrastructure initialized"
│
├── 5. MCP Server Initialization (MCPServer.initialize)
│ ├── Register all enhanced tools from ServerTools.ts
│ ├── Setup dynamic list_tools implementation
│ ├── Initialize JSON-RPC transport handler
│ ├── Set initialized = true
│ └── Log: "✅ Enhanced MCP Server initialized with X tools"
│
├── 6. MCP Host Server Addition (MCPHost.addServer)
│ ├── Generate unique client ID
│ ├── Create new MCPClient instance
│ ├── Call: client.initialize() [JSON-RPC INITIALIZE]
│ ├── Store client and config in maps
│ └── Log: "✅ Server connected with client ID: xxx"
│
├── 7. MCP Client Initialization (MCPClient.initialize)
│ ├── Get MCP transport instance
│ ├── Build initialize request with protocol version "2025-06-18"
│ ├── Send JSON-RPC: transport.sendRequest(INITIALIZE, request)
│ ├── Receive server capabilities
│ ├── Set connected = true, healthy = true
│ └── Log: "✅ Connected to server with capabilities"
│
├── 8. Conversation Setup (ApplicationHost.initializeConversation)
│ ├── Get MCP context: mcpHost.getMCPContext()
│ ├── Build system prompt with tool information
│ ├── Call: conversationManager.addSystemMessage(prompt)
│ └── Log: "✅ Conversation initialized with system prompt"
│
├── 9. System Summary (ApplicationHost.logSystemSummary)
│ ├── Get application stats
│ ├── Perform health check
│ ├── Log server count, tool availability, LLM status
│ └── Log runtime and overall status
│
└── 10. UI Update
├── Set loading state: false
├── Display: "Agent Started Successfully"
├── Enable chat input
└── Show agent status: "Ready"
⏱️ Total Initialization Time: ~100-200ms
✅ Agent Ready for User Interaction
When a user sends a message, here's the complete processing sequence:
User Input: "Hello, how are you?"
💬 User Action: Send Message
│
├── 1. UI Message Submission
│ ├── User types in chat input
│ ├── Press Enter or click Send
│ ├── Disable input, show "Agent is thinking..."
│ └── Call: agentManager.processMessage(agentId, message)
│
├── 2. Agent Manager Routing
│ ├── Find agent by ID in registry
│ ├── Validate agent is active
│ └── Call: applicationHost.processMessage(message)
│
├── 3. Application Host Processing
│ ├── Log: "👤 User: Hello, how are you?"
│ ├── Call: conversationManager.addUserMessage(message)
│ ├── Call: mcpHost.getMCPContext()
│ ├── Call: getApplicationContext()
│ ├── Call: mergeContexts(mcpContext, appContext)
│ └── Call: getLLMResponse(fullContext)
│
├── 4. MCP Context Aggregation
│ ├── Iterate through all connected clients
│ ├── Call: client.listTools() for each client
│ ├── JSON-RPC: TOOLS_LIST → Server → Response
│ ├── Aggregate all tools: [calculator, dom_query, browser_storage, list_tools]
│ ├── Collect server capabilities and status
│ └── Return: MCPContext with 4 available tools
│
├── 5. LLM Request Preparation
│ ├── Get conversation messages for LLM
│ ├── Convert MCP tools to OpenAI function format
│ ├── Build request payload with tools array
│ ├── Log: "🔧 DEBUG: Available tools from MCP context: 4"
│ └── Call: llmClient.getResponse(messages, {tools})
│
├── 6. LLM Client Processing
│ ├── Validate configuration and messages
│ ├── Add debug event: LLM request started
│ ├── POST /api/llm → Backend → OpenAI API
│ ├── Receive response (no tool_calls for greeting)
│ ├── Add debug event: LLM response received
│ └── Return: {content: "Hello! I'm doing well, thank you for asking..."}
│
├── 7. Response Processing (No Tool Calls)
│ ├── Check: llmResponse.tool_calls = undefined
│ ├── Extract content: "Hello! I'm doing well..."
│ ├── Call: conversationManager.addAssistantMessage(content)
│ ├── Log: "🤖 LLM Response: Hello! I'm doing well..."
│ └── Return response to user
│
└── 8. UI Update
├── Display assistant response in chat
├── Re-enable chat input
├── Update conversation history
└── Show "Ready" status
⏱️ Total Processing Time: ~800-1200ms
✅ Simple conversation completed
User Input: "Calculate sqrt(16) + 5"
💬 User Action: Send Message
│
├── 1-5. [Same as Scenario 1: UI → Agent Manager → App Host → MCP Context → LLM Prep]
│
├── 6. LLM Client Processing
│ ├── POST /api/llm with tools array
│ ├── OpenAI determines tool call needed
│ └── Return: {tool_calls: [{function: {name: "calculator", arguments: '{"expression":"sqrt(16) + 5"}'}}]}
│
├── 7. Tool Call Processing
│ ├── Check: llmResponse.tool_calls.length = 1
│ ├── Call: processToolCalls(llmResponse)
│ ├── Add assistant message with tool_calls to conversation
│ └── Begin tool execution loop
│
├── 8. Tool Execution Loop
│ ├── Extract tool call: calculator("sqrt(16) + 5")
│ ├── Parse arguments: {expression: "sqrt(16) + 5"}
│ ├── Add debug event: Tool call parsed
│ ├── Call: mcpHost.executeTool("calculator", args)
│ └── Route to appropriate MCP client
│
├── 9. MCP Tool Execution
│ ├── MCPHost finds client with calculator tool
│ ├── Call: client.executeTool("calculator", args)
│ ├── JSON-RPC: TOOLS_CALL → MCPServer
│ ├── Server: toolRegistry.executeTool("calculator", args)
│ ├── Tool execution: SafeMathEvaluator.evaluate("sqrt(16) + 5")
│ ├── Result: {success: true, data: {result: 9}}
│ ├── JSON-RPC response: {content: [{type: "text", data: {result: 9}}]}
│ └── Return MCP-compliant response
│
├── 10. Tool Result Processing
│ ├── Extract result: "9"
│ ├── Add debug event: Tool result received
│ ├── Call: conversationManager.addToolResult(toolCallId, "calculator", "9")
│ ├── Log: "✅ Tool execution successful: calculator"
│ └── Conversation now includes tool result
│
├── 11. Final LLM Response
│ ├── Send updated conversation (with tool result) to LLM
│ ├── LLM generates natural language response
│ ├── Response: "The calculation sqrt(16) + 5 equals 9..."
│ ├── No additional tool_calls needed
│ └── Return final response
│
└── 12. UI Update
├── Display: "The calculation sqrt(16) + 5 equals 9..."
├── Show tool execution in activity log
├── Update debug panel with JSON-RPC details
└── Re-enable input
⏱️ Total Processing Time: ~1200-1800ms
✅ Single tool call completed successfully
User Input: "Find the login button and click it"
💬 User Action: Send Message
│
├── 1-6. [Same initial flow as previous scenarios]
│
├── 7. First Tool Call - Page Analysis
│ ├── LLM decides: Need to analyze page first
│ ├── Tool call: dom_query({action: "get_page_info"})
│ ├── JSON-RPC execution → DOM analysis
│ ├── Result: "Found buttons: ['Sign In', 'Register', 'Forgot Password']"
│ ├── Add tool result to conversation
│ └── Continue to next LLM call
│
├── 8. Agent Analysis & Decision
│ ├── Send conversation (with tool result) to LLM
│ ├── LLM analyzes: "No 'Login' button, but 'Sign In' likely matches"
│ ├── LLM decides: Try clicking 'Sign In' button
│ └── Return: {tool_calls: [{function: {name: "dom_query", arguments: '{"textContent":"Sign In","action":"click"}'}}]}
│
├── 9. Second Tool Call - Button Click
│ ├── Tool call: dom_query({textContent: "Sign In", action: "click"})
│ ├── JSON-RPC execution → DOM interaction
│ ├── Result: "Button clicked successfully"
│ ├── Add tool result to conversation
│ └── Continue to final LLM call
│
├── 10. Final Response Generation
│ ├── Send complete conversation to LLM
│ ├── LLM analyzes all tool results
│ ├── No additional tools needed
│ └── Generate: "I found and clicked the login button (labeled 'Sign In')..."
│
└── 11. UI Update
├── Display final response
├── Show both tool executions in activity log
├── Debug panel shows complete JSON-RPC sequence
└── Conversation complete
⏱️ Total Processing Time: ~2000-3000ms
🔄 Multi-step process: 2 tool calls + final response
✅ Complex task completed with adaptation
User Input: "Click the 'NonExistent' button"
💬 Error Handling Flow
│
├── 1-7. [Normal flow until tool execution]
│
├── 8. Tool Execution Failure
│ ├── Tool call: dom_query({textContent: "NonExistent", action: "click"})
│ ├── DOM search finds no matching elements
│ ├── Tool returns: {success: false, error: "No elements found"}
│ ├── MCP response: {content: [{type: "text", text: "No elements found"}], isError: true}
│ └── Add error result to conversation
│
├── 9. Agent Error Recovery
│ ├── LLM receives tool error in conversation
│ ├── LLM analyzes: "Button not found, need to check page"
│ ├── LLM decides: Use get_page_info to see available options
│ └── Generate recovery tool call
│
├── 10. Recovery Tool Execution
│ ├── Tool call: dom_query({action: "get_page_info"})
│ ├── Successful execution returns page elements
│ ├── Add recovery result to conversation
│ └── Continue to final response
│
├── 11. Adaptive Response
│ ├── LLM generates helpful response
│ ├── "I couldn't find a 'NonExistent' button, but I found: [list]"
│ ├── Suggests alternatives based on actual page content
│ └── Graceful error handling completed
│
└── 12. Debug Information
├── Error logged in debug panel
├── Tool execution history shows failure + recovery
├── Complete error context preserved
└── User informed of issue and alternatives
⏱️ Error Recovery Time: ~1500-2500ms
🔄 Error → Analysis → Recovery → Response
✅ Graceful error handling with user guidance
The system follows the official MCP protocol for all tool interactions:
sequenceDiagram
participant User as User Interface
participant AppHost as Application Host
participant MCPHost as MCP Host
participant MCPClient as MCP Client
participant Transport as JSON-RPC Transport
participant MCPServer as MCP Server
participant LLM as LLM Client
participant Backend as Backend Proxy
participant OpenAI as OpenAI API
User->>AppHost: Send Message
AppHost->>MCPHost: getMCPContext()
Note over MCPClient,MCPServer: MCP Initialization (if needed)
MCPHost->>MCPClient: Request context
MCPClient->>Transport: sendRequest(INITIALIZE)
Transport->>MCPServer: JSON-RPC INITIALIZE
MCPServer-->>Transport: Capabilities response
Transport-->>MCPClient: Server capabilities
MCPClient-->>MCPHost: Connection established
MCPHost->>MCPClient: listTools()
MCPClient->>Transport: sendRequest(TOOLS_LIST)
Transport->>MCPServer: JSON-RPC TOOLS_LIST
MCPServer-->>Transport: Available tools
Transport-->>MCPClient: Tools array
MCPClient-->>MCPHost: MCP tools
MCPHost-->>AppHost: MCP context with tools
AppHost->>LLM: getResponse(messages, tools)
LLM->>Backend: POST /api/llm
Backend->>OpenAI: Chat Completions API
OpenAI-->>Backend: Response with tool_calls
Backend-->>LLM: Structured response
LLM-->>AppHost: Parsed response with tool calls
alt Tool calls detected
loop For each tool call
AppHost->>MCPHost: executeTool(name, args)
MCPHost->>MCPClient: executeTool(name, args)
MCPClient->>Transport: sendRequest(TOOLS_CALL)
Transport->>MCPServer: JSON-RPC TOOLS_CALL
MCPServer->>MCPServer: Execute via ToolRegistry
MCPServer-->>Transport: MCP tool result
Transport-->>MCPClient: Tool response
MCPClient-->>MCPHost: Tool result
MCPHost-->>AppHost: Formatted result
AppHost->>AppHost: Add to conversation
end
AppHost->>LLM: getResponse() for final answer
LLM->>Backend: POST /api/llm
Backend->>OpenAI: Chat Completions API
OpenAI-->>Backend: Final response
Backend-->>LLM: Final answer
LLM-->>AppHost: Final response
end
AppHost-->>User: Display response
When a user sends a message, the system follows this precise MCP-compliant flow:
async getMCPContext(): Promise<MCPContext> {
const allTools: MCPTool[] = [];
const serverCapabilities: MCPCapabilities[] = [];
const serverStatus: ServerStatus[] = [];
// Aggregate from all connected MCP servers
for (const [clientId, client] of this.clients.entries()) {
const tools = await client.listTools(); // JSON-RPC TOOLS_LIST
allTools.push(...tools);
const capabilities = client.getServerCapabilities();
serverCapabilities.push(capabilities);
}
return {
availableTools: allTools,
serverCapabilities,
serverStatus,
aggregatedCapabilities: this.aggregateCapabilities(serverCapabilities)
};
}async listTools(): Promise<MCPTool[]> {
const transport = getMCPTransport();
const response: ToolsListResponse = await transport.sendRequest(MCPMethods.TOOLS_LIST);
return response.tools;
}const tools = mcpContext.availableTools.map(tool => ({
type: "function" as const,
function: {
name: tool.name,
description: tool.description,
parameters: tool.inputSchema
}
}));
const llmResponse = await this.llmClient.getResponse(messages, { tools });async executeTool(toolName: string, args: any): Promise<ToolCallResponse> {
const transport = getMCPTransport();
const request: ToolCallRequest = {
name: toolName,
arguments: args
};
const response: ToolCallResponse = await transport.sendRequest(
MCPMethods.TOOLS_CALL,
request
);
return response; // MCP-compliant response format
}private async handleToolsCall(request: JsonRpcRequest): Promise<JsonRpcResponse> {
const params = request.params as ToolCallRequest;
const result = await this.callTool(params.name, params.arguments);
return {
jsonrpc: "2.0",
id: request.id,
result: result // ToolCallResponse format
};
}mcp-browser-agent/
├── backend/ # Express.js API server
│ ├── index.js # OpenAI API proxy server
│ ├── package.json # Backend dependencies
│ └── .env.example # Environment template
├── frontend/ # React MCP application
│ ├── src/
│ │ ├── app/ # 🏠 Application Layer
│ │ │ ├── ApplicationHost.ts # Main application orchestrator
│ │ │ ├── ConversationManager.ts # Chat history management
│ │ │ ├── HostConfiguration.ts # Centralized configuration
│ │ │ └── MultiAgentManager.ts # Multi-agent coordination
│ │ ├── mcp-core/ # 🔌 MCP Protocol Implementation
│ │ │ ├── MCPHost.ts # MCP host coordinator
│ │ │ ├── MCPClient.ts # MCP client protocol handler
│ │ │ ├── MCPServer.ts # MCP server implementation
│ │ │ ├── MCPTransport.ts # JSON-RPC transport layer
│ │ │ └── MCPProtocol.ts # Protocol definitions
│ │ ├── mcp-tools/ # 🛠️ Tool Management System
│ │ │ ├── ToolRegistry.ts # Tool registration & execution
│ │ │ ├── Tool.ts # Tool abstraction framework
│ │ │ └── ServerTools.ts # Built-in tool implementations
│ │ ├── components/ # 🎨 UI Components
│ │ │ ├── MCPHostInterface.tsx # Multi-agent interface
│ │ │ ├── MCPDebugPanel.tsx # Debug & monitoring
│ │ │ └── TokenUsageDisplay.tsx # Token usage tracking
│ │ ├── llm/ # 🧠 LLM Integration
│ │ │ └── LLMClient.ts # OpenAI API client
│ │ ├── debug/ # 📊 Debug & Monitoring
│ │ │ └── DebugEventManager.ts # Event tracking system
│ │ └── App.tsx # Main application entry
│ ├── package.json # Frontend dependencies
│ └── vite.config.ts # Vite configuration
└── README.md # This file
// Official MCP protocol methods (2025-06-18)
export const MCPMethods = {
INITIALIZE: 'initialize',
TOOLS_LIST: 'tools/list',
TOOLS_CALL: 'tools/call'
} as const;
// JSON-RPC 2.0 message structures
export interface JsonRpcRequest {
jsonrpc: '2.0';
id: string | number;
method: string;
params?: any;
}
export interface JsonRpcResponse {
jsonrpc: '2.0';
id: string | number;
result?: any;
error?: JsonRpcError;
}Core Functionality:
- Defines official MCP protocol methods and message formats
- Implements JSON-RPC 2.0 specification for MCP communication
- Provides type safety for all MCP protocol interactions
- Ensures compliance with MCP specification version 2025-06-18
- Includes comprehensive error codes and capability definitions
export class MCPTransport {
async sendRequest(method: string, params?: any): Promise<any> {
const request: JsonRpcRequest = {
jsonrpc: "2.0",
id: this.generateRequestId(),
method,
params
};
// Simulate proper JSON-RPC serialization/deserialization
const serializedRequest = JSON.stringify(request);
const deserializedRequest = JSON.parse(serializedRequest);
const response = await this.messageHandler(deserializedRequest);
// Handle JSON-RPC response with proper error checking
if (response.error) {
throw new MCPError(response.error.message, response.error.code);
}
return response.result;
}
}Core Functionality:
- In-Memory JSON-RPC: Simulates network JSON-RPC with full serialization/deserialization
- Request/Response Correlation: Unique request IDs for proper message matching
- Error Handling: Standard JSON-RPC error codes and MCP-specific errors
- Performance Optimization: Zero-latency communication within browser context
- Debug Integration: Comprehensive request/response logging for monitoring
export class MCPHost {
private clients: Map<string, MCPClient> = new Map();
private serverConfigs: Map<string, ServerConfig> = new Map();
async addServer(config: ServerConfig): Promise<string> {
const clientId = this.generateClientId();
const client = new MCPClient(config.name);
await client.initialize(); // JSON-RPC INITIALIZE
this.clients.set(clientId, client);
this.serverConfigs.set(clientId, config);
return clientId;
}
async getMCPContext(): Promise<MCPContext> {
const allTools: MCPTool[] = [];
// Aggregate tools from all connected servers
for (const [clientId, client] of this.clients.entries()) {
const tools = await client.listTools(); // JSON-RPC TOOLS_LIST
allTools.push(...tools);
}
return {
availableTools: allTools,
serverCapabilities: this.getAggregatedCapabilities(),
serverStatus: await this.getServerStatus(),
aggregatedCapabilities: this.aggregateCapabilities()
};
}
}Core Functionality:
- Multi-Client Management: Creates and manages multiple MCP client instances
- Connection Lifecycle: Handles client connection permissions and lifecycle
- Context Aggregation: Merges context from all connected MCP servers
- Tool Routing: Routes tool execution requests to appropriate clients
- Security Enforcement: Maintains proper isolation between MCP components
- Health Monitoring: Continuous monitoring of all server connections
export class MCPClient {
async initialize(): Promise<void> {
const transport = getMCPTransport();
const initRequest: InitializeRequest = {
protocolVersion: "2025-06-18",
capabilities: this.clientCapabilities,
clientInfo: {
name: "mcp-browser-client",
version: "1.0.0"
}
};
const initResponse = await transport.sendRequest(MCPMethods.INITIALIZE, initRequest);
this.serverCapabilities = initResponse.capabilities;
this.connected = true;
}
async executeTool(toolName: string, args: any): Promise<ToolCallResponse> {
const transport = getMCPTransport();
const response = await transport.sendRequest(MCPMethods.TOOLS_CALL, {
name: toolName,
arguments: args
});
return response; // MCP-compliant ToolCallResponse
}
}Core Functionality:
- Stateful Sessions: Maintains persistent connection state with MCP server
- Protocol Negotiation: Handles capability exchange during initialization
- Message Routing: Bidirectional JSON-RPC message routing with correlation
- Health Management: Connection health monitoring and recovery
- Security Boundaries: Maintains isolation between different servers
export class MCPServer {
async handleJsonRpcMessage(request: JsonRpcRequest): Promise<JsonRpcResponse> {
switch (request.method) {
case MCPMethods.INITIALIZE:
return await this.handleInitialize(request);
case MCPMethods.TOOLS_LIST:
return await this.handleToolsList(request);
case MCPMethods.TOOLS_CALL:
return await this.handleToolsCall(request);
default:
return this.createErrorResponse(request.id, MCPErrorCodes.METHOD_NOT_FOUND);
}
}
private async handleToolsCall(request: JsonRpcRequest): Promise<JsonRpcResponse> {
const params = request.params as ToolCallRequest;
const result = await this.toolRegistry.executeTool(params.name, params.arguments);
const mcpResponse: ToolCallResponse = {
content: [{
type: "text",
text: JSON.stringify(result.data, null, 2),
data: result.data
}],
isError: !result.success
};
return {
jsonrpc: "2.0",
id: request.id,
result: mcpResponse
};
}
}Core Functionality:
- JSON-RPC Server: Handles all MCP protocol requests with proper response formatting
- Tool Management: Integration with ToolRegistry for tool execution
- Capability Negotiation: Proper MCP capability exchange during initialization
- Error Handling: Comprehensive error handling with MCP-specific error codes
- Resource Management: Manages tools, resources, and server state
export class ToolRegistry {
private tools: Map<string, Tool> = new Map();
private executionHistory: Array<{
toolName: string;
parameters: any;
result: ToolResult;
timestamp: string;
}> = [];
async executeTool(name: string, parameters: any): Promise<ToolResult> {
const tool = this.tools.get(name);
if (!tool) {
return {
success: false,
error: `Tool '${name}' not found. Available: ${Array.from(this.tools.keys()).join(', ')}`
};
}
const result = await tool.execute(parameters);
this.recordExecution(name, parameters, result);
return result;
}
getExecutionStats(): {
totalExecutions: number;
successfulExecutions: number;
failedExecutions: number;
toolUsageCounts: Record<string, number>;
recentErrors: string[];
} {
// Comprehensive execution analytics
}
}Core Functionality:
- Tool Registration: Dynamic tool registration with validation and conflict detection
- Execution Engine: Safe tool execution with comprehensive error handling
- History Tracking: Complete execution history with timing and result tracking
- Statistics: Detailed execution statistics and performance metrics
- Search & Discovery: Tool search by name, description, and capabilities
export class Tool {
constructor(
public readonly name: string,
public readonly description: string,
public readonly inputSchema: any,
private readonly handler: ToolHandler,
public readonly title?: string
) {}
validate(): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!this.name || typeof this.name !== 'string') {
errors.push('Tool name must be a non-empty string');
}
if (!this.description || typeof this.description !== 'string') {
errors.push('Tool description must be a non-empty string');
}
// JSON Schema validation for inputSchema
if (!this.inputSchema || typeof this.inputSchema !== 'object') {
errors.push('Tool inputSchema must be a valid JSON Schema object');
}
return { valid: errors.length === 0, errors };
}
async execute(parameters: any): Promise<ToolResult> {
try {
// Parameter validation against schema
const validation = this.validateParameters(parameters);
if (!validation.valid) {
return {
success: false,
error: `Parameter validation failed: ${validation.errors.join(', ')}`
};
}
const result = await this.handler(parameters);
return {
success: true,
data: result,
metadata: {
toolName: this.name,
executedAt: new Date().toISOString(),
parameters
}
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
metadata: {
toolName: this.name,
executedAt: new Date().toISOString(),
parameters
}
};
}
}
}Core Functionality:
- Tool Abstraction: Unified interface for all tool implementations
- Schema Validation: JSON Schema validation for tool parameters
- Error Handling: Comprehensive error handling with detailed error messages
- Result Standardization: Consistent result format across all tools
- Metadata Management: Tool metadata, descriptions, and capability information
Calculator Tool - Safe Mathematical Expression Evaluation
export const calculatorTool = createTool(
'calculator',
'Calculate mathematical expressions safely with support for basic arithmetic, exponents (^), and square root (sqrt)',
{
type: 'object',
properties: {
expression: {
type: 'string',
description: 'Mathematical expression to evaluate (e.g., "2 + 3 * 4", "sqrt(16)", "2^3")'
}
},
required: ['expression']
},
async (args) => {
const evaluator = new SafeMathEvaluator();
const result = evaluator.evaluate(args.expression);
return { result, expression: args.expression };
}
);DOM Query Tool - Comprehensive Webpage Interaction
export const domQueryTool = createTool(
'dom_query',
'Interact with webpage elements: click buttons, fill forms, read content, scroll, and manipulate DOM elements',
{
type: 'object',
properties: {
action: {
type: 'string',
enum: ['click', 'value', 'scroll', 'get_page_info'],
description: 'Action to perform on the element or page'
},
textContent: { type: 'string', description: 'Exact text content to find' },
partialText: { type: 'string', description: 'Partial text to search for' },
value: { type: 'string', description: 'Value to set (for input fields)' }
},
required: ['action']
},
async (args) => {
// Advanced element finding with multiple fallback strategies
// Supports clicking, form filling, content reading, scrolling
// Text-based element location with intelligent matching
}
);Browser Storage Tool - Persistent Data Management
export const browserStorageTool = createTool(
'browser_storage',
'Manage persistent browser storage: get, set, remove, clear, and list keys',
{
type: 'object',
properties: {
action: {
type: 'string',
enum: ['get', 'set', 'remove', 'clear', 'keys'],
description: 'Storage action to perform'
},
key: { type: 'string', description: 'Storage key' },
value: { type: 'string', description: 'Value to store' }
},
required: ['action']
},
async (args) => {
// localStorage integration with error handling
// Supports all standard storage operations
// Automatic JSON serialization/deserialization
}
);export class ApplicationHost {
private mcpHost: MCPHost;
private llmClient: LLMClient;
private conversationManager: ConversationManager;
async processMessage(userMessage: string): Promise<string> {
// 1. Get MCP context (tools, capabilities, server status)
const mcpContext = await this.mcpHost.getMCPContext();
// 2. Get application context (conversation, preferences, etc.)
const appContext = this.getApplicationContext();
// 3. Merge contexts for LLM
const fullContext = this.mergeContexts(mcpContext, appContext);
// 4. Get LLM response with tools
const llmResponse = await this.getLLMResponse(fullContext);
// 5. Process tool calls if any
if (llmResponse.tool_calls?.length > 0) {
return await this.processToolCalls(llmResponse);
}
// 6. Return direct response
return llmResponse.content || 'I understand, but I don\'t have a specific response.';
}
}Core Functionality:
- Application Orchestration: Coordinates all application-level concerns
- MCP Integration: Seamless integration with MCP infrastructure
- Conversation Management: Maintains chat history and context
- LLM Coordination: Handles LLM requests with tool integration
- Error Recovery: Comprehensive error handling with graceful degradation
- Multi-Step Processing: Supports recursive tool calling for complex tasks
export class ConversationManager {
private messages: Message[] = [];
private maxMessages: number = 20;
addUserMessage(content: string): void {
this.messages.push({
role: 'user',
content,
timestamp: Date.now()
});
this.trimIfNeeded();
}
addToolResult(toolCallId: string, toolName: string, result: string): void {
this.messages.push({
role: 'tool',
tool_call_id: toolCallId,
name: toolName,
content: result,
timestamp: Date.now()
});
this.trimIfNeeded();
}
getMessagesForLLM(): Omit<Message, 'timestamp'>[] {
return this.messages.map(({ timestamp, ...message }) => message);
}
}Core Functionality:
- Message History: Maintains complete conversation history with timestamps
- Context Trimming: Automatic conversation trimming to stay within limits
- LLM Formatting: Formats messages for LLM consumption
- Tool Integration: Proper handling of tool call results in conversation
- Statistics: Comprehensive conversation analytics and metrics
export class Configuration {
get llmConfig(): LLMConfig {
return {
model: "gpt-4o-mini",
temperature: 0.3,
maxTokens: 1000,
timeout: 30000
};
}
get serverConfig(): ServerConfig {
return {
name: "browser-mcp-server",
version: "1.0.0",
protocolVersion: "2025-06-18",
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true, listChanged: true }
}
};
}
}Core Functionality:
- Singleton Pattern: Ensures consistent configuration across entire application
- Environment Integration: Loads from environment variables with fallbacks
- MCP Compliance: Proper protocol version and capability configuration
- Validation: Comprehensive configuration validation with helpful error messages
- Independent Agents: Run up to 5 AI agents simultaneously with complete isolation
- Shared Infrastructure: Efficient resource sharing through MCP architecture
- Individual Controls: Start, stop, rename, and manage each agent independently
- Context Isolation: Each agent maintains separate conversation history and state
- Safe Calculator: Secure mathematical expression evaluator with sqrt, exponents, and validation
- DOM Interaction: Comprehensive webpage manipulation with intelligent element finding
- Browser Storage: Persistent data storage with localStorage integration
- Dynamic Discovery: Real-time tool registration and capability detection
- MCP Standard Compliance: Full OpenAI Chat Completions API compatibility
- Context Management: Smart conversation history with automatic trimming
- Multi-Step Processing: Recursive tool calling for complex problem solving
- Error Recovery: Robust error handling with automatic fallback strategies
- Real-time Debugging: Complete request/response monitoring with JSON-RPC details
- Performance Analytics: Execution statistics, timing metrics, and error tracking
- Tool Visibility: Live tool discovery and schema inspection
- Health Monitoring: Continuous system health checks and status reporting
The MCP Browser Agent excels at chaining multiple tool calls together to solve complex, multi-step problems. The system uses a recursive processing pattern that allows it to:
- Execute initial tools based on user request
- Analyze tool results to understand what was accomplished
- Determine next steps needed to complete the task
- Execute additional tools as needed
- Repeat until task is complete
The key capability is that tools provide results back to the agent, which can then analyze those results and decide to use more tools before providing a final response to the user.
User Request: "Find the login form, fill it with username 'john' and password 'secret123', then click submit"
Agent Execution Flow:
Step 1: 🔍 Find Login Form
├── Tool: dom_query(action="get_page_info")
├── Result: "Found login form with username and password fields"
└── Agent Decision: "I can see the form, now I need to locate the username field"
Step 2: 📝 Fill Username Field
├── Tool: dom_query(partialText="username", action="value", value="john")
├── Result: "Username field filled successfully"
└── Agent Decision: "Username filled, now I need to find the password field"
Step 3: 🔐 Fill Password Field
├── Tool: dom_query(partialText="password", action="value", value="secret123")
├── Result: "Password field filled successfully"
└── Agent Decision: "Both fields filled, now I need to find and click submit"
Step 4: ✅ Submit Form
├── Tool: dom_query(partialText="submit", action="click")
├── Result: "Submit button clicked successfully"
└── Agent Decision: "Task completed successfully"
Final Response: "I've successfully found the login form, filled in the username 'john' and password, then clicked the submit button. The login process is complete."
User Request: "Calculate the area of a circle with radius 5, then store it and also calculate what percentage that is of a square with side 10"
Agent Execution Flow:
Step 1: 🧮 Calculate Circle Area
├── Tool: calculator(expression="3.14159 * 5^2")
├── Result: {"result": 78.53975}
└── Agent Decision: "Got circle area, now I need to store it and calculate square area"
Step 2: 💾 Store Circle Area
├── Tool: browser_storage(action="set", key="circle_area", value="78.53975")
├── Result: {"success": true}
└── Agent Decision: "Stored successfully, now calculate square area"
Step 3: 🔲 Calculate Square Area
├── Tool: calculator(expression="10 * 10")
├── Result: {"result": 100}
└── Agent Decision: "Got square area, now calculate percentage"
Step 4: 📊 Calculate Percentage
├── Tool: calculator(expression="78.53975 / 100 * 100")
├── Result: {"result": 78.53975}
└── Agent Decision: "All calculations complete"
Final Response: "I've calculated the area of a circle with radius 5 as 78.54 square units, stored this value in browser storage, and determined that this represents 78.54% of the area of a square with side 10 (which has an area of 100 square units)."
User Request: "Click the 'Login' button"
Agent Execution Flow:
Step 1: 🎯 Try Direct Text Match
├── Tool: dom_query(textContent="Login", action="click")
├── Result: "No elements found matching the criteria"
└── Agent Decision: "Exact match failed, let me try partial text search"
Step 2: 🔍 Try Partial Text Match
├── Tool: dom_query(partialText="login", action="click")
├── Result: "No elements found matching the criteria"
└── Agent Decision: "Text search failed, let me check what's actually on the page"
Step 3: 📋 Analyze Page Content
├── Tool: dom_query(action="get_page_info")
├── Result: "Found buttons: ['Sign In', 'Register', 'Forgot Password']"
└── Agent Decision: "I see 'Sign In' which is likely the login button"
Step 4: ✅ Click Correct Button
├── Tool: dom_query(textContent="Sign In", action="click")
├── Result: "Button clicked successfully"
└── Agent Decision: "Successfully found and clicked the login button"
Final Response: "I found and clicked the login button (labeled 'Sign In') after adapting my search strategy when the initial attempts didn't find an exact 'Login' button."
The recursive tool calling is implemented through the MCP protocol, where tool results are added to the conversation context and the agent can analyze those results to decide on further actions:
// Process tool calls and add results to conversation
for (const toolCall of llmResponse.tool_calls) {
const result = await this.mcpHost.executeTool(toolCall.function.name, args);
// Add tool result to conversation context
this.conversationManager.addToolResult(
toolCall.id,
toolCall.function.name,
resultContent
);
}
// Agent analyzes all tool results and decides next steps
const finalResponse = await this.llmClient.getResponse(this.context.messages, {
tools: tools.length > 0 ? tools : undefined
});
// If agent decides more tools are needed based on results, recursively process
if (finalResponse.tool_calls && finalResponse.tool_calls.length > 0) {
return await this.processToolCalls(finalResponse);
}- Adaptive Problem Solving: Agent can change strategy based on tool results
- Error Recovery: Failed tool calls can trigger alternative approaches
- Context Building: Each tool result provides information for subsequent decisions
- Complex Workflows: Multi-step processes can be completed automatically
- Data Integration: Results from multiple tools can be combined and analyzed
- Click "
▶️ Start Agent" to initialize the MCP system - Type questions or requests in the chat interface
- Watch real-time tool execution in the activity log
- Monitor performance statistics and JSON-RPC communication
- Click "Switch to Multi-Agent Mode"
- Create multiple agents with custom names
- Switch between agent tabs for independent conversations
- Start/stop agents individually as needed
- Each agent maintains separate context and MCP connections
- Math: "Calculate the square root of 144 plus 5 times 3"
- Web Interaction: "Click the login button and fill in the username field"
- Data Storage: "Remember that my favorite color is blue"
- Tool Discovery: "What tools do you have available?"
- Complex Tasks: "Find all buttons on the page, then click the one that says 'Submit'"
Create new tools using the MCP-compliant tool framework:
import { createTool } from './mcp-tools/Tool';
const myCustomTool = createTool(
'my_tool',
'Description of what this tool does',
{
type: 'object',
properties: {
input: { type: 'string', description: 'Input parameter' }
},
required: ['input']
},
async (args) => {
// Tool implementation
return { result: 'success', data: args.input };
}
);
// Register in mcp-tools/ServerTools.ts
export const enhancedTools = [
// ... existing tools
myCustomTool
];The system uses centralized configuration management:
- Backend: Environment variables in
.envfile - Frontend: Configuration through
HostConfiguration.ts - MCP Protocol: Automatic capability negotiation
- Runtime: Dynamic configuration updates supported
- POST /api/llm: LLM proxy with OpenAI Chat Completions API
- GET /api/health: Health check and system status
-
"Failed to start agent"
- Check that backend is running on port 3001
- Verify OpenAI API key is set in
backend/.env - Check browser console for MCP initialization errors
-
"No tools available"
- Ensure MCP server initialization completed successfully
- Check JSON-RPC transport connectivity in debug panel
- Verify tool registration in browser console
-
"Tool execution failed"
- Check tool parameters match the expected schema
- Review tool execution history in debug panel
- Verify DOM elements exist for DOM query operations
- JSON-RPC Monitor: Complete request/response logging with correlation IDs
- MCP Debug Panel: Real-time protocol communication monitoring
- Tool Execution Logs: Detailed tool execution history with timing
- Health Dashboard: System health monitoring with error tracking
- Browser Console: Comprehensive logging with MCP protocol details
- Zero Network Latency: In-memory JSON-RPC between MCP components
- Shared Memory: Efficient data sharing within browser context
- Optimized Transport: Custom transport layer for browser environment
- Sandboxed Environment: Browser security model provides natural isolation
- No External Processes: All MCP components run in controlled environment
- Direct DOM Access: Tools can interact with page elements securely
- Unified Debugging: All MCP components debuggable in browser dev tools
- Hot Reloading: Development changes reflected immediately
- Rich Tooling: Browser debugging and profiling capabilities
| Aspect | Traditional Function Calling | MCP Implementation |
|---|---|---|
| Architecture | Direct LLM ↔ Functions | Host ↔ Client ↔ Server |
| Protocol | Vendor-specific formats | Standardized MCP protocol |
| Capability Discovery | Static tool definitions | Dynamic capability negotiation |
| Security | Function-level isolation | Component-level boundaries |
| Extensibility | Monolithic tool sets | Composable server ecosystem |
| Context Management | LLM manages all context | Host coordinates context |
| Multi-Agent Support | Limited sharing | Efficient resource sharing |
| Debugging | Basic logging | Comprehensive protocol monitoring |
// Multiple MCP servers (future enhancement)
const fileServer = new FileMCPServer();
const apiServer = new APIMCPServer();
const dbServer = new DatabaseMCPServer();
// Host manages multiple client connections
host.addServer(fileServer);
host.addServer(apiServer);
host.addServer(dbServer);- Resource Subscriptions: Real-time updates from MCP servers
- Prompt Templates: Reusable prompt templates from servers
- Sampling: Server-initiated LLM requests
- Notifications: Server-to-client event streaming
- External Servers: Connection to remote MCP servers
- Tool Execution: Sub-millisecond latency for most operations
- JSON-RPC Overhead: ~1-2ms for serialization/deserialization
- Memory Usage: Efficient shared infrastructure across agents
- Conversation Trimming: Automatic optimization to prevent memory bloat
- Concurrent Agents: Up to 5 agents with shared MCP infrastructure
- Tool Registry: Supports hundreds of registered tools
- Execution History: Configurable history limits with automatic cleanup
- Real-time Monitoring: Minimal performance impact from debug features
MIT License - see LICENSE file for details.
- Model Context Protocol (MCP) for the revolutionary standard specification
- OpenAI for LLM capabilities and Chat Completions API
- React & Vite for the modern frontend framework
- Express.js for the robust backend server
- JSON-RPC 2.0 for the standardized communication protocol
This MCP implementation demonstrates how AI architectures can run entirely in the browser while maintaining protocol compliance. Start with the Quick Start guide above to get started.