Skip to content

feat: add embeddedResourceProps for annotations#99

Merged
liady merged 2 commits intomainfrom
feat/embedded-resource-props
Sep 16, 2025
Merged

feat: add embeddedResourceProps for annotations#99
liady merged 2 commits intomainfrom
feat/embedded-resource-props

Conversation

@liady
Copy link
Collaborator

@liady liady commented Aug 29, 2025

Description

This PR addresses #98, by adding an embeddedResourceProps optional key to createUIResource(), which is being spread to the top-level of the created embedded resource.
This allows the consumer to define annotations, for example, on the top level - which matches the spec - https://modelcontextprotocol.io/specification/2025-06-18/schema#embeddedresource.

Usage

This kind of function call:

const resource = createUIResource({
  uri: 'ui://test-url' as const,
  content: { type: 'externalUrl', iframeUrl: 'https://example.com' },
  encoding: 'text' as const,

  // resource level metadata and props, as before
  uiMetadata: { 'preferred-frame-size': ['100px', '100px']},
  metadata: { 'arbitrary-metadata': 'some-resource-level-metadata' },
  resourceProps: { 'other-prop': 'resource-level-prop' },

  // (NEW) embedded resource top-level props
  embeddedResourceProps: {
    annotations: {
      audience: ['user'],
    },
  },
});

will generate:

{
  type: 'resource',
  resource: {
    uri: 'ui://test-url',
    mimeType: 'text/uri-list',
    text: 'https://example.com',
    // resource level metadata, as before
    _meta: {
      'arbitrary-metadata': 'some-resource-level-metadata',
      'ui-preferred-frame-size': ['100px', '100px'],
    },
   'other-prop': 'resource-level-prop',
  },
  // embedded resource (top-level) annotations
  annotations: {
    audience: ['user'],
  },
}

@cloudflare-workers-and-pages
Copy link

Deploying mcp-ui with  Cloudflare Pages  Cloudflare Pages

Latest commit: 44c1437
Status: ✅  Deploy successful!
Preview URL: https://63ef5fa0.mcp-ui.pages.dev
Branch Preview URL: https://feat-embedded-resource-props.mcp-ui.pages.dev

View logs

@aharvard
Copy link
Contributor

Oh, this reminds me... can you double-check that the client package getUIResourceMetadata() is extracting properties correctly? I saw some TS errors, but did not have time to dig in.

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

@liady
Copy link
Collaborator Author

liady commented Aug 29, 2025

@aharvard about getUIResourceMetadata - are you passing it the whole embedded resource or just the actual resource?

✅ This is correct (passing only the resource, the same object which you pass to <UIResourceRenderer />):

getUIResourceMetadata({
  uri: 'ui://example/demo',
  mimeType: 'text/html',
  text: '<div>Hello</div>',
  _meta: {
    'mcpui.dev/ui-preferred-frame-size': ['800px', '600px'],
    author: 'Development Team'
  }
})

❌ This is incorrect (passing the whole embedded resource):

getUIResourceMetadata({ // wrong, should pass only the resource
  type: 'resource',
  resource: {
    uri: 'ui://example/demo',
    mimeType: 'text/html',
    text: '<div>Hello</div>',
    _meta: {
      'mcpui.dev/ui-preferred-frame-size': ['800px', '600px'],
      author: 'Development Team'
    }
  }
})

If you feel that passing it the entire embedded resource makes sense, let me know. Since the actualResource object is the only thing UIResourceRenderer "cares" about, then getUIResourceMetadata() also expects to receive this object.

We can also open a spearate ticket for this discussion.

@aharvard
Copy link
Contributor

are you passing it the whole embedded resource or just the actual resource?

@liady, oh! I am passing the WHOLE resource! I think that's because I got used to passing the whole thing for the resource prop.

Currently working on a PoC outside of Goose and have this code:

image

I could destructure and pass just the resource but that does not feel very ergonomic... my mental model to simply deal with the top-level content item in the model message parts.

