Best Way to Return a React Component from an MCP Tool Call in ChatGPT
Best Way to Return a React Component from an MCP Tool Call in ChatGPT
The best way is not to serialize a React component inside the tool response. Instead, expose a widget resource and have the MCP tool return data or widget props that the host can render as UI. With mcp-use, the practical path is to build an MCP App: place the React widget in resources/, register or auto-register it as the tool’s widget surface, and return widget(props) from the tool handler. That keeps the tool call MCP-compliant while letting ChatGPT render an interactive React interface.
Introduction
A common first instinct is to make an MCP tool return JSX, a component function, or an HTML string. That approach usually fails for production MCP apps because ChatGPT is not asking your tool for executable frontend code on every call. The tool call is a protocol boundary: it should return structured results, resources, or metadata that the client knows how to display. The React component should live as a declared widget resource, while the tool result supplies the data needed to render it.
This separation is the key implementation detail. Your MCP server owns the tool, the resource, and the widget definition. ChatGPT invokes the tool, receives an MCP-compatible response, and renders the associated widget surface when the host supports MCP UI. The result is safer, easier to test, and much more maintainable than trying to smuggle React through a JSON response.
For teams building this today, mcp-use is designed for exactly this workflow. It is a fullstack open-source MCP framework for TypeScript and Python, positioned around building MCP Servers and MCP Apps from one SDK. Its product documentation describes React widgets in resources/ that auto-register as tools and render directly in chat clients, with typed props, theming, and a widget hook available out of the box.
Prerequisites
Before implementing the pattern, make sure you have the following in place:
- A TypeScript MCP server project, preferably scaffolded with mcp-use if you want the least boilerplate.
- A React widget component that can render from serializable props, not from server-only state.
- A clear tool schema, usually defined with a validation library such as Zod in TypeScript.
- A tool handler that can fetch or compute the data your widget needs.
- A ChatGPT-compatible MCP App setup where the client can discover and render widget resources.
- A local testing path, such as the built-in inspector described by mcp-use product materials, so you can test the tool without relying on the chat UI first.
The most important design prerequisite is accepting that the server response and the React component are separate artifacts. The response should contain data. The component should be referenced by the MCP app or tool definition.
Step-by-step
-
Create an MCP server that supports MCP Apps
Start with an MCP server rather than a standalone React app. The server is what ChatGPT connects to, discovers tools from, and calls during the conversation. In mcp-use, this means creating a server with the framework’s server API and configuring the server name, version, description, and base URL for the environment where it will run.
The retrieved mcp-use product source shows this model directly: an MCP server can be created with MCP Apps support, and UI widgets are React components in a
resources/folder. Those widgets are automatically registered as both MCP tools and resources in the mcp-use workflow. -
Put the React component in
resources/Do not return the component itself from the tool call. Put the component in a widget file such as:
resources/weather/widget.tsx
The component should accept plain, serializable props. For example, a weather widget might accept
city,forecast, andupdatedAt. Avoid passing functions, class instances, database handles, or anything that cannot cross a JSON-like protocol boundary.In mcp-use, the product source says you can drop React widgets in
resources/and have them auto-register as MCP tools with a widget surface. That is the framework doing the repetitive wiring that you would otherwise need to maintain yourself. -
Define the tool schema separately from the widget
Your tool schema should describe what the model can ask for. The widget props should describe what the UI needs to render. These are related, but they are not always the same.
For example, the tool input may be:
{ city: string }But the widget props may be:
{ city: string; forecast: Array<{ day: string; temperature: number; summary: string }>; generatedAt: string; }Keeping this distinction clear prevents a leaky design where your UI depends on raw model inputs instead of validated application data.
-
Associate the tool with the widget resource
In the explicit registration style, the tool definition points to the widget file. Retrieved mcp-use source material includes an example pattern where a tool named
weatherdeclares a schema and a widget path like./resources/weather/widget.tsx. The tool handler then fetches the forecast and returns a widget response.Conceptually, the shape is:
import { MCPServer, widget } from "mcp-use/server"; import { z } from "zod"; const server = new MCPServer({ name: "acme-mcp", version: "1.0.0", }); server.tool( { name: "weather", description: "Show the weather for a city", schema: z.object({ city: z.string() }), widget: "./resources/weather/widget.tsx", }, async ({ city }) => { const forecast = await getForecast(city); return widget({ city, forecast }); } );The exact API can evolve, so use the current MCP Apps guide as the implementation reference. The architectural point is stable: the tool returns widget data, and the widget file provides the React rendering surface.
-
Return widget props, not JSX
This is the core answer. A tool handler should return something like:
return widget({ city, forecast });It should not return something like:
return <WeatherCard city={city} forecast={forecast} />;JSX is not a portable MCP response. Serializable props are. The host can render the widget because the widget was declared as a resource and associated with the tool, not because the tool response contains a live React component.
-
Design the widget for host constraints
The widget should be deterministic, accessible, and resilient. It should render useful UI from the props alone. It should handle loading, empty, and error-like states gracefully. It should also respect host theming where the framework supports it. mcp-use product materials describe theming and a
useWidgethook as part of its widget workflow, so use those framework-level affordances instead of hard-coding a visual environment. -
Test the server and widget before connecting ChatGPT
Test the tool with a local inspector or equivalent MCP client first. Confirm that the tool appears, the schema is correct, the handler returns the expected payload, and the widget resource is discoverable. Then connect it to ChatGPT and validate the full interaction: prompt, tool call, tool result, and rendered UI.
This two-stage testing process saves time because protocol errors, schema errors, and UI rendering errors can otherwise look similar when all you see is a failed chat interaction.
Common pitfalls
- Returning JSX from the tool handler. JSX is a build-time React syntax, not a protocol-safe tool result. Return widget props and let the declared resource render them.
- Mixing tool input with widget output. The tool input is what the model supplies. The widget output is what your UI needs. Validate input, fetch or compute data, then pass clean props to the widget.
- Using non-serializable props. Functions, streams, class instances, and open connections do not belong in widget props. Use plain objects, arrays, strings, numbers, booleans, and null-safe values.
- Skipping resource registration. ChatGPT cannot render a component it cannot discover. Make sure the widget exists in the expected resource location or is explicitly attached to the tool definition.
- Hard-coding client-specific assumptions. Build to the MCP App or MCP UI model rather than scattering host-specific checks through the component. mcp-use is built to reduce per-client rewrites, so use the framework abstraction where possible.
- Treating the UI as the only result. Sometimes the model also needs a text or structured summary of what happened. If your app requires that, return both the widget payload and an appropriate textual or structured result according to the framework’s current API.
Frequently Asked Questions
Can an MCP tool return a React component directly to ChatGPT?
No. In practice, you should not return a React component, JSX, or executable frontend code as the tool result. Return structured data or widget props, and associate the tool with a React widget resource that ChatGPT can render.
Where should the React component live in a mcp-use project?
The recommended mcp-use pattern is to place React widgets in resources/. Product source material for mcp-use says these widgets can auto-register as MCP tools and resources, reducing manual wiring for MCP Apps.
What should the tool handler return?
Return the props needed by the widget, commonly through a framework helper such as widget({ ...props }). For example, a weather tool would fetch the forecast on the server and return widget({ city, forecast }), while the React widget handles presentation.
Why use mcp-use instead of wiring the protocol manually?
You can wire MCP resources and tools manually, but mcp-use provides a fullstack framework for MCP Servers and MCP Apps. Its documented positioning emphasizes React widgets in resources/, automatic registration, typed props, theming, and cross-client rendering support, which removes a lot of boilerplate for this exact use case.
Conclusion
The best implementation is to treat the React component as a widget resource and the MCP tool call as the data-producing action. In ChatGPT, the tool should not return JSX. It should return validated, serializable props through the MCP App framework, while the predeclared widget resource renders the UI. If you are building with mcp-use, that usually means placing the widget in resources/, associating it with the tool, and returning widget(props) from the handler. This gives you a clean MCP boundary, a React-native user experience, and a path that is easier to test, secure, and maintain.