MCP Apps: Give the Model Data, Give the User a UI — Dustin Mihalik, Indeed
Read the talk
MCP Apps: Give the Model Data, Give the User a UI
Dustin Mihalik explains how Indeed separates job search from presentation so an assistant can explore results, understand user interactions, and display a useful shortlist without stopping its work too early.
From a talk by Dustin Mihalik
At a glance
Ideas worth remembering
A widget can make job details and application controls dependable, but its displayed data and changing interaction state must also reach the model for follow-up questions to work.
Tool descriptions should explain what the UI already presents and where rendering inputs come from. This steers the assistant’s behavior, without guaranteeing that duplicate output disappears.
Separate retrieval from rendering so exploratory calls do not each produce a visible answer. Indeed’s job-ID renderer lets the assistant filter a pool of 100 jobs to five before displaying them.
Start with the data the model needs and use small tools it can compose. Rendering can then express the selected results and, as a proposed extension, the model’s reasons for selecting them.
Why job search benefits from an interface
Dustin Mihalik introduces practical lessons from Indeed’s work on MCP connectors and apps for Claude, ChatGPT, and its own job seeker agent. His team works on AI platform infrastructure, including guardrails, gateways, and compliance. Job search provides the examples throughout: a conversation must help someone discover opportunities and then act on them.
A plain text response to a search for barista jobs in Austin can already be useful. Claude can organize results into jobs in the city and jobs in nearby suburbs. But the host controls the presentation, leaving Indeed with limited control over branding and the information emphasized. More consequentially, Mihalik reports that getting Claude and ChatGPT to include outbound links consistently was difficult. Five job suggestions without usable links leave the user without a practical next step; Indeed spent substantial time on evaluations to improve that behavior. His explanation that hosts prefer users to stay in their environment is an interpretation of the difficulty.
An MCP app gives the provider explicit presentation controls. Indeed can supply an apply button, highlight important information, and offer a details control that opens the full job description in a modal. The user can inspect a job without leaving the conversation, while the provider can make the path to applying visible and dependable.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Explain the presentation in the tool description
Returning model-readable data solves visibility, but it introduces another problem: the assistant may produce its usual full text presentation underneath the widget. The user receives the interface and then a second version of essentially the same results. The model has the data, yet still lacks an explanation of what the app already displayed.
The second rule is to tell the model, through the tool description, that the app displays results. This can steer it toward a short summary rather than reconstructing an entire result list in text. Mihalik says that placing a statement near the top of the description explaining that results are automatically displayed as UI components covers many cases. He describes an ongoing tension between interface output and model output, so the description is a steering mechanism rather than a guarantee that duplication disappears.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Make user interactions visible to the assistant
An interactive app can drift away from the model’s knowledge after the initial tool response. Opening a job’s details may load a description dynamically or through another API. Even if the initial response contained 10 jobs, the model does not automatically know which one the user clicked. Requests to summarize this job description or write a cover letter for this job require both the selected job’s identity and the relevant details.
Mihalik extends the visibility requirement to user interactions and describes a method for updating model context. In the mechanism he presents, the app supplies a single string; retaining multiple events over time requires appending information to that string. His shopping-cart example includes the cart’s items and total cost, allowing the assistant to answer questions about its contents. The important capability is communicating the app’s changing state to the assistant; the talk does not establish an exact API signature.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Why attaching a widget can reduce exploration
An app that exposes its data and interactions can still undermine the reason someone asked an assistant for help. Mihalik’s harder job-search example combines a target title, willingness to relocate across several cities, a preference for the highest-paying options, and industries the user wants to exclude. This requires exploration and comparison. He reports that text-based MCP works well for this pattern: Claude may perform 10 or 15 searches, filter the results, extract relevant information, and assemble a table.
When the search tool also displayed a widget, Mihalik observed Claude calling it once and then stopping because results were already on screen. Repeated searches would also produce repeated presentation: a user would not want 10 separate carousels. Coupling retrieval to rendering therefore made an exploratory call look like a completed answer. This is a reported behavior from Indeed’s implementation, rather than a universal claim about every MCP app.
His third rule addresses that coupling: separate data processing from UI rendering. He gives this rule priority over the earlier rules because making information visible and explaining the widget’s behavior still does not give the assistant room to search freely before presenting its answer.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Search freely, then render selected results
Mihalik attributes the separation principle’s wording to OpenAI’s Apps SDK documentation. Indeed implements it with a search tool that returns no UI and a separate job-rendering widget. The assistant can call the search tool repeatedly without adding a visible result carousel each time. Rendering becomes an explicit action after the model has decided what to show.
A render tool can accept the full data to display or references to that data. Indeed chooses references: its widget takes a list of job IDs. In Mihalik’s example, Claude can gather 100 jobs, filter them to five, and pass those five to the renderer. This separates the size of the candidate pool from the size of the visible answer. Tool descriptions must explain where rendering inputs come from and what format they require; he suggests describing the prerequisite retrieval tools that should be called first.
The same boundary can apply to other domains. An assistant can explore e-commerce options before choosing which ones to present. For a map, it might first identify five addresses and then pass those addresses to a renderer. The design question is where the model needs freedom to investigate data and where it should commit to a particular presentation.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Let the presentation carry the model’s explanation
Separating rendering also creates room for the model to contribute more than a list of identifiers. In text responses, Mihalik has seen the assistant explain why it selected a particular job. He proposes extending the rendering widget to accept a job ID together with a reason the job fits, or an ID together with a passage from the job description to highlight. The interface could then present both the opportunity and the assistant’s explanation of its relevance.
This is a proposed extension, with no measured improvement reported in the talk. Its purpose is to preserve the assistant’s useful explanatory contribution within the rendered experience, giving the widget more character and helping the user understand why a result was selected.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Build around data and small composable tools
Mihalik closes by recommending that developers decide what data the model should receive and what it should be able to do with that data before focusing on the interface. Rendering follows the model’s exploration. This changes the starting point for an MCP app: the useful capability is the assistant’s access to information and freedom to work with it, with the UI presenting the resulting selection.
Small composable tools support that freedom. If there are two or three ways to search for jobs, those can become separate search tools feeding one renderer. Alternatively, two rendering tools might serve different purposes: displaying a list and highlighting a single job. Mihalik recommends keeping descriptions fairly simple so they do not overload the model, while preserving its flexibility to choose how to explore and how to present. He ends by offering to answer questions in the hall.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Read the complete timestamped transcript
- 0:01
[music]
- 0:13
>> Uh hey everyone, I'm Dustin Macholic. Uh
- 0:15
I work at Indeed.
- 0:18
Uh we're the number one job site in the
- 0:19
world. And I have to apologize for my
- 0:22
voice. I'm recovering from a cold that I
- 0:24
had last week.
- 0:25
Um
- 0:26
Yeah, so at Indeed, we build
- 0:30
uh
- 0:30
job search. And uh I also work on a team
- 0:34
that does AI platform. And I do like AI
- 0:37
guardrails and and gateways and
- 0:40
compliance stuff. Uh occasionally uh my
- 0:43
team gets cool projects to work on
- 0:45
because we have relationships with the
- 0:46
vendors. Uh MCP apps is one of those.
- 0:49
And um
- 0:51
MCP connectors. So, this is a little bit
- 0:53
of like practical MCP apps. They gave a
- 0:56
great introduction uh to MCP apps. This
- 0:59
is a little bit of a lessons from the
- 1:01
trenches, uh which is a little bit of
- 1:03
like what did we learn in building MCP
- 1:06
and MCP apps for Claude chat chat GPT
- 1:09
and our own internal
- 1:11
uh
- 1:12
uh career scout, which is our uh agent
- 1:15
that we have for job seekers.
- 1:18
So, this is the this is the chat-based
- 1:20
interface. So, a lot of my examples are
- 1:22
going to be job search. This will I have
- 1:24
a lot of screenshots of job searches.
- 1:27
Um So, this is this is a job search,
- 1:30
which is basically uh you know, I'm
- 1:32
looking for barista in Austin. And this
- 1:34
is a text-based response. And this works
- 1:37
pretty well. Uh as we kind of discussed,
- 1:40
like there's no branding here. There's
- 1:41
no Indeed branding. Uh you know, Claude
- 1:45
decided to say these are some jobs in
- 1:47
Austin. These are some jobs in some
- 1:48
suburbs of Austin. That's cool. That's
- 1:50
probably good for the user. There may be
- 1:52
some limitations uh
- 1:55
of like what we can do for branding or
- 1:58
how we can uh
- 1:59
you know, how we can control things. And
- 2:02
you'd be actually be surprised uh unless
- 2:04
you've tried to do this yourself that um
- 2:07
it's really hard to get Claude or
- 2:09
ChatGPT to link to things cuz they don't
- 2:12
want you to leave their environment. It
- 2:14
makes complete sense, but uh if you get
- 2:17
back, here's five jobs that are
- 2:19
somewhere on the internet uh without any
- 2:22
links, that's a terrible user
- 2:24
experience. Uh so it's it took us a
- 2:27
ridiculous number of hours and evals to
- 2:29
make sure that like Claude would
- 2:31
consistently link to things.
- 2:34
So with MCP apps and with uh apps SDK,
- 2:37
uh we could kind of control that. Uh we
- 2:40
can decide
- 2:42
uh you know, that we've got an apply
- 2:43
button. We can decide what stuff is
- 2:46
important that we want to highlight at
- 2:47
the top. We can provide a link to view
- 2:50
details
- 2:52
um so that when you click it, you get uh
- 2:55
a a pop-up, a a modal that has all the
- 2:58
job details so you don't have to leave
- 3:00
the environment. So it's it's a win-win
- 3:03
uh for for both for both uh for both
- 3:06
companies.
- 3:09
So uh MCP apps is really good, but if
- 3:13
you're thinking about hey, I want to
- 3:15
build an MCP app or I want to take my
- 3:16
website, I want to put it in ChatGPT or
- 3:18
Claude, uh it's not quite just as easy
- 3:22
as like dragging and dropping into a
- 3:24
chat interface. Uh you really want to
- 3:26
think about how you are representing the
- 3:29
data, how you're making it available to
- 3:31
the user.
- 3:32
So if you do a very naive thing,
- 3:35
uh which is you still call your existing
- 3:37
APIs uh for loading data, um then it
- 3:42
basically becomes a black box to the
- 3:44
model, right? So you say, "Hey, I want
- 3:46
to do something." The model says, "Cool,
- 3:48
I'll call a tool."
- 3:50
The tool says, "Okay, I'm going to show
- 3:52
some stuff, but the model has no idea
- 3:56
what what you're displaying." So,
- 3:58
there's all these like follow-up
- 3:59
questions, like tell me about the first
- 4:01
result, or please rank this list of
- 4:03
companies. The model has no idea what
- 4:06
data is being displayed.
- 4:07
So, the very first rule
- 4:10
uh for me
- 4:11
uh building MCP apps, anything that you
- 4:13
show to the user also needs to be
- 4:16
provided as data to the model. I think
- 4:19
this makes sense, but uh I've definitely
- 4:22
seen some MCP apps where they just, you
- 4:24
know, use it to inject some HTML on the
- 4:26
page and then call some APIs,
- 4:29
uh and that's that just makes a big
- 4:31
black box for the model. So, this is
- 4:33
really easy to do
- 4:35
uh using MCP spec and the MCP app spec.
- 4:38
Uh this structured content, this is what
- 4:40
you would already be returning if you
- 4:41
were doing just text-based MCP. Um and
- 4:45
then this resource URI, that's points to
- 4:47
where the HTML is. You need to return
- 4:50
both of these, and you need to keep them
- 4:51
in sync, right? If you add something new
- 4:53
to the API, you make sure you add
- 4:55
something add that same data
- 4:57
uh back.
- 5:00
So, kind of the next step um that uh
- 5:05
that once you do that,
- 5:07
uh you'll find is that now you've
- 5:10
provided data to the model,
- 5:12
and you provided this black box that it
- 5:14
has no idea about, it's still going to
- 5:16
try and describe the it's still going to
- 5:19
try and take the output and describe it
- 5:21
as it normally would. So, you end up
- 5:23
with like, "Here's your display." and
- 5:24
then here's the model doing basically
- 5:27
the same thing that it would normally
- 5:28
do. Um so, what you need to do is you
- 5:30
need to update your description in order
- 5:33
to tell it that you're going to be
- 5:34
displaying stuff in your MCP app. Uh
- 5:37
that way you get this like nice, you
- 5:39
know, here's a list of you know, here's
- 5:41
a little summary of things, and you
- 5:43
know, the results are are are showed
- 5:45
above the links, rather than it trying
- 5:47
to like do a whole text-based display.
- 5:50
Um
- 5:51
you'll end up with this a little bit of
- 5:52
a battle between like what gets
- 5:54
displayed in UI and what gets displayed
- 5:56
by the model. You can try and steer that
- 6:00
with descriptions. So, even something as
- 6:02
simple as results were automatically
- 6:04
displayed to the user as UI components
- 6:07
at the top of your description, your
- 6:09
tool description,
- 6:10
uh that covers that covers quite a bit
- 6:13
of the cases. Um
- 6:16
So, that's one of the next things that
- 6:18
you're going to want to do uh once
- 6:20
you're providing both data and API
- 6:23
access.
- 6:24
Uh the next thing is there's these
- 6:26
interactable pieces, right? There's the
- 6:28
apply button, there's a view details
- 6:30
button, which pops up a big job
- 6:32
description.
- 6:33
Um this is the same case where, you
- 6:36
know, as you interact with those, the
- 6:38
model's not going to know necessarily
- 6:40
what you're looking at. So, you click
- 6:41
view details, you get a big modal that's
- 6:44
here's everything about the job.
- 6:47
There's once again a whole bunch of
- 6:49
questions that the user could ask.
- 6:51
Uh write a cover letter for this job,
- 6:53
summarize this job description. It has
- 6:56
no idea because you've loaded that data
- 6:58
in either in via API or you loaded it in
- 7:01
dynamically,
- 7:02
uh you know, hit you return 10 jobs and
- 7:05
user clicks on one,
- 7:07
the model has no idea which one you
- 7:09
clicked on.
- 7:11
Uh so, any information about user
- 7:14
interactions, you also need to provide
- 7:17
to the model.
- 7:20
And
- 7:21
uh once again, MCP abstract has a pretty
- 7:24
easy way to handle it. There's this
- 7:25
update model context
- 7:27
um method, which lets you pass in a
- 7:30
string
- 7:31
uh that is
- 7:33
So, for MCP apps, there's a single
- 7:35
string. Uh so, if you like want to track
- 7:37
multiple events over time, you kind of
- 7:39
have to append
- 7:41
uh multiple things to the string, but
- 7:43
this is this is this is an example from
- 7:46
the MCP apps uh documentation where
- 7:49
basically, you know, this is a shopping
- 7:51
cart application and they add the total
- 7:53
cost and all the items that are on the
- 7:56
shopping cart so the user can ask for
- 7:59
uh you know, "Tell me information about
- 8:01
the items that are in my shopping cart."
- 8:03
>> [snorts]
- 8:05
>> So, these
- 8:07
two things give you an app that like the
- 8:10
model could kind of see what's going on.
- 8:13
Um but it doesn't necessarily make a
- 8:15
really good MCP app yet
- 8:19
uh because it gives you it gives you
- 8:20
some UI that looks like what you want,
- 8:23
but
- 8:25
the thing that I usually do, I don't
- 8:27
give I don't give the I don't I don't
- 8:29
give Claude
- 8:31
uh my easy problems to solve. I give
- 8:33
Claude my really hard problems to solve,
- 8:36
right? Like if I just wanted to do one
- 8:37
search, I would go to the web and do one
- 8:40
search. I want to do a whole bunch of
- 8:41
searches. Uh
- 8:43
this is this is out of date cuz there's
- 8:45
no Sonic 5 here yet, but uh this is
- 8:49
screenshot from 2 days ago.
- 8:51
Uh but yeah, so that So, like this job
- 8:53
search is "Hey, I'm looking for this I'm
- 8:55
looking for this title.
- 8:57
I'm willing to relocate, so I want to
- 8:59
search across a whole bunch of different
- 9:01
cities.
- 9:02
Um you know, I'm looking for the highest
- 9:04
paying option, so I want you to just
- 9:05
cherry-pick a few out of there. There's
- 9:07
some industries that I absolutely don't
- 9:09
want to work in."
- 9:11
Text-based MCP
- 9:14
does really well. Like
- 9:15
we all see this, right? Claude will do
- 9:18
10 different searches, 15 different
- 9:20
searches. It'll filter, it'll pull out
- 9:23
all the individual pieces,
- 9:25
uh and then it gives you a nice table at
- 9:26
the bottom, uh which is which is super
- 9:29
nice.
- 9:30
>> [snorts]
- 9:30
>> But with the MCP app that we were just
- 9:32
discussing, you know, we we said, "Hey,
- 9:36
uh you know, my results are going to be
- 9:38
displayed in this UI. Uh
- 9:43
We know Claude Claude will call it once
- 9:46
and then it'll be like, "Oh, I guess the
- 9:47
results are already displayed. I'm not
- 9:48
going to like do a deep dive, right?"
- 9:50
Like it doesn't It's you as a user are
- 9:53
not going to want 10 different carousels
- 9:56
and Claude
- 9:58
will also notice that like it's already
- 9:59
been displaying some stuff and it won't
- 10:02
call to show 10 different carousels.
- 10:05
Um
- 10:06
and so what you really want to do, and
- 10:07
this is rule three, this like supersedes
- 10:10
all the other rules,
- 10:12
uh which is basically you want to
- 10:13
separate your data processing from your
- 10:15
UI rendering.
- 10:18
And this particular wording I stole uh
- 10:22
from OpenAI uh
- 10:24
>> [snorts]
- 10:24
>> OpenAI
- 10:26
uh
- 10:27
Apps SDK documentation. There's a couple
- 10:29
places where they say this.
- 10:32
Um but basically they want to You want
- 10:34
to separate your data processing from
- 10:36
your UI rendering. So the job search
- 10:41
that we had
- 10:42
we're just going to have that be a
- 10:43
standard text-based MCP application.
- 10:47
Claude can call that as many times as it
- 10:49
wants. And then we have a render tool
- 10:52
that either
- 10:53
you can pass
- 10:55
you can have the model basically pass
- 10:57
all the data that it wants to render in
- 10:59
or you can do a reference uh to it. So
- 11:02
in our particular case
- 11:04
uh you know, we had search jobs. Now we
- 11:07
have a search jobs that doesn't return
- 11:09
any UI and we have a render jobs widget
- 11:12
that takes a list of IDs.
- 11:14
Um and so that list of IDs
- 11:17
uh
- 11:18
can be, you know, Claude can do a
- 11:19
search. It can get
- 11:22
a hundred different jobs that it cares
- 11:23
about. It can filter those. It can find
- 11:26
five that it cares about and it can show
- 11:28
those five to the user.
- 11:30
Um now as you make these like render
- 11:34
uh as you make these render calls, you
- 11:36
need to update the descriptions to say
- 11:38
like where did they get the data, what
- 11:40
format the data should be.
- 11:42
But it's a fairly easy fairly easy
- 11:45
mechanism to update your tool
- 11:46
description to say you need to always
- 11:48
call one of these three tools first in
- 11:51
order to get
- 11:53
the data that you're going to be using
- 11:54
for rendering.
- 11:56
So, search jobs, this is a pretty good
- 11:59
um
- 12:00
It's a pretty good example
- 12:03
of of how we where we want to split data
- 12:06
from rendering. I think there's a ton of
- 12:08
other like
- 12:10
in most industries, you can kind of come
- 12:12
up with like, "Hey, where do I want to
- 12:15
you know, where do I want to split?"
- 12:16
Like I want to be I want
- 12:18
the model to be able to explore this
- 12:20
data and then I want it to turn around
- 12:22
and choose to be able to render it.
- 12:24
Right? Like there's there's examples of
- 12:28
you know
- 12:29
e-commerce, right? Like
- 12:32
a bunch of e-commerce options.
- 12:34
If you've got a map, you know, maybe you
- 12:36
want to come up with like five different
- 12:38
addresses and then you pass in
- 12:40
addresses.
- 12:41
The other thing that you can do is you
- 12:42
can let the model be a lot more
- 12:44
creative. Like one of the things that
- 12:46
we've seen in some of this text-based
- 12:49
stuff is that
- 12:52
you know, the model will say like, "This
- 12:53
is a
- 12:54
>> [clears throat]
- 12:54
>> This is a reason why I picked this one."
- 12:56
Or "This is a reason why this is really
- 12:58
good."
- 12:59
And so, you know, potentially we can add
- 13:01
something to the render jobs widget
- 13:03
where you say give us an ID and a reason
- 13:07
why you think that this is a good fit.
- 13:09
Or give us an ID and highlight a section
- 13:13
of the job description that is really
- 13:14
good. Um, so you can get really creative
- 13:18
with your render tools to be able to
- 13:20
have to be able to give some extra
- 13:23
character that the model can inject
- 13:26
into those so that
- 13:29
so that you've got a much better
- 13:30
experience for the user.
- 13:34
So [snorts] the key takeaways
- 13:36
right
- 13:37
when building MCP apps, you want to
- 13:39
focus on the data before you focus on
- 13:42
the UI which sounds sounds opposite of
- 13:44
you know how we were thinking about it
- 13:46
of like hey there's all these there's
- 13:47
these MCP apps like it's how I put UI
- 13:50
into chat GPT.
- 13:52
I think if you want to really have a
- 13:55
good MCP apps experience, you need to
- 13:58
you need to look at and see what data do
- 14:01
I want to what data do I want to give to
- 14:02
the model, what data do I want it to be
- 14:04
able to do and then
- 14:07
rendering is a side effect of of that or
- 14:10
it's a result of the the model exploring
- 14:14
the data.
- 14:15
And then I think small composable tools,
- 14:18
right? So
- 14:20
uh
- 14:22
basically, you know
- 14:24
maybe there's like two or three
- 14:25
different ways you can search for jobs.
- 14:27
We could build two or three different
- 14:28
search tools and then there's one render
- 14:30
tool or maybe there's like maybe there's
- 14:33
two render tools there's one that's
- 14:35
render a list of jobs, one that's, you
- 14:37
know, highlight one particular job. So
- 14:40
you could build a bunch of much smaller
- 14:41
tools. The descriptions can be fairly
- 14:44
simple so you don't overload the model,
- 14:46
but it gives the model flexibility about
- 14:49
how it wants to explore the data and how
- 14:51
it wants to render the tools.
- 14:54
So that's
- 14:56
basically basically my talk. I'm a few
- 14:59
minutes few minutes fast.
- 15:01
But I don't have a booth that I'm going
- 15:04
to hang out in, but I'll I'll hang out
- 15:05
in the hall if anyone has any questions.
- 15:08
And I am either my last name or my first
- 15:12
initial last name on most social media
- 15:14
platforms.
- 15:15
Thank you.
- 15:32
>> [music]
- 15:33
>> Mhm.