Clients & Integrations
Pillar Guide

MCP in Browser & Mobile AI Applications

Implementing MCP in browser-based and mobile AI applications — WebSocket transports, security considerations, and practical examples.

22 min read
Updated February 25, 2026
By MCP Server Spot

MCP is not limited to desktop applications and CLI tools. Browser-based and mobile AI applications can connect to MCP servers to give users AI assistants with real tool capabilities directly in web and mobile interfaces. This opens up possibilities for SaaS products, internal dashboards, mobile productivity apps, and any application where users interact with AI.

This guide covers the technical implementation of MCP clients in browser environments, mobile considerations, security best practices for web-based MCP, and practical examples using modern web frameworks.

How MCP Works in the Browser

In a browser context, MCP clients connect to remote MCP servers using HTTP-based transports. The browser cannot start local processes (no stdio), so all MCP communication happens over the network.

Browser MCP Architecture

┌──────────────────────────────────┐
│         Browser Application       │
│  ┌────────────────────────────┐  │
│  │    MCP Client (JavaScript) │  │
│  │    - SSE EventSource       │  │
│  │    - fetch() for requests  │  │
│  └────────────┬───────────────┘  │
│               │                  │
│  ┌────────────▼───────────────┐  │
│  │    UI Components           │  │
│  │    - Tool results display  │  │
│  │    - Chat interface        │  │
│  │    - Resource browser      │  │
│  └────────────────────────────┘  │
└───────────────┬──────────────────┘
                │ HTTPS
                ▼
┌──────────────────────────────────┐
│     Backend / MCP Gateway        │
│  ┌────────────────────────────┐  │
│  │  Authentication            │  │
│  │  Rate Limiting             │  │
│  │  CORS Headers              │  │
│  └────────────┬───────────────┘  │
│               │                  │
│  ┌────────────▼───────────────┐  │
│  │  MCP Server(s)             │  │
│  │  - Tools                   │  │
│  │  - Resources               │  │
│  │  - Prompts                 │  │
│  └────────────────────────────┘  │
└──────────────────────────────────┘

Transport Options for Browser

TransportBrowser SupportConnection ModelImplementation
SSE (Server-Sent Events)Native EventSource APIPersistent server-to-client streamBest for real-time updates
Streamable HTTPNative fetch() APIRequest-response with optional streamingSimpler, stateless
WebSocketNative WebSocket APIFull-duplexNot in official spec, community only
stdioNot availableN/ACannot use in browser

Building a Browser MCP Client

Basic SSE Client in JavaScript

// mcp-browser-client.ts

interface MCPTool {
  name: string;
  description: string;
  inputSchema: Record<string, unknown>;
}

interface MCPToolResult {
  content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
  isError?: boolean;
}

class BrowserMCPClient {
  private serverUrl: string;
  private sessionId: string | null = null;
  private messagesEndpoint: string | null = null;
  private eventSource: EventSource | null = null;
  private pendingRequests: Map<number, {
    resolve: (value: unknown) => void;
    reject: (error: Error) => void;
  }> = new Map();
  private requestId = 0;
  private authToken: string;

  constructor(serverUrl: string, authToken: string) {
    this.serverUrl = serverUrl;
    this.authToken = authToken;
  }

  async connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      // Connect to SSE endpoint
      this.eventSource = new EventSource(
        `${this.serverUrl}/sse`,
        // Note: EventSource doesn't support custom headers natively.
        // Use a polyfill like eventsource-polyfill for auth headers,
        // or pass tokens as query parameters.
      );

      this.eventSource.addEventListener("endpoint", (event: MessageEvent) => {
        // Server sends the messages endpoint URL
        this.messagesEndpoint = new URL(event.data, this.serverUrl).toString();
        this.initialize().then(resolve).catch(reject);
      });

      this.eventSource.addEventListener("message", (event: MessageEvent) => {
        const message = JSON.parse(event.data);
        this.handleMessage(message);
      });

