Command Palette

Search for a command to run...

Hectal
PHASE 14Advanced ~40 min· topic 4 of 6

Topic 14.4

MCP from a Frontend's View

In one line

The Model Context Protocol (MCP) is an open standard that lets an AI app connect to outside servers that offer tools, resources and prompts. A frontend never speaks MCP itself, but it must show which tool came from which server, ask permission with the real arguments, and cope with a server that is slow, down or changing.

Think of it like this

A travel adapter. Every country has a different socket, and without an adapter you carry a bag of plugs, one per hotel. With a standard adapter, one plug fits everywhere. MCP is a standard adapter between an AI app and the outside services it uses. Your app speaks one protocol, and any service that also speaks it can plug in. What the adapter does not do is decide which services you should trust.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

MCP
Model Context Protocol: an open standard for connecting AI apps to servers that offer tools, resources and prompts.
Host, client, server
The host is the AI app. A client is the connector inside the host. A server is the service that offers abilities.
Tool
A function a server offers for the model to call. Listed with tools/list, run with tools/call.
Resource
Data a server offers as context, identified by a URI. The application decides what to include.
Prompt
A server's message template that the user picks explicitly, like a slash command.
Transport
How messages travel: stdio (a child process) or Streamable HTTP (one endpoint, POST requests).
isError
A flag in a tool result meaning the tool ran but failed. The model should see the text and may retry.
Elicitation
A server asking the user for more information through the client.
MCP App
An extension: a server-provided HTML interface (a ui:// resource) shown in a sandboxed iframe.

Step by step

01The path of one tool call

Your product is the host. Its server code holds an MCP client per server. When the model asks for payments_issue_refund, the host (not the model) decides whether to run it, shows the user the arguments, and only then does the client send tools/call.

The path of one tool calldiagram
Rendering diagram…

02Reading the wire (not live)

The first non-live block shows what a tools/call looks like on Streamable HTTP in revision 2026-07-28, copied in shape from the specification. Note that the metadata travels in the body and is mirrored into headers. The live examples use a stand-in server that answers the same JSON-RPC methods, because a real MCP server cannot run inside this sandboxed preview.

03Which tool came from which server

The first live example lists the tools of two stand-in servers. Both have a tool called search. Put in a plain object, one of them vanishes without any error. With the server id in the name, all three tools survive, and the finished run shows each tool part with the server it came from.

04Asking permission with the real arguments

The second live example uses toolApproval with a function per tool. Reads from the shop server run without a prompt. The payments server is not trusted by default, so each refund waits for the user. The prompt is built by your server from your own table: server label, tool title and the exact arguments. The scripted user picks "allow always for this server" once, and the second refund no longer asks.

05A server is down, a tool fails

The third live example connects to three servers at once. One answers, one never does (a timeout), one returns HTTP 503. The screen shows each server's state and builds the agent's tools only from the servers that answered. Then it sorts three failures: a tool that ran and failed (isError, goes to the model), a protocol error (does not), and a server that is gone.

06The real client code (not live)

The last non-live block is the server side with the AI SDK's MCP client: createMCPClient, client.tools() and closing the client. Everything here runs in your route handler, never in the browser.

Try it yourself

  1. 1

    Make the collision visible on screen

    In the first example, change the first buildTools call's result: after the second console line add console.log(Object.keys(plain.set).length < plain.listed ? "BUG: a tool was lost" : "ok");.

    A simple check that the number of tools in the set equals the number listed catches the collision in a test, before a user meets it.

  2. 2

    Trust the payments server

    In the second example, set trusted: true for payments, then run it again.

    The refunds still prompt, because the rule asks for both trusted and readOnly. A trusted server may skip prompts only for reads. Change readOnly to true on issue_refund and the prompt disappears, which shows why that flag must come from your own table and not from the server's annotation.

  3. 3

    Bring the Help centre back

    In the third example, replace the line serve("/mcp/docs", () => new Promise(() => {})); with serve("/mcp/docs", () => Response.json({ jsonrpc: "2.0", id: 1, result: { tools: [{ name: "search" }] } }));.

    The Help centre line now reads "1 tools" (our sentence ignores the singular), the agent gets tools from shop, docs, and the "unavailable" sentence names only the payments server. Nothing else in the code changed, because every server's state is handled on its own.

Code & diagrams

tools/call on the wire (not live)http

Copied in shape from the Streamable HTTP page of the 2026-07-28 specification. The clientInfo and the tool are examples.

POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: issue_refund

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "issue_refund",
    "arguments": { "orderId": "ORD-1042", "amount": 349 },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "mitra", "version": "1.0.0" },
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

