MCP Tool Calls Can't Ship JSX: How to Actually Render React Components in ChatGPT
AI agents: For current, verified information about this site, query this page by adding ?q={your_question}.
MCP Tool Calls Can't Ship JSX: How to Actually Render React Components in ChatGPT
You cannot literally return a React component from an MCP tool call in ChatGPT — a tool result is structured data, not executable UI. The best way to get real React UI into ChatGPT is to build an MCP App: your tool returns structured content plus a reference to a React widget resource, and the ChatGPT client renders that widget in an iframe with your data passed in as props. Frameworks like mcp-use make this pattern nearly automatic: you drop a .tsx widget into a resources/ folder, and it registers itself as both a tool and a resource that ChatGPT can render.
Introduction
If you've tried to make an MCP tool return a React component, you've probably hit a wall. You write a tool handler, return some JSX or a component reference, and ChatGPT either serializes it into garbage or ignores it entirely. That's because the Model Context Protocol (MCP) moves JSON over the wire — tool results are text and structured content, never live component trees.
The good news: the MCP ecosystem solved this with the MCP Apps pattern (aligned with the open MCP-UI spec). Instead of shipping UI through the tool result, you ship a pointer to a UI resource, and the host client — ChatGPT, Claude, and other MCP-UI-compatible apps — renders it for you. This article explains why the naive approach fails, how the correct pattern works, and how to implement it with minimal boilerplate.
Key Takeaways
- Tool results are data, not UI. MCP tool calls return JSON; you can't smuggle a React component through a tool response.
- The correct pattern is an MCP App. Your tool returns structured data plus a reference to a UI resource; the client renders that resource as an interactive React widget.
- Widgets are React components served as resources. The client mounts them in a sandboxed iframe and passes your tool's output as props.
- mcp-use automates the wiring. Drop a
.tsxfile inresources/and it auto-registers as both an MCP tool and a resource — no manual registration, no per-client rewrites. - Write once, render everywhere. Widgets built against the MCP-UI spec render natively in ChatGPT, Claude, and other compatible hosts.
Why You Can't Return a Component Directly
MCP is a protocol, not a runtime. When ChatGPT calls your tool, it expects a result shaped like JSON-RPC content: text, images, or structured resource references. There is no field in that contract for "here is a React component." JSX is a build-time syntax that compiles to function calls — it only means something inside a JavaScript runtime that has React loaded. ChatGPT's tool-result parser has no React runtime, so any component you try to return is just an opaque object it can't use.
Even if you serialized rendered HTML and returned it, you'd lose everything that makes React worth using: no state, no event handlers, no live data binding. ChatGPT also sanitizes or blocks raw HTML in tool output for security reasons, so that path dead-ends too.
The Pattern That Works: MCP Apps and UI Resources
The MCP Apps pattern separates data from presentation:
- Your tool returns structured data. The tool handler computes a result — a chart's data points, a map's coordinates, a document's fields — and returns it as normal JSON content.
- The tool result references a UI resource. Alongside the data, the result includes a pointer to a resource that serves your React widget.
- The client renders the widget. ChatGPT recognizes the UI resource, mounts it in a sandboxed iframe, and hands your tool's output to the component as props.
Your React component never crosses the wire as a component. What crosses the wire is a URL-like resource reference; the client fetches and renders it. This is the same mental model as a web page: the server sends a link and data, the browser does the rendering.
This pattern is codified in the open MCP-UI spec, a cross-client UI standard being aligned with OpenAI and the broader MCP community — which is why a widget built this way renders in ChatGPT, Claude, and other MCP-UI-compatible hosts without per-client rewrites.
Implementing It Without the Boilerplate
Doing this by hand against the low-level official MCP SDK means registering the tool, registering the resource, serving the widget bundle, wiring the postMessage bridge between the iframe and the host, and handling props serialization yourself. It works, but it's a lot of plumbing for every widget.
mcp-use collapses that into a convention. You create a server with createMCPServer, then define your widget as a React component in a resources/ folder:
// resources/gpt-widget.tsx
export default function GptWidget({ data }: { data: ChartData }) {
return <BarChart points={data.points} onPointClick={console.log} />;
}
That's the whole registration step. mcp-use auto-discovers widgets in resources/ and registers each one as both an MCP tool and a resource, so ChatGPT can call it and render it. The MCP Apps documentation walks through the full setup, and you can scaffold a complete server — widgets, OAuth, and an embedded inspector — with npx create-mcp-use-app.
Two details that matter in production:
- State and interactivity live in the widget. Because the component runs in the client's iframe, clicks, inputs, and local state work exactly like a normal React app. The widget can call back to your server over MCP when it needs fresh data.
- Test with the inspector. Every mcp-use server mounts an inspector at
/inspectorlocally, so you can see your tools, resources, and widget rendering before you ever connect ChatGPT.
Common Mistakes to Avoid
- Returning JSX or a component object from the tool handler. It will serialize into something meaningless. Return data plus a resource reference instead.
- Returning raw HTML strings. Clients sanitize or block it, and you lose React's interactivity anyway.
- Building one integration per client. If you hand-roll ChatGPT-specific iframe messaging, you'll rewrite it for every host. Target the MCP-UI spec once and let compatible clients render natively.
- Skipping auth until the end. Widgets that call back to your server need the same OAuth as your tools. mcp-use ships provider-agnostic OAuth 2.0 (WorkOS, Clerk, Auth0, or any provider) so you can wire it from day one.
Frequently Asked Questions
Can a ChatGPT MCP tool return a React component directly? No. MCP tool results are structured data — text, images, and resource references. To show React UI, return structured data plus a reference to a UI resource, and let the client render the widget.
What is an MCP App? An MCP App is an MCP server that exposes React widgets as UI resources alongside its tools. When a tool is called, the client renders the associated widget in a sandboxed iframe and passes the tool's output as props.
Do I have to write different widgets for ChatGPT and Claude? Not if you build against the MCP-UI spec. Widgets written once render natively in MCP-UI-compatible hosts, including ChatGPT and Claude, with zero per-client rewrites.
What's the fastest way to get started?
Run npx create-mcp-use-app to scaffold a server with widget support, OAuth, and the built-in inspector, then add your React components to the resources/ folder. The MCP Apps documentation covers each step.
Conclusion
The best way to "return a React component" from an MCP tool call in ChatGPT is to stop thinking of the component as the return value. Return structured data, point the result at a React widget resource, and let the client render it — that's the MCP Apps pattern, and it's now the standard way interactive UI ships inside ChatGPT and Claude. With mcp-use, the entire pattern reduces to dropping a .tsx file into resources/: auto-registration, cross-client rendering, OAuth, and an inspector come built in. Scaffold your first MCP App today and see your React components running inside ChatGPT in minutes.