Skip to main content

From Prop Drilling to MCP: How the Model Context Protocol Is Changing Frontend Data Architecture

Updated 8 min readMarkdown
Part of seriesEmerging frontiers & the future stack

Every React developer has felt the pain of prop drilling, a piece of data needed deep in the component tree gets threaded through five layers of components that don't care about it, just so the one that does can render it. We solved that with Context, Zustand, Redux, and TanStack Query, tools that let a component pull the data it needs instead of having it pushed through intermediaries.

A structurally similar problem has appeared in AI-enabled frontends. Every AI feature, a summarize button, a support copilot, a natural-language filter, needs context, and most teams assemble that context by hand, grab state from the component, serialize it into a prompt, send it to an LLM, and repeat the whole ritual for the next feature. That is "context drilling" and it scales just as badly as prop drilling did.

The Model Context Protocol (MCP) offers a different shape, instead of the UI pushing context to the model, the model pulls what it needs through a standard interface your backend exposes once.

One caveat up front, because this topic attracts hype, MCP does not replace React Context, Zustand, or any UI state management. It solves a different problem. This guide explains the analogy where it holds, the architecture that results, and where each tool belongs.

A Quick Recap: Why Prop Drilling Hurts

// Prop drilling: every layer forwards data it never uses
<Dashboard user={user}>
  <Sidebar user={user}>
    <UserMenu user={user} />
  </Sidebar>
</Dashboard>

The costs are familiar, components coupled to data they don't use, refactors that touch many files, and no single place that defines how a piece of data is fetched or shaped. The fix was always the same idea, give consumers a standard way to pull what they need.

"Context Drilling" in AI Features

Now look at how many teams wire up AI features today:

// Each AI feature hand-assembles its own context
async function summarizeOrders() {
  const prompt = `
    User: ${user.name} (${user.role})
    Active filters: ${JSON.stringify(filters)}
    Orders: ${JSON.stringify(orders.slice(0, 50))}
    Summarize anything unusual.
  `;
  return llm.complete(prompt);
}

This works for one feature. By the fifth, you have the same problems prop drilling gave you:

  • Duplicated glue code. Each feature re-implements serialization, truncation, and permission checks.
  • Stale or partial context. The model sees only what the component happened to have in memory, not what actually exists.
  • No shared contract. Nothing defines what the model may read or do, so behavior varies feature by feature.
  • Security drift. Authorization logic gets copied into prompts and UI code rather than enforced in one place.

What MCP Changes

MCP is an open protocol for exposing context and capabilities to LLM applications through three primitives:

  • Tools - functions the model can call (query orders, create a ticket).
  • Resources - read-only data identified by URI (schemas, docs, configuration).
  • Prompts - reusable, parameterized prompt templates.

The key architectural shift is the direction of flow:

BEFORE: UI assembles context → pushes into prompt → model
        (every feature, every time)

AFTER:  UI - messages → Assistant API → Model(discovers & calls tools/resources)
                                │              │
                                │              |
                                ▼              ▼
                           MCP client → MCP server (your backend)
                                            ├─ tools: query_orders, ...
                                            ├─ resources: schema://...
                                            └─ prompts: incident_analysis

The frontend stops being the context assembler. It sends the user's message, renders the response, and the model fetches what it needs through the tools your MCP server defines, with permissions enforced where the data lives.

If you haven't built an MCP server yet, our guide to building custom MCP servers covers tools, resources, prompts, and transports in detail.

The Frontend Pattern: A Thin UI over a Server-Side MCP Client

The most important design decision, keep the MCP client on the server, not in the browser. Browser code can't safely hold credentials for your MCP server, and you want a single trusted place to enforce per-user authorization.

// app/api/assistant/route.ts — server-side sketch
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

export async function POST(req: Request) {
  const { messages } = await req.json();
  const user = await requireUser(req);          // your auth

  const mcp = new Client({ name: "web-assistant", version: "1.0.0" });
  await mcp.connect(
    new StreamableHTTPClientTransport(new URL(process.env.MCP_URL!), {
      requestInit: {
        headers: { Authorization: `Bearer ${await mintUserToken(user)}` },
      },
    }),
  );

  // Discover what the server offers instead of hardcoding it per feature
  const { tools } = await mcp.listTools();

  // Hand `tools` to your LLM SDK; when the model requests a tool call,
  // route it back through the MCP client:
  //   const result = await mcp.callTool({ name, arguments: args });
  return runAssistantLoop({ messages, tools, callTool: mcp.callTool.bind(mcp) });
}

The runAssistantLoop helper stands in for whichever LLM SDK you use, the pattern is, list tools, let the model choose, execute through MCP, feed results back, repeat until the model answers.

