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 withtools/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.
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
Make the collision visible on screen
In the first example, change the first
buildToolscall's result: after the second console line addconsole.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
Trust the payments server
In the second example, set
trusted: trueforpayments, then run it again.The refunds still prompt, because the rule asks for both
trustedandreadOnly. A trusted server may skip prompts only for reads. ChangereadOnlytotrueonissue_refundand the prompt disappears, which shows why that flag must come from your own table and not from the server's annotation. - 3
Bring the Help centre back
In the third example, replace the line
serve("/mcp/docs", () => new Promise(() => {}));withserve("/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
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
}
}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
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)"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
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-availableThe 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
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.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 }.
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 ownschemas, 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
headersfor a fixed token and anauthProviderfor 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_changedas 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
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
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
Three things a server can offer. Tools are functions for the AI model to call (
tools/listshows them,tools/callruns 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
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
What changed in the current revision. Earlier revisions started a connection with an
initializehandshake 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_metafield, and a client can callserver/discoverto learn what a server supports. On HTTP, headers such asMCP-Protocol-VersionandMcp-Methodmirror those fields. The AI SDK's MCP client triesserver/discoverfirst 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
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
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 secondsearchsilently 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
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 withisError: trueand 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
Tools can change. A server may announce that its list of tools changed. A client that opened a
subscriptions/listenstream receivesnotifications/tools/list_changedand should calltools/listagain. 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
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 throughpostMessage. 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 namedexperimental_MCPAppRenderer. Theexperimental_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
What are tools, resources and prompts in MCP, and who controls each?
Why does a web product usually run its MCP client on the server, and what does the browser show?
Two servers both offer a tool named search. What goes wrong, and how do you fix it?
What is the difference between a tool execution error and a protocol error, and how does the screen treat each?
Practice
Write prefixTools(serverId, tools) that returns the tools with names serverId__name, and throws if two names would still collide.
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.