ui-widget-developer
Copilot MCP Server Development
๐ PATH SELECTION
Before proceeding, ask the user which path they want to take using AskUserQuestion. Both are supported for M365 Copilot widget rendering. Present the tradeoffs and let them choose:
| OAI Apps (this skill) | MCP Apps Extension (modelcontextprotocol/ext-apps) |
|
|---|---|---|
| Standard | OpenAI-specific | Official MCP standard |
| Works in | ChatGPT + M365 Copilot | M365 Copilot, ChatGPT, VSCode, and more |
| Maturity | Battle-tested, production-ready | New official standard, growing ecosystem |
| Design | OpenAI Apps SDK | MCP Apps protocol (cross-platform) |
| When to choose | Existing OAI app investment | Prefer the open standard, want broadest client support |
Ask: "Would you like to build an OAI app (OpenAI Apps SDK โ battle-tested, works in ChatGPT and M365 Copilot) or an MCP app (new official standard โ works in M365 Copilot, ChatGPT, VSCode, and more)?"
- OAI apps โ Continue below. This skill covers everything you need.
- MCP apps โ Install the
modelcontextprotocol/ext-appsplugin (see below), then use the appropriate skill from that plugin.
MCP Apps: Install ext-apps Plugin
If the user chooses MCP Apps, provide these instructions:
To build an MCP App, install the ext-apps plugin from the marketplace:
1. /plugin marketplace add modelcontextprotocol/ext-apps
2. /plugin install mcp-apps@mcp-apps
Then use one of these skills from that plugin:
- create-mcp-app โ Scaffold a new MCP App with interactive UI from scratch
- add-app-to-server โ Add interactive UI to an existing MCP server's tools
- migrate-oai-app โ Convert an existing OAI app to use MCP Apps
- convert-web-app โ Turn a web app into a hybrid web + MCP App
After installing, invoke the relevant skill to continue.
Note: The ext-apps plugin lives in the external
modelcontextprotocol/ext-appsmarketplace โ it is not part of this plugin collection.
๐ PROJECT DETECTION ๐
This skill triggers when building MCP servers with OAI app or widget rendering for Microsoft 365 Copilot Chat. The MCP server can be written in any language that supports the MCP protocol (TypeScript, Python, C#, etc.). The agent project and MCP server may live in the same repo, separate folders, or entirely different projects.
Scenario Routing
| Starting Point | What You Need | Path |
|---|---|---|
| Prefer MCP Apps standard | Cross-platform widget support (M365 Copilot, ChatGPT, VSCode, and more) | Install modelcontextprotocol/ext-apps, then use create-mcp-app or add-app-to-server โ see Path Selection above |
| From scratch (no agent, no MCP server) | Full OAI app setup | Delegate agent scaffolding to declarative-agent-developer first, then return here for MCP server + widgets |
| Existing M365 agent, new MCP server | MCP server + widgets + mcpPlugin.json | Start at Implementation |
| Existing MCP server, add Copilot widgets | Widget support added to existing server | Start at Copilot Widget Protocol |
| Language choice (non-TypeScript) | Protocol requirements | See Copilot Widget Protocol for what to implement, MCP Server Pattern (TypeScript) as a reference |
๐จ CRITICAL EXECUTION RULES ๐จ
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:
# Start devtunnel
$t = Start-Process -FilePath "devtunnel" `
-ArgumentList "host","<tunnel-name>","-a" `
-WindowStyle Hidden -PassThru `
-RedirectStandardOutput "tunnel.log" -RedirectStandardError "tunnel-err.log"
# Start MCP server โ use cmd.exe /c to set the working directory and inherit PATH
$s = Start-Process -FilePath "cmd.exe" `
-ArgumentList "/c","cd /d <abs-path-to-mcp-server> && <start-command>" `
-WindowStyle Hidden -PassThru `
-RedirectStandardOutput "server.log" -RedirectStandardError "server-err.log"
# Save PIDs so they can be stopped later
"$($t.Id),$($s.Id)" | Out-File pids.txt
Write-Host "Started tunnel PID $($t.Id), server PID $($s.Id)"
To stop: Stop-Process -Id (Get-Content pids.txt).Split(',') or Stop-Process -Id <pid>.
Linux/Mac โ use nohup with &:
nohup devtunnel host <tunnel-name> > tunnel.log 2>tunnel-err.log &
echo "tunnel:$!" >> pids.txt
nohup <start-command> > server.log 2>server-err.log &
echo "server:$!" >> pids.txt
To stop: kill $(grep -oP '\d+' pids.txt).
After starting, tail the logs to confirm both processes are up before proceeding:
# Windows
Start-Sleep 3; Get-Content tunnel.log, server.log
# Linux/Mac
sleep 3 && tail tunnel.log server.log
FULL AUTOMATION: Never tell the user to run commands manually. Install tools, authenticate, start services โ do everything automatically. Only ask the user for interactive input that truly requires them (like device code confirmation during devtunnel user login -g -d). If a tool isn't installed, install it. If a service needs starting, start it. The user expects full automation.
PATH SELECTION (REQUIRED โ STOP BEFORE ANY CODE): You MUST use AskUserQuestion to ask the user whether they want OAI Apps or MCP Apps Extension before writing any code, running any commands, or making any architectural decisions.
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 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.
When provisioning is needed:
- Bump the version in
manifest.json(increment the patch version, e.g.,1.0.0โ1.0.1) - Deploy the agent:
npx -y --package @microsoft/m365agentstoolkit-cli atk provision --env local
WIDGET TESTING LINKS: Every time you return to the user with a result while the MCP server is running, you MUST include links to ALL widgets so they can test them locally. Format:
๐งช Test widgets locally:
- http://localhost:3001/widgets/widget-name.html
- http://localhost:3001/widgets/another-widget.html
List every .html file in the mcp-server/widgets/ directory (or equivalent widget folder). This helps users verify widget rendering before testing in Copilot.
AUTO-DEPLOY ON COMPLETION (REQUIRED โ DO NOT SKIP): When coding is complete, proceed automatically without waiting for the user:
- Start MCP server + devtunnel in the background (per BACKGROUND PROCESSES above)
- Run E2E verification with MCP Inspector (per MCP TOOL CONFIGURATION RULE below) โ fix any failures before continuing
- Provision the agent if needed (per AGENT PROVISIONING above)
- Print a project summary in this format:
## โ
<Project Name> โ Ready
### Widgets
- [widget-name.html](http://localhost:<PORT>/widgets/widget-name.html)
- [widget-name2.html](http://localhost:<PORT>/widgets/widget-name2.html)
### Endpoints
- MCP server: http://localhost:<PORT>/mcp
- MCP via tunnel: https://<tunnel-url>/mcp
### Test in Copilot
Local: https://m365.cloud.microsoft/chat/?titleId={M365_TITLE_ID from env/.env.local}
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.
For each new widget, complete this checklist:
- โ Create widget HTML file in
widgets/directory (see widget-patterns.md) - โ Define a
ui://widget/<name>.htmlURI constant - โ Add a
Resourceentry to theresourcesarray with:uri: theui://widget/<name>.htmlURImimeType:"text/html+skybridge"_meta: CSP config withopenai/widgetDomainandopenai/widgetCSP(from environment)
- โ Add a handler for
resources/readthat returns the widget HTML for this URI - โ Add the tool with
_meta.openai/outputTemplatepointing to the sameui://widget/<name>.htmlURI - โ Verify the server capabilities include
resources: {}in the initialize response
Widget HTML size considerations:
- Simple widgets: The
resources/readhandler 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:
Use the<!doctype html><html><head> <script type="module" src="${serverUrl}/assets/my-widget.js"></script> <link rel="stylesheet" href="${serverUrl}/assets/my-widget.css"> </head><body> <div id="widget-root"></div> </body></html>WIDGET_BASE_URLorMCP_SERVER_URLenvironment variable for the asset URL base (see mcp-server-pattern.md "Configurable Widget Base URL" section).
See mcp-server-pattern.md for the complete resource and asset serving patterns.
โ ๏ธ MCP TOOL CONFIGURATION RULE โ ๏ธ
NEVER manually write tool definitions in mcpPlugin.json. Always use MCP Inspector to get the complete tool definitions from the running MCP server.
TOOL NAMING CONVENTION: Tool names MUST match the pattern ^[A-Za-z0-9_]+$ (letters, numbers, and underscores only). NEVER use hyphens (-) in tool names. Use underscores instead (e.g., render_profile not render-profile).
MANDATORY WORKFLOW:
- Start the MCP server (in background)
- Use MCP Inspector to get the latest tool definitions:
npx @modelcontextprotocol/inspector@0.20.0 --cli https://my-mcp-server.example.com --transport http --method tools/list - Copy the COMPLETE tool definition from the inspector (including
name,description,inputSchema,_meta,annotations,title) - Paste into
mcpPlugin.jsonunderruntimes[].spec.mcp_tool_description.tools(inside theRemoteMCPServerruntime'sspecobject) - Run E2E verification through the devtunnel โ call each tool and confirm the response contains
structuredContentand_meta.openai/widgetAccessible: true:
Also verifynpx @modelcontextprotocol/inspector@0.20.0 --cli https://<tunnel-url>/mcp --transport http --method tools/call --tool-name <tool_name>GET https://<tunnel-url>/healthreturns{"status":"ok"}. Fix any failures before provisioning.
The MCP Inspector shows the exact tool schema from your server. Copy it completely โ do not manually write or modify these definitions. This ensures mcpPlugin.json stays in sync with the MCP server.
Build MCP servers that integrate with Microsoft 365 Copilot Chat and render rich interactive widgets.
Architecture
M365 Copilot โโโถ mcpPlugin.json โโโถ MCP Server โโโถ structuredContent โโโถ HTML Widget
โ (RemoteMCPServer) (Streamable HTTP) (window.openai.toolOutput)
โ
โโโ Capabilities (People, etc.) provide data to pass to MCP tools
Project Structure
Example project structure, not a hard requirement but a common pattern for organizing MCP server + widget development:
project/
โโโ appPackage/
โ โโโ manifest.json # Teams manifest (bump version on deploy)
โ โโโ declarativeAgent.json # Agent config + capabilities
โ โโโ mcpPlugin.json # Tool definitions with _meta
โ โโโ instruction.txt # Agent behavior instructions
โโโ mcp-server/
โ โโโ src/index.ts # Server with Streamable HTTP
โ โโโ widgets/*.html # OpenAI Apps SDK widgets
โ โโโ package.json
โโโ scripts/
โ โโโ setup-devtunnel.sh # Linux/Mac devtunnel setup
โ โโโ setup-devtunnel.ps1 # Windows devtunnel setup
โโโ env/.env.local # MCP_SERVER_URL, MCP_SERVER_DOMAIN
Language note: This shows a TypeScript project layout. For Python, replace mcp-server/src/index.ts with your Python entry point (e.g., server.py). For C#, use a standard .NET project structure. The appPackage/, widgets/, scripts/, and env/ directories are language-agnostic.
Copilot Widget Protocol
Your MCP server must implement these protocol requirements to render widgets in Copilot Chat. This applies regardless of language:
- Streamable HTTP transport โ
/mcpendpoint handling POST, GET, DELETE with session management - CORS headers โ Origin-checking on
/mcpallowingm365.cloud.microsoftand*.m365.cloud.microsoft, with required MCP headers - Server capabilities โ
initializeresponse must declareresources: {}andtools: {} - MCP resources โ Register widgets with
ui://widget/<name>.htmlURIs,text/html+skybridgemime type, and CSP_meta - Tool response format โ Return
content(text) +structuredContent(widget data) +_metawithopenai/outputTemplate - Widget serving โ HTTP route at
/widgets/*.htmlwith origin-checking CORS
For full protocol details, JSON shapes, and an adaptation checklist for existing MCP servers, see references/copilot-widget-protocol.md.
Implementation
MCP Server Pattern (TypeScript Reference)
See references/mcp-server-pattern.md for complete implementation.
For other languages, implement the requirements described in Copilot Widget Protocol using your language's MCP SDK. See the Language SDK References table for SDK packages.
Core requirements:
- Expose Streamable HTTP transport on
/mcp - Return
structuredContent+_metawithopenai/outputTemplate - Serve widgets via HTTP endpoint
- Handle CORS for cross-origin requests
- Handle partial data gracefully (fill in "Unknown" for missing fields)
Tool response format:
return {
content: [{ type: "text", text: "Summary" }],
structuredContent: { /* widget data */ },
_meta: { "openai/outputTemplate": "ui://widget/name.html", "openai/widgetAccessible": true }
};
Handling Partial Data
Always normalize input data to handle missing fields:
server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
const args = request.params.arguments as { title?: string; items?: Partial<Item>[] };
// Normalize data - fill in "Unknown" for missing fields
const title = args.title || "Default Title";
const items = (args.items || []).map(item => ({
name: item.name || "Unknown",
value: item.value || "Unknown",
}));
// Build structuredContent for widget
const structuredContent = { title, items };
// ...
});
Widget Pattern
See references/widget-patterns.md for complete examples.
Core requirements:
- Access data:
window.openai.toolOutput(primary source) - Theme support:
window.openai.themeorprefers-color-scheme - Debug fallback: Embedded mock data when
window.openaiunavailable - CSS variables for theming at
:rootlevel - Handle "Unknown" values gracefully (e.g., hide action buttons)
Plugin Schema
See references/plugin-schema.md for mcpPlugin.json format.
Core requirements:
- Schema
v2.4withRemoteMCPServerruntime run_for_functionsarray matching tool names_metain tool definitions for widget bindinginputSchema- make properties optional for flexibility, describe defaults in descriptions
DevTunnels Setup
Local testing only. DevTunnels are for development and testing on your machine. Before sharing the agent more broadly, deploy both the MCP server and widget assets to a hosted environment (e.g., Azure App Service, Azure Static Web Apps, or another hosting provider) and update the agent manifest URLs accordingly.
DevTunnels expose your localhost MCP server to M365 Copilot using named tunnels for stable URLs. See references/devtunnels.md for setup scripts, command reference, and troubleshooting.
The setup script (npm run tunnel / npm run tunnel:win):
- Creates a named tunnel on first run (or reuses the existing one)
- Starts hosting the tunnel on the configured port
- Updates
env/.env.localwithMCP_SERVER_URLandMCP_SERVER_DOMAIN(first run only) - Continues hosting the tunnel
Quick Start
Terminal 1 - Start MCP Server:
cd mcp-server
npm install
npm run dev
Terminal 2 - Start DevTunnel:
npm run tunnel
# Or on Windows:
npm run tunnel:win
On first run, provision the agent once the tunnel is up (see AGENT PROVISIONING rule). On subsequent runs the tunnel URL is stable โ no re-provisioning needed unless the agent manifest changes.
Development Workflow
-
Start the MCP server (dev mode with hot reload):
- TypeScript:
cd mcp-server && npm install && npm run dev - Python:
cd mcp-server && pip install -r requirements.txt && python server.py - C#:
cd mcp-server && dotnet run
- TypeScript:
-
Start the devtunnel (creates named tunnel on first run, reuses on subsequent runs):
npm run tunnel -
Provision + test โ see AGENT PROVISIONING rule for when this is needed; bump
versionin manifest.json if Copilot doesn't reflect changes
Best Practices
See references/best-practices.md for detailed guidance.
Key points:
- Rendering tools: Accept data as input, don't fetch internally
- Instructions: Tell agent to use capabilities FIRST, then pass data to MCP tools
- Themes: Always support dark/light via CSS variables
- Debug mode: Include fallback data for local widget testing
- Partial data: Handle missing fields with "Unknown" defaults
- Action buttons: Hide email/chat buttons when data is "Unknown"
- Version bumping: Bump manifest version when changes aren't reflected in Copilot