      this.eventSource.onerror = () => {
        reject(new Error("SSE connection failed"));
      };
    });
  }

  private async initialize(): Promise<void> {
    await this.sendRequest("initialize", {
      protocolVersion: "2024-11-05",
      capabilities: {},
      clientInfo: {
        name: "browser-client",
        version: "1.0.0",
      },
    });

    // Send initialized notification
    await this.sendNotification("notifications/initialized", {});
  }

  private async sendRequest(method: string, params: unknown): Promise<unknown> {
    const id = ++this.requestId;

    return new Promise((resolve, reject) => {
      this.pendingRequests.set(id, { resolve, reject });

      const message = {
        jsonrpc: "2.0",
        id,
        method,
        params,
      };

      fetch(this.messagesEndpoint!, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${this.authToken}`,
        },
        body: JSON.stringify(message),
      }).catch(reject);
    });
  }

  private async sendNotification(method: string, params: unknown): Promise<void> {
    const message = {
      jsonrpc: "2.0",
      method,
      params,
    };

    await fetch(this.messagesEndpoint!, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${this.authToken}`,
      },
      body: JSON.stringify(message),
    });
  }

  private handleMessage(message: { id?: number; result?: unknown; error?: unknown }) {
    if (message.id && this.pendingRequests.has(message.id)) {
      const { resolve, reject } = this.pendingRequests.get(message.id)!;
      this.pendingRequests.delete(message.id);

      if (message.error) {
        reject(new Error(JSON.stringify(message.error)));
      } else {
        resolve(message.result);
      }
    }
  }

  async listTools(): Promise<MCPTool[]> {
    const result = await this.sendRequest("tools/list", {}) as { tools: MCPTool[] };
    return result.tools;
  }

  async callTool(name: string, args: Record<string, unknown>): Promise<MCPToolResult> {
    const result = await this.sendRequest("tools/call", {
      name,
      arguments: args,
    });
    return result as MCPToolResult;
  }

  async listResources(): Promise<Array<{ uri: string; name: string }>> {
    const result = await this.sendRequest("resources/list", {}) as {
      resources: Array<{ uri: string; name: string }>;
    };
    return result.resources;
  }

  async readResource(uri: string): Promise<string> {
    const result = await this.sendRequest("resources/read", { uri }) as {
      contents: Array<{ text: string }>;
    };
    return result.contents.map((c) => c.text).join("\n");
  }

  disconnect(): void {
    this.eventSource?.close();
    this.eventSource = null;
    this.pendingRequests.clear();
  }
}

React Integration

// hooks/useMCP.ts
import { useState, useEffect, useCallback, useRef } from "react";

interface MCPTool {
  name: string;
  description: string;
  inputSchema: Record<string, unknown>;
}

interface UseMCPResult {
  connected: boolean;
  tools: MCPTool[];
  callTool: (name: string, args: Record<string, unknown>) => Promise<string>;
  error: string | null;
}

export function useMCP(serverUrl: string, authToken: string): UseMCPResult {
  const [connected, setConnected] = useState(false);
  const [tools, setTools] = useState<MCPTool[]>([]);
  const [error, setError] = useState<string | null>(null);
  const clientRef = useRef<BrowserMCPClient | null>(null);

  useEffect(() => {
    const client = new BrowserMCPClient(serverUrl, authToken);
    clientRef.current = client;

    client
      .connect()
      .then(async () => {
        setConnected(true);
        const discoveredTools = await client.listTools();
        setTools(discoveredTools);
      })
      .catch((err) => {
        setError(err.message);
        setConnected(false);
      });

    return () => {
      client.disconnect();
    };
  }, [serverUrl, authToken]);

  const callTool = useCallback(
    async (name: string, args: Record<string, unknown>): Promise<string> => {
      if (!clientRef.current) {
        throw new Error("MCP client not connected");
      }

      const result = await clientRef.current.callTool(name, args);

      if (result.isError) {
        throw new Error(
          result.content.map((c) => c.text).join("\n") || "Tool call failed"
        );
      }

      return result.content
        .filter((c) => c.type === "text")
        .map((c) => c.text)
        .join("\n");
    },
    []
  );

  return { connected, tools, callTool, error };
}
// components/MCPToolPanel.tsx
import { useMCP } from "../hooks/useMCP";