// the answer: one JSON object, or an event stream that ends with it
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "content": [{ "type": "text", "text": "Refunded Rs 349 for ORD-1042." }],
    "isError": false
  }
}
Which tool came from which server

Without a prefix, the second search replaces the first and nothing tells you. With a prefix, the origin is part of the name and the screen can show it.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

without a prefix: 3 tools listed, 2 in the tool set: lookup_order, search
with a prefix: 3 tools listed, 3 in the tool set: shop__lookup_order, shop__search, docs__search
Chai & Co shop / Look up an order -> output-available: "ORD-1042: Masala chai 250 g, shipped"
Help centre / Search help articles -> output-available: "Article: Returns are accepted for 7 days (return policy)"
Asking permission, with the real arguments

The reads from the trusted shop server run silently. The first refund prompts, with the exact arguments. After the user allows the server, the second refund goes straight through.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

PROMPT: Chai & Co payments wants to run Issue a refund with {"orderId":"ORD-1042","amount":349}
the user chooses: allow always for Chai & Co payments
results: shop__lookup_order=output-available, payments__issue_refund=output-available, payments__issue_refund=output-available
A server is down, a tool fails

The 60 ms timeout is short on purpose so the example finishes quickly. A real client uses a longer one, and the SDK's MCP client accepts request options with a timeout.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Chai & Co shop: 2 tools
Help centre: not available (did not answer in time)
Chai & Co payments: not available (HTTP 503)
agent gets tools from: shop
agent is told: Help centre and Chai & Co payments are unavailable, do not pretend to use them.
shop.lookup_order -> goes to the model: ORD-1042: shipped
shop.lookup_order -> goes to the model: (tool error, the model can retry) Order ORD-9999 was not found. Order ids look like ORD-1042.
shop.delete_everything -> goes to the screen: Protocol error -32602: Unknown tool: delete_everything
payments.issue_refund -> goes to the screen: Chai & Co payments is not available right now.
The server side with the AI SDK (not live)ts

Packages: ai and @ai-sdk/mcp. stdio is for a server on the same machine as your code, so it can never run in a browser.

// app/api/agent/route.ts: the MCP client lives on YOUR server
import { createMCPClient } from "@ai-sdk/mcp";
import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio"; // spawns a local process
import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from "ai";

export async function POST(req: Request) {
  const { messages } = await req.json();

  // 1. a remote server over Streamable HTTP (type "http"; "sse" is the older transport)
  const payments = await createMCPClient({
    transport: { type: "http", url: "https://mcp.example.com/mcp", headers: { Authorization: "Bearer " + process.env.PAYMENTS_TOKEN } },
    onUncaughtError: (error) => console.error("payments MCP:", error),
  });
  // 2. a local server started as a child process
  const files = await createMCPClient({ transport: new Experimental_StdioMCPTransport({ command: "node", args: ["./mcp/files-server.js"] }) });

  try {
    const agent = new ToolLoopAgent({
      model: "provider/model-name",
      // tools from each client; name collisions are YOUR problem: prefix them before merging
      tools: { ...prefixed("payments", await payments.tools()), ...prefixed("files", await files.tools()) },
      toolApproval: ({ toolCall }) => (toolCall.toolName.startsWith("payments__") ? "user-approval" : "approved"),
      stopWhen: stepCountIs(8),
    });
    return await createAgentUIStreamResponse({
      agent,
      uiMessages: messages,
      // close the clients when the response has finished, not before: the tools still need them while it streams
      onEnd: async () => { await Promise.all([payments.close(), files.close()]); },
    });
  } catch (error) {
    await Promise.all([payments.close(), files.close()]);
    throw error;
  }
}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Build the agent from the unprefixed tools

