cd /news/ai-agents/fetching-data-from-an-mcp-server-in-… · home › topics › ai-agents › article
[ARTICLE · art-165324] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Fetching Data from an MCP Server in Your Frontend

A developer outlined two approaches for pulling data from Model Context Protocol (MCP) servers into a frontend web app: running the official TypeScript SDK directly in the browser over Streamable HTTP, or routing calls through a thin backend proxy that holds MCP credentials and enforces per-user authorization and a tool allowlist. The writeup includes working Express and React code showing JSON-RPC 2.0 tools/call and resources/read requests, error handling, and passing through Server-Sent Events progress notifications.

by read3 min views1 publishedNov 6, 2026

MCP servers are usually discussed as tools for AI assistants, but the same servers can feed your web app directly. A dashboard can call the same search_orders tool your agent uses, so both get one consistent data layer. This article shows how a frontend gets that data and what to watch for.

Option 1: an MCP client SDK in the browser. The official TypeScript SDK (@modelcontextprotocol/sdk) can run in the browser and talk to a remote server over Streamable HTTP. It's quick for demos and internal tools, but the browser then needs a token for the MCP server and the server must allow CORS.

Option 2: a thin backend proxy (recommended). Your backend runs the MCP client and exposes ordinary endpoints like /api/tools/search_orders. The frontend never sees MCP credentials, you check the user's session before every call, and you can allow only specific tools.

MCP uses JSON-RPC 2.0. After an initialize handshake, the two calls a frontend cares about most look like this.

Calling a tool:

{ "jsonrpc": "2.0", "id": 7, "method": "tools/call",
  "params": { "name": "search_orders", "arguments": { "customer": "Acme" } } }

A successful result:

{ "jsonrpc": "2.0", "id": 7,
  "result": { "content": [ { "type": "text", "text": "[{\"id\":101,\"total\":250}]" } ],
              "isError": false } }

Reading a resource uses resources/read with a URI, such as { "uri": "orders://recent" }, and returns contents with text or binary data. Use tools/list and resources/list to discover what's available.

import express from "express";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const client = new Client({ name: "web-proxy", version: "1.0.0" });
await client.connect(new StreamableHTTPClientTransport(new URL(process.env.MCP_URL!)));

const ALLOWED = new Set(["search_orders", "get_order"]);
const app = express();
app.use(express.json());

app.post("/api/tools/:name", async (req, res) => {
  if (!req.headers.authorization) return res.status(401).json({ error: "Sign in first" });
  if (!ALLOWED.has(req.params.name)) return res.status(403).json({ error: "Tool not allowed" });
  try {
    const result = await client.callTool({ name: req.params.name, arguments: req.body });
    if (result.isError) return res.status(422).json({ error: result.content });
    res.json(result.content);
  } catch (e) {
    res.status(502).json({ error: "MCP server unavailable" });
  }
});

app.listen(3000);
js
import { useState } from "react";

export function OrderSearch() {
  const [rows, setRows] = useState<any[]>([]);
  const [error, setError] = useState("");
  const [, set] = useState(false);

  async function search(customer: string) {
    set(true); setError("");
    try {
      const res = await fetch("/api/tools/search_orders", {
        method: "POST",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${localToken()}` },
        body: JSON.stringify({ customer }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(typeof data.error === "string" ? data.error : "Tool failed");
      setRows(JSON.parse(data[0].text));
    } catch (e: any) {
      setError(e.message);
    } finally {
      set(false);
    }
  }

  return (
    <div>
      <button onClick={() => search("Acme")} disabled={}>Search Acme</button>
      {error && <p role="alert">{error}</p>}
      <ul>{rows.map(r => <li key={r.id}>#{r.id}: ${r.total}</li>)}</ul>
    </div>
  );
}

The same fetch call works unchanged in Vue or plain JavaScript.

With Streamable HTTP, the server can answer with Server-Sent Events, sending progress notifications before the final result. Pass those through from the proxy as SSE and read them with fetch plus a stream reader (or EventSource for GET):

const res = await fetch("/api/tools/long_report/stream", { method: "POST", body: "{}" });
const reader = res.body.getReader();
const decoder = new TextDecoder();
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  showProgress(decoder.decode(value));
}

There are three layers to handle:

error object with a code, such as -32602 for invalid params or -32601 for an unknown method. Usually a bug, so log it.isError: true. These are expected failures like "customer not found". Show them to the user. Also add timeouts with AbortController, and cancel requests when a component unmounts.

An MCP server can be a shared data layer for both your agents and your UI. Put a thin proxy in front of it, speak JSON-RPC behind the scenes, and handle the three kinds of errors, and your frontend gets the same data your AI tools do, safely.

I write about Applied AI, MCP, Azure AI Search, and RAG. Follow me on Dev.to and Hashnode for more.

── more in #ai-agents 4 stories · sorted by recency
── more on @model context protocol 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/fetching-data-from-a…] indexed:0 read:3min 2026-11-06 · —