export function MCPToolPanel() {
  const { connected, tools, callTool, error } = useMCP(
    "https://mcp.example.com",
    "your-auth-token"
  );

  const [selectedTool, setSelectedTool] = useState<string | null>(null);
  const [result, setResult] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  async function handleToolCall(name: string, args: Record<string, unknown>) {
    setLoading(true);
    try {
      const response = await callTool(name, args);
      setResult(response);
    } catch (err) {
      setResult(`Error: ${err.message}`);
    } finally {
      setLoading(false);
    }
  }

  if (error) return <div className="error">MCP Error: {error}</div>;
  if (!connected) return <div>Connecting to MCP server...</div>;

  return (
    <div className="mcp-panel">
      <h3>Available Tools ({tools.length})</h3>
      <ul>
        {tools.map((tool) => (
          <li key={tool.name} onClick={() => setSelectedTool(tool.name)}>
            <strong>{tool.name}</strong>
            <p>{tool.description}</p>
          </li>
        ))}
      </ul>

      {selectedTool && (
        <ToolCallForm
          tool={tools.find((t) => t.name === selectedTool)!}
          onSubmit={(args) => handleToolCall(selectedTool, args)}
          loading={loading}
        />
      )}

      {result && (
        <div className="result">
          <h4>Result</h4>
          <pre>{result}</pre>
        </div>
      )}
    </div>
  );
}

Next.js API Route as MCP Proxy

For better security, proxy MCP requests through your Next.js backend:

// app/api/mcp/route.ts (Next.js App Router)
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { NextRequest, NextResponse } from "next/server";

// Cache client connections per session
const clientSessions = new Map<string, Client>();

export async function POST(request: NextRequest) {
  // Authenticate the frontend request
  const authHeader = request.headers.get("authorization");
  const user = await validateToken(authHeader);

  if (!user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { action, toolName, arguments: args } = await request.json();

  // Get or create MCP client for this user session
  let client = clientSessions.get(user.sessionId);

  if (!client) {
    client = new Client(
      { name: "web-proxy", version: "1.0" },
      { capabilities: {} }
    );

    const transport = new SSEClientTransport(
      new URL(process.env.MCP_SERVER_URL!),
      {
        requestInit: {
          headers: {
            Authorization: `Bearer ${process.env.MCP_SERVER_TOKEN}`,
            "X-Tenant-ID": user.tenantId,
          },
        },
      }
    );

    await client.connect(transport);
    clientSessions.set(user.sessionId, client);
  }

  switch (action) {
    case "listTools": {
      const tools = await client.listTools();
      return NextResponse.json(tools);
    }

    case "callTool": {
      const result = await client.callTool({
        name: toolName,
        arguments: args,
      });
      return NextResponse.json(result);
    }

    default:
      return NextResponse.json({ error: "Unknown action" }, { status: 400 });
  }
}

This proxy pattern provides several advantages:

  • Server-side authentication and authorization
  • MCP server credentials stay on the backend
  • Rate limiting and request validation
  • Audit logging before proxying requests

Mobile MCP Integration

React Native

// services/MCPClient.ts (React Native)
import EventSource from "react-native-sse";

class MobileMCPClient {
  private serverUrl: string;
  private authToken: string;
  private eventSource: EventSource | null = null;
  private messagesEndpoint: string | null = null;

  constructor(serverUrl: string, authToken: string) {
    this.serverUrl = serverUrl;
    this.authToken = authToken;
  }

  async connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      this.eventSource = new EventSource(`${this.serverUrl}/sse`, {
        headers: {
          Authorization: `Bearer ${this.authToken}`,
        },
      });

      this.eventSource.addEventListener("endpoint", (event) => {
        this.messagesEndpoint = new URL(event.data, this.serverUrl).toString();
        this.initialize().then(resolve).catch(reject);
      });

      this.eventSource.addEventListener("error", (event) => {
        reject(new Error("SSE connection failed"));
      });
    });
  }

  // ... similar to browser client
}

iOS (Swift)

// MCPClient.swift
import Foundation

class MCPClient {
    private let serverURL: URL
    private let authToken: String
    private var session: URLSession
    private var messagesEndpoint: URL?
    private var requestId = 0

