Skip to content

Commit 293a6b7

Browse files
AVthekingAnkit VarshneyAnkit Varshneyvercel-ai-sdk[bot]lgrammel
authored
feat(mcp): tool title (#9200)
## Background MCP supports for human readable title for the tool which is currently not supported by ai sdk ToolUIPart. ## Summary Add a way to be able to use the mcp tool title in ui. ## Manual Verification Wrote an next js eg with mcp server and tools with title and using that title in the ui as display name for the tool. ## Checklist - [x] Tests have been added / updated (for bug fixes / features) - [ ] Documentation has been added / updated (for bug fixes / features) - [x] A _patch_ changeset for relevant packages has been added (for bug fixes / features - run `pnpm changeset` in the project root) - [x] Formatting issues have been fixed (run `pnpm prettier-fix` in the project root) - [x] I have reviewed this pull request (self-review) ## Related Issues Fixes #9163 --------- Co-authored-by: Ankit Varshney <[email protected]> Co-authored-by: Ankit Varshney <[email protected]> Co-authored-by: vercel-ai-sdk[bot] <225926702+vercel-ai-sdk[bot]@users.noreply.github.com> Co-authored-by: Lars Grammel <[email protected]> Co-authored-by: Gregor Martynus <[email protected]> Co-authored-by: Aayush Kapoor <[email protected]>
1 parent a9da06b commit 293a6b7

29 files changed

+804
-69
lines changed

.changeset/large-waves-glow.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@ai-sdk/provider-utils': patch
3+
'ai': patch
4+
---
5+
6+
Added a title to the tools

examples/next-openai/app/mcp/chat/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { convertToModelMessages, stepCountIs, streamText } from 'ai';
44
import { experimental_createMCPClient } from '@ai-sdk/mcp';
55

66
export async function POST(req: Request) {
7-
const url = new URL('http://localhost:3000/mcp/server');
7+
const requestUrl = new URL(req.url);
8+
const url = new URL('/mcp/server', requestUrl.origin);
89
const transport = new StreamableHTTPClientTransport(url);
910

1011
const [client, { messages }] = await Promise.all([

examples/next-openai/app/mcp/page.tsx

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,123 @@
22

33
import ChatInput from '@/components/chat-input';
44
import { useChat } from '@ai-sdk/react';
5-
import { DefaultChatTransport } from 'ai';
5+
import {
6+
DefaultChatTransport,
7+
isToolOrDynamicToolUIPart,
8+
getToolOrDynamicToolName,
9+
type DynamicToolUIPart,
10+
type ToolUIPart,
11+
} from 'ai';
612

713
export default function Chat() {
814
const { error, status, sendMessage, messages, regenerate, stop } = useChat({
915
transport: new DefaultChatTransport({ api: '/mcp/chat' }),
1016
});
1117

1218
return (
13-
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
19+
<div className="flex flex-col w-full max-w-2xl py-24 mx-auto stretch">
1420
{messages.map(m => (
15-
<div key={m.id} className="whitespace-pre-wrap">
16-
{m.role === 'user' ? 'User: ' : 'AI: '}
17-
{m.parts
18-
.map(part => (part.type === 'text' ? part.text : ''))
19-
.join('')}
21+
<div key={m.id} className="mb-4">
22+
<div className="font-semibold mb-2">
23+
{m.role === 'user' ? '👤 User' : '🤖 Assistant'}
24+
</div>
25+
<div className="pl-4 space-y-2">
26+
{m.parts.map((part, index) => {
27+
// Handle text parts
28+
if (part.type === 'text') {
29+
return (
30+
<div key={index} className="whitespace-pre-wrap">
31+
{part.text}
32+
</div>
33+
);
34+
}
35+
36+
if (part.type === 'step-start') {
37+
return index > 0 ? (
38+
<div key={index} className="my-4">
39+
<hr className="border-gray-300" />
40+
</div>
41+
) : null;
42+
}
43+
44+
if (isToolOrDynamicToolUIPart(part)) {
45+
const toolPart = part as ToolUIPart<any> | DynamicToolUIPart;
46+
const toolName = getToolOrDynamicToolName(toolPart);
47+
48+
// Display tool title if available, fallback to tool name
49+
const displayName = toolPart.title || toolName;
50+
51+
return (
52+
<div
53+
key={index}
54+
className="p-4 border border-gray-300 rounded-lg bg-gray-50"
55+
>
56+
<div className="flex items-center gap-2 mb-2">
57+
<span className="text-xl">🔧</span>
58+
<div>
59+
<div className="font-semibold text-sm">
60+
{displayName}
61+
</div>
62+
{toolPart.title && (
63+
<div className="text-xs text-gray-500">
64+
Tool ID: {toolName}
65+
</div>
66+
)}
67+
</div>
68+
</div>
69+
70+
{toolPart.state === 'input-streaming' && (
71+
<div className="text-sm text-gray-600">
72+
<div className="mb-1">Streaming input...</div>
73+
{toolPart.input && (
74+
<pre className="text-xs bg-white p-2 rounded overflow-x-auto">
75+
{JSON.stringify(toolPart.input, null, 2)}
76+
</pre>
77+
)}
78+
</div>
79+
)}
80+
81+
{toolPart.state === 'input-available' && (
82+
<div className="text-sm text-gray-600">
83+
<div className="mb-1">Input:</div>
84+
<pre className="text-xs bg-white p-2 rounded overflow-x-auto">
85+
{JSON.stringify(toolPart.input, null, 2)}
86+
</pre>
87+
</div>
88+
)}
89+
90+
{toolPart.state === 'output-available' && (
91+
<div className="text-sm">
92+
<div className="mb-1 text-gray-600">Output:</div>
93+
<div className="bg-white p-2 rounded text-xs">
94+
{typeof toolPart.output === 'string'
95+
? toolPart.output
96+
: JSON.stringify(toolPart.output, null, 2)}
97+
</div>
98+
</div>
99+
)}
100+
101+
{toolPart.state === 'output-error' && (
102+
<div className="text-sm text-red-600">
103+
Error: {toolPart.errorText}
104+
</div>
105+
)}
106+
</div>
107+
);
108+
}
109+
110+
return null;
111+
})}
112+
</div>
20113
</div>
21114
))}
22115

23116
{(status === 'submitted' || status === 'streaming') && (
24-
<div className="mt-4 text-gray-500">
117+
<div className="mt-4 text-gray-500 text-sm">
25118
{status === 'submitted' && <div>Loading...</div>}
26119
<button
27120
type="button"
28-
className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md"
121+
className="px-4 py-2 mt-4 text-sm text-blue-500 border border-blue-500 rounded-md hover:bg-blue-50"
29122
onClick={stop}
30123
>
31124
Stop
@@ -35,10 +128,10 @@ export default function Chat() {
35128

36129
{error && (
37130
<div className="mt-4">
38-
<div className="text-red-500">An error occurred.</div>
131+
<div className="text-red-500 text-sm">An error occurred.</div>
39132
<button
40133
type="button"
41-
className="px-4 py-2 mt-4 text-blue-500 border border-blue-500 rounded-md"
134+
className="px-4 py-2 mt-4 text-sm text-blue-500 border border-blue-500 rounded-md hover:bg-blue-50"
42135
onClick={() => regenerate()}
43136
>
44137
Retry

examples/next-openai/util/mcp/handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const mcpApiHandler = initializeMcpApiHandler({
1414
{
1515
values: z.array(z.number()),
1616
},
17+
{ title: '🔢 Calculator' },
1718
async ({ values }: { values: number[] }) => ({
1819
content: [
1920
{

packages/ai/src/generate-text/__snapshots__/generate-text.test.ts.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result > callb
7878
},
7979
"providerExecuted": undefined,
8080
"providerMetadata": undefined,
81+
"title": "Tool One",
8182
"toolCallId": "call-1",
8283
"toolName": "tool1",
8384
"type": "tool-call",
@@ -244,6 +245,7 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result > callb
244245
},
245246
"providerExecuted": undefined,
246247
"providerMetadata": undefined,
248+
"title": "Tool One",
247249
"toolCallId": "call-1",
248250
"toolName": "tool1",
249251
"type": "tool-call",
@@ -435,6 +437,7 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result > resul
435437
},
436438
"providerExecuted": undefined,
437439
"providerMetadata": undefined,
440+
"title": "Tool One",
438441
"toolCallId": "call-1",
439442
"toolName": "tool1",
440443
"type": "tool-call",
@@ -582,6 +585,7 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result with pr
582585
},
583586
"providerExecuted": undefined,
584587
"providerMetadata": undefined,
588+
"title": undefined,
585589
"toolCallId": "call-1",
586590
"toolName": "tool1",
587591
"type": "tool-call",
@@ -773,6 +777,7 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result with pr
773777
},
774778
"providerExecuted": undefined,
775779
"providerMetadata": undefined,
780+
"title": undefined,
776781
"toolCallId": "call-1",
777782
"toolName": "tool1",
778783
"type": "tool-call",

packages/ai/src/generate-text/__snapshots__/stream-text.test.ts.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ exports[`streamText > result.fullStream > should send delayed asynchronous tool
253253
},
254254
"providerExecuted": undefined,
255255
"providerMetadata": undefined,
256+
"title": "Tool 1",
256257
"toolCallId": "call-1",
257258
"toolName": "tool1",
258259
"type": "tool-call",
@@ -319,6 +320,7 @@ exports[`streamText > result.fullStream > should send tool calls 1`] = `
319320
"signature": "sig",
320321
},
321322
},
323+
"title": "Tool 1",
322324
"toolCallId": "call-1",
323325
"toolName": "tool1",
324326
"type": "tool-call",
@@ -371,6 +373,7 @@ exports[`streamText > result.fullStream > should send tool results 1`] = `
371373
},
372374
"providerExecuted": undefined,
373375
"providerMetadata": undefined,
376+
"title": "Tool 1",
374377
"toolCallId": "call-1",
375378
"toolName": "tool1",
376379
"type": "tool-call",
@@ -969,6 +972,7 @@ exports[`streamText > tools with custom schema > should send tool calls 1`] = `
969972
},
970973
"providerExecuted": undefined,
971974
"providerMetadata": undefined,
975+
"title": "Tool 1",
972976
"toolCallId": "call-1",
973977
"toolName": "tool1",
974978
"type": "tool-call",

0 commit comments

Comments
 (0)