Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/src/guide/server/typescript/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ MCP-UI specific configuration options. These keys are automatically prefixed wit
- **`initial-render-data`**: Provide data that should be passed to the iframe when rendering

### `resourceProps`
Additional properties that are spread directly onto the resource definition, allowing you to add any MCP specification-supported properties like `annotations`.
Additional properties that are spread directly onto the actual resource definition, allowing you to add/override any MCP specification-supported properties.

### `embeddedResourceProps`
Additional properties that are spread directly onto the embedded resource top-level definition, allowing you to add any MCP embedded resource [specification-supported](https://modelcontextprotocol.io/specification/2025-06-18/schema#embeddedresource) properties, like `annotations`.

## Building

Expand Down
19 changes: 10 additions & 9 deletions docs/src/guide/server/typescript/usage-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ console.log('Resource 5:', JSON.stringify(resource5, null, 2));

## Metadata Configuration Examples

The `createUIResource` function supports three types of metadata configuration to enhance your UI resources:
The `createUIResource` function supports several types of metadata configuration to enhance your UI resources:

```typescript
import { createUIResource } from '@mcp-ui/server';
Expand Down Expand Up @@ -212,14 +212,14 @@ console.log('Resource with UI metadata:', JSON.stringify(resourceWithUIMetadata,
}
*/

// Example 9: Using resourceProps for additional MCP properties
// Example 9: Using embeddedResourceProps for additional MCP properties
const resourceWithProps = createUIResource({
uri: 'ui://form/user-profile',
content: { type: 'rawHtml', htmlString: '<form id="profile">...</form>' },
encoding: 'text',
resourceProps: {
embeddedResourceProps: {
annotations: {
audience: ['developers', 'admins'],
audience: ['user'],
priority: 'high'
}
}
Expand All @@ -232,10 +232,10 @@ console.log('Resource with additional props:', JSON.stringify(resourceWithProps,
"uri": "ui://form/user-profile",
"mimeType": "text/html",
"text": "<form id=\"profile\">...</form>",
"annotations": {
"audience": ["developers", "admins"],
"priority": "high"
}
},
"annotations": {
"audience": ["user"],
"priority": "high"
}
}
*/
Expand All @@ -245,7 +245,8 @@ console.log('Resource with additional props:', JSON.stringify(resourceWithProps,

- **Use `metadata` for standard MCP resource information** like titles, descriptions, timestamps, and authorship
- **Use `uiMetadata` for client rendering hints** like preferred sizes, initial data, and context preferences
- **Use `resourceProps` for MCP specification properties** like annotations, descriptions at the resource level, and other standard fields
- **Use `resourceProps` for MCP specification properties**, descriptions at the resource level, and other standard fields
- **Use `embeddedResourceProps` for MCP embedded resource properties** like annotations.

## Advanced URI List Example

Expand Down
36 changes: 36 additions & 0 deletions sdks/typescript/server/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,42 @@ describe('@mcp-ui/server', () => {
expect(resource.resource._meta).toEqual({ foo: 'bar', 'arbitrary-prop': 'arbitrary2' });
});

it('should create a text-based external URL resource with embedded resource props', () => {
const options = {
uri: 'ui://test-url' as const,
content: { type: 'externalUrl' as const, iframeUrl: 'https://example.com' },
encoding: 'text' as const,
uiMetadata: { 'preferred-frame-size': ['100px', '100px'] as [string, string] },
resourceProps: { _meta: { 'arbitrary-metadata': 'resource-level-metadata' } },
embeddedResourceProps: {
annotations: {
audience: ['user'],
},
_meta: { 'arbitrary-metadata': 'embedded-resource-metadata' },
},
};
const resource = createUIResource(options);
expect(resource).toEqual({
type: 'resource',
resource: {
uri: 'ui://test-url',
mimeType: 'text/uri-list',
text: 'https://example.com',
blob: undefined,
_meta: {
'arbitrary-metadata': 'resource-level-metadata',
[`${UI_METADATA_PREFIX}preferred-frame-size`]: ['100px', '100px'],
},
},
annotations: {
audience: ['user'],
},
_meta: {
'arbitrary-metadata': 'embedded-resource-metadata',
},
});
});

it('should create a blob-based external URL resource', () => {
const options = {
uri: 'ui://test-url-blob' as const,
Expand Down
3 changes: 3 additions & 0 deletions sdks/typescript/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { getAdditionalResourceProps, utf8ToBase64 } from './utils.js';
export type UIResource = {
type: 'resource';
resource: HTMLTextContent | Base64BlobContent;
annotations?: Record<string, unknown>;
_meta?: Record<string, unknown>;
};

/**
Expand Down Expand Up @@ -96,6 +98,7 @@ export function createUIResource(options: CreateUIResourceOptions): UIResource {
return {
type: 'resource',
resource: resource,
...(options.embeddedResourceProps ?? {}),
};
}

Expand Down
7 changes: 5 additions & 2 deletions sdks/typescript/server/src/types.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a nit about naming:

  • uiMetadata
  • metadata
  • resourceProps
  • embeddedResourceProps

I am confused about which to use for what. I recall we had some discussion around reserved properties and arbitrary properties. I think we still need that! But, I wonder if it would be clearer to have literal properties for _meta and annotations... then keep uiMetadata as is or maybe name it something close.

Copy link
Collaborator Author

@liady liady Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so:

Metadata handling
uiMetadata - reserved for mcp-ui props
metadata - populates the _meta property on the resource (for any other arbitraray metadata)

Props handling
resourceProps - are being spread as-is on the resource level
embeddedResourceProps - are being spread as-is on the embedded resource level (top-level) - good for annotations

Note: In general - resourceProps shouldn't be used for _meta at all

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. This explanation helps. Please don't over-index on my personal preference; we might want to open this up to the community for other opinions. But I do feel like the current approach is hard to understand and easy to get them mixed up.

I did not realize there is a distinction between "resource" and "embedded resource" from a tool response POV. I thought that tools return embedded resources, not resources.

I think it could be simpler if we avoided specific terms and conformed to the MCP spec. So I am curious to learn if there are any reasons why we might not be able to simplify down to two properties.

createUIResource({
  ...
  _meta: {
    // users may use the reserved keys
    // or supply their own properties
    // automatically placed at the top-level for mental-model alignment with annotations 
  }
  annotations: {
    // MCP compliant and is automatically placed at the expected top-level
  }
}

The TL;DR is

  1. Use _meta and parse for reserved keys
  2. support annotations as opposed to being open-ended on resourceProps and embeddedResourceProps
  3. extract key/value pairs and place on top-level of the raw response object

Copy link
Collaborator Author

@liady liady Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that according to the spec there are two _meta fields - one on the embedded resource (top level), and one on the actual resource.
mcp-ui "cares" only about the one on the inner resource (and not on the top level), since this is what's being passed to it, and this is where the actual UI definitions lie.
So the uiMetadata should be there (on the inner resource _meta field)

However the annotations field is on the top level. So we have to make a distinction between the top level props and the inner resource props.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. Would you entertain the idea of renaming the properties for metadata to better align with the mental model of resourceProps and embeddedResourceProps? I think I have these conversions right:

  1. top level: metadata could become embeddedResourceMeta
  2. resource level: uiMetadata could become resourceMeta

So if a server author wants to define stuff at the top level, they can use:

  • embeddedResourceMeta
  • embeddedResourceProps

And if they want to define stuff at the resources level, they can use:

  • resourceMeta
  • resourceProps

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aharvard maybe we'll open an issue with this proposal? We should probably consider this for the next major

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Resource } from '@modelcontextprotocol/sdk/types.js';
import type { EmbeddedResource, Resource } from '@modelcontextprotocol/sdk/types.js';

// Primary identifier for the resource. Starts with ui://`
export type URI = `ui://${string}`;
Expand Down Expand Up @@ -43,11 +43,14 @@ export interface CreateUIResourceOptions {
uiMetadata?: UIResourceMetadata;
// additional metadata to be passed on _meta
metadata?: Record<string, unknown>;
// additional resource props to be passed on resource (i.e. annotations)
// additional resource props to be passed on the resource itself
resourceProps?: UIResourceProps;
// additional resource props to be passed on the top-level embedded resource (i.e. annotations)
embeddedResourceProps?: EmbeddedUIResourceProps;
}

export type UIResourceProps = Omit<Partial<Resource>, 'uri' | 'mimeType'>;
export type EmbeddedUIResourceProps = Omit<Partial<EmbeddedResource>, 'resource' | 'type'>;

export const UIMetadataKey = {
PREFERRED_FRAME_SIZE: 'preferred-frame-size',
Expand Down