    init(serverURL: URL, authToken: String) {
        self.serverURL = serverURL
        self.authToken = authToken
        self.session = URLSession(configuration: .default)
    }

    func connect() async throws {
        // Connect to SSE endpoint
        let sseURL = serverURL.appendingPathComponent("sse")
        var request = URLRequest(url: sseURL)
        request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
        request.setValue("text/event-stream", forHTTPHeaderField: "Accept")

        let (bytes, _) = try await session.bytes(for: request)

        for try await line in bytes.lines {
            if line.hasPrefix("data: ") {
                let data = String(line.dropFirst(6))
                if let endpointURL = URL(string: data, relativeTo: serverURL) {
                    self.messagesEndpoint = endpointURL
                    try await initialize()
                    return
                }
            }
        }
    }

    func callTool(name: String, arguments: [String: Any]) async throws -> String {
        guard let endpoint = messagesEndpoint else {
            throw MCPError.notConnected
        }

        requestId += 1

        let message: [String: Any] = [
            "jsonrpc": "2.0",
            "id": requestId,
            "method": "tools/call",
            "params": [
                "name": name,
                "arguments": arguments,
            ],
        ]

        var request = URLRequest(url: endpoint)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
        request.httpBody = try JSONSerialization.data(withJSONObject: message)

        let (data, _) = try await session.data(for: request)
        let response = try JSONSerialization.jsonObject(with: data) as! [String: Any]

        if let result = response["result"] as? [String: Any],
           let content = result["content"] as? [[String: Any]] {
            return content
                .compactMap { $0["text"] as? String }
                .joined(separator: "\n")
        }

        throw MCPError.invalidResponse
    }

    private func initialize() async throws {
        // Send initialize request
        // ... similar to JavaScript client
    }
}

Android (Kotlin)

// MCPClient.kt
import okhttp3.*
import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener
import okhttp3.sse.EventSources
import kotlinx.coroutines.*
import org.json.JSONObject

class MCPClient(
    private val serverUrl: String,
    private val authToken: String,
) {
    private val httpClient = OkHttpClient()
    private var messagesEndpoint: String? = null
    private var requestId = 0

    suspend fun connect() = suspendCancellableCoroutine { cont ->
        val request = Request.Builder()
            .url("$serverUrl/sse")
            .header("Authorization", "Bearer $authToken")
            .build()

        val factory = EventSources.createFactory(httpClient)
        factory.newEventSource(request, object : EventSourceListener() {
            override fun onEvent(
                eventSource: EventSource,
                id: String?,
                type: String?,
                data: String
            ) {
                if (type == "endpoint") {
                    messagesEndpoint = "$serverUrl$data"
                    cont.resume(Unit) {}
                }
            }

            override fun onFailure(
                eventSource: EventSource,
                t: Throwable?,
                response: Response?
            ) {
                cont.resumeWithException(
                    t ?: Exception("SSE connection failed")
                )
            }
        })
    }

    suspend fun callTool(
        name: String,
        arguments: Map<String, Any>
    ): String {
        val endpoint = messagesEndpoint
            ?: throw IllegalStateException("Not connected")

        requestId++

        val message = JSONObject().apply {
            put("jsonrpc", "2.0")
            put("id", requestId)
            put("method", "tools/call")
            put("params", JSONObject().apply {
                put("name", name)
                put("arguments", JSONObject(arguments))
            })
        }

        val request = Request.Builder()
            .url(endpoint)
            .post(
                message.toString()
                    .toRequestBody("application/json".toMediaType())
            )
            .header("Authorization", "Bearer $authToken")
            .build()

        return withContext(Dispatchers.IO) {
            val response = httpClient.newCall(request).execute()
            val body = response.body?.string()
                ?: throw Exception("Empty response")
            val json = JSONObject(body)
            val content = json.getJSONObject("result")
                .getJSONArray("content")

            (0 until content.length())
                .map { content.getJSONObject(it) }
                .filter { it.getString("type") == "text" }
                .joinToString("\n") { it.getString("text") }
        }
    }
}

Security Considerations for Browser MCP

CORS Configuration

Your MCP server must include proper CORS headers for browser access:

// MCP server with CORS headers
import cors from "cors";

