From 923fd0bbe023abae9811b35e9ea606f409e5f947 Mon Sep 17 00:00:00 2001 From: Rabia Williams Date: Thu, 30 Apr 2026 12:04:12 +1000 Subject: [PATCH] updates to add react fluent ui component and mcp app patterns Co-authored-by: Copilot --- .../skills/ui-widget-developer/SKILL.md | 69 ++- .../references/best-practices.md | 403 +++++++++------ .../references/copilot-widget-protocol.md | 38 +- .../references/mcp-server-pattern.md | 4 +- .../references/widget-patterns.md | 481 +++++++----------- 5 files changed, 520 insertions(+), 475 deletions(-) diff --git a/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/SKILL.md b/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/SKILL.md index d36572f..4cc85f7 100644 --- a/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/SKILL.md +++ b/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/SKILL.md @@ -3,7 +3,7 @@ name: ui-widget-developer description: | Build MCP servers for Copilot Chat using the OpenAI Apps SDK or MCP Apps SDK widget rendering support (any language). Use this skill when: - Creating MCP servers that integrate with M365 Copilot declarative agents - - Building rich interactive widgets (HTML) that render in Copilot Chat + - Building rich interactive widgets (React + Fluent UI) that render in Copilot Chat - Implementing tools that return structuredContent for widget rendering - Adapting an existing MCP server to support Copilot widget rendering - Setting up devtunnels for localhost MCP server exposure @@ -33,7 +33,15 @@ description: | ### MCP Apps: Install ext-apps Plugin -If the user chooses MCP Apps, provide these instructions: +If the user chooses MCP Apps, do this automatically (do not stop at explanation-only): + +1. Run `/plugin marketplace add modelcontextprotocol/ext-apps` +2. Run `/plugin install mcp-apps@mcp-apps` +3. Confirm the plugin is available, then invoke the correct ext-apps skill based on user intent + +If plugin commands are unavailable in the current environment, provide the exact commands below and ask the user to run them once, then continue by invoking the selected ext-apps skill. + +Reference commands: ``` To build an MCP App, install the ext-apps plugin from the marketplace: @@ -52,6 +60,12 @@ After installing, invoke the relevant skill to continue. > **Note:** The ext-apps plugin lives in the external `modelcontextprotocol/ext-apps` marketplace — it is not part of this plugin collection. +**Handoff mapping after install:** +- New MCP app from scratch → `create-mcp-app` +- Add app UI to existing MCP server → `add-app-to-server` +- Migrate existing OAI app → `migrate-oai-app` +- Convert an existing web app → `convert-web-app` + --- ## 📛 PROJECT DETECTION 📛 @@ -72,6 +86,21 @@ This skill triggers when building MCP servers with OAI app or widget rendering f ## 🚨 CRITICAL EXECUTION RULES 🚨 + +**FLUENT UI ENFORCEMENT (REQUIRED):** Widget implementations MUST use React + Fluent UI components. Before writing any widget code, the agent MUST read and follow: +- `references/widget-patterns.md` +- `references/best-practices.md` +**FLUENT UI PACKAGE REQUIREMENT (REQUIRED):** The widget project MUST include Fluent UI dependencies before implementation. At minimum, install and keep these in the widget package dependencies: +- `@fluentui/react-components` +- `react` +- `react-dom` + +If any of these packages are missing, install them automatically before continuing with widget code generation. + +If the generated widget does not include React entry files (for example `widgets/src//main.tsx` and a React component file) and Fluent imports from `@fluentui/react-components`, the task is incomplete and MUST be corrected before returning results. + +**NO RAW HTML-ONLY WIDGETS (DEFAULT):** Do not implement app content directly with static HTML templates and inline JS as the final widget solution. A minimal shell HTML file is allowed only as a loader for built React assets. Raw/self-contained HTML-only widgets are allowed only when the user explicitly requests a non-React prototype. + **BACKGROUND PROCESSES:** MCP server and devtunnel MUST be spawned as independent OS processes — NOT run inside the agent's shell session. `isBackground: true`, `mode: "async"`, and `Start-Job` all run inside the agent's shell session and will be killed between messages. The only reliable approach is to spawn a detached OS process. **Windows — use `Start-Process -WindowStyle Hidden`:** @@ -119,7 +148,7 @@ sleep 3 && tail tunnel.log server.log **There is no exception to this rule.** The most common failure mode is reasoning "the user's request makes it obvious, so asking is redundant." This reasoning is always wrong — invoke `AskUserQuestion` regardless. A user saying "build an MCP server with widgets" is NOT an answer to this question. A user invoking this skill by name is NOT an answer. Only an explicit answer to the question counts. See [PATH SELECTION](#-path-selection) above for the exact question to ask. -**AGENT PROVISIONING:** Re-provisioning is only required when the **agent manifest** changes (e.g., mcpPlugin.json tool definitions, MCP server URL, declarativeAgent.json, instruction.txt). MCP server code changes (tool implementations, widget HTML, server logic) do **NOT** require re-provisioning the agent — running or deploying the server picks up changes automatically. +**AGENT PROVISIONING:** Re-provisioning is only required when the **agent manifest** changes (e.g., mcpPlugin.json tool definitions, MCP server URL, declarativeAgent.json, instruction.txt). MCP server code changes (tool implementations, React widget code, server logic) do **NOT** require re-provisioning the agent — running or deploying the server picks up changes automatically. When provisioning is needed: 1. **Bump the version** in `manifest.json` (increment the patch version, e.g., `1.0.0` → `1.0.1`) @@ -159,22 +188,24 @@ Other envs: {SHARE_LINK from env/.env.{environment}} **AGENT PROJECT DELEGATION:** This skill builds MCP servers and widgets, NOT declarative agent projects. If the user's request involves creating or configuring the declarative agent itself (scaffolding, `m365agents.yml`, `m365agents.local.yml`, `declarativeAgent.json`, manifest lifecycle), delegate to the `declarative-agent-developer` skill. -**MCP RESOURCE REGISTRATION:** Every widget MUST have a matching MCP resource. Without resources, Copilot cannot fetch widget HTML through the MCP protocol and widgets will not render. +**MCP RESOURCE REGISTRATION:** Every widget MUST have a matching MCP resource. Without resources, Copilot cannot fetch widget shells through the MCP protocol and widgets will not render. For each new widget, complete this checklist: -1. ☐ Create widget HTML file in `widgets/` directory (see widget-patterns.md) +1. ☐ Create a widget shell HTML file in `widgets/` and a React widget entry under `widgets/src//` (see widget-patterns.md) 2. ☐ Define a `ui://widget/.html` URI constant 3. ☐ Add a `Resource` entry to the `resources` array with: - `uri`: the `ui://widget/.html` URI - `mimeType`: `"text/html+skybridge"` - `_meta`: CSP config with `openai/widgetDomain` and `openai/widgetCSP` (from environment) -4. ☐ Add a handler for `resources/read` that returns the widget HTML for this URI +4. ☐ Add a handler for `resources/read` that returns the widget shell HTML for this URI 5. ☐ Add the tool with `_meta.openai/outputTemplate` pointing to the same `ui://widget/.html` URI 6. ☐ Verify the server capabilities include `resources: {}` in the initialize response -**Widget HTML size considerations:** -- **Simple widgets**: The `resources/read` handler can return the full self-contained HTML (inline CSS/JS) -- **Complex widgets** (React, large UIs): The resource HTML should be a minimal shell that links to JS/CSS assets served from the MCP server's `/assets/` route: +**Widget shell + asset considerations:** +- **Preferred (React + Fluent UI)**: Resource HTML should be a minimal shell that links to built JS/CSS assets served from the MCP server's `/assets/` route. +- **Exception only**: Self-contained HTML via `resources/read` is for explicit user-requested prototypes only. Default and production path is React + Fluent UI. + +Example shell for React build output: ```html @@ -218,7 +249,7 @@ Build MCP servers that integrate with Microsoft 365 Copilot Chat and render rich ## Architecture ``` -M365 Copilot ──▶ mcpPlugin.json ──▶ MCP Server ──▶ structuredContent ──▶ HTML Widget +M365 Copilot ──▶ mcpPlugin.json ──▶ MCP Server ──▶ structuredContent ──▶ React + Fluent UI Widget │ (RemoteMCPServer) (Streamable HTTP) (window.openai.toolOutput) │ └── Capabilities (People, etc.) provide data to pass to MCP tools @@ -237,7 +268,10 @@ project/ │ └── instruction.txt # Agent behavior instructions ├── mcp-server/ │ ├── src/index.ts # Server with Streamable HTTP -│ ├── widgets/*.html # OpenAI Apps SDK widgets +│ ├── widgets/ # Widget shells + React source +│ │ ├── my-widget.html # Minimal shell returned by resources/read +│ │ └── src/my-widget/ # React + Fluent UI source +│ ├── assets/ # Built widget bundles served at /assets │ └── package.json ├── scripts/ │ ├── setup-devtunnel.sh # Linux/Mac devtunnel setup @@ -256,7 +290,7 @@ Your MCP server must implement these protocol requirements to render widgets in 3. **Server capabilities** — `initialize` response must declare `resources: {}` and `tools: {}` 4. **MCP resources** — Register widgets with `ui://widget/.html` URIs, `text/html+skybridge` mime type, and CSP `_meta` 5. **Tool response format** — Return `content` (text) + `structuredContent` (widget data) + `_meta` with `openai/outputTemplate` -6. **Widget serving** — HTTP route at `/widgets/*.html` with origin-checking CORS +6. **Widget serving** — HTTP route at `/widgets/*.html` for shell files and `/assets/*` for built bundles, both with origin-checking CORS For full protocol details, JSON shapes, and an adaptation checklist for existing MCP servers, see [references/copilot-widget-protocol.md](references/copilot-widget-protocol.md). @@ -310,10 +344,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) See [references/widget-patterns.md](references/widget-patterns.md) for complete examples. Core requirements: -- Access data: `window.openai.toolOutput` (primary source) -- Theme support: `window.openai.theme` or `prefers-color-scheme` -- Debug fallback: Embedded mock data when `window.openai` unavailable -- CSS variables for theming at `:root` level +- Use React + Fluent UI components (`@fluentui/react-components`) +- Ensure widget package dependencies include `@fluentui/react-components`, `react`, and `react-dom` +- Theme with `FluentProvider` (`webLightTheme`/`webDarkTheme`) and Fluent `tokens` +- Access data through shared hooks (e.g., `useOpenAiGlobal("toolOutput")`) +- Debug fallback: embedded mock data when `window.openai` unavailable - Handle "Unknown" values gracefully (e.g., hide action buttons) ### Plugin Schema @@ -377,7 +412,7 @@ See [references/best-practices.md](references/best-practices.md) for detailed gu Key points: 1. **Rendering tools**: Accept data as input, don't fetch internally 2. **Instructions**: Tell agent to use capabilities FIRST, then pass data to MCP tools -3. **Themes**: Always support dark/light via CSS variables +3. **Themes**: Use `FluentProvider` + Fluent `tokens` for dark/light support 4. **Debug mode**: Include fallback data for local widget testing 5. **Partial data**: Handle missing fields with "Unknown" defaults 6. **Action buttons**: Hide email/chat buttons when data is "Unknown" diff --git a/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/references/best-practices.md b/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/references/best-practices.md index 7d37f35..a3ef6d5 100644 --- a/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/references/best-practices.md +++ b/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/references/best-practices.md @@ -5,24 +5,65 @@ > tool response format, widget-resource-tool triplet) are language-agnostic concepts. ## Table of Contents -- [1. Rendering Tools Pattern](#1-rendering-tools-pattern) -- [2. Handle Partial Data](#2-handle-partial-data) -- [3. Agent Instructions](#3-agent-instructions) -- [4. Theme Support](#4-theme-support) -- [5. Debug Mode](#5-debug-mode) -- [6. Version Management](#6-version-management) -- [7. Input Schema Descriptions](#7-input-schema-descriptions) -- [8. Consistent Tool Definitions](#8-consistent-tool-definitions) -- [9. CORS Configuration](#9-cors-configuration) -- [10. Widget Security](#10-widget-security) -- [11. DevTunnels](#11-devtunnels) -- [12. Declarative Agent Capabilities](#12-declarative-agent-capabilities) -- [13. Error Handling in Widgets](#13-error-handling-in-widgets) -- [14. Tool Response Format](#14-tool-response-format) -- [15. Conversation Starters](#15-conversation-starters) -- [16. Widget-Resource-Tool Triplet](#16-widget-resource-tool-triplet) - -## 1. Rendering Tools Pattern +- [1. ALWAYS Use Fluent UI for Widgets](#1-always-use-fluent-ui-for-widgets) +- [2. Rendering Tools Pattern](#2-rendering-tools-pattern) +- [3. Handle Partial Data](#3-handle-partial-data) +- [4. Agent Instructions](#4-agent-instructions) +- [5. Theme Support with FluentProvider](#5-theme-support-with-fluentprovider) +- [6. Use Shared Hooks](#6-use-shared-hooks) +- [7. Build Before Serve](#7-build-before-serve) +- [8. Debug Mode](#8-debug-mode) +- [9. Version Management](#9-version-management) +- [10. Input Schema Descriptions](#10-input-schema-descriptions) +- [11. Consistent Tool Definitions](#11-consistent-tool-definitions) +- [12. CORS Configuration](#12-cors-configuration) +- [13. Widget Security](#13-widget-security) +- [14. DevTunnels](#14-devtunnels) +- [15. MCP Server Working Directory](#15-mcp-server-working-directory) +- [16. Environment Variable Initialization](#16-environment-variable-initialization) +- [17. Declarative Agent Capabilities](#17-declarative-agent-capabilities) +- [18. Tool Response Format](#18-tool-response-format) +- [19. Conversation Starters](#19-conversation-starters) +- [20. Widget-Resource-Tool Triplet](#20-widget-resource-tool-triplet) + +## 1. ALWAYS Use Fluent UI for Widgets + +MANDATORY: All widget UI must be built with React and `@fluentui/react-components`. +Do not use raw HTML/CSS templates or other UI frameworks for widget rendering. + +Required Fluent UI components: +- `Card` for containers +- `Badge` for labels/status +- `Table` or `DataGrid` for tabular data +- `Button` for actions +- `Avatar` for entity visuals +- `Tooltip` for hints +- `Spinner` for loading states +- `tokens` and `makeStyles` for styling + +```tsx +import { Card, Badge, makeStyles, tokens } from "@fluentui/react-components"; + +const useStyles = makeStyles({ + root: { + padding: tokens.spacingVerticalM, + backgroundColor: tokens.colorNeutralBackground1, + }, +}); + +export function MyWidget() { + const styles = useStyles(); + return ( +
+ + Title + +
+ ); +} +``` + +## 2. Rendering Tools Pattern Design MCP tools as **rendering tools** that accept data from the caller rather than fetching data internally. @@ -33,56 +74,63 @@ Design MCP tools as **rendering tools** that accept data from the caller rather **Pattern**: ```typescript -// Good: Accept data as input -inputSchema: { - type: "object", - properties: { - items: { type: "array", items: { /* ... */ } } - } -} +// Good: accept and validate caller-provided data +const parser = z.object({ + items: z.array(z.object({ name: z.string(), value: z.string() })), +}); // Avoid: Fetching data internally // const data = await fetchFromAPI(); // Don't do this ``` -## 2. Handle Partial Data +## 3. Handle Partial Data Always normalize input data to handle missing fields gracefully. Fill in "Unknown" for any missing properties. -**Server Pattern**: +**Server Pattern** - use Zod defaults: ```typescript +const parser = z.object({ + title: z.string().default("Untitled"), + items: z.array(z.object({ + name: z.string().default("Unknown"), + email: z.string().default("Unknown"), + location: z.string().default("Unknown"), + })).default([]), +}); + server.setRequestHandler(CallToolRequestSchema, async (request) => { - const args = request.params.arguments as { title?: string; items?: Partial[] }; - - // Normalize data - fill in "Unknown" for missing fields - const title = args.title || "Default Title"; - const items = (args.items || []).map(item => ({ - name: item.name || "Unknown", - email: item.email || "Unknown", - location: item.location || "Unknown", - })); - - const structuredContent = { title, items }; - // ... + const parsed = parser.parse(request.params.arguments ?? {}); + return { + content: [{ type: "text", text: `Rendered ${parsed.items.length} items` }], + structuredContent: parsed, + _meta: invocationMeta(MY_WIDGET), + }; }); ``` -**Widget Pattern** - Hide action buttons when data is "Unknown": -```javascript -function renderItem(item) { - const hasEmail = item.email && item.email !== 'Unknown'; - const actionsHtml = hasEmail ? ` - Email - Chat - ` : ''; - - return ` -
-
${escapeHtml(item.name || 'Unknown')}
- ${actionsHtml} -
- `; +**Widget Pattern** - hide action buttons when data is "Unknown": +```tsx +import { Button } from "@fluentui/react-components"; +import { MailRegular, ChatRegular } from "@fluentui/react-icons"; + +function ContactActions({ email }: { email: string }) { + if (!email || email === "Unknown") return null; + return ( + <> + + + + ); } ``` @@ -98,7 +146,7 @@ function renderItem(item) { } ``` -## 3. Agent Instructions +## 4. Agent Instructions Tell the agent to use capabilities FIRST, then pass data to MCP tools. @@ -113,27 +161,77 @@ fetch data. You must: CRITICAL: Never call the MCP tools without first retrieving data from the capability. ``` -## 4. Theme Support +## 5. Theme Support with FluentProvider + +Theme should be handled by `FluentProvider` in widget entry points. + +```tsx +import { FluentProvider, webLightTheme, webDarkTheme } from "@fluentui/react-components"; + +function getTheme() { + const openaiTheme = (window as any).openai?.theme; + if (openaiTheme === "dark") return webDarkTheme; + if (openaiTheme === "light") return webLightTheme; + return window.matchMedia?.("(prefers-color-scheme: dark)").matches + ? webDarkTheme + : webLightTheme; +} +``` -See [widget-patterns.md](widget-patterns.md) for complete theme CSS variable implementation. Key rule: always support dark/light mode via CSS variables at `:root` level, detecting via `window.openai.theme` or `prefers-color-scheme`. +Never hand-code color systems for widget UI. Use Fluent `tokens` + `makeStyles`. -## 5. Debug Mode +## 6. Use Shared Hooks + +Reuse shared hooks from [widget-patterns.md](widget-patterns.md): +- `useOpenAiGlobal(key)` for polling `window.openai[key]` +- `useThemeColors()` for semantic theme palette +- `useWidgetState(initial)` for state persistence through Apps SDK host + +```tsx +function MyWidget() { + const toolOutput = useOpenAiGlobal("toolOutput"); + const [state, setState] = useWidgetState({ expanded: false }); + + if (!toolOutput) return ; + + return {/* render with toolOutput */}; +} +``` + +## 7. Build Before Serve + +Always build widgets before starting the server. The server serves pre-built assets. + +```bash +npm run install:all +npm run build:widgets +npm run dev:server +``` + +After every widget code change: + +```bash +npm run build:widgets +``` + +Server reads assets on each request; restart is typically not required after rebuild. + +## 8. Debug Mode Include fallback data for local widget testing without BizChat. -```javascript -const DEBUG_DATA = { /* realistic test data */ }; +```tsx +const DEBUG_DATA = { title: "Test", items: [{ name: "Alice", value: "123" }] }; -function getWidgetData() { - if (window.openai) { - return window.openai.toolOutput || /* other sources */; - } - console.log('Debug mode - using DEBUG_DATA'); +function useToolData() { + const toolOutput = useOpenAiGlobal("toolOutput"); + if (toolOutput) return toolOutput; + console.log("Debug mode - using DEBUG_DATA"); return DEBUG_DATA; } ``` -## 6. Version Management +## 9. Version Management Bump manifest version for each deployment when changes aren't reflected. @@ -142,7 +240,7 @@ Bump manifest version for each deployment when changes aren't reflected. { "version": "1.0.5" } // Increment on each change ``` -## 7. Input Schema Descriptions +## 10. Input Schema Descriptions Provide detailed descriptions with examples and default values in inputSchema. @@ -161,7 +259,7 @@ Provide detailed descriptions with examples and default values in inputSchema. } ``` -## 8. Consistent Tool Definitions +## 11. Consistent Tool Definitions Keep inputSchema identical in: 1. MCP server tool definitions @@ -169,56 +267,99 @@ Keep inputSchema identical in: Mismatches cause runtime errors. -## 9. CORS Configuration +## 12. CORS Configuration -Always configure CORS for the MCP endpoint. Use origin-checking instead of a wildcard — validate the `Origin` header against an allowlist and reflect it back. - -**Required allowed origins**: `m365.cloud.microsoft` and `*.m365.cloud.microsoft`. +Always configure CORS with an allowlist origin check. ```typescript -// Origin checking — allow *.m365.cloud.microsoft and m365.cloud.microsoft -function isAllowedOrigin(origin: string | undefined): boolean { - if (!origin) return false; - try { - const { hostname } = new URL(origin); - return hostname === "m365.cloud.microsoft" || hostname.endsWith(".m365.cloud.microsoft"); - } catch { - return false; - } -} +import cors from "cors"; + +const corsOptions: cors.CorsOptions = { + origin: (origin, callback) => { + if (isOriginAllowed(origin)) { + callback(null, origin ?? true); + } else { + callback(null, false); + } + }, + methods: ["GET", "POST", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Content-Type", "Accept", + "Mcp-Session-Id", "mcp-session-id", + "Last-Event-ID", + "Mcp-Protocol-Version", "mcp-protocol-version", + ], + exposedHeaders: ["Mcp-Session-Id"], + credentials: false, +}; -// Preflight -if (req.method === "OPTIONS" && url.pathname === "/mcp") { - const origin = req.headers.origin; - if (isAllowedOrigin(origin)) { - res.writeHead(204, { - "Access-Control-Allow-Origin": origin!, - "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, mcp-session-id, Last-Event-ID, mcp-protocol-version", - "Access-Control-Expose-Headers": "mcp-session-id, mcp-protocol-version", - }); - } -} +app.use(cors(corsOptions)); +app.options("*", cors(corsOptions)); +``` -// All MCP requests — reflect origin if allowed -const origin = req.headers.origin; -if (isAllowedOrigin(origin)) { - res.setHeader("Access-Control-Allow-Origin", origin!); - res.setHeader("Access-Control-Expose-Headers", "mcp-session-id, mcp-protocol-version"); -} +See [mcp-server-pattern.md](mcp-server-pattern.md) for full allowlist and `isOriginAllowed()` implementation. + +## 13. Widget Security + +React escapes JSX by default. Do not use `dangerouslySetInnerHTML`. + +```tsx +// Safe +{userData.name} + +// Unsafe in widgets +//
+``` + +Validate dynamic URLs before rendering links. + +## 14. DevTunnels + +Use random tunnels for simple local loops: + +```bash +devtunnel host -p 3001 --allow-anonymous +``` + +Pre-flight: +1. Kill old tunnels: `pkill -f "devtunnel host" 2>/dev/null` +2. Verify auth: `devtunnel user show` +3. Verify server: `curl -s http://localhost:3001/health` + +Because URL changes each run, update `SERVER_BASE_URL` and provision again. + +## 15. MCP Server Working Directory + +Monorepo pattern (`server/` and `widgets/` each with separate package files): + +```bash +# From mcp-server root +npm run install:all +npm run build:widgets +npm run dev:server +npm run start + +# Directly +cd server && npm run dev +cd widgets && npm run build ``` -## 10. Widget Security +Common failure: running `npm run dev` at root when only `dev:server` is defined. + +## 16. Environment Variable Initialization -See the **Security** section in [widget-patterns.md](widget-patterns.md) for the `escapeHtml` implementation and XSS prevention patterns. Key rule: always sanitize user-provided data before inserting into widget HTML. +Before first provision, populate all `${{VAR_NAME}}` placeholders used by `appPackage/` +in `env/.env.local`. -## 11. DevTunnels +Set at least: -Use **named tunnels** for stable URLs that persist across restarts. See [devtunnels.md](devtunnels.md) for complete setup scripts and command reference. +```env +SERVER_BASE_URL=http://localhost:3001 +``` -Key behavior: the URL stays the same across restarts, so `npx -y --package @microsoft/m365agentstoolkit-cli atk provision` is only needed once (and again only when the agent manifest changes — mcpPlugin.json, declarativeAgent.json, etc.). +This placeholder allows initial provision before a tunnel URL is available. -## 12. Declarative Agent Capabilities +## 17. Declarative Agent Capabilities Only enable capabilities you need. @@ -236,28 +377,7 @@ Available: - `OneDriveAndSharePoint` - File access - `WebSearch` - Web search -## 13. Error Handling in Widgets - -Handle data loading failures gracefully. - -```javascript -if (!rendered) { - let attempts = 0; - const poll = setInterval(() => { - attempts++; - const data = getWidgetData(); - if (data) { - renderWidget(data); - clearInterval(poll); - } else if (attempts >= 50) { - clearInterval(poll); - showError("Unable to load data"); - } - }, 100); -} -``` - -## 14. Tool Response Format +## 18. Tool Response Format Always include both text content and structuredContent. @@ -265,18 +385,13 @@ Always include both text content and structuredContent. return { content: [{ type: "text", text: "Human-readable summary" }], structuredContent: { /* data for widget */ }, - _meta: { - "openai/outputTemplate": "ui://widget/name.html", - "openai/widgetAccessible": true, - "openai/toolInvocation/invoking": "Processing...", - "openai/toolInvocation/invoked": "Complete", - } + _meta: invocationMeta(MY_WIDGET), }; ``` -The text content serves as fallback and accessibility. The `_meta` fields are **required** — without `openai/widgetAccessible: true`, Copilot will not render the widget and the tool response will appear as null. +The text content serves as fallback and accessibility. -## 15. Conversation Starters +## 19. Conversation Starters Add relevant conversation starters to help users discover your agent's capabilities. @@ -293,23 +408,23 @@ Add relevant conversation starters to help users discover your agent's capabilit } ``` -## 16. Widget-Resource-Tool Triplet +## 20. Widget-Resource-Tool Triplet Every widget in a Copilot MCP server requires three coordinated parts: | Part | What it does | Where it lives | |------|-------------|----------------| -| **Widget HTML** | The rendered UI | `widgets/.html` (simple) or `assets/.js` + shell HTML (complex) | -| **MCP Resource** | Serves widget HTML to Copilot via `ui://widget/.html` | `resources` array + `ReadResourceRequestSchema` handler | +| **Widget shell + assets** | Shell HTML loads rendered React + Fluent UI bundle | `widgets/.html` + `assets/.js` | +| **MCP Resource** | Serves widget shell to Copilot via `ui://widget/.html` | `resources` array + `ReadResourceRequestSchema` handler | | **MCP Tool** | Triggers widget rendering via `_meta.openai/outputTemplate` | `tools` array + `CallToolRequestSchema` handler | If any part is missing: -- No Resource → Copilot can't fetch widget HTML, widget won't render +- No Resource → Copilot can't fetch widget shell, widget won't render - No Tool → Widget exists but nothing triggers it -- No Widget HTML → Resource returns 404, tool invocation shows empty widget +- No shell/assets → Resource returns 404 or shell loads without scripts, tool invocation shows empty widget **Simple vs Complex widgets:** -- Simple: Self-contained HTML with inline CSS/JS → `ReadResource` returns the full file -- Complex (React, etc.): Minimal HTML shell linking to JS/CSS assets served via `/assets/` route → `ReadResource` returns the shell, assets load from `MCP_SERVER_URL/assets/` +- Simple: Self-contained shell/widget for quick validation → `ReadResource` returns full file +- Complex (React + Fluent UI, preferred): Minimal HTML shell linking to JS/CSS assets served via `/assets/` route → `ReadResource` returns shell HTML, assets load from `MCP_SERVER_URL/assets/` Always create all three parts together. When adding a new tool+widget, start from the resource pattern in [mcp-server-pattern.md](mcp-server-pattern.md). diff --git a/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/references/copilot-widget-protocol.md b/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/references/copilot-widget-protocol.md index 0b9c552..8cab286 100644 --- a/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/references/copilot-widget-protocol.md +++ b/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/references/copilot-widget-protocol.md @@ -8,7 +8,7 @@ Language-agnostic protocol requirements for MCP servers that render widgets in M - [Server Capabilities](#server-capabilities) - [MCP Resources for Widgets](#mcp-resources-for-widgets) - [MCP Tool Response Format](#mcp-tool-response-format) -- [Widget HTML Serving](#widget-html-serving) +- [Widget Shell and Asset Serving](#widget-shell-and-asset-serving) - [Widget-Resource-Tool Triplet](#widget-resource-tool-triplet) - [Environment Configuration](#environment-configuration) - [Adaptation Checklist: Existing MCP Server](#adaptation-checklist-existing-mcp-server) @@ -107,7 +107,7 @@ Without `resources: {}`, Copilot will not call `resources/list` or `resources/re ## MCP Resources for Widgets -Each widget requires an MCP resource registration. Resources tell Copilot how to fetch widget HTML via the MCP protocol. +Each widget requires an MCP resource registration. Resources tell Copilot how to fetch the widget shell via the MCP protocol. **`resources/list` response:** ```json @@ -137,7 +137,7 @@ Each widget requires an MCP resource registration. Resources tell Copilot how to { "uri": "ui://widget/my-widget.html", "mimeType": "text/html+skybridge", - "text": "...widget HTML...", + "text": "...widget shell HTML...", "_meta": { "openai/widgetDomain": "https://your-server.example.com", "openai/widgetCSP": { @@ -187,22 +187,24 @@ Tool responses must include three parts: a text summary, structured data for the | `_meta.openai/toolInvocation/invoking` | Status text shown while tool executes | | `_meta.openai/toolInvocation/invoked` | Status text shown when tool completes | -## Widget HTML Serving +## Widget Shell and Asset Serving -Your server must serve widget HTML files over HTTP at a `/widgets/` route: +Your server must serve widget shell HTML files over HTTP at a `/widgets/` route: ``` GET /widgets/my-widget.html → 200 OK (Content-Type: text/html, Access-Control-Allow-Origin: ) ``` Requirements: -- Serve files from a `widgets/` directory (or equivalent) +- Serve shell files from a `widgets/` directory (or equivalent) +- Serve built JS/CSS bundles from an `/assets/` route - Apply the same origin-checking CORS as the `/mcp` endpoint (see [CORS Configuration](#cors-configuration)) -- Guard against path traversal (ensure resolved paths stay within the widgets directory) +- Guard against path traversal for both widgets and assets directories -Widget HTML uses the OpenAI Apps SDK — see [widget-patterns.md](widget-patterns.md) for templates and patterns. Key points: -- Data is available at `window.openai.toolOutput` -- Theme detection via `window.openai.theme` or `prefers-color-scheme` +Widgets should be built with React + Fluent UI and loaded by the shell. See [widget-patterns.md](widget-patterns.md) for templates and patterns. Key points: +- Wrap app UI with `FluentProvider` (`webLightTheme`/`webDarkTheme`) +- Build UI with `@fluentui/react-components` and `@fluentui/react-icons` +- Read data from `window.openai` through shared hooks (for example, `useOpenAiGlobal("toolOutput")`) - Include debug fallback data for local testing ## Widget-Resource-Tool Triplet @@ -211,16 +213,16 @@ Every widget requires three coordinated parts. If any part is missing, the widge | Part | What it does | Key identifiers | |------|-------------|-----------------| -| **Widget HTML** | The rendered UI served via HTTP | `GET /widgets/.html` | -| **MCP Resource** | Serves widget HTML to Copilot via MCP protocol | `uri: "ui://widget/.html"`, `mimeType: "text/html+skybridge"` | +| **Widget shell + assets** | Shell returned by resource loads built React bundle | `GET /widgets/.html`, `GET /assets/.js` | +| **MCP Resource** | Serves shell HTML to Copilot via MCP protocol | `uri: "ui://widget/.html"`, `mimeType: "text/html+skybridge"` | | **MCP Tool** | Triggers widget rendering, returns data | `_meta.openai/outputTemplate: "ui://widget/.html"` | **When a part is missing:** -- **No Resource** → Copilot can't fetch widget HTML, widget won't render +- **No Resource** → Copilot can't fetch the shell, widget won't render - **No Tool** → Widget exists but nothing triggers it -- **No Widget HTML** → Resource returns empty/404, tool invocation shows empty widget +- **No shell/assets** → Resource loads empty/404 or missing scripts, tool invocation shows empty widget -Always create all three parts together for each new widget. See [best-practices.md](best-practices.md#16-widget-resource-tool-triplet) for additional detail. +Always create all three parts together for each new widget. See [best-practices.md](best-practices.md#20-widget-resource-tool-triplet) for additional detail. ## Environment Configuration @@ -251,12 +253,12 @@ The resource `_meta` CSP fields must reference these values so that Copilot's Co If you have an existing MCP server and want to add Copilot widget support, complete this checklist: - [ ] Add `resources: {}` capability to your server's `initialize` response (see [Server Capabilities](#server-capabilities)) -- [ ] Create widget HTML files in a `widgets/` directory (see [widget-patterns.md](widget-patterns.md) for templates) +- [ ] Create widget shell HTML files in `widgets/` and build React + Fluent UI bundles into `assets/` (see [widget-patterns.md](widget-patterns.md)) - [ ] Register MCP resources with `ui://widget/.html` URIs and `text/html+skybridge` mime type (see [MCP Resources for Widgets](#mcp-resources-for-widgets)) - [ ] Add `_meta` to resources with CSP configuration (`openai/widgetDomain`, `openai/widgetCSP`) -- [ ] Implement `resources/read` handler that returns widget HTML for each registered URI +- [ ] Implement `resources/read` handler that returns shell HTML for each registered URI - [ ] Update tool responses to return `structuredContent` + `_meta` with `openai/outputTemplate` (see [MCP Tool Response Format](#mcp-tool-response-format)) -- [ ] Add `/widgets/*.html` HTTP serving route with origin-checking CORS (see [CORS Configuration](#cors-configuration)) +- [ ] Add `/widgets/*.html` and `/assets/*` HTTP serving routes with origin-checking CORS (see [CORS Configuration](#cors-configuration)) - [ ] Configure CORS on `/mcp` endpoint (see [CORS Configuration](#cors-configuration)) - [ ] Create `mcpPlugin.json` manifest (see [plugin-schema.md](plugin-schema.md)) - [ ] Set up devtunnel for local testing (see [devtunnels.md](devtunnels.md)) diff --git a/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/references/mcp-server-pattern.md b/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/references/mcp-server-pattern.md index 2ee4f6b..fd5c4f2 100644 --- a/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/references/mcp-server-pattern.md +++ b/plugins/microsoft-365-agents-toolkit/skills/ui-widget-developer/references/mcp-server-pattern.md @@ -381,7 +381,7 @@ httpServer.listen(port, () => { For local development, the MCP server can serve static assets (JS, CSS, images) that widgets reference via ` +
+ + +// main.tsx +import React from "react"; +import { createRoot } from "react-dom/client"; +import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components"; +import { Widget } from "./Widget"; +import { useOpenAiGlobal } from "../hooks/useOpenAiGlobal"; + +function App() { + const theme = (useOpenAiGlobal("theme") ?? "light").toLowerCase(); + return ( + + + + ); +} + +createRoot(document.getElementById("root")!).render(); + +// Widget.tsx +import React from "react"; +import { + Body1, + Card, + Table, + TableBody, + TableCell, + TableCellLayout, + TableHeader, + TableHeaderCell, + TableRow, + Title3, + makeStyles, + tokens, +} from "@fluentui/react-components"; +import { useOpenAiGlobal } from "../hooks/useOpenAiGlobal"; + +type WidgetData = { + title?: string; + items?: Array<{ name: string; value: string }>; +}; + +const useStyles = makeStyles({ + root: { padding: "16px", display: "grid", gap: "12px" }, + empty: { color: tokens.colorNeutralForeground3 }, +}); + +export function Widget() { + const styles = useStyles(); + const data = useOpenAiGlobal("toolOutput") ?? { title: "Untitled", items: [] }; + + if (!data.items?.length) { + return
No items
; + } + + return ( +
+ {data.title ?? "Untitled"} + + + + + Name + Value + + + + {data.items.map((item, idx) => ( + + {item.name} + {item.value} + + ))} + +
+
+
+ ); +} ``` ## Data Access Pattern -```javascript -// Priority order for data access -const data = window.openai.toolOutput || // Primary: MCP tool response - window.openai.widgetState || // Alternative state - window.openai.structuredContent || // Structured content - window.openai.data || // Generic data - DEBUG_DATA; // Local testing fallback +```tsx +import { useEffect, useState } from "react"; + +type OpenAIKey = + | "toolOutput" + | "widgetState" + | "structuredContent" + | "data" + | "theme" + | "displayMode"; + +declare global { + interface Window { + openai?: Record; + } +} + +export function useOpenAiGlobal(key: OpenAIKey): T | undefined { + const [value, setValue] = useState(() => window.openai?.[key] as T | undefined); + + useEffect(() => { + const id = setInterval(() => { + const next = window.openai?.[key] as T | undefined; + setValue((prev) => (JSON.stringify(prev) !== JSON.stringify(next) ? next : prev)); + }, 200); + + return () => clearInterval(id); + }, [key]); + + return value; +} + +// Priority order for widget content data +const data = useOpenAiGlobal("toolOutput") ?? + useOpenAiGlobal("widgetState") ?? + useOpenAiGlobal("structuredContent") ?? + useOpenAiGlobal("data"); ``` ## Theme Support Pattern -```javascript -function applyTheme() { - if (window.openai && window.openai.theme) { - const theme = window.openai.theme.toLowerCase(); - document.body.classList.remove('theme-light', 'theme-dark'); - document.body.classList.add(theme === 'dark' ? 'theme-dark' : 'theme-light'); - } - // Otherwise falls back to prefers-color-scheme media query +```tsx +import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components"; + +function ThemedRoot({ children }: { children: React.ReactNode }) { + const theme = (window.openai?.theme as string | undefined)?.toLowerCase() ?? "light"; + + return ( + + {children} + + ); } ``` ## CSS Variables (Required) -Always define at `:root` level with dark mode override: +Use Fluent tokens first. If custom CSS is needed, keep variables at `:root` and support dark mode: ```css :root { - --text-color: #1a1a1a; - --secondary-text: #666; - --border-color: #e5e5e5; - --card-bg: #ffffff; - --hover-bg: #f5f5f5; - --bg-color: #f9fafb; + --widget-surface: #f9fafb; + --widget-card-bg: #ffffff; + --widget-border: #e5e7eb; } @media (prefers-color-scheme: dark) { :root { - --text-color: #f5f5f5; - --secondary-text: #a3a3a3; - --border-color: #404040; - --card-bg: #262626; - --hover-bg: #333333; - --bg-color: #1a1a1a; + --widget-surface: #1b1b1b; + --widget-card-bg: #262626; + --widget-border: #3f3f46; } } @@ -327,42 +210,52 @@ body.theme-light { /* Same as light :root */ } Always include fallback data for local testing: -```javascript +```tsx const DEBUG_DATA = { - // Match your structuredContent shape title: "Debug Mode", - items: [ - { name: "Test Item", value: "Test Value" } - ] + items: [{ name: "Test Item", value: "Test Value" }], }; function getWidgetData() { if (window.openai) { - return window.openai.toolOutput || /* ... */; + return window.openai.toolOutput || + window.openai.widgetState || + window.openai.structuredContent || + window.openai.data || + null; } - return DEBUG_DATA; // Local testing + + return DEBUG_DATA; } ``` ## XSS Prevention -Always escape user-provided content: +Prefer React rendering over `innerHTML`. React escapes text content by default: -```javascript -function escapeHtml(str) { - const div = document.createElement('div'); - div.textContent = str || ''; - return div.innerHTML; -} +```tsx +// Safe by default in React +{userData} -// Usage -content.innerHTML = `${escapeHtml(userData)}`; +// Avoid raw HTML unless trusted and sanitized first +//
``` ## Action Buttons -```html -Email -Chat +```tsx +import { Button } from "@fluentui/react-components"; + + + + ```