44`_meta.ui.resourceUri` points at a `ui://` resource (an HTML document served
55with the `text/html;profile=mcp-app` MIME type) that the host renders in a
66sandboxed iframe. See https://modelcontextprotocol.io/specification/draft/extensions/apps
7- and SEP-2133 for the extension framework.
7+ and the ext-apps spec for the wire format, and SEP-2133 for the extension framework.
88
99This is a self-contained, additive `Extension`: it contributes tools and
1010resources and advertises the capability, but does not intercept any core method.
@@ -22,17 +22,22 @@ def get_time(ctx: Context) -> str:
2222
2323Per SEP-2133, an extension MUST degrade gracefully: a UI-enabled tool should
2424still return meaningful text for clients that did not negotiate Apps. Use
25- `client_supports_apps(ctx)` to branch on the client's advertised support.
25+ `client_supports_apps(ctx)` to branch on the client's advertised support. (The SDK
26+ keeps Apps in-core under `mcp.server.apps` rather than a separate package; the
27+ TypeScript and C# SDKs ship it as a standalone package.)
2628"""
2729
2830from __future__ import annotations
2931
3032from collections .abc import Callable , Sequence
31- from typing import Any , TypeVar
33+ from typing import Any , Literal , TypeVar
34+
35+ from pydantic import BaseModel , ConfigDict
36+ from pydantic .alias_generators import to_camel
3237
3338from mcp .server .context import ServerRequestContext
39+ from mcp .server .extension import Extension , ResourceBinding , ToolBinding
3440from mcp .server .mcpserver .context import Context
35- from mcp .server .mcpserver .extension import Extension , ResourceBinding , ToolBinding
3641from mcp .server .mcpserver .resources import TextResource
3742
3843EXTENSION_ID = "io.modelcontextprotocol/ui"
@@ -41,9 +46,34 @@ def get_time(ctx: Context) -> str:
4146APP_MIME_TYPE = "text/html;profile=mcp-app"
4247"""MIME type for a `ui://` app resource."""
4348
49+ Visibility = Literal ["model" , "app" ]
50+ """Where a UI-bound tool is surfaced (`_meta.ui.visibility`)."""
51+
4452_CallableT = TypeVar ("_CallableT" , bound = Callable [..., Any ])
4553
4654
55+ class ResourcePermissions (BaseModel ):
56+ """Iframe permissions a `ui://` resource requests (`_meta.ui.permissions`)."""
57+
58+ model_config = ConfigDict (alias_generator = to_camel , populate_by_name = True )
59+
60+ camera : dict [str , Any ] | None = None
61+ microphone : dict [str , Any ] | None = None
62+ geolocation : dict [str , Any ] | None = None
63+ clipboard_write : dict [str , Any ] | None = None
64+
65+
66+ class ResourceCsp (BaseModel ):
67+ """Content-Security-Policy domains for a `ui://` resource (`_meta.ui.csp`)."""
68+
69+ model_config = ConfigDict (alias_generator = to_camel , populate_by_name = True )
70+
71+ connect_domains : list [str ] | None = None
72+ resource_domains : list [str ] | None = None
73+ frame_domains : list [str ] | None = None
74+ base_uri_domains : list [str ] | None = None
75+
76+
4777class Apps (Extension ):
4878 """The MCP Apps extension: bind tools to `ui://` UI resources.
4979
@@ -58,23 +88,36 @@ def __init__(self) -> None:
5888 self ._tools : list [ToolBinding ] = []
5989 self ._resources : list [ResourceBinding ] = []
6090
61- def tool (self , * , resource_uri : str , ** tool_kwargs : Any ) -> Callable [[_CallableT ], _CallableT ]:
91+ def tool (
92+ self ,
93+ * ,
94+ resource_uri : str ,
95+ visibility : Sequence [Visibility ] | None = None ,
96+ meta : dict [str , Any ] | None = None ,
97+ ** tool_kwargs : Any ,
98+ ) -> Callable [[_CallableT ], _CallableT ]:
6299 """Decorator registering a tool bound to a `ui://` resource.
63100
64- Stamps `_meta.ui.resourceUri` on the tool. `tool_kwargs` are forwarded to
65- `MCPServer.add_tool` (name, title, description, annotations, ...).
101+ Stamps `_meta.ui.resourceUri` (and `_meta.ui.visibility` when given) on the
102+ tool. `tool_kwargs` are forwarded to `MCPServer.add_tool` (name, title,
103+ description, annotations, ...); pass `meta=` to merge extra `_meta` keys
104+ alongside the `ui` entry.
66105
67106 Args:
68107 resource_uri: The `ui://` URI of the UI resource this tool renders.
108+ visibility: Where the tool is surfaced (`["model", "app"]`).
109+ meta: Additional `_meta` keys to merge with the `ui` entry.
69110
70111 Raises:
71112 ValueError: If `resource_uri` does not use the `ui://` scheme.
72113 """
73114 _require_ui_scheme (resource_uri )
115+ ui : dict [str , Any ] = {"resourceUri" : resource_uri }
116+ if visibility is not None :
117+ ui ["visibility" ] = list (visibility )
74118
75119 def decorator (fn : _CallableT ) -> _CallableT :
76- meta = {"ui" : {"resourceUri" : resource_uri }}
77- self ._tools .append (ToolBinding (fn = fn , meta = meta , kwargs = tool_kwargs ))
120+ self ._tools .append (ToolBinding (fn = fn , meta = {** (meta or {}), "ui" : ui }, kwargs = tool_kwargs ))
78121 return fn
79122
80123 return decorator
@@ -87,9 +130,16 @@ def add_html_resource(
87130 name : str | None = None ,
88131 title : str | None = None ,
89132 description : str | None = None ,
133+ csp : ResourceCsp | None = None ,
134+ permissions : ResourcePermissions | None = None ,
135+ domain : str | None = None ,
136+ prefers_border : bool | None = None ,
90137 ) -> None :
91138 """Register a `ui://` HTML resource served as `text/html;profile=mcp-app`.
92139
140+ `csp`, `permissions`, `domain`, and `prefers_border` populate the
141+ resource's `_meta.ui` per the ext-apps spec.
142+
93143 Args:
94144 uri: The `ui://` URI; a tool references it via `resource_uri`.
95145 html: The HTML document the host renders.
@@ -98,12 +148,22 @@ def add_html_resource(
98148 ValueError: If `uri` does not use the `ui://` scheme.
99149 """
100150 _require_ui_scheme (uri )
151+ ui : dict [str , Any ] = {}
152+ if csp is not None :
153+ ui ["csp" ] = csp .model_dump (by_alias = True , exclude_none = True )
154+ if permissions is not None :
155+ ui ["permissions" ] = permissions .model_dump (by_alias = True , exclude_none = True )
156+ if domain is not None :
157+ ui ["domain" ] = domain
158+ if prefers_border is not None :
159+ ui ["prefersBorder" ] = prefers_border
101160 resource = TextResource (
102161 uri = uri ,
103162 name = name or uri ,
104163 title = title ,
105164 description = description ,
106165 mime_type = APP_MIME_TYPE ,
166+ meta = {"ui" : ui } if ui else None ,
107167 text = html ,
108168 )
109169 self ._resources .append (ResourceBinding (resource = resource ))
@@ -118,12 +178,17 @@ def resources(self) -> Sequence[ResourceBinding]:
118178def client_supports_apps (ctx : Context [Any ] | ServerRequestContext [Any , Any ]) -> bool :
119179 """Whether the connected client negotiated MCP Apps support.
120180
121- Returns `False` when the client did not advertise the extension (or sent no
122- capabilities), so a UI-enabled tool can fall back to text-only output.
181+ Returns `True` only when the client advertised the extension AND listed the
182+ `text/html;profile=mcp-app` MIME type in its settings, so a UI-enabled tool
183+ can fall back to text-only output otherwise.
123184 """
124185 capabilities = _client_capabilities (ctx )
125186 extensions = capabilities .extensions if capabilities else None
126- return bool (extensions and EXTENSION_ID in extensions )
187+ settings = extensions .get (EXTENSION_ID ) if extensions else None
188+ if settings is None :
189+ return False
190+ mime_types = settings .get ("mimeTypes" )
191+ return mime_types is None or APP_MIME_TYPE in mime_types
127192
128193
129194def _client_capabilities (ctx : Context [Any ] | ServerRequestContext [Any , Any ]) -> Any :
0 commit comments