An MCP app must provide the model with the same data that it displays to the user.
2
Tool descriptions should tell the model when results are being rendered in a UI, so it does not repeat the UI's contents in text.
3
Data processing and UI rendering should be separate tools, allowing the model to search and filter freely before choosing what to display.
Summary
Dustin Mihalik describes lessons from building MCP apps for Claude, ChatGPT, and Indeed's internal Career Scout agent. A UI can add branding, apply buttons, detail views, and links that are difficult to produce reliably in a chat host. It also changes model behavior. When a search tool immediately renders results, the model may call it once and stop exploring. Mihalik's solution is to design around data first. Everything shown in the UI should also be returned to the model as structured data. Tool descriptions should say that results are automatically displayed. User interactions, such as opening a job, should be sent back through model context. Most importantly, search and filtering should remain separate from rendering. A search tool can return many jobs, while a render tool receives the selected IDs and displays only the chosen results. The model can also provide reasons or highlighted text for the rendered items.
A UI solves real problems that plain text leaves behind
Mihalik starts with a text-based search for barista jobs in Austin. It works, but it has no Indeed branding, and links are difficult to make reliable because chat hosts prefer users to stay inside their environment. He says the team spent many hours and evaluations getting Claude to link consistently. An MCP app can provide an apply button, choose which information appears first, and open job details in a modal without sending the user away from the conversation. The UI adds control over the job-search experience, but it also changes how the model handles the task.
Anything displayed to the user must also be available to the model
If an app calls existing APIs and only injects HTML, the model cannot see what the user sees. Mihalik calls this a black box. Follow-up requests such as "tell me about the first result" or "rank this list of companies" then fail because the model has no access to the displayed records. The app should return both structured content for the model and a resource URI for the HTML. Those two representations need to stay synchronized. When a new field is added to the API, the same field should be added to the data returned to the model.
The tool description must explain that the UI already contains the results
Returning data to the model does not by itself stop it from narrating the same results in text. Mihalik says the tool description should state that results are automatically displayed as UI components. This helps the model produce a short summary while leaving the detailed list to the interface. Without that instruction, the model and the UI compete to show the same content. The description can also explain where the displayed results appear, such as at the top of the interface.
Buttons and detail views create another visibility problem. If a user clicks "view details" for one of ten jobs, the model does not automatically know which job was selected. It therefore cannot answer requests such as writing a cover letter or summarizing that job description. Mihalik recommends passing interaction information back through the MCP apps update model context method. The example from the MCP apps documentation records shopping-cart items and the total cost so the model can answer questions about the current cart. For MCP apps, the method passes a string, so multiple events may need to be appended over time.
Search and filtering should remain independent from rendering
Mihalik's third rule addresses the hardest case, where the model needs to explore many results before showing a few. In a text-based job search, Claude may search ten or fifteen times, filter the results, and build a table. When the search tool immediately renders a carousel, the model may call it once and stop because it sees that results are already on screen. The solution is to keep search as a normal text-based MCP tool and create a separate rendering tool. The model can then explore as much as needed before selecting what the user should see.
At Indeed, the search tool returns job data without a UI, while a render-jobs widget takes a list of job IDs. Claude can search and find a hundred jobs, filter them, choose five, and pass only those five to the renderer. The render tool description needs to explain where its data comes from and what format it expects. It can require the model to call one of the search tools first. This pattern also applies to other domains, such as selecting products for an e-commerce display or choosing addresses to put on a map.
The model can add its reasoning to rendered results
Separating rendering from data processing lets the model contribute more than a list of records. Mihalik describes adding a reason for why a job is a good fit or asking the renderer to highlight a useful section of the job description. The render tool could accept an ID together with that reason or highlighted text. This preserves the model's own filtering and explanation while giving the user a structured interface. Rendering becomes the result of the model's exploration instead of the thing that limits that exploration.
Data-first design leads to smaller, composable tools
Mihalik's final advice is to decide what data the model should receive before deciding how to put UI into ChatGPT or another host. Rendering should follow the model's exploration of that data. He recommends several small tools rather than one large tool, such as multiple job-search tools and separate renderers for a list of jobs or a single highlighted job. Simple descriptions avoid overloading the model while leaving it free to choose how to search and how to render.