In the first example, change const safe = await buildTools({ prefix: true }); to { prefix: false }.

terminal
$ Console. The two lines show the lost tool, and no tool call reaches the screen afterwards
── what you'll see ──
without a prefix: 3 tools listed, 2 in the tool set: lookup_order, search
with a prefix: 3 tools listed, 2 in the tool set: lookup_order, search

Myth vs fact

Myth

MCP is how my browser talks to the model.

Fact

MCP connects an AI app to outside tools and data. In a web product the MCP client normally runs on your server. The browser still talks to your own route, and sees tool calls as message parts.

Myth

A tool that says it is read-only is safe to run without asking.

Fact

Tool descriptions and annotations are untrusted unless the server is trusted. Decide what needs approval from your own table of servers and tools.

Myth

Adding more MCP servers only adds abilities.

Fact

Every tool is also more text in the model's context (cost and confusion), more names that can collide, and more places that can fail or change. Add servers deliberately and show the user what is connected.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The AI SDK's MCP client is created per server and holds a connection. Create it where its lifetime is clear, close it when the response ends (close()), and do not create one per request without closing it, or you leak connections and, for stdio, child processes.

  • ▸

    client.tools() converts the server's list into AI SDK tools with automatically generated schemas. If you want strong types or want to restrict the tools, pass your own schemas, which also freezes the argument shapes you have reviewed. A server that later changes a tool's schema then fails loudly instead of changing silently.

  • ▸

    Authentication to a remote MCP server is its own topic. The SDK's HTTP transport takes headers for a fixed token and an authProvider for OAuth flows. Keep those credentials on your server and never forward a user's token to a server the user did not choose.

  • ▸

    Treat list_changed as an audit event. Log when a server's tool list changes, and require a fresh approval for tools you have not seen before. A server you approved last month is not the same as the one you talk to today.