app.use(cors({
  origin: ["https://your-app.com", "https://staging.your-app.com"],
  methods: ["GET", "POST"],
  allowedHeaders: ["Content-Type", "Authorization"],
  credentials: true,
}));

Authentication in SPAs

Use OAuth 2.1 with PKCE (Proof Key for Code Exchange) for browser authentication:

// OAuth PKCE flow for browser MCP
async function authenticateAndConnect(mcpServerUrl: string) {
  // Generate PKCE challenge
  const codeVerifier = generateRandomString(64);
  const codeChallenge = await sha256(codeVerifier);

  // Redirect to auth server
  const authUrl = new URL("https://auth.example.com/authorize");
  authUrl.searchParams.set("response_type", "code");
  authUrl.searchParams.set("client_id", "your-spa-client-id");
  authUrl.searchParams.set("redirect_uri", window.location.origin + "/callback");
  authUrl.searchParams.set("scope", "mcp:tools mcp:resources");
  authUrl.searchParams.set("code_challenge", codeChallenge);
  authUrl.searchParams.set("code_challenge_method", "S256");

  window.location.href = authUrl.toString();
}

// After redirect back
async function handleCallback() {
  const code = new URLSearchParams(window.location.search).get("code");

  // Exchange code for token
  const tokenResponse = await fetch("https://auth.example.com/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code: code!,
      redirect_uri: window.location.origin + "/callback",
      client_id: "your-spa-client-id",
      code_verifier: storedCodeVerifier,
    }),
  });

  const { access_token } = await tokenResponse.json();

  // Connect to MCP with the token
  const client = new BrowserMCPClient("https://mcp.example.com", access_token);
  await client.connect();
}

Security Best Practices

PracticeWhy It Matters
Always use HTTPSPrevents MCP messages (including tool parameters) from being intercepted
Backend proxy for secretsMCP server credentials never reach the browser
Token-based authShort-lived access tokens with refresh flow
CORS restrictionsOnly your domain can connect to MCP servers
Input sanitizationValidate tool results before rendering in DOM (prevent XSS)
Rate limitingPrevent abuse from browser-based clients
Content Security PolicyRestrict which endpoints the browser can connect to

Sanitizing Tool Results

Tool results displayed in the browser must be sanitized to prevent XSS:

import DOMPurify from "dompurify";

function renderToolResult(result: MCPToolResult): string {
  const rawText = result.content
    .filter((c) => c.type === "text")
    .map((c) => c.text)
    .join("\n");

  // Sanitize before rendering as HTML
  return DOMPurify.sanitize(rawText);
}

Performance Optimization

Connection Pooling

Limit simultaneous SSE connections (browsers typically allow 6 per domain):

class MCPConnectionPool {
  private maxConnections: number;
  private activeConnections: Map<string, BrowserMCPClient> = new Map();

  constructor(maxConnections = 4) {
    this.maxConnections = maxConnections;
  }

  async getConnection(serverUrl: string, authToken: string): Promise<BrowserMCPClient> {
    if (this.activeConnections.has(serverUrl)) {
      return this.activeConnections.get(serverUrl)!;
    }

    if (this.activeConnections.size >= this.maxConnections) {
      // Close the least recently used connection
      const oldestKey = this.activeConnections.keys().next().value;
      if (oldestKey) {
        this.activeConnections.get(oldestKey)!.disconnect();
        this.activeConnections.delete(oldestKey);
      }
    }

    const client = new BrowserMCPClient(serverUrl, authToken);
    await client.connect();
    this.activeConnections.set(serverUrl, client);
    return client;
  }
}

Reconnection with Exponential Backoff

SSE connections can drop. Implement automatic reconnection:

class ResilientMCPClient extends BrowserMCPClient {
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private baseDelay = 1000;

  async connectWithReconnection(): Promise<void> {
    try {
      await this.connect();
      this.reconnectAttempts = 0;
    } catch (error) {
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        const delay = this.baseDelay * Math.pow(2, this.reconnectAttempts);
        this.reconnectAttempts++;
        console.error(
          `Connection failed, retrying in ${delay}ms (attempt ${this.reconnectAttempts})`
        );
        await new Promise((resolve) => setTimeout(resolve, delay));
        return this.connectWithReconnection();
      }
      throw error;
    }
  }
}

