Hey everyone, I'm Dustin Mahalik.
I work at Indeed. We're the number one job site in the world. And I have to apologize for my voice. I'm recovering from a cold that I had last week. Yeah, so at Indeed we build Job Search and I also work on a team that does AI platform. And I work on AI guardrails and gateways and compliance stuff. Occasionally my team gets cool projects to work on because we have relationships with the vendors. MCP apps is one of those and MCP connectors. So this is practical MCP apps. They gave a great introduction to MCP apps. This is lessons from the trenches which is what did we learn in building MCP and MCP apps for Claude, ChatGPT, and our own internal CareerScout
which is our agent that we have for job seekers. So this is the chat based interface. So a lot of my examples are going to be job search. I have a lot of screenshots of job searches. So this is a job search which is basically, I'm looking for a barista in Austin and this is a text based response. And this works pretty well. As we discussed, there's no branding here. There's no Indeed branding. Claude decided to say these are some jobs in Austin. These are some jobs in some suburbs of Austin. That's cool. That's probably good for the user. There may be some limitations of what we can do for branding or how we can control things.
And you'd actually be surprised unless you've tried to do this yourself that it's really hard to get Claude or ChatGPT to link to things because they don't want you to leave their environment. It makes complete sense. But if you get back here's five jobs that are somewhere on the internet without any links, that's a terrible user experience. So it took us a ridiculous number of hours in evals to make sure that Claude would consistently link to things. So with MCP apps and with apps SDK, we can control that. We can decide that we've got an apply button. We can decide what stuff is important that we want to highlight at the top.
We can provide a link to view details so that when you click it, you get a pop-up, a modal that has all the job details so you don't have to leave the environment. So it's a win-win for both companies. So MCP apps is really good. But if you're thinking about, hey, I want to build an MCP app or I want to take my website, I want to put it in ChatGPT or Claude, it's not quite as easy as dragging and dropping into a chat interface. You really want to think about how you are representing the data, how you're making it available to the user. So if you do a very naive thing, which is you still call your existing APIs for loading data,
then it basically becomes a black box to the model, right? So you say, hey, I want to do something. The model says, cool, I'll call a tool. The tool says, okay, I'm going to show some stuff, but the model has no idea what you're displaying. So there's all these follow-up questions, like tell me about the first result or please rank this list of companies. The model has no idea what data is being displayed. So the very first rule for me building MCP apps, anything that you show to the user also needs to be provided as data to the model. I think this makes sense, but I've definitely seen some MCP apps where they just use it to inject some HTML on the page
and then call some APIs, and that just makes a big black box for the model. So this is really easy to do using MCP spec and the MCP app spec. This structured content, this is what you would already be returning if you were doing just text-based MCP. And then this resource URI, that points to where the HTML is. You need to return both of these and you need to keep them in sync, right? If you add something new to the API, you make sure you add that same data back. So kind of the next step that once you do that, you'll find is that now you've provided data to the model and you provided this black box that it has no idea about, it's still going to try and describe the output.
It's still going to try and take the output and describe it as it normally would. So you end up with here's your display and then here's the model doing basically the same thing that it would normally do. So what you need to do is you need to update your description in order to tell it that you're going to be displaying stuff in your MCP app. That way you get this nice, here's a list of, here's a little summary of things and the results are shown above rather than it trying to do a whole text-based display. You'll end up with a little bit of a battle between what gets displayed in UI and what gets displayed by the model. You can try and steer that with descriptions.
So even something as simple as results were automatically displayed to the user as UI components at the top of your description, your tool description, that covers quite a bit of the cases. So that's one of the next things that you're going to want to do once you're providing both data and API access. The next thing is there's these interactable pieces, right? There's the apply button, there's a view details button, which pops up a big job description. This is the same case where, as you interact with those, the model's not going to know necessarily what you're looking at. So you click view details, you get a big modal that's here's everything about the job.
There's a whole bunch of questions that the user could ask. Write a cover letter for this job. Summarize this job description. It has no idea because you've loaded that data in either via API or you loaded it in dynamically. You've returned 10 jobs and user clicks on one. The model has no idea which one you clicked on. So any information about user interactions, you also need to provide to the model. And once again, MCP App Spec has a pretty easy way to handle it. There's this update model context method, which lets you pass in a string. So for MCP Apps, there's a single string. So if you want to track multiple events over time,
you have to append multiple things to the string. But this is an example from the MCP Apps documentation, where basically this is a shopping cart application, and they add the total cost and all the items that are on the shopping cart. So the user can ask for information about the items that are in their shopping cart. So these two things give you an app that the model can see what's going on. But it doesn't necessarily make a really good MCP App yet. Because it gives you some UI that looks like what you want. But the thing that I usually do, I don't give Claude my easy problems to solve. I give Claude my really hard problems to solve, right?
If I just wanted to do one search, I would go to the web and do one search.
I want to do a whole bunch of searches. This is out of date because there's no Sonnet 5 here yet. But this is a screenshot from two days ago. But yeah, so this job search is, hey, I'm looking for this title. I'm willing to relocate. So I want to search across a whole bunch of different cities. I'm looking for the highest paying options. So I want you to cherry pick a few out of there. There's some industries that I absolutely don't want to work in. Text-based MCP does really well. We all see this, right? Claude will do 10 different searches, 15 different searches. It'll filter, it'll pull out all the individual pieces. And then it gives you a nice table at the bottom,
which is super nice. But with the MCP app that we were just discussing, you know, we said, hey, my results are going to be displayed in this UI. Claude will call it once, and then it'll be like, oh, I guess the results are already displayed. I'm not going to do a deep dive, right? You as a user are not going to want 10 different carousels. And Claude will also notice that it's already been displaying some stuff, and it won't call to show 10 different carousels. And so what you really want to do, and this is rule three, this supersedes all the other rules, which is basically you want to separate your data processing from your UI rendering.
And this particular wording I stole from OpenAI's apps SDK documentation, there's a couple places where they say this. But basically, you want to separate your data processing from your UI rendering. So the job search that we had, we're just going to have that be a standard text-based MCP application. Claude can call that as many times as it wants. And then we have a render tool that either you could pass, you could have the model basically pass all the data that it wants to render in, or you can do a reference to it.
So in our particular case, you know, we had search jobs. Now we have a search jobs that doesn't return in the UI. And we have a render jobs widget that takes a list of IDs. And so that list of IDs can be Claude can do a search, it can get 100 different jobs that it cares about. It can filter those, it could find five that it cares about, and it can show those five to the user. Now, as you make these render calls, you need to update the descriptions to say, where they get the data, what format the data should be. But it's a fairly easy mechanism to update your tool description to say, you need to always call one of these three tools first
in order to get the data that you're going to be using for rendering. So search jobs, this is a pretty good example of where we want to split data from rendering. I think there's a ton of other examples. In most industries, you can come up with, where do I want to split? I want the model to be able to explore this data, and then I want it to turn around and choose to render it, right? There's examples of e-commerce, right? A bunch of e-commerce options. If you've got a map, maybe you want to come up with five different addresses, and then you pass in addresses. The other thing that you can do is you can let the model be a lot more creative.
One of the things that we've seen in some of this text-based stuff is that the model will say, this is a reason why I picked this one, or this is a reason why this is really good. And so potentially we can add something to the render jobs widget where you say, give us an ID and a reason why you think that this is a good fit. Or give us an ID and highlight a section of the job description that is really good. So you can get really creative with your render tools to be able to have the model inject some extra character into those so that you've got a much better experience for the user. So the key takeaways, right, when building MCP apps, you want to
focus on the data before you focus on the UI, which sounds opposite of how we were thinking about it, of hey, there's all these MCP apps. It's how I put UI into ChatGPT. I think if you want to really have a good MCP apps experience, you need to look and see what data do I want to give to the model, what data do I want it to be able to do, and then rendering is a side effect of that, or it's a result of the model exploring the data. And then I think small composable tools, right, so basically, maybe there's two or three different ways you can search for jobs. We can build two or three different search tools, and then there's one render tool.
Or maybe there's two render tools. There's one that's render a list of jobs, one that's highlight one particular job. So you can build a bunch of much smaller tools. The descriptions can be fairly simple, so you don't overload the model, but it gives the model flexibility about how it wants to explore the data and how it wants to render the tools. So that's basically my talk. I'm a few minutes fast. But I don't have a booth that I'm going to hang out in, but I'll hang out in the hall if anyone has any questions. And I am either my last name or my first initial last name on most social media platforms. Thank you. highlight one particular job.
So you can build a bunch of much smaller tools. The descriptions can be fairly simple, so you don't overload the model, but it gives the model flexibility about how it wants to explore the data and how it wants to render the tools. So that's basically my talk. I'm a few minutes fast. But I don't have a booth that I'm going to hang out in, but I'll hang out in the hall if anyone has any questions. And I am either my last name or my first initial last name on most social media platforms. Thank you. . . . . . . .
!