The component that consumes it becomes almost trivially small:

function AssistantPanel() {
  const { messages, send, isLoading } = useAssistant("/api/assistant");

  return (
    <Chat
      messages={messages}
      onSend={send}
      disabled={isLoading}
    />
  );
}

No serialized state, no prompt construction, no per-feature context logic. Add a new capability by adding a tool to the MCP server, every assistant surface that connects to it gains the capability without frontend changes.

What MCP Does Not Replace

This is where the "prop drilling to MCP" framing can mislead. MCP is not a state-management library. It doesn't re-render components, hold UI state, or cache server data for rendering. Here's where each concern belongs:

Concern Right tool
Local UI state (modal open, form draft) useState / useReducer
Shared UI state across components Context / Zustand / Redux
Server data rendered in the UI TanStack Query / server components
Data and actions an AI agent needs MCP tools
Reference material for the model (schemas, docs) MCP resources
Reusable AI task framing MCP prompts

A useful rule: if a component needs it to render, it's a state or data-fetching concern. If a model needs it to reason or act, it's an MCP concern. Many apps need both, and they sit side by side.

Letting the Model Act on the UI

One legitimate gap: a model calling a backend tool can change data, but how does the UI reflect it? Two practical approaches:

1. Refresh through your existing data layer. After a tool call completes, invalidate the relevant queries so the UI re-renders with fresh data:

const queryClient = useQueryClient();

async function onToolResult(toolName: string) {
  if (toolName === "update_order_status") {
    await queryClient.invalidateQueries({ queryKey: ["orders"] });
  }
}

2. Use client-side tool definitions for UI-only actions. Actions like "navigate to this page" or "apply this table filter" live in the browser, so they're typically defined through your LLM SDK's tool-calling feature and executed in the client, while data and business actions go through MCP on the server. Keeping this split avoids pushing browser concerns into a shared server.

Security Considerations for Frontend-Facing MCP

Connecting a model to real tools expands your attack surface. Treat these as requirements, not nice-to-haves:

Authenticate and authorize per user. Mint a token scoped to the signed-in user so tools enforce that user's permissions. An assistant should never see more than the person using it.

Require confirmation for destructive actions. For tools that write, delete, or spend money, surface an approval step in the UI before execution.

Treat tool output as untrusted input. Data returned from tools, ticket text, user-generated content, web pages, can contain instructions aimed at the model (prompt injection). Don't let retrieved content silently trigger sensitive tool calls.

Never expose secrets to the browser. The server-side client pattern above exists for this reason.

Log every tool call with user identity, tool name, and arguments. Agent actions deserve the same audit trail as API access.

Common Pitfalls

Connecting the browser straight to an internal MCP server. Convenient in a demo, risky in production. Put an authenticated assistant API in between.

Mirroring your whole REST API as tools. Dozens of overlapping tools confuse model selection. Design a smaller set of task-oriented tools with precise descriptions.

Returning unbounded data. Large tool results flood the model's context window and degrade answers. Paginate and cap results.

Replacing state management with the assistant. If a screen only works when the model is involved, you've built fragility. Core UI should function without the AI layer, the assistant augments it.

Skipping loading and error states. Tool-calling loops take longer than a normal API call. Stream progress and handle failures visibly.

Conclusion

Prop drilling taught frontend developers a lasting lesson, consumers should pull what they need through a standard interface rather than relying on every layer to forward it. MCP applies a similar lesson to AI features, instead of each UI feature hand-assembling context into prompts, you expose capabilities once through a standard protocol and let the model retrieve what it needs.

The result is a thinner frontend, a single enforcement point for permissions, and AI features that gain new abilities when the server does. But the analogy has limits, MCP complements your state-management layer, it doesn't replace it. Keep UI state in UI tools, put agent-facing capabilities behind MCP, and keep the boundary between them explicit.

Are you wiring AI features into an existing React or Next.js app? Share how you handle context today, hand-built prompts, a shared assistant API, or MCP, in the comments.

#MCP #PropDrilling #React #FrontendArchitecture #AIEngineering

About the author

  • I am a passionate and results-driven Software Developer & DevOps Engineer with a strong interest in building a clean and maintainable codebase. With hands-on experience across the full development lifecycle, from planning and design to implementation and deployment, I specialize in developing clean performant code and automating robust infrastructure workflows. I thrive at the intersection of software engineering and DevOps practices, where I can streamline processes, improve reliability, and accelerate delivery cycles. I’m always seeking new opportunities to impact meaningful projects, work with talented teams, and continue growing at the forefront of technology.

No comments yet. Be the first to comment.

More from this blog