# Fetching Data from an MCP Server in Your Frontend

> Source: <https://dev.to/who_ai/fetching-data-from-an-mcp-server-in-your-frontend-26ap>
> Published: 2026-11-06 12:46:26+00:00

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.

``` python
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 [loading, setLoading] = useState(false);

  async function search(customer: string) {
    setLoading(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 {
      setLoading(false);
    }
  }

  return (
    <div>
      <button onClick={() => search("Acme")} disabled={loading}>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):

``` js
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](https://dev.to/who_ai) and [Hashnode](https://hashnode.com/@whoai) for more.