Caching Tool Results

Cache results for idempotent tool calls to reduce server load:

class CachingMCPClient extends BrowserMCPClient {
  private cache = new Map<string, { result: MCPToolResult; timestamp: number }>();
  private cacheTTL = 60000; // 1 minute

  async callToolCached(
    name: string,
    args: Record<string, unknown>,
    ttl?: number
  ): Promise<MCPToolResult> {
    const cacheKey = `${name}:${JSON.stringify(args)}`;
    const cached = this.cache.get(cacheKey);

    if (cached && Date.now() - cached.timestamp < (ttl || this.cacheTTL)) {
      return cached.result;
    }

    const result = await this.callTool(name, args);
    this.cache.set(cacheKey, { result, timestamp: Date.now() });
    return result;
  }
}

What to Read Next

Summary

Building MCP into browser and mobile applications extends AI capabilities beyond desktop clients into the web. The key architectural decisions are: use SSE or Streamable HTTP transport for browser connectivity, proxy through your backend for security and credential management, implement OAuth 2.1 with PKCE for authentication, and handle connection resilience with reconnection and caching strategies.

The same MCP servers that work with Claude Desktop and Cursor work identically with browser and mobile clients. The protocol does not change -- only the transport and the security context adapt to the web environment. This means your investment in MCP servers pays off across every platform where your users interact with AI.

Frequently Asked Questions

Can MCP servers run directly in the browser?

MCP servers are typically backend processes, but lightweight MCP servers can run in the browser using Web Workers. The more common pattern is for a browser-based MCP client to connect to remote MCP servers via SSE or Streamable HTTP transport over HTTPS. The browser client discovers tools and calls them through standard HTTP requests.

What transport does MCP use in browser applications?

Browser-based MCP clients use SSE (Server-Sent Events) or Streamable HTTP transport. SSE uses the browser's native EventSource API for server-to-client streaming and fetch() for client-to-server requests. Streamable HTTP uses standard fetch() for both directions. Both work over HTTPS and are compatible with standard web infrastructure.

How do I build an MCP client in a React or Next.js application?

Use the @modelcontextprotocol/sdk package in your frontend or backend. For server-side rendering (Next.js API routes), use the full SDK. For client-side, use the SSE client transport to connect to remote MCP servers. Create a custom hook or service that manages the MCP session lifecycle, tool discovery, and tool calling.

Is it secure to connect to MCP servers from browser JavaScript?

Yes, with proper security measures: always use HTTPS, implement authentication (OAuth 2.1 tokens), validate CORS headers on the MCP server, and never expose sensitive server capabilities directly to the browser. Consider using a backend proxy that handles authentication and filters which tools are exposed to the frontend.

Can I use MCP in a mobile app (iOS/Android)?

Yes, mobile apps can act as MCP clients using HTTP-based transports. For native iOS, use URLSession for SSE connections. For Android, use OkHttp's SSE support. For React Native or Flutter, use their HTTP/SSE libraries. The key requirement is a persistent HTTP connection for SSE or standard HTTP for Streamable HTTP transport.

How do I handle MCP authentication in a single-page application?

Use the OAuth 2.1 PKCE flow: the SPA redirects to your auth server, the user authenticates, and the app receives an access token. Pass this token in the Authorization header when connecting to MCP servers. Store tokens securely (in memory, not localStorage) and implement token refresh for long-lived sessions.

What are the performance considerations for MCP in the browser?

Key considerations: minimize the number of simultaneous SSE connections (browsers limit concurrent connections per domain), implement connection pooling for multiple servers, handle reconnection gracefully when SSE connections drop, and use Web Workers for heavy processing to keep the UI responsive.

Can I use MCP with WebSocket instead of SSE?

The official MCP specification defines SSE and Streamable HTTP as the standard remote transports. WebSocket is not part of the official spec, but community implementations exist. SSE is preferred because it works with standard HTTP infrastructure (load balancers, proxies, CDNs) without requiring WebSocket-specific configuration.

Related Guides