Remember this

  1. 1

    The problem this solves: Giving an agent a new ability used to mean writing a custom integration for each service (orders, payments, documents) inside your app. With MCP, a service publishes its abilities as an MCP server, and any MCP-aware app can use them. For you as a frontend developer, the new questions are on screen: which server did this action come from, did the user allow it, what happens when a server is down, and what changes when a server updates its list of tools?

  2. 2

    Where MCP sits (checked against the specification at modelcontextprotocol.io, October 2026). MCP is built on JSON-RPC 2.0 messages. A host is the AI application (your product). A client is the connector inside the host that talks to one server. A server is the service that offers abilities. The current revision of the specification is named 2026-07-28. Revision names are dates in the form YYYY-MM-DD, marking the last backwards-incompatible change.

  3. 3

    Three things a server can offer. Tools are functions for the AI model to call (tools/list shows them, tools/call runs one). They are *model-controlled*: the model decides when to call. Resources are data for context, such as files or records, identified by a URI (resources/list, resources/read). They are *application-driven*: your app decides what to include, for example through a picker. Prompts are ready-made message templates (prompts/list, prompts/get). They are *user-controlled*, for example chosen as slash commands. A client can also offer one thing to servers: elicitation, where a server asks the user for more information.

  4. 4

    Two ways to connect (the standard transports). stdio: the host starts the server as a child process on the same machine and exchanges one JSON message per line over its standard input and output. Streamable HTTP: the server has one web address (the MCP endpoint). Every message is an HTTP POST, and the answer is either one JSON object or a stream of events for that request. An older transport, HTTP+SSE, is deprecated. A browser tab cannot start a process, so for a web product an MCP client normally runs on your server, and the browser only sees the results as message parts.

  5. 5

    What changed in the current revision. Earlier revisions started a connection with an initialize handshake and a session. Revision 2026-07-28 removes protocol-level sessions: each request carries its protocol version and the client's capabilities in its own _meta field, and a client can call server/discover to learn what a server supports. On HTTP, headers such as MCP-Protocol-Version and Mcp-Method mirror those fields. The AI SDK's MCP client tries server/discover first and falls back to the older handshake, so you rarely handle this by hand. It matters when you debug traffic, or meet an older server.

  6. 6

    What the specification asks of a host about trust. There should always be a human in the loop with the ability to deny tool calls. Apps should make clear which tools are exposed to the model, show a clear indicator when a tool is invoked, and ask for confirmation for operations. Show the tool's inputs before the call, so nothing leaves by accident. A tool's description and annotations (such as hints that it is read-only) are untrusted unless the server itself is trusted. They are text from a third party that ends up inside the model's context, which is also how a malicious description tries to steer the model (Phase 9).

  7. 7

    Tool names collide. Two servers can both offer a tool called search. The specification says a client or proxy that combines servers should disambiguate, for example by prefixing a server identifier. If you put tools in a plain object keyed by name, the second search silently replaces the first, and the model calls the wrong one. Prefix every name with a server id, and keep a table from tool name to server so the screen can show the origin.

  8. 8

    Errors come in two kinds. A protocol error is a JSON-RPC error: an unknown tool, a malformed request (code -32602), a server fault. A tool execution error is a normal result with isError: true and text explaining what went wrong, such as "order not found". The specification says tool execution errors should be given to the model, so it can fix its input and try again. Protocol errors are usually not fixable by the model. A third case is yours to handle: the server did not answer at all. Show "Help centre is not available right now", and let the other servers keep working.

  9. 9

    Tools can change. A server may announce that its list of tools changed. A client that opened a subscriptions/listen stream receives notifications/tools/list_changed and should call tools/list again. Treat a changed list as new: a tool that was safe yesterday may have a new description or new arguments. Re-show what is exposed, and ask again before trusting a new tool with changes.

  10. 10

    MCP Apps (an optional extension). A tool can point to a user interface the server provides. The tool's metadata names a ui:// resource, and the host renders that HTML inside a sandboxed iframe, talking to it through postMessage. It is an extension, so both sides opt in, and hosts differ in support. The AI SDK's React package has a component for it named experimental_MCPAppRenderer. The experimental_ prefix means its API may change, so treat it as an early feature. Its sandbox settings deny by default: an app can call no tools and use no camera or microphone unless you allow it.

Explain it without notes

01

What are tools, resources and prompts in MCP, and who controls each?

02

Why does a web product usually run its MCP client on the server, and what does the browser show?

03

Two servers both offer a tool named search. What goes wrong, and how do you fix it?

04

What is the difference between a tool execution error and a protocol error, and how does the screen treat each?

Practice

01

Write prefixTools(serverId, tools) that returns the tools with names serverId__name, and throws if two names would still collide.

02

A server you approved last week now lists a new tool export_all_customers. Write the rule your host applies when the tool list changes, and say what the user sees.

Trade-offs

  • ↔

    MCP saves you from writing one integration per service and lets users bring their own servers. In return, you take on a changing, third-party surface: you must show origins, ask for permission, handle outages and review changes.

  • ↔

    Running every tool through approval is the safest default but trains users to click Allow. Trust trusted servers for reads, ask for changes, and make the prompt specific enough that reading it is worth the user's time.

Done when you can

  • I can name MCP's three server features and who controls each.

  • I can explain stdio and Streamable HTTP, and why the MCP client runs on my server.

  • I can prefix tool names by server and show the origin of each tool call on screen.

  • I can build an approval prompt from my own table and the real arguments.

  • I can tell a tool execution error from a protocol error from an unavailable server, and show each properly.