feat: HTML widget support (MCP Apps UI)#490
Draft
corinagum wants to merge 14 commits into
Draft
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Match the convention used by ExperimentalTeamsTargeted in the PY SDK. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Change examples/html-widgets requires-python to >=3.11,<4.0 (matches all other examples) - Move HtmlWidgetMarkdownOptions import to file top (no inline imports per repo convention) - Apply ruff formatting to new files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces experimental HTML widget support to the Teams Python SDK, enabling bots to send MCP Apps UI-compatible HTML widgets in messages, plus a typed invoke route for widgets calling bot tools.
Changes:
- Added API models for HTML widget payloads and the
htmlwidget/calltoolinvoke request/response wire format. - Added app-side helpers for protocol injection, widget markdown/message builders, and advisory CSP/security-policy validation.
- Added comprehensive unit tests and a new
examples/html-widgetsdemo bot showcasing the widget contract.
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Updates workspace lockfile members and Python version marker. |
| packages/apps/tests/test_html_widget.py | Adds test coverage for widget builders, protocol injection, and security-policy validation. |
| packages/apps/src/microsoft_teams/apps/utils/html_widget.py | Implements widget helpers (builders, protocol injection, static security checks). |
| packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py | Adds on_widget_call_tool handler registration API. |
| packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py | Registers widget.call_tool invoke routing selector and response type. |
| packages/apps/src/microsoft_teams/apps/init.py | Re-exports widget helper APIs from the apps package. |
| packages/api/src/microsoft_teams/api/models/invoke_response.py | Adds HtmlWidgetCallToolResponse to the invoke response body union. |
| packages/api/src/microsoft_teams/api/models/html_widget/html_widget_payload.py | Adds HtmlWidgetPayload, HtmlWidgetSecurityPolicy, and permissions models. |
| packages/api/src/microsoft_teams/api/models/html_widget/call_tool_result.py | Adds McpUiCallToolResult and HtmlWidgetCallToolResponse wire model. |
| packages/api/src/microsoft_teams/api/models/html_widget/call_tool_request.py | Adds CallToolRequest model for htmlwidget/calltool invoke payloads. |
| packages/api/src/microsoft_teams/api/models/html_widget/init.py | Exposes html-widget models via package __all__. |
| packages/api/src/microsoft_teams/api/models/init.py | Exports html-widget models at the top-level models namespace. |
| packages/api/src/microsoft_teams/api/activities/invoke/html_widget/call_tool.py | Adds typed HtmlWidgetCallToolInvokeActivity. |
| packages/api/src/microsoft_teams/api/activities/invoke/html_widget/init.py | Exposes the html-widget invoke activity module. |
| packages/api/src/microsoft_teams/api/activities/invoke/init.py | Adds html-widget invoke activity to the discriminated invoke union. |
| examples/html-widgets/src/widgets/update_context.py | Adds demo widget for ui/update-model-context. |
| examples/html-widgets/src/widgets/simple.py | Adds demo widget for basic rendering. |
| examples/html-widgets/src/widgets/open_link.py | Adds demo widget for ui/open-link. |
| examples/html-widgets/src/widgets/multi_tool.py | Adds demo widget exercising multi-tool dispatch via tools/call. |
| examples/html-widgets/src/widgets/messageback.py | Adds demo widget for ui/message (messageBack-like) behavior. |
| examples/html-widgets/src/widgets/host_context.py | Adds demo widget that inspects hostContext and listens for updates. |
| examples/html-widgets/src/widgets/fullscreen.py | Adds demo widget requesting fullscreen display mode. |
| examples/html-widgets/src/widgets/calltool.py | Adds demo widget calling a refresh tool via tools/call. |
| examples/html-widgets/src/widgets/init.py | Re-exports widget HTML constants for the example bot. |
| examples/html-widgets/src/main.py | Implements the example bot command router and typed on_widget_call_tool handler. |
| examples/html-widgets/README.md | Documents the example bot commands and how to run it. |
| examples/html-widgets/pyproject.toml | Adds the example package metadata and dependencies. |
- Use McpUiCallToolResultContent instances instead of raw dicts - Type args as dict[str, Any] to fix partial-unknown errors - Cast counter/sides through int() with Any input Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Copy InjectWidgetProtocolOptions before overriding name (avoid caller mutation) - Deep-copy DEFAULT_SECURITY_POLICY when used as fallback (avoid shared mutable state) - Fix call_tool_request.py import to use relative ..custom_base_model (consistency) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rsion The inline script injection used only backslash/quote escaping, which allowed a </script> sequence in the widget name or version to break out of the script context and inject arbitrary HTML/JS into the widget iframe. Fix: add _escape_for_inline_script() that also escapes </ (prevents </script> breakout) and newlines (prevents JS string literal breakout). Added 3 tests covering the breakout cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced Jun 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds HTML widget support to the Teams Python SDK, enabling bots to send rich interactive MCP Apps UI widgets in Teams messages.
What's included
API types (
microsoft_teams.api)HtmlWidgetPayload,HtmlWidgetSecurityPolicy,HtmlWidgetPermissionsMcpUiCallToolResult,HtmlWidgetCallToolResponse(wire-format wrapper)HtmlWidgetCallToolInvokeActivityand invoke response mappingextendedmarkdownadded toTextFormatApp helpers (
microsoft_teams.apps)build_html_widget_markdown- wraps payload in```html-widgetcode fencebuild_html_widget_message- builds a ready-to-sendMessageActivityInputinject_widget_protocol- optional convenience helper that injects the MCP Apps protocol (ui/initialize handshake, size reporting, notification hooks)validate_security_policy- static analysis of widget HTML against declared security policywidget.call_toolinvoke route +on_widget_call_tooldecoratorInternal (not exported):
_validate_html_widget_payload- runtime validation (name, html, domain) called automatically by buildersExample bot (
examples/html-widgets/)on_widget_call_toolhandler with multi-tool dispatchTests
Preview markers
@experimentalDesign decisions
inject_widget_protocolis called internally by the builders, but widgets that implement the MCP Apps protocol themselves are returned unchanged (detected by presence ofui/initialize)validate_security_policyis a static linter; CSP enforcement in the iframe is the actual security boundaryMessageActivityInput:build_html_widget_messagereturns a proper model (not a dict) so it integrates withctx.send()seamlesslyRelated
Status
Draft - pending: