All skills
microsoft avatar

/ui-widget-developer

@a43d2c6
by microsoftmicrosoft/skills3.1k stars
351

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 (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 - Configuring mcpPlugin.json manifests with RemoteMCPServer runtime Do NOT use this skill for general agent development (scaffolding, manifests, deployment) — use declarative-agent-developer instead. This skill is ONLY for MCP server + widget development. Triggers: "MCP server for Copilot", "OpenAI Apps SDK", "Copilot widget", "structuredContent", "MCP plugin", "devtunnels MCP", "OAI app", "widget rendering", "UI widget"

Use this Skill: https://skilld.dev/gh/microsoft/skills/ui-widget-developer

This session only. Nothing lands on disk.

referenceswidget-patterns.md

≈1.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Widget Patterns

Table of Contents

React widgets for OpenAI Apps SDK with Copilot Chat.

MANDATORY: Use Fluent UI (@fluentui/react-components and @fluentui/react-icons) for widget UI. Avoid raw HTML string rendering for app content.

Required Dependencies

Widget projects MUST include these package dependencies before implementation:

  • @fluentui/react-components
  • @fluentui/react-icons
  • react
  • react-dom

If any required dependency is missing, install it before generating widget code.

Widget Template

// index.html (minimal shell)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Widget Name</title>
  <style>
    html, body { margin: 0; padding: 0; overflow: hidden; height: 100%; }
    #root { height: 100%; overflow-y: auto; }
  </style>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="./main.tsx"></script>
</body>
</html>

// 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<string>("theme") ?? "light").toLowerCase();
  return (
    <FluentProvider theme={theme === "dark" ? webDarkTheme : webLightTheme}>
      <Widget />
    </FluentProvider>
  );
}

createRoot(document.getElementById("root")!).render(<App />);

// 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<WidgetData>("toolOutput") ?? { title: "Untitled", items: [] };

  if (!data.items?.length) {
    return <div className={styles.root}><Body1 className={styles.empty}>No items</Body1></div>;
  }

  return (
    <div className={styles.root}>
      <Title3>{data.title ?? "Untitled"}</Title3>
      <Card>
        <Table size="small">
          <TableHeader>
            <TableRow>
              <TableHeaderCell>Name</TableHeaderCell>
              <TableHeaderCell>Value</TableHeaderCell>
            </TableRow>
          </TableHeader>
          <TableBody>
            {data.items.map((item, idx) => (
              <TableRow key={idx}>
                <TableCell><TableCellLayout>{item.name}</TableCellLayout></TableCell>
                <TableCell>{item.value}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </Card>
    </div>
  );
}

Data Access Pattern

import { useEffect, useState } from "react";

type OpenAIKey =
  | "toolOutput"
  | "widgetState"
  | "structuredContent"
  | "data"
  | "theme"
  | "displayMode";

declare global {
  interface Window {
    openai?: Record<string, unknown>;
  }
}

export function useOpenAiGlobal<T = unknown>(key: OpenAIKey): T | undefined {
  const [value, setValue] = useState<T | undefined>(() => 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

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 (
    <FluentProvider theme={theme === "dark" ? webDarkTheme : webLightTheme}>
      {children}
    </FluentProvider>
  );
}

CSS Variables (Required)

Use Fluent tokens first. If custom CSS is needed, keep variables at :root and support dark mode:

:root {
  --widget-surface: #f9fafb;
  --widget-card-bg: #ffffff;
  --widget-border: #e5e7eb;
}

@media (prefers-color-scheme: dark) {
  :root {
    --widget-surface: #1b1b1b;
    --widget-card-bg: #262626;
    --widget-border: #3f3f46;
  }
}

body.theme-dark { /* Same as dark :root */ }
body.theme-light { /* Same as light :root */ }

Debug Data Pattern

Always include fallback data for local testing:

const DEBUG_DATA = {
  title: "Debug Mode",
  items: [{ name: "Test Item", value: "Test Value" }],
};

function getWidgetData() {
  if (window.openai) {
    return window.openai.toolOutput ||
           window.openai.widgetState ||
           window.openai.structuredContent ||
           window.openai.data ||
           null;
  }

  return DEBUG_DATA;
}

XSS Prevention

Prefer React rendering over innerHTML. React escapes text content by default:

// Safe by default in React
<Body1>{userData}</Body1>

// Avoid raw HTML unless trusted and sanitized first
// <div dangerouslySetInnerHTML={{ __html: trustedHtml }} />

Action Buttons

import { Button } from "@fluentui/react-components";

<Button appearance="primary" as="a" href={`mailto:${email}`}>
  Email
</Button>

<Button
  appearance="outline"
  as="a"
  target="_blank"
  href={`https://teams.microsoft.com/l/chat/0/0?users=${encodeURIComponent(email)}`}
>
  Chat
</Button>

Source: SKILL.md on GitHub

2 warnings3mo3 checks · Risk SAFE
  • Gen Agent Trust Hub3mo

    This skill provides a comprehensive framework for developing Model Context Protocol (MCP) servers and interactive widgets for Microsoft 365 Copilot. It includes automation for environment configuration, local service management, and robust reference implementations. The security analysis found that the skill follows industry best practices for local development, including path security and input validation.

  • Socket3mo

    1 alert: gptAnomaly

  • Snyk3mo

    Risk: MEDIUM · 1 issue

Signed by skilld at a43d2c6. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 20 hours ago.

Activeupdated 4 months ago

README badge

README badge for microsoft/skills/ui-widget-developer