@liady
Copy link
Collaborator Author

liady commented Aug 29, 2025

@aharvard you should pass to getUIResourceMetadata() exactly what you pass to UIResourceRenderer - so c.resource in your case is indeed the correct argument. I wonder which TS errors do you see?

@aharvard
Copy link
Contributor

My apologies, you are right! Let's table this for now, and I can open another issue when/if I see the errors again. My current hunch about the errors I saw is that they are due to user error.

@liady
Copy link
Collaborator Author

liady commented Sep 16, 2025

Merging this for now, since the key names change will require a breaking change (so we should consider them for the next major)

@liady liady merged commit b96ec44 into main Sep 16, 2025
11 checks passed
github-actions bot pushed a commit that referenced this pull request Sep 16, 2025
…25-09-16)

### Features

* add embeddedResourceProps for annotations ([#99](#99)) ([b96ec44](b96ec44))
github-actions bot pushed a commit that referenced this pull request Oct 10, 2025
# 1.0.0-alpha.1 (2025-10-10)

### Bug Fixes

* adapter version ([259c842](259c842))
* add a bridge to pass messages in and out of the proxy ([#38](#38)) ([30ccac0](30ccac0))
* bump client version ([75c9236](75c9236))
* **client:** specify iframe ([fd0b70a](fd0b70a))
* **client:** styling ([6ff9b68](6ff9b68))
* dependencies ([887f61f](887f61f))
* export RemoteDomResource ([2b86f2d](2b86f2d))
* export ResourceRenderer and HtmlResource ([2b841a5](2b841a5))
* exports ([3a93a16](3a93a16))
* iframe handle ([#15](#15)) ([66bd4fd](66bd4fd))
* lint ([4487820](4487820))
* lint ([d0a91f9](d0a91f9))
* minor typo ([a0bee9c](a0bee9c))
* move react dependencies to be peer dependencies ([#91](#91)) ([f672f3e](f672f3e)), closes [#90](#90)
* package config ([8dc1e53](8dc1e53))
* packaging ([9e6babd](9e6babd))
* pass ref explicitly using iframeProps ([#33](#33)) ([d01b5d1](d01b5d1))
* publish ([0943e7a](0943e7a))
* ref passing to UIResourceRenderer ([#32](#32)) ([d28c23f](d28c23f))
* release ([420efc0](420efc0))
* remove shared dependency ([e66e8f4](e66e8f4))
* rename components and methods to fit new scope ([#22](#22)) ([6bab1fe](6bab1fe))
* rename delivery -> encoding and flavor -> framework ([#36](#36)) ([9a509ed](9a509ed))
* Ruby comment ([b22dc2e](b22dc2e))
* support react-router ([21ffb95](21ffb95))
* text and blob support in RemoteDOM resources ([ec68eb9](ec68eb9))
* trigger release ([aaca831](aaca831))
* typescript ci publish ([e7c0ebf](e7c0ebf))
* typescript types to be compatible with MCP SDK ([#10](#10)) ([74365d7](74365d7))
* update deps ([4091ef4](4091ef4))
* update isUIResource to use EmbeddedResource type ([#122](#122)) ([5a65a0b](5a65a0b)), closes [#117](#117)
* use targetOrigin in the proxy message relay ([#40](#40)) ([b3fb54e](b3fb54e))
* validate URL ([b7c994d](b7c994d))
* wc dist overwrite ([#63](#63)) ([9e46c56](9e46c56))

### Documentation

* bump ([#4](#4)) ([ad4d163](ad4d163))

### Features

* add adapters infra (appssdk) ([#125](#125)) ([2e016cd](2e016cd))
* add convenience function isUIResource to client SDK ([#86](#86)) ([607c6ad](607c6ad))
* add embeddedResourceProps for annotations ([#99](#99)) ([b96ec44](b96ec44))
* add proxy option to externalUrl ([#37](#37)) ([7b95cd0](7b95cd0))
* add remote-dom content type ([#18](#18)) ([5dacf37](5dacf37))
* add Ruby server SDK ([#31](#31)) ([5ffcde4](5ffcde4))
* add sandbox permissions instead of an override ([#83](#83)) ([b1068e9](b1068e9))
* add ui-request-render-data message type ([#111](#111)) ([26135ce](26135ce))
* add UIResourceRenderer Web Component ([#58](#58)) ([ec8f299](ec8f299))
* auto resize with the autoResizeIframe prop ([#56](#56)) ([76c867a](76c867a))
* change onGenericMcpAction to optional onUiAction ([1913b59](1913b59))
* **client:** allow setting supportedContentTypes for HtmlResource ([#17](#17)) ([e009ef1](e009ef1))
* consolidate ui:// and ui-app:// ([#8](#8)) ([2e08035](2e08035))
* pass iframe props down ([#14](#14)) ([112539d](112539d))
* refactor UTFtoB64 (bump server version) ([#95](#95)) ([2d5e16b](2d5e16b))
* send render data to the iframe ([#51](#51)) ([d38cfc7](d38cfc7))
* separate html and remote-dom props ([#24](#24)) ([a7f0529](a7f0529))
* support generic messages response ([#35](#35)) ([10b407b](10b407b))
* support passing resource metadata ([#87](#87)) ([f1c1c9b](f1c1c9b))
* support ui action result types ([#6](#6)) ([899d152](899d152))
* switch to ResourceRenderer ([#21](#21)) ([6fe3166](6fe3166))

### BREAKING CHANGES

* The existing naming is ambiguous. Renaming delivery to encoding and flavor to framework should clarify the intent.
* exported names have changed
* removed deprecated client API
* (previous one didn't take due to semantic-release misalignment)
github-actions bot pushed a commit that referenced this pull request Oct 10, 2025
# 1.0.0-alpha.1 (2025-10-10)

### Bug Fixes

* adapter version ([259c842](259c842))
* add a bridge to pass messages in and out of the proxy ([#38](#38)) ([30ccac0](30ccac0))
* bump client version ([75c9236](75c9236))
* **client:** specify iframe ([fd0b70a](fd0b70a))
* **client:** styling ([6ff9b68](6ff9b68))
* dependencies ([887f61f](887f61f))
* export RemoteDomResource ([2b86f2d](2b86f2d))
* export ResourceRenderer and HtmlResource ([2b841a5](2b841a5))
* exports ([3a93a16](3a93a16))
* iframe handle ([#15](#15)) ([66bd4fd](66bd4fd))
* lint ([4487820](4487820))
* lint ([d0a91f9](d0a91f9))
* minor typo ([a0bee9c](a0bee9c))
* move react dependencies to be peer dependencies ([#91](#91)) ([f672f3e](f672f3e)), closes [#90](#90)
* package config ([8dc1e53](8dc1e53))
* packaging ([9e6babd](9e6babd))
* pass ref explicitly using iframeProps ([#33](#33)) ([d01b5d1](d01b5d1))
* publish ([0943e7a](0943e7a))
* ref passing to UIResourceRenderer ([#32](#32)) ([d28c23f](d28c23f))
* release ([420efc0](420efc0))
* remove shared dependency ([e66e8f4](e66e8f4))
* rename components and methods to fit new scope ([#22](#22)) ([6bab1fe](6bab1fe))
* rename delivery -> encoding and flavor -> framework ([#36](#36)) ([9a509ed](9a509ed))
* Ruby comment ([b22dc2e](b22dc2e))
* server versioning ([2324371](2324371))
* support react-router ([21ffb95](21ffb95))
* text and blob support in RemoteDOM resources ([ec68eb9](ec68eb9))
* trigger release ([aaca831](aaca831))
* typescript ci publish ([e7c0ebf](e7c0ebf))
* typescript types to be compatible with MCP SDK ([#10](#10)) ([74365d7](74365d7))
* update deps ([4091ef4](4091ef4))
* update isUIResource to use EmbeddedResource type ([#122](#122)) ([5a65a0b](5a65a0b)), closes [#117](#117)
* use targetOrigin in the proxy message relay ([#40](#40)) ([b3fb54e](b3fb54e))
* validate URL ([b7c994d](b7c994d))
* wc dist overwrite ([#63](#63)) ([9e46c56](9e46c56))

### Documentation

* bump ([#4](#4)) ([ad4d163](ad4d163))

### Features

* add adapters infra (appssdk) ([#125](#125)) ([2e016cd](2e016cd))
* add convenience function isUIResource to client SDK ([#86](#86)) ([607c6ad](607c6ad))
* add embeddedResourceProps for annotations ([#99](#99)) ([b96ec44](b96ec44))
* add proxy option to externalUrl ([#37](#37)) ([7b95cd0](7b95cd0))
* add remote-dom content type ([#18](#18)) ([5dacf37](5dacf37))
* add Ruby server SDK ([#31](#31)) ([5ffcde4](5ffcde4))
* add sandbox permissions instead of an override ([#83](#83)) ([b1068e9](b1068e9))
* add ui-request-render-data message type ([#111](#111)) ([26135ce](26135ce))
* add UIResourceRenderer Web Component ([#58](#58)) ([ec8f299](ec8f299))
* auto resize with the autoResizeIframe prop ([#56](#56)) ([76c867a](76c867a))
* change onGenericMcpAction to optional onUiAction ([1913b59](1913b59))
* **client:** allow setting supportedContentTypes for HtmlResource ([#17](#17)) ([e009ef1](e009ef1))
* consolidate ui:// and ui-app:// ([#8](#8)) ([2e08035](2e08035))
* pass iframe props down ([#14](#14)) ([112539d](112539d))
* refactor UTFtoB64 (bump server version) ([#95](#95)) ([2d5e16b](2d5e16b))
* send render data to the iframe ([#51](#51)) ([d38cfc7](d38cfc7))
* separate html and remote-dom props ([#24](#24)) ([a7f0529](a7f0529))
* support generic messages response ([#35](#35)) ([10b407b](10b407b))
* support passing resource metadata ([#87](#87)) ([f1c1c9b](f1c1c9b))
* support ui action result types ([#6](#6)) ([899d152](899d152))
* switch to ResourceRenderer ([#21](#21)) ([6fe3166](6fe3166))

### BREAKING CHANGES

* The existing naming is ambiguous. Renaming delivery to encoding and flavor to framework should clarify the intent.
* exported names have changed
* removed deprecated client API
* (previous one didn't take due to semantic-release misalignment)
github-actions bot pushed a commit that referenced this pull request Nov 4, 2025
# 1.0.0 (2025-11-04)

### Bug Fixes

* add a bridge to pass messages in and out of the proxy ([#38](#38)) ([30ccac0](30ccac0))
* bump client version ([75c9236](75c9236))
* **client:** specify iframe ([fd0b70a](fd0b70a))
* **client:** styling ([6ff9b68](6ff9b68))
* dependencies ([887f61f](887f61f))
* Enable bidirectional message relay in rawhtml proxy mode ([#138](#138)) ([f0bdefb](f0bdefb))
* ensure Apps SDK adapter is bundled properly and initialized wth config ([#137](#137)) ([4f7c25c](4f7c25c))
* export RemoteDomResource ([2b86f2d](2b86f2d))
* export ResourceRenderer and HtmlResource ([2b841a5](2b841a5))
* exports ([3a93a16](3a93a16))
* fix file extension reference in package.json ([927989c](927989c))
* iframe handle ([#15](#15)) ([66bd4fd](66bd4fd))
* lint ([4487820](4487820))
* lint ([d0a91f9](d0a91f9))
* minor typo ([a0bee9c](a0bee9c))
* move react dependencies to be peer dependencies ([#91](#91)) ([f672f3e](f672f3e)), closes [#90](#90)
* package config ([8dc1e53](8dc1e53))
* packaging ([9e6babd](9e6babd))
* pass ref explicitly using iframeProps ([#33](#33)) ([d01b5d1](d01b5d1))
* publish ([0943e7a](0943e7a))
* ref passing to UIResourceRenderer ([#32](#32)) ([d28c23f](d28c23f))
* remove shared dependency ([e66e8f4](e66e8f4))
* rename components and methods to fit new scope ([#22](#22)) ([6bab1fe](6bab1fe))
* rename delivery -> encoding and flavor -> framework ([#36](#36)) ([9a509ed](9a509ed))
* Ruby comment ([b22dc2e](b22dc2e))
* support react-router ([21ffb95](21ffb95))
* text and blob support in RemoteDOM resources ([ec68eb9](ec68eb9))
* trigger release ([aaca831](aaca831))
* typescript ci publish ([e7c0ebf](e7c0ebf))
* typescript types to be compatible with MCP SDK ([#10](#10)) ([74365d7](74365d7))
* update deps ([4091ef4](4091ef4))
* update isUIResource to use EmbeddedResource type ([#122](#122)) ([5a65a0b](5a65a0b)), closes [#117](#117)
* use targetOrigin in the proxy message relay ([#40](#40)) ([b3fb54e](b3fb54e))
* validate URL ([b7c994d](b7c994d))
* wc dist overwrite ([#63](#63)) ([9e46c56](9e46c56))

### Documentation

* bump ([#4](#4)) ([ad4d163](ad4d163))

### Features

* add convenience function isUIResource to client SDK ([#86](#86)) ([607c6ad](607c6ad))
* add embeddedResourceProps for annotations ([#99](#99)) ([b96ec44](b96ec44))
* add proxy option to externalUrl ([#37](#37)) ([7b95cd0](7b95cd0))
* add remote-dom content type ([#18](#18)) ([5dacf37](5dacf37))
* add Ruby server SDK ([#31](#31)) ([5ffcde4](5ffcde4))
* add sandbox permissions instead of an override ([#83](#83)) ([b1068e9](b1068e9))
* add ui-request-render-data message type ([#111](#111)) ([26135ce](26135ce))
* add UIResourceRenderer Web Component ([#58](#58)) ([ec8f299](ec8f299))
* auto resize with the autoResizeIframe prop ([#56](#56)) ([76c867a](76c867a))
* change onGenericMcpAction to optional onUiAction ([1913b59](1913b59))
* **client:** allow setting supportedContentTypes for HtmlResource ([#17](#17)) ([e009ef1](e009ef1))
* consolidate ui:// and ui-app:// ([#8](#8)) ([2e08035](2e08035))
* pass iframe props down ([#14](#14)) ([112539d](112539d))
* refactor UTFtoB64 (bump server version) ([#95](#95)) ([2d5e16b](2d5e16b))
* send render data to the iframe ([#51](#51)) ([d38cfc7](d38cfc7))
* separate html and remote-dom props ([#24](#24)) ([a7f0529](a7f0529))
* support adapters ([#127](#127)) ([d4bd152](d4bd152))
* support generic messages response ([#35](#35)) ([10b407b](10b407b))
* support metadata in Python SDK ([#134](#134)) ([9bc3c64](9bc3c64))
* support passing resource metadata ([#87](#87)) ([f1c1c9b](f1c1c9b))
* support proxy for rawHtml ([#132](#132)) ([1bbeb09](1bbeb09))
* support ui action result types ([#6](#6)) ([899d152](899d152))
* switch to ResourceRenderer ([#21](#21)) ([6fe3166](6fe3166))

### BREAKING CHANGES

* The existing naming is ambiguous. Renaming delivery to encoding and flavor to framework should clarify the intent.
* exported names have changed
* removed deprecated client API
* (previous one didn't take due to semantic-release misalignment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants