Skip to content

feat: HTML widget support (MCP Apps UI)#626

Draft
corinagum wants to merge 12 commits into
mainfrom
cg/widgets
Draft

feat: HTML widget support (MCP Apps UI)#626
corinagum wants to merge 12 commits into
mainfrom
cg/widgets

Conversation

@corinagum

Copy link
Copy Markdown
Collaborator

Summary

Adds HTML widget support to the Teams TypeScript SDK, enabling bots to send rich interactive MCP Apps UI widgets in Teams messages.

What's included

API types (@microsoft/teams.api)

  • IHtmlWidgetPayload, IHtmlWidgetSecurityPolicy, IHtmlWidgetPermissions
  • IMcpUiCallToolResult, IHtmlWidgetCallToolResponse (wire-format wrapper)
  • IHtmlWidgetCallToolInvokeActivity and invoke response mapping
  • extendedmarkdown added to TextFormat

App helpers (@microsoft/teams.apps)

  • buildHtmlWidgetMarkdown - wraps payload in ```html-widget code fence
  • buildHtmlWidgetMessage - builds a ready-to-send message activity
  • injectWidgetProtocol - optional convenience helper that injects the MCP Apps protocol (ui/initialize handshake, size reporting, notification hooks)
  • validateHtmlWidgetPayload - runtime validation (name, html, domain)
  • widget.callTool invoke route

Example bot (examples/html-widgets/)

  • 10 commands demonstrating the full widget contract
  • Typed widget.callTool handler with multi-tool dispatch

Integration tests

  • Send widget, send with toolInput/toolOutput, update widget, delete widget

Preview markers

  • All public APIs marked @experimental with Diagnostic: ExperimentalTeamsHtmlWidget

Design decisions

  • Protocol injection is opt-in: injectWidgetProtocol is called internally by the builders, but widgets that implement the MCP Apps protocol themselves are returned unchanged (detected by presence of ui/initialize)
  • Notification hooks are explicit: only mapped notification names are injected; unknown names are silently ignored
  • Payload validation throws early: prevents silent client-side "Couldn't load widget" failures
  • Default security policy: restrictive defaults per MCP Apps spec (no external network, self+data resources only)

Related

Status

Draft - pending:

  • Python SDK port
  • .NET core port
  • .NET Libraries port
  • Documentation in teams-sdk/teams.md

corinagum and others added 10 commits June 19, 2026 12:09
Add types for the HTML widget contract:
- IHtmlWidgetPayload, IHtmlWidgetSecurityPolicy, IHtmlWidgetPermissions
- ICallToolRequest, IMcpUiCallToolResult
- IHtmlWidgetCallToolInvokeActivity (htmlwidget/calltool invoke)
- Add 'extendedmarkdown' to TextFormat union
- Add htmlwidget/calltool to InvokeResponseBody map

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add 'htmlwidget/calltool' -> 'widget.callTool' invoke alias
- Add WidgetCallToolRoutes type for typed handler registration
- Add buildHtmlWidgetMarkdown() and buildHtmlWidgetMessage() helpers
- Add unit tests (14 cases including edge cases)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread packages/apps/src/utils/html-widget.ts Fixed
Comment thread packages/apps/src/utils/html-widget.ts Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class “HTML widget” (MCP Apps UI) support to the Teams TypeScript SDK by introducing new API models for widget payloads + callTool invoke wiring, app-side helpers for building/injecting widget protocol HTML, and accompanying examples + tests.

Changes:

  • Added @microsoft/teams.api models for HTML widget payloads/security policy and the htmlwidget/calltool invoke request/response contract.
  • Added @microsoft/teams.apps helpers to build widget markdown/message activities and to optionally inject the MCP Apps protocol + validate security policy references.
  • Added an examples/html-widgets sample bot and integration/unit tests covering send/update/delete and policy validation.

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
test/integration/src/html-widgets.test.ts New integration tests for sending/updating/deleting widget messages using extendedmarkdown.
test/integration/src/fixture.ts Makes meetingId optional and stops requiring TEST_MEETING_ID for integration test config.
test/integration/jest.config.ts Increases integration test timeout for slower widget/canary operations.
packages/apps/src/utils/html-widget.ts New widget helpers: payload validation, protocol injection, markdown/message builders, security policy static validation.
packages/apps/src/utils/html-widget.spec.ts New unit tests validating protocol injection, markdown formatting, payload validation, and security policy warnings.
packages/apps/src/routes/invoke/widget-calltool.ts Adds typed route surface for widget.callTool.
packages/apps/src/routes/invoke/index.ts Adds invoke alias mapping htmlwidget/calltool -> widget.callTool and exports the new route type.
packages/apps/src/index.ts Re-exports the new HTML widget utilities from @microsoft/teams.apps.
packages/api/src/models/invoke-response.ts Adds htmlwidget/calltool invoke response body typing.
packages/api/src/models/index.ts Exports new html-widget models.
packages/api/src/models/html-widget/index.ts Barrel export for widget model types.
packages/api/src/models/html-widget/html-widget-payload.ts Defines widget payload, permissions, and security policy types.
packages/api/src/models/html-widget/call-tool-result.ts Defines MCP UI call tool result + Teams wire wrapper response type.
packages/api/src/models/html-widget/call-tool-request.ts Defines htmlwidget/calltool invoke request payload.
packages/api/src/activities/invoke/index.ts Extends InvokeActivity union to include widget callTool invoke.
packages/api/src/activities/invoke/html-widget/index.ts Barrel export for widget invoke activity types.
packages/api/src/activities/invoke/html-widget/call-tool.ts Defines the htmlwidget/calltool invoke activity shape.
package-lock.json Adds the new example workspace entry and updates lockfile dependencies.
examples/html-widgets/tsconfig.json TypeScript config for the new example workspace.
examples/html-widgets/src/widgets/update-context.ts Example widget exercising ui/update-model-context.
examples/html-widgets/src/widgets/simple.ts Minimal static widget HTML example.
examples/html-widgets/src/widgets/open-link.ts Example widget exercising ui/open-link.
examples/html-widgets/src/widgets/multi-tool.ts Example widget that calls multiple tools (tools/call) for dispatch testing.
examples/html-widgets/src/widgets/messageback.ts Example widget exercising ui/message (messageBack-like behavior).
examples/html-widgets/src/widgets/host-context.ts Example widget showing hostContext + listening for host-context-changed notifications.
examples/html-widgets/src/widgets/fullscreen.ts Example widget requesting fullscreen display mode.
examples/html-widgets/src/widgets/calltool.ts Example widget calling a refresh tool and rendering results.
examples/html-widgets/src/index.ts Example bot wiring commands + typed widget.callTool handler returning the wrapper response.
examples/html-widgets/README.md Example documentation and architecture overview for widget contract.
examples/html-widgets/package.json New example workspace package definition and scripts.
examples/html-widgets/eslint.config.js ESLint config wiring for the new example workspace.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +25 to +27
if (!payload.domain?.trim() || !payload.domain.startsWith('https://')) {
throw new Error('HTML widget payload requires "domain" to be a valid URL starting with "https://".');
}
* @experimental This API is in preview and may change in the future.
* Diagnostic: ExperimentalTeamsHtmlWidget
*/
function validateHtmlWidgetPayload(payload: IHtmlWidgetPayload): void {
export { toThreadedConversationId } from './utils/thread';

// HTML Widget utilities
export { buildHtmlWidgetMarkdown, buildHtmlWidgetMessage, injectWidgetProtocol, validateSecurityPolicy } from './utils/html-widget';
Comment on lines +31 to +32
Bot returns:
{ status: 200, body: { content: [...], structuredContent: {...}, isError: false } }
+ 'if(d.id===id&&d.result){window.parent.postMessage({jsonrpc:\'2.0\',method:\'ui/notifications/initialized\'},\'*\');setTimeout(notifySize,100);}'
+ hookLines
+ '});'
+ `window.parent.postMessage({jsonrpc:'2.0',id:id,method:'ui/initialize',params:{protocolVersion:'${MCP_PROTOCOL_VERSION}',appInfo:{name:'${name}',version:'${version}'},appCapabilities:${capsJson}}},'*');`
// connectDomains: fetch(), XMLHttpRequest.open(), new WebSocket(), new EventSource()
const connectPatterns: Array<{ regex: RegExp; source: string }> = [
{ regex: /fetch\(\s*["']([^"']+)["']/gi, source: 'fetch()' },
{ regex: /\.open\(\s*["'][A-Z]+["']\s*,\s*["']([^"']+)["']/gi, source: 'XMLHttpRequest.open()' },
test target. It verifies that:
- The SDK's invoke route alias correctly matches `htmlwidget/calltool`
- The typed handler receives `{ name, arguments }` in `activity.value`
- The response body format (`McpUiCallToolResult`) is accepted by the client
const iframeTagRegex = /<iframe\s[^>]*>/gi;
const iframeSrcRegex = /src=["']([^"']+)["']/i;
let tagMatch;
while ((tagMatch = iframeTagRegex.exec(html)) !== null) {
// connectDomains: <form action> (form submissions can exfiltrate data)
const formTagRegex = /<form\s[^>]*>/gi;
const formActionRegex = /action=["']([^"']+)["']/i;
while ((tagMatch = formTagRegex.exec(html)) !== null) {
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.

3 participants