MCP Apps: Give the Model Data, Give the User a UI — Dustin Mihalik, Indeed
频道: AI Engineer
视频: https://www.youtube.com/watch?v=lbaXnx0KLA8
原文语言: en
统计: 共 18 轮 · Dustin 18
[0:01] Dustin
[music]
(音乐)
[0:13] Dustin
Uh hey everyone, I'm Dustin Macholic. Uh I work at Indeed. Uh 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. Um Yeah, so at Indeed, we build uh job search. And uh I also work on a team that does AI platform. And I do like AI guardrails and and gateways and compliance stuff. Uh occasionally uh my team gets cool projects to work on because we have relationships with the vendors. Uh MCP apps is one of those. And um MCP connectors. So, this is a little bit of like practical MCP apps. They gave a great introduction uh to MCP apps. This is a little bit of a lessons from the trenches, uh which is a little bit of like what did we learn in building MCP and MCP apps for Claude chat chat GPT and our own internal uh uh career scout, which is our uh agent that we have for job seekers. So, this is the this is the chat-based interface. So, a lot of my examples are going to be job search. This will I have a lot of screenshots of job searches. Um So, this is this is a job search, which is basically uh you know, I'm looking for barista in Austin. And this is a text-based response. And this works pretty well. Uh as we kind of discussed, like there's no branding here. There's no Indeed branding. Uh you know, 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 uh of like what we can do for branding or how we can uh you know, how we can control things. And you'd be actually be surprised uh unless you've tried to do this yourself that um it's really hard to get Claude or ChatGPT to link to things cuz they don't want you to leave their environment. It makes complete sense, but uh if you get back, here's five jobs that are somewhere on the internet uh without any links, that's a terrible user experience. Uh so it's it took us a
大家好,我是 Dustin Mihalik,在 Indeed 工作,我们是全球第一的求职网站。先为我的嗓子道个歉,上周得了感冒,还没完全好。在 Indeed,我们做的是职位搜索。我同时也在一个做 AI 平台的团队里,负责 AI guardrails、网关和合规这类事情。因为我们和各家厂商有合作关系,我的团队偶尔能接到一些很酷的项目,MCP Apps 就是其中之一,还有 MCP connector。所以这场分享讲的是 MCP Apps 的实战。前面他们已经把 MCP Apps 介绍得很精彩了,我这场更像是来自一线的经验教训:我们在为 Claude(聊天端)、ChatGPT,以及我们自家的 Career Scout(我们给求职者用的 agent)构建 MCP 和 MCP Apps 的过程中,都学到了些什么。这就是那个聊天式界面。我的很多例子都会围绕职位搜索,会有大量职位搜索的截图。比如这就是一次职位搜索,大意就是「我想在奥斯汀找咖啡师的工作」。这是一个纯文本的回复,效果其实挺好。不过就像前面讨论过的,这里没有任何品牌露出,没有 Indeed 的品牌。Claude 自己决定这样说:这些是奥斯汀的职位,这些是奥斯汀周边一些郊区的职位。挺好的,对用户来说大概也不错。但在品牌露出上能做多少、或者说我们能掌控多少东西,可能就会有些限制。而且除非你亲自试过,否则你真的会很意外:想让 Claude 或 ChatGPT 附上链接非常难,因为它们不希望你离开它们的环境。这完全可以理解,但如果你拿到的回复是「这里有五个职位,就在互联网上的某个地方」,却一个链接都没有,那用户体验就太糟糕了。所以我们花了
[2:27] Dustin
ridiculous number of hours and evals to make sure that like Claude would consistently link to things. So with MCP apps and with uh apps SDK, uh we could kind of control that. Uh we can decide uh you know, 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 um so that when you click it, you get uh a a pop-up, a a modal that has all the job details so you don't have to leave the environment. So it's it's a win-win uh for for both for both uh for both companies. So uh 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, uh it's not quite just as easy as like dragging and dropping into a chat interface. Uh 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, uh which is you still call your existing APIs uh for loading data, um 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 what you're displaying." So, there's all these like 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.
多得离谱的工时、跑了无数轮 eval,才确保 Claude 能稳定地给出链接。而有了 MCP Apps 和 Apps SDK,这件事我们基本就能掌控了。我们可以决定放一个「申请」按钮,可以决定哪些信息重要、要突出显示在最上面,还可以提供一个「查看详情」链接,点开后会弹出一个弹窗(modal),里面是完整的职位详情,这样用户就不用离开当前环境。所以对双方公司来说,这是双赢。MCP Apps 确实很好,但如果你在想「我要做一个 MCP App」,或者「我想把我的网站搬进 ChatGPT 或 Claude」,那可不是往聊天界面里拖拽一下那么简单。你真的得好好想想:数据要怎么表示,要怎么把它提供给用户。如果你用一种很天真的做法,也就是加载数据时还是调你现有的 API,那它对模型来说基本就成了一个黑箱,对吧?你说「我想做某件事」,模型说「好,我来调一个 tool」,tool 说「好,我来展示点东西」,但模型完全不知道你展示的是什么。于是就会出现各种追问,比如「给我讲讲第一条结果」,或者「请给这份公司列表排个序」,而模型根本不知道界面上显示的是什么数据。
[4:07] Dustin
So, the very first rule uh for me uh 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 uh I've definitely seen some MCP apps where they just, you know, use it to inject some HTML on the page and then call some APIs, uh and that's that just makes a big black box for the model. So, this is really easy to do uh using MCP spec and the MCP app spec. Uh this structured content, this is what you would already be returning if you were doing just text-based MCP. Um and then this resource URI, that's 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 something add that same data uh back. So, kind of the next step um that uh that once you do that, uh 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 it's still going to try and take the output and describe it as it normally would. So, you end up with like, "Here's your display." and then here's the model doing basically the same thing that it would normally do. Um 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. Uh that way you get this like nice, you know, here's a list of you know, here's a little summary of things, and you know, the results are are are showed above the links, rather than it trying to like do a whole text-based display.
所以,我做 MCP Apps 的第一条规则是:凡是展示给用户看的东西,也都要作为数据提供给模型。这听起来理所当然,但我确实见过一些 MCP App,只是拿它往页面里注入一些 HTML,然后调几个 API,这对模型来说就是一个大黑箱。其实用 MCP 规范和 MCP Apps 规范,这件事很容易做到。这个 structuredContent,就是你只做纯文本 MCP 时本来就会返回的东西;然后这个 resource URI,指向 HTML 所在的位置。这两样你都得返回,而且要保持同步,对吧?如果你往 API 里加了新东西,一定要确保把同样的数据也加回去。做到这一步之后,你会发现下一个问题:现在你已经给了模型数据,同时又给了它一个它一无所知的黑箱,它还是会像平时一样,拿着输出再描述一遍。结果就是:这边是你的界面展示,然后这边模型又把平时会做的事基本照做了一遍。所以你需要做的,是更新你的 description,告诉模型你会在 MCP App 里展示这些内容。这样你就能得到比较理想的效果:模型只给一小段总结,结果和链接都已经展示在上方,而不是它自己再搞一整套纯文本展示。
[5:50] Dustin
Um you'll end up with this a little bit of a battle between like 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, uh that covers that covers quite a bit of the cases. Um So, that's one of the next things that you're going to want to do uh once you're providing both data and API access. Uh 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. Um this is the same case where, you know, 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 once again a whole bunch of questions that the user could ask. Uh write a cover letter for this job, summarize this job description. It has no idea because you've loaded that data in either in via API or you loaded it in dynamically, uh you know, hit you return 10 jobs and user clicks on one, the model has no idea which one you clicked on.
你最后会发现,UI 里显示什么、模型自己输出什么,这两者之间会有点「打架」。你可以试着用 description 去引导。哪怕只是在 description(也就是 tool description)的开头写一句很简单的话,比如「结果已作为 UI 组件自动展示给用户」,就能覆盖相当多的情况。所以,一旦你同时提供了数据和 API 访问,这就是你接下来要做的事情之一。再下一个问题是那些可交互的部分,对吧?有「申请」按钮,有「查看详情」按钮,点开会弹出一大段职位描述。这也是同样的情况:你和这些元素交互时,模型并不一定知道你在看什么。你点了「查看详情」,弹出一个大弹窗,里面是这个职位的所有信息。用户同样可能会问一大堆问题,比如「帮我给这个职位写封求职信」「总结一下这份职位描述」。模型完全不知道,因为这些数据要么是你通过 API 加载的,要么是动态加载进来的,比如你返回了 10 个职位,用户点了其中一个,模型根本不知道你点的是哪一个。
[7:11] Dustin
Uh so, any information about user interactions, you also need to provide to the model. And uh once again, MCP abstract has a pretty easy way to handle it. There's this update model context um method, which lets you pass in a string uh that is So, for MCP apps, there's a single string. Uh so, if you like want to track multiple events over time, you kind of have to append uh multiple things to the string, but this is this is this is an example from the MCP apps uh documentation where basically, you know, 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 uh you know, "Tell me information about the items that are in my shopping cart."
所以,任何关于用户交互的信息,你也都要提供给模型。同样,MCP Apps 规范里有个很简单的处理办法:有一个 update model context(更新模型上下文)方法,可以让你传入一个字符串——在 MCP Apps 里,就只有这么一个字符串。所以如果你想随着时间追踪多个事件,基本就得把多条内容追加到这个字符串里。这是 MCP Apps 文档里的一个例子:一个购物车应用,他们把总价和购物车里的所有商品都加了进去,这样用户就可以问「告诉我购物车里这些商品的信息」。
[8:03] Dustin
[snorts]
(吸鼻子)
[8:05] Dustin
So, these two things give you an app that like the model could kind of see what's going on. Um but it doesn't necessarily make a really good MCP app yet uh because it gives you it gives you some UI that looks like what you want, but the thing that I usually do, I don't give I don't give the I don't I don't give Claude uh my easy problems to solve. I give Claude my really hard problems to solve, right? Like 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. Uh this is this is out of date cuz there's no Sonic 5 here yet, but uh this is screenshot from 2 days ago. Uh but yeah, so that So, like this job search is "Hey, I'm looking for this I'm looking for this title. I'm willing to relocate, so I want to search across a whole bunch of different cities. Um you know, I'm looking for the highest paying option, so I want you to just 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. Like 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, uh and then it gives you a nice table at the bottom, uh which is which is super nice.
有了这两点,你就得到了一个模型大致能看清发生了什么的应用。但这还不一定算得上一个真正好的 MCP App,因为它只是给了你一些看起来符合预期的 UI。可我平时的用法是:我不会把简单的问题交给 Claude,我交给 Claude 的都是真正难的问题,对吧?如果我只想搜一次,我直接上网搜一次就好了。我要的是做一大堆搜索。这张截图有点过时了,上面还没有 Sonnet 5,不过它是两天前截的。所以,比如这个职位搜索是这样的:「我想找这个头衔的职位。我愿意搬家,所以想在一大堆不同的城市里搜。我在找薪水最高的机会,所以你帮我从里面挑几个出来就行。还有一些行业我是绝对不想去的。」纯文本 MCP 在这类任务上表现非常好,这个大家都见过,对吧?Claude 会做 10 次、15 次不同的搜索,会做过滤,会把各条零散的信息都提取出来,最后在底部给你一张漂亮的表格,这真的非常棒。
[9:30] Dustin
[snorts]
(吸鼻子)
[9:30] Dustin
But with the MCP app that we were just discussing, you know, we we said, "Hey, uh you know, my results are going to be displayed in this UI. Uh We know Claude Claude will call it once and then it'll be like, "Oh, I guess the results are already displayed. I'm not going to like do a deep dive, right?" Like it doesn't It's you as a user are not going to want 10 different carousels and Claude will also notice that like it's already been displaying some stuff and it won't call to show 10 different carousels. Um and so what you really want to do, and this is rule three, this like supersedes all the other rules, uh which is basically you want to separate your data processing from your UI rendering. And this particular wording I stole uh from OpenAI uh
但换成我们刚才讲的那种 MCP App,我们已经告诉模型「我的结果会展示在这个 UI 里」。我们知道,Claude 会调用一次,然后就会想:「哦,结果应该已经展示出来了,那我就不深挖了。」而且作为用户,你也不会想要 10 个不同的轮播卡片;Claude 也会注意到自己已经展示过一些东西,所以它不会去调用 10 次、展示 10 个不同的轮播。所以你真正该做的,也就是第三条规则,它的优先级高于前面所有规则:把数据处理和 UI 渲染分开。这句话的措辞是我从 OpenAI 那儿偷来的,
[10:24] Dustin
[snorts]
(吸鼻子)
[10:24] Dustin
OpenAI uh Apps SDK documentation. There's a couple places where they say this. Um but basically they want to 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 can pass you can have the model basically pass all the data that it wants to render in or you can do a reference uh to it. So in our particular case uh you know, we had search jobs. Now we have a search jobs that doesn't return any UI and we have a render jobs widget that takes a list of IDs. Um and so that list of IDs uh can be, you know, Claude can do a search. It can get a hundred different jobs that it cares about. It can filter those. It can find five that it cares about and it can show those five to the user. Um now as you make these like render uh as you make these render calls, you need to update the descriptions to say like where did they get the data, what format the data should be. But it's a fairly easy 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.
出自 OpenAI 的 Apps SDK 文档,里面有好几处都这么写。总之核心就是:你要把数据处理和 UI 渲染分开。所以我们原来那个职位搜索,就让它做成一个标准的纯文本 MCP 应用,Claude 想调多少次就调多少次。然后我们再做一个 render tool:你可以让模型把它想渲染的数据全部传进去,也可以只传一个引用。拿我们自己来说,原来有一个 search jobs,现在我们有一个不返回任何 UI 的 search jobs,外加一个接收 ID 列表的 render jobs widget。这样一来,Claude 可以先搜索,拿到上百个它关心的职位,再做过滤,从中找出它看中的五个,然后把这五个展示给用户。当然,在做这些 render 调用的时候,你需要更新 description,说明数据从哪来、应该是什么格式。不过这是个相当简单的机制:在 tool description 里写明「你必须总是先调用这三个 tool 之一,拿到要用来渲染的数据」就行。
[11:56] Dustin
So, search jobs, this is a pretty good um It's a pretty good example of of how we where we want to split data from rendering. I think there's a ton of other like in most industries, you can kind of come up with like, "Hey, where do I want to you know, where do I want to split?" Like I want to be I want the model to be able to explore this data and then I want it to turn around and choose to be able to render it. Right? Like there's there's examples of you know e-commerce, right? Like a bunch of e-commerce options. If you've got a map, you know, maybe you want to come up with like 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. Like one of the things that we've seen in some of this text-based stuff is that you know, the model will say like, "This is a
所以 search jobs 是一个很好的例子,说明我们想在什么地方把数据和渲染拆开。我觉得还有大量其他例子,在大多数行业里,你都能想清楚「我想在哪里拆分」:我希望模型先能探索这些数据,然后再回过头来,自己决定要渲染什么。对吧?比如电商,一大堆可选的商品。如果你做的是地图,也许你想让模型先找出五个不同的地址,然后再把这些地址传进来。另一件你可以做的事,是让模型发挥更多的创造力。比如我们在一些纯文本场景里看到过,模型会说:「这是……
[12:54] Dustin
[clears throat]
(清嗓子)
[12:54] Dustin
This is a reason why I picked this one." Or "This is a reason why this is really good." And so, you know, 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. Um, so you can get really creative with your render tools to be able to have to be able to give some extra character that the model can inject into those so that so that you've got a much better experience for the user. So [snorts] the key takeaways right when building MCP apps, you want to focus on the data before you focus on the UI which sounds sounds opposite of you know how we were thinking about it of like hey there's all these there's these MCP apps like it's how I put UI into chat GPT. I think if you want to really have a good MCP apps experience, you need to you need to look at and see what data do I want to 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 of that or it's a result of the the model exploring the data. And then I think small composable tools, right? So uh basically, you know maybe there's like two or three different ways you can search for jobs.
「这就是我选这一个的理由」,或者「这就是它特别好的原因」。所以我们也许可以给 render jobs widget 加点东西,比如要求「给我们一个 ID,再附上你认为它合适的理由」,或者「给我们一个 ID,并高亮职位描述里特别好的那一段」。所以你可以在 render tool 上发挥很多创意,让模型能往里面注入一些额外的「个性」,这样用户体验就会好得多。所以,做 MCP Apps 的关键要点是:先关注数据,再关注 UI。这听起来和我们原先的想法正好相反,我们原本会觉得,MCP Apps 嘛,就是我把 UI 放进 ChatGPT 的办法。但我认为,如果你真想做出好的 MCP Apps 体验,就得先看清楚:我想把哪些数据给模型、想让它能拿这些数据做什么;而渲染只是这件事的副产品,或者说是模型探索数据之后的结果。再就是小而可组合的 tool,对吧?比如说,搜索职位也许有两三种不同的方式,
[14:27] Dustin
We could build two or three different search tools and then there's one render tool or maybe there's like maybe there's two render tools there's one that's render a list of jobs, one that's, you know, highlight one particular job. So you could 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 basically my talk. I'm a few minutes few minutes fast. But I don't have a booth that I'm going to hang out in, but I'll 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.
那我们就可以做两三个不同的 search tool,再配一个 render tool;或者配两个 render tool,一个用来渲染职位列表,一个用来突出展示某个特定职位。这样你就能做出一堆小得多的 tool。每个 description 都可以写得相当简单,不会让模型负担过重,同时又给了模型灵活性,让它自己决定怎么探索数据、怎么渲染。我的分享基本就到这里,比预定时间提前了几分钟。我没有展位可以待,不过我会在走廊里转悠,有问题的话欢迎来找我。在大多数社交媒体平台上,我的账号要么是我的姓,要么是名字首字母加姓。谢谢大家。
[15:32] Dustin
[music]
(音乐)
[15:33] Dustin
Mhm.
嗯。