Skip to content

Commit 899d152

Browse files
authored
feat: support ui action result types (#6)
BREAKING CHANGE
1 parent 2ce7ba6 commit 899d152

File tree

18 files changed

+321
-92
lines changed

18 files changed

+321
-92
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ yarn add @mcp-ui/server @mcp-ui/client
115115
return (
116116
<HtmlResource
117117
resource={mcpResource.resource}
118-
onUiAction={(tool, params) => {
119-
console.log('Action:', tool, params);
118+
onUiAction={(result) => {
119+
console.log('Action:', result);
120120
return { status: 'ok' };
121121
}}
122122
/>

docs/src/guide/client/html-resource.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@ import type { Resource } from '@modelcontextprotocol/sdk/types';
99

1010
export interface HtmlResourceProps {
1111
resource: Partial<Resource>;
12-
onUiAction?: (
13-
tool: string,
14-
params: Record<string, unknown>,
15-
) => Promise<any>;
12+
onUiAction?: (result: UiActionResult) => Promise<any>;
1613
style?: React.CSSProperties;
1714
}
1815
```
1916

2017
- **`resource`**: The resource object from an `HtmlResourceBlock`. It should include `uri`, `mimeType`, and either `text` or `blob`.
21-
- **`onUiAction`**: An optional callback that fires when the iframe content (for `ui://` resources) posts a message to your app. The message should look like `{ tool: string, params: Record<string, unknown> }`.
18+
- **`onUiAction`**: An optional callback that fires when the iframe content (for `ui://` resources) posts a message to your app. The message should look like:
19+
```typescript
20+
{ type: 'tool', payload: { toolName: string, params: Record<string, unknown> } } |
21+
{ type: 'intent', payload: { intent: string, params: Record<string, unknown> } } |
22+
{ type: 'prompt', payload: { prompt: string } } |
23+
{ type: 'notification', payload: { message: string } } |
24+
{ type: 'link', payload: { url: string } } |
25+
```
26+
If you don't provide a callback for a specific type, the default handler will be used.
2227
- **`style`** (optional): Custom styles for the iframe.
2328

2429
## How It Works

docs/src/guide/client/usage-examples.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pnpm add @mcp-ui/client react @modelcontextprotocol/sdk
1414

1515
```tsx
1616
import React, { useState } from 'react';
17-
import { HtmlResource } from '@mcp-ui/client';
17+
import { HtmlResource, UiActionResult } from '@mcp-ui/client';
1818

1919
// Simulate fetching an MCP resource block
2020
const fetchMcpResource = async (id: string): Promise<HtmlResource> => {
@@ -24,12 +24,12 @@ const fetchMcpResource = async (id: string): Promise<HtmlResource> => {
2424
resource: {
2525
uri: 'ui://example/direct-html',
2626
mimeType: 'text/html',
27-
text: "<h1>Direct HTML via Text</h1><p>Content loaded directly.</p><button onclick=\"window.parent.postMessage({tool: 'uiInteraction', params: { action: 'directClick', value: Date.now() }}, '*')\">Click Me (Direct)</button>",
27+
text: "<h1>Direct HTML via Text</h1><p>Content loaded directly.</p><button onclick=\"window.parent.postMessage({ type: 'tool', payload: { toolName: 'uiInteraction', params: { action: 'directClick', value: Date.now() } } }, '*')\">Click Me (Direct)</button>",
2828
},
2929
};
3030
} else if (id === 'blob') {
3131
const html =
32-
"<h1>HTML from Blob</h1><p>Content was Base64 encoded.</p><button onclick=\"window.parent.postMessage({tool: 'uiInteraction', params: { action: 'blobClick', value: 'test' }}, '*')\">Click Me (Blob)</button>";
32+
"<h1>HTML from Blob</h1><p>Content was Base64 encoded.</p><button onclick=\"window.parent.postMessage({ type: 'tool', payload: { toolName: 'uiInteraction', params: { action: 'blobClick', value: 'test' } } }, '*')\">Click Me (Blob)</button>";
3333
return {
3434
type: 'resource',
3535
resource: {
@@ -72,12 +72,23 @@ const App: React.FC = () => {
7272
setLoading(false);
7373
};
7474

75-
const handleGenericMcpAction = async (
76-
tool: string,
77-
params: Record<string, unknown>,
78-
) => {
79-
console.log(`Action received in host app - Tool: ${tool}, Params:`, params);
80-
setLastAction({ tool, params });
75+
const handleGenericMcpAction = async (result: UiActionResult) => {
76+
if (result.type === 'tool') {
77+
console.log(`Action received in host app - Tool: ${result.payload.toolName}, Params:`, result.payload.params);
78+
setLastAction({ tool: result.payload.toolName, params: result.payload.params });
79+
} else if (result.type === 'prompt') {
80+
console.log(`Prompt received in host app:`, result.payload.prompt);
81+
setLastAction({ prompt: result.payload.prompt });
82+
} else if (result.type === 'link') {
83+
console.log(`Link received in host app:`, result.payload.url);
84+
setLastAction({ url: result.payload.url });
85+
} else if (result.type === 'intent') {
86+
console.log(`Intent received in host app:`, result.payload.intent);
87+
setLastAction({ intent: result.payload.intent });
88+
} else if (result.type === 'notification') {
89+
console.log(`Notification received in host app:`, result.payload.message);
90+
setLastAction({ message: result.payload.message });
91+
}
8192
return {
8293
status: 'Action handled by host application',
8394
receivedParams: params,

docs/src/guide/getting-started.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,18 @@ function App() {
101101
setMcpData(fakeMcpResponse);
102102
}, []);
103103
104-
const handleResourceAction = async (
105-
tool: string,
106-
params: Record<string, unknown>,
107-
) => {
108-
console.log(`Action from resource (tool: ${tool}):`, params);
104+
const handleResourceAction = async (result: UiActionResult) => {
105+
if (result.type === 'tool') {
106+
console.log(`Action from resource (tool: ${result.payload.toolName}):`, result.payload.params);
107+
} else if (result.type === 'prompt') {
108+
console.log(`Prompt from resource:`, result.payload.prompt);
109+
} else if (result.type === 'link') {
110+
console.log(`Link from resource:`, result.payload.url);
111+
} else if (result.type === 'intent') {
112+
console.log(`Intent from resource:`, result.payload.intent);
113+
} else if (result.type === 'notification') {
114+
console.log(`Notification from resource:`, result.payload.message);
115+
}
109116
// Add your handling logic (e.g., initiate followup tool call)
110117
return { status: 'Action received by client' };
111118
};

docs/src/guide/protocol-details.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ For `ui://` resources, you can use `window.parent.postMessage` to send data or a
100100
const data = { action: 'formData', value: 'someValue' };
101101
// IMPORTANT: Always specify the targetOrigin for security!
102102
// Use '*' only if the parent origin is unknown or variable and security implications are understood.
103-
window.parent.postMessage({ tool: 'myCustomTool', params: data }, '*');
103+
window.parent.postMessage(
104+
{ type: 'tool', payload: { toolName: 'myCustomTool', params: data } },
105+
'*',
106+
);
104107
}
105108
</script>
106109
```

examples/server/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ yarn add @mcp-ui/server @mcp-ui/client
115115
return (
116116
<HtmlResource
117117
resource={mcpResource.resource}
118-
onUiAction={(tool, params) => {
119-
console.log('Action:', tool, params);
118+
onUiAction={(result) => {
119+
console.log('Action:', result);
120120
return { status: 'ok' };
121121
}}
122122
/>

examples/server/app/graph/graph.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -402,11 +402,14 @@ const CustomAvatarXAxisTick = (props: {
402402
// @ts-expect-error - window is not typed correctly
403403
if (memberInfo && window.parent) {
404404
const message = {
405-
tool: 'show_user_status',
406-
params: {
407-
id: memberInfo.id,
408-
name: memberInfo.name,
409-
avatarUrl: memberInfo.avatarUrl,
405+
type: 'tool',
406+
payload: {
407+
toolName: 'show_user_status',
408+
params: {
409+
id: memberInfo.id,
410+
name: memberInfo.name,
411+
avatarUrl: memberInfo.avatarUrl,
412+
},
410413
},
411414
};
412415
// @ts-expect-error - window is not typed correctly

examples/server/app/user/user.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ export function User({ user }: { user: UserInfo }) {
3232
// @ts-expect-error - window is not typed correctly
3333
if (user.id && window.parent) {
3434
const message = {
35-
tool: 'nudge_team_member',
36-
params: {
37-
name: user.name,
35+
type: 'tool',
36+
payload: {
37+
toolName: 'nudge_team_member',
38+
params: {
39+
name: user.name,
40+
},
3841
},
3942
};
4043
// @ts-expect-error - window is not typed correctly

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"@testing-library/jest-dom": "^6.1.5",
2828
"@testing-library/react": "^14.1.2",
2929
"@types/jest": "^29.5.11",
30-
"@types/node": "^22.0.0",
30+
"@types/node": "^22.15.18",
3131
"@typescript-eslint/eslint-plugin": "^6.13.2",
3232
"@typescript-eslint/parser": "^6.13.2",
3333
"@vitejs/plugin-react-swc": "^3.9.0",

packages/client/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ yarn add @mcp-ui/server @mcp-ui/client
115115
return (
116116
<HtmlResource
117117
resource={mcpResource.resource}
118-
onUiAction={(tool, params) => {
119-
console.log('Action:', tool, params);
118+
onUiAction={(result) => {
119+
console.log('Action:', result);
120120
return { status: 'ok' };
121121
}}
122122
/>

0 commit comments

Comments
 (0)