Skip to content

Commit 607c6ad

Browse files
authored
feat: add convenience function isUIResource to client SDK (#86)
1 parent 4925bfd commit 607c6ad

6 files changed

Lines changed: 116 additions & 7 deletions

File tree

docs/src/guide/client/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The `@mcp-ui/client` package helps you render UI resources sent from an MCP-enab
77
- **`<UIResourceRenderer />`**: The main component you'll use. It inspects the resource's `mimeType` and renders either `<HTMLResourceRenderer />` or `<RemoteDOMResourceRenderer />` internally.
88
- **`<HTMLResourceRenderer />`**: Internal component for HTML/URL resources
99
- **`<RemoteDOMResourceRenderer />`**: Internal component for remote DOM resources
10+
- **`isUIResource()`**: Utility function to check if content is a UI resource (replaces manual `content.type === 'resource' && content.resource.uri?.startsWith('ui://')` checks)
1011

1112
## Purpose
1213
- **Standardized UI**: mcp-ui's client guarantees full compatibility with the latest MCP UI standards.

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

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {
2121
UIActionResult,
2222
basicComponentLibrary,
2323
remoteTextDefinition,
24-
remoteButtonDefinition
24+
remoteButtonDefinition,
25+
isUIResource
2526
} from '@mcp-ui/client';
2627

2728
const remoteDomScript = `
@@ -83,7 +84,8 @@ import {
8384
UIActionResult,
8485
basicComponentLibrary,
8586
remoteTextDefinition,
86-
remoteButtonDefinition
87+
remoteButtonDefinition,
88+
isUIResource
8789
} from '@mcp-ui/client';
8890

8991
// Simulate fetching an MCP UI resource
@@ -230,6 +232,52 @@ export default App;
230232

231233
---
232234

235+
## Using the `isUIResource` Utility
236+
237+
Instead of manually checking if content is a UI resource, you can use the `isUIResource()` utility function:
238+
239+
```tsx
240+
import React from 'react';
241+
import { UIResourceRenderer, isUIResource } from '@mcp-ui/client';
242+
243+
function ResourceList({ mcpResponses }) {
244+
return (
245+
<div>
246+
{mcpResponses.map((response, index) => {
247+
// Use isUIResource instead of manual checking
248+
if (isUIResource(response)) {
249+
return (
250+
<div key={index}>
251+
<h3>UI Resource: {response.resource.uri}</h3>
252+
<UIResourceRenderer
253+
resource={response.resource}
254+
onUIAction={handleAction}
255+
/>
256+
</div>
257+
);
258+
}
259+
260+
// Handle other response types
261+
return (
262+
<div key={index}>
263+
<p>Non-UI content: {response.type}</p>
264+
</div>
265+
);
266+
})}
267+
</div>
268+
);
269+
}
270+
```
271+
272+
This is equivalent to the manual check:
273+
```typescript
274+
response.type === 'resource' && response.resource.uri?.startsWith('ui://')
275+
```
276+
277+
But provides better type safety and is more concise.
278+
279+
---
280+
233281
## Handling Asynchronous Actions with Message IDs
234282

235283
When an action from the iframe requires asynchronous processing on the host, the `messageId` property can be used to track the action's lifecycle and result. This allows the iframe to fetch data from the host, present feedback to the user (e.g., loading indicators), success messages, etc.

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ See [Custom Component Libraries](./custom-component-libraries.md) for a detailed
7676

7777
```tsx
7878
import React from 'react';
79-
import { UIResourceRenderer, UIActionResult } from '@mcp-ui/client';
79+
import { UIResourceRenderer, UIActionResult, isUIResource } from '@mcp-ui/client';
8080

8181
function App({ mcpResource }) {
8282
const handleUIAction = async (result: UIActionResult) => {
@@ -105,10 +105,7 @@ function App({ mcpResource }) {
105105
return { status: 'handled' };
106106
};
107107

108-
if (
109-
mcpResource.type === 'resource' &&
110-
mcpResource.resource.uri?.startsWith('ui://')
111-
) {
108+
if (isUIResource(mcpResource)) {
112109
return (
113110
<UIResourceRenderer
114111
resource={mcpResource.resource}
@@ -121,6 +118,28 @@ function App({ mcpResource }) {
121118
}
122119
```
123120

121+
## Utility Functions
122+
123+
### `isUIResource()`
124+
125+
The `isUIResource()` utility function is a convenient way to check if content from an MCP response is a UI resource. Instead of manually checking:
126+
127+
```typescript
128+
content.type === 'resource' && content.resource.uri?.startsWith('ui://')
129+
```
130+
131+
You can simply use:
132+
133+
```typescript
134+
import { isUIResource } from '@mcp-ui/client';
135+
136+
if (isUIResource(content)) {
137+
// This is a UI resource, safe to render
138+
}
139+
```
140+
141+
The function provides type narrowing, so TypeScript will understand that `content.resource` is available after the check.
142+
124143
## Advanced Usage
125144

126145
### Content Type Restrictions

sdks/typescript/client/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { UIResourceRenderer } from './components/UIResourceRenderer';
2+
export { isUIResource } from './utils/isUIResource';
23

34
// The types needed to create a custom component library
45
export type {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { isUIResource } from '../isUIResource';
3+
4+
describe('isUIResource', () => {
5+
6+
const uiResources = [
7+
{ type: 'resource', resource: { uri: 'ui://test' } },
8+
{ type: 'resource', resource: { uri: 'ui://test', mimeType: 'text/html', text: 'Hello, world!' }, arbitraryProp: 'arbitrary' },
9+
{ type: 'resource', resource: { uri: 'ui://test', mimeType: 'text/uri-list', text: 'https://example.com' } },
10+
{ type: 'resource', resource: { uri: 'ui://test', mimeType: 'application/vnd.mcp-ui.remote-dom', text: 'Hello, world!' } },
11+
]
12+
13+
const nonUiResources = [
14+
{ type: 'resource', resource: { uri: 'https://example.com' } },
15+
{ type: 'text', text: 'Hello, world!' },
16+
{ type: 'text', resource: { uri: 'ui://test' } },
17+
]
18+
19+
it(`should return true if the content is a UI resource: ${JSON.stringify(uiResources)}`, () => {
20+
uiResources.forEach(content => {
21+
expect(isUIResource(content)).toBe(true);
22+
});
23+
});
24+
25+
nonUiResources.forEach(content => {
26+
it(`should return false if the content is not a UI resource: ${JSON.stringify(content)}`, () => {
27+
expect(isUIResource(content)).toBe(false);
28+
});
29+
});
30+
31+
it('should return false if the content is not a UI resource', () => {
32+
const content = { type: 'resource', resource: { uri: 'https://example.com' } };
33+
expect(isUIResource(content)).toBe(false);
34+
});
35+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { Resource } from '@modelcontextprotocol/sdk/types.js';
2+
3+
export function isUIResource<T extends { type: string; resource?: Partial<Resource> }>(content: T): content is T & { type: 'resource'; resource: Partial<Resource> } {
4+
return (content.type === 'resource' && content.resource?.uri?.startsWith('ui://')) ?? false;
5+
}

0 commit comments

Comments
 (0)