Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Microsoft Copilot Cowork supports extensibility through M365 App Packages—the same distribution mechanism used by Teams apps, Copilot agents, and Office add-ins. You can extend Cowork with:
- Skills: Prompt-based workflows that teach Cowork new domain expertise, such as financial analysis, legal research, or HR workflows.
- Connectors: Remote servers that give Cowork access to external data sources and APIs.
Both are packaged together in a standard Microsoft 365 app package and distributed through the Microsoft 365 App Store.
Important
Microsoft Purview Information Barriers (IB) aren't currently supported for plugin or skill management and sharing. In tenants where IB is enabled, embedded knowledge file uploads are blocked at the tenant level. This prevents affected plugins and skills from being uploaded or published.
What you'll build
A Cowork plugin is a .zip package containing:
my-extension.zip
├── manifest.json # M365 Unified App Manifest (v1.28)
├── color.png # 192×192 full-color app icon
├── outline.png # 32×32 outline icon
└── skills/ # Agent Skills (SKILL.md files)
├── skill-one/
│ ├── SKILL.md
│ └── references/ # Optional deep-dive docs
└── skill-two/
└── SKILL.md
Skills use the Agent Skills open standard-the same format supported by Claude Code, Visual Studio Code Copilot, Gemini CLI, Cursor, JetBrains Junie, and 30+ other AI tools.
Choose your starting point
| Starting point | Path | Time to first package |
|---|---|---|
| I have an existing Claude Code or Cursor plugin | Import it | ~5 minutes |
| I'm starting from scratch | Build from scratch | ~30 minutes |
Import an existing plugin
If you already have a Claude Code or Cursor plugin with skills and MCP servers, the Microsoft 365 Agents Toolkit CLI (atk) imports it directly. The CLI runs on Windows, macOS, and Linux.
Install the CLI (requires version 1.1.12 or later):
npm install -g @microsoft/m365agentstoolkit-cliVerify the version:
atk --versionImport your plugin:
atk import openplugin --path ./my-claude-plugin --output ./my-plugin-project \ --privacy-url https://contoso.com/privacy \ --terms-url https://contoso.com/terms
The command reads your plugin's .claude-plugin/plugin.json (or .cursor-plugin/plugin.json), .mcp.json, and skills/ directory, then scaffolds an Agents Toolkit project containing appPackage/manifest.json, your skills, and generated icons.
You must include --privacy-url and --terms-url because plugin manifests don't have equivalent fields, and the Microsoft 365 manifest requires both.
Note
atk import openplugin locates a plugin manifest in a dot-prefixed directory—.claude-plugin/plugin.json, .cursor-plugin/plugin.json, or .plugin/plugin.json—alongside a .mcp.json. The Agent Plugins 1.0.0 specification places the manifest at a top-level plugin.json and MCP configuration at mcp.json. To import a plugin that follows the 1.0.0 layout, move its manifest to .plugin/plugin.json and rename mcp.json to .mcp.json.
Package the result into an uploadable .zip:
cd my-plugin-project
atk package --manifest-file ./appPackage/manifest.json \
--output-package-file ./appPackage/build/appPackage.zip \
--output-folder ./appPackage/build
Note
atk import openplugin generates a devPreview manifest. The manifest examples elsewhere in this article target schema v1.28. If you're publishing through a channel that requires v1.28, update manifestVersion and $schema in the generated appPackage/manifest.json, and add the mcpToolDescription property to each connector as described in Describe your connector's tools.
What gets imported
| Plugin artifact | M365 equivalent | Notes |
|---|---|---|
.claude-plugin/plugin.json |
manifest.json |
Name, description, and developer fields mapped; GUID autogenerated (deterministic UUID v5) |
skills/*/SKILL.md |
agentSkills[] entries + skills/ folder |
Copied verbatim - identical format |
.mcp.json servers |
agentConnectors[] entries |
URL and auth type autodetected |
color.png / outline.png |
Icons in package | Used if present; placeholders generated if missing |
Important
For each connector imported from .mcp.json, the generated authorization.referenceId is a placeholder derived from the plugin and server name. Replace it with your actual OAuth client registration ID before you publish. See Supported auth types.
What's not converted
The following Claude plugin features aren't yet supported in the Microsoft 365 manifest:
| Claude plugin feature | Status |
|---|---|
commands/ (slash commands) |
Not yet supported |
agents/ (sub-agents) |
Not yet supported |
hooks/ (event handlers) |
Not yet supported |
settings.json |
Not applicable |
bin/ (executables) |
Not applicable |
Import options
| Option | Description |
|---|---|
--path, -p |
Required. Plugin directory containing .claude-plugin/plugin.json, .cursor-plugin/plugin.json, or .plugin/plugin.json |
--output, -o |
Destination project folder (default: ./<plugin-name>) |
--privacy-url |
developer.privacyUrl for the generated manifest |
--terms-url |
developer.termsOfUseUrl for the generated manifest |
--website-url |
developer.websiteUrl. Falls back to homepage, then author.url |
--app-id |
Override the deterministic UUID v5 generated for the manifest id |
--default-auth-type |
Auto (default), None, OAuthPluginVault, or ApiKeyPluginVault |
Auth type autodetection:
| Source | Default auth type | Reason |
|---|---|---|
| External HTTPS URLs | OAuthPluginVault |
Most remote APIs need auth |
localhost and non-HTTPS URLs |
None |
Local development servers |
If the autodetection doesn't match your setup, use --default-auth-type to override it.
Export back to a plugin directory
To move an Agents Toolkit project back to a plugin directory—for example, to keep a Claude Code plugin and a Cowork package in sync—use atk export openplugin:
atk export openplugin --path ./my-plugin-project \
--output ./my-claude-plugin --manifest-kind claude-plugin
| Option | Description |
|---|---|
--path, -p |
Required. Agents Toolkit project folder containing appPackage/manifest.json |
--output, -o |
Destination plugin directory (default: ./<plugin-name>-openplugin) |
--manifest-kind |
open-plugin (default, writes .plugin/plugin.json), claude-plugin, or cursor-plugin |
Export writes an x-microsoft-365-agents-toolkit block into the generated plugin.json. That block carries the manifest id, developer URLs, and connector settings, so a later atk import openplugin round-trips without needing --privacy-url or --terms-url again.
Note
The x-microsoft-365-agents-toolkit block is specific to Agents Toolkit, and the default open-plugin kind writes the manifest to .plugin/plugin.json. Agent Plugins 1.0.0 uses a top-level plugin.json and carries client-specific data under an extensions key with a reverse-domain namespace, so other clients ignore this block rather than acting on it. When your target is Claude Code or Cursor, use --manifest-kind claude-plugin or cursor-plugin.
Legacy: PowerShell conversion script
Before atk supported plugin import, conversion used a Windows-only PowerShell script, which remains available as the conversion script:
.\Convert-ClaudePluginToMOS3.ps1 -PluginPath ./my-claude-plugin -OutputPath ./output
Use atk import openplugin instead. It's cross-platform, supports Cursor as well as Claude Code sources, and can export back to a plugin directory.
Build a plugin from scratch
Follow these steps to create a plugin package from the ground up, starting with your first skill and building up to a complete, publishable package.
Step 1: Create your first skill
A skill is a folder containing a SKILL.md file. Create the following folder structure:
my-extension/
└── skills/
└── contract-analysis/
└── SKILL.md
Write SKILL.md with YAML frontmatter and a Markdown body:
---
name: contract-analysis
description: |
Analyzes contracts for key terms, risks, and obligations.
Use when user asks to "review this contract", "find the liability clause",
"summarize the key terms", or "compare these two agreements".
license: MIT
metadata:
author: Contoso Legal Tech
version: "1.0"
---
# Contract Analysis
## What This Skill Does
Guides Cowork through systematic contract review, identifying:
- Key commercial terms (pricing, payment, renewal)
- Risk clauses (indemnification, limitation of liability, IP)
- Obligations and deadlines
- Non-standard or unusual provisions
## Workflow
1. Read the uploaded contract document
2. Extract and categorize all clauses
3. Flag risk areas with severity ratings
4. Generate a structured summary with recommendations
## Output Format
Present findings in a structured table:
| Clause | Category | Risk Level | Summary |
|--------|----------|------------|---------|
| Section 4.2-Indemnification | Risk | High | Unlimited indemnification for IP claims |
| Section 7.1-Term | Commercial | Low | 12-month auto-renewal with 30-day notice |
SKILL.md frontmatter fields
Required fields:
| Field | Constraints | Description |
|---|---|---|
name |
1-64 characters, kebab-case | Skill identifier - must match the folder name exactly |
description |
1-1024 characters | When to use this skill - include trigger phrases |
Important
- The folder name must match the
namefield in the frontmatter. This mismatch is the most common cause of skill failures. - Plugin listing
descriptionfields shouldn't include calls to action directing users to external marketplaces to purchase subscriptions.
| Folder path | name field |
Valid? | Why |
|---|---|---|---|
skills/contract-analysis/SKILL.md |
contract-analysis |
Yes | Folder and name match |
skills/contract-analysis/SKILL.md |
ContractAnalysis |
No | Name uses PascalCase instead of matching folder |
skills/my-skill/SKILL.md |
contract-analysis |
No | Folder is my-skill but name is contract-analysis |
Naming rules (kebab-case): Use only lowercase alphanumeric characters and hyphens. Don't use consecutive hyphens, and don't use leading or trailing hyphens.
| Example | Valid? | Issue |
|---|---|---|
bond-relative-value |
Yes | Lowercase with hyphens |
fx-carry-trade |
Yes | Lowercase with hyphens |
email |
Yes | Single word, no hyphens needed |
Bond_Relative_Value |
No | Underscores and uppercase letters |
--my-skill-- |
No | Leading and trailing hyphens |
my--skill |
No | Consecutive hyphens |
Step 2: Add reference materials (optional)
For complex skills, keep the main SKILL.md lean and move detailed content to subdirectories. These additional files are companion files. The skill loads them when needed.
skills/
└── contract-analysis/
├── SKILL.md # Core workflow (~1,500-2,000 words ideal)
├── references/ # Deep-dive docs loaded on demand
│ ├── clause-taxonomy.md
│ └── risk-scoring.md
└── scripts/ # Executable utilities
└── extract-clauses.py
Companion file limits
Each skill can include up to 20 companion files (any file other than SKILL.md). The following limits apply per skill:
| Limit | Value |
|---|---|
| Maximum companion files | 20 |
| Maximum size per companion file | 5 MB |
| Maximum total companion size | 10 MB |
| Download timeout (all companions) | 15 seconds |
Companion file rules
Companion file paths must follow these rules:
- Use relative paths only (no absolute paths)
- No path traversal (
..segments) - No backslashes or null bytes in file names
- No hidden files (names starting with
.) - No Windows reserved names (
CON,PRN,AUX,NUL,COM1–COM9,LPT1–LPT9) - The file
SKILL.mditself doesn't count as a companion file - File names must use safe characters: alphanumeric, hyphens, underscores, dots, spaces, and
!
To keep the context window efficient, the system loads skills in three layers:
| Layer | When loaded | Target size |
|---|---|---|
Frontmatter (name + description) |
Always - at startup | ~100 tokens |
SKILL.md body |
When skill triggers | Less than 5,000 tokens (1,500-2,000 words) |
References (references/) |
On demand by the agent | Unlimited |
Scripts (scripts/) |
Executed, not loaded into context | N/A |
Reference the subdirectories explicitly in SKILL.md so the agent knows they exist:
## Additional Resources
- **`references/clause-taxonomy.md`**-Full taxonomy of contract clause types
- **`references/risk-scoring.md`**-Risk scoring methodology and thresholds
- **`scripts/extract-clauses.py`**-Automated clause extraction utility
Step 3: Add a connector (optional)
If your extension needs access to external data, add a remote MCP server. This step is optional. Skills-only packages work well for prompt-based workflows.
Tip
If your server gates tool visibility by client or attributes incoming traffic, see Identify Cowork traffic to your server for the client identity Cowork presents.
Note
Custom plugins aren't supported in Cowork on mobile.
Connector requirements
| Requirement | Details |
|---|---|
| Transport | Streamable HTTP (HTTPS required, TLS 1.2+) |
| Protocol | JSON-RPC 2.0 message format |
| Tool discovery | Support tools/list for dynamic discovery (recommended) |
| Tool execution | Support tools/call for invocation |
| Availability | 99.9% uptime SLA recommended for store-published apps |
| Response time | Less than 30 seconds per tool call |
Tool design guidelines
- One tool per action for small APIs (fewer than 15 operations):
search_case_law,get_ruling,cite_precedent - Search + execute for large APIs (50+ operations):
search_actions+execute_action - Descriptive names:
get_bond_pricenotgetData - Rich input schemas: Include a description for every parameter—this is what the agent reads
- Structured output: Return JSON that the agent can format for the user
- File inputs: To accept a file from the user's workspace, declare the parameter with
contentEncoding: base64. Learn more in Accept files from the Cowork workspace.
Describe your connector's tools (mcpToolDescription)
Every remoteMcpServer connector must include an mcpToolDescription object. Its nested file property points to a tool-description JSON file that you package inside your .zip and reference by a relative path from the package root. If you omit mcpToolDescription, the package service rejects the upload with an HTTP 400 error:
Required properties are missing from object: mcpToolDescription.
"remoteMcpServer": {
"mcpServerUrl": "https://api.contoso.com/legal/mcp",
"mcpToolDescription": {
"file": "./tools/contoso-legal-tools.json"
},
"authorization": {
"type": "OAuthPluginVault",
"referenceId": "A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u"
}
}
The referenced file (for example, tools/contoso-legal-tools.json) describes the tools the connector exposes and must be present in the ZIP package. Include it alongside your manifest.json and skills/ folder when you package the plugin.
Supported auth types
| Auth type | When to use | User experience |
|---|---|---|
None |
Public or anonymous APIs, internal services | Transparent - no auth prompt |
OAuthPluginVault |
OAuth 2.0 APIs (recommended for production) | User completes OAuth consent once |
ApiKeyPluginVault |
API key-based services | User provides key once |
Note
- Support for API key authentication isn't available in Cowork yet.
- If your MCP server requires an API key, use
OAuthPluginVaultor Dynamic Client Registration instead, or expose an endpoint that acceptsNone.
For OAuthPluginVault and ApiKeyPluginVault, the referenceId points to credentials stored in the Microsoft Enterprise Token Store - secrets never appear in the manifest or skill files. The referenceId value is the OAuth client registration ID that you create when you register an OAuth client with Agents Toolkit.
Important
When registering your OAuth client, set the usage by organization to Any Microsoft 365 Organization to ensure your plugin works across tenants.
MCP authentication
To use OAuth or ApiKey for authentication, refer to Configure authentication for MCP and API plugins in agents in Microsoft 365 Copilot for setup and configuration details.
Dynamic Client Registration
If your MCP server supports Dynamic Client Registration (DCR), you can omit an authentication configuration from your connector definition, and Cowork automatically creates an OAuth client on your plugin's behalf.
You can omit the authorization object, but you still need to include mcpToolDescription. Configure your MCP server URL and tool description, and Cowork takes care of the OAuth client:
"remoteMcpServer": {
"mcpServerUrl": "https://api.contoso.com/legal/mcp",
"mcpToolDescription": {
"file": "./tools/contoso-legal-tools.json"
}
}
Step 4: Create the manifest
Create manifest.json in your package root:
{
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.28/MicrosoftTeams.schema.json",
"manifestVersion": "1.28",
"version": "1.0.0",
"id": "YOUR-GUID-HERE",
"developer": {
"name": "Contoso Legal Tech",
"websiteUrl": "https://contoso.com",
"privacyUrl": "https://contoso.com/privacy",
"termsOfUseUrl": "https://contoso.com/terms"
},
"name": {
"short": "Contoso Legal Tools",
"full": "Contoso Legal Tools for Copilot Cowork"
},
"description": {
"short": "Contract analysis, clause extraction, and legal research",
"full": "Comprehensive legal tools for Copilot Cowork including contract analysis, clause extraction, risk assessment, and legal research capabilities."
},
"icons": {
"color": "color.png",
"outline": "outline.png"
},
"accentColor": "#2B579A",
"agentSkills": [
{ "folder": "./skills/contract-analysis" }
]
}
To add a connector, include agentConnectors:
{
"agentConnectors": [
{
"id": "contoso-legal-api",
"displayName": "Contoso Legal Database",
"description": "Access to case law, statutes, and regulatory databases",
"toolSource": {
"remoteMcpServer": {
"mcpServerUrl": "https://api.contoso.com/legal/mcp",
"mcpToolDescription": {
"file": "./tools/contoso-legal-tools.json"
},
"authorization": {
"type": "OAuthPluginVault",
"referenceId": "A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u"
}
}
}
}
]
}
In the connector configuration, referenceId should be the OAuth registration ID, and mcpToolDescription.file must point to a tool-description JSON file that's included in the ZIP package.
Important
The v1.28 manifest schema is strict: it sets additionalProperties: false at the root, so any field that isn't defined in the schema is rejected. Fields that are valid in standard Teams app manifests—such as packageName—cause the upload to fail with an error like Property 'packageName' has not been defined and the schema does not allow additional properties. Include only the fields shown here.
Step 5: Add icons
Create two PNG icons:
| Icon | Size | Purpose |
|---|---|---|
color.png |
192×192 px | Full-color app icon shown in store and app list |
outline.png |
32×32 px | Single-color outline icon for compact views |
If you don't have icons yet, atk import openplugin generates solid-color placeholders. Replace them before store submission.
Step 6: Package
Create a ZIP file with all contents at the root level:
contoso-legal-tools.zip
├── manifest.json
├── color.png
├── outline.png
├── tools/
│ └── contoso-legal-tools.json # Referenced by mcpToolDescription (connectors only)
└── skills/
└── contract-analysis/
├── SKILL.md
└── references/
└── clause-taxonomy.md
If your package includes an agentConnectors entry, include the tool-description JSON file referenced by mcpToolDescription.file. Skills-only packages don't need a tools/ folder.
Windows (PowerShell):
Compress-Archive -Path manifest.json, color.png, outline.png, tools, skills -DestinationPath contoso-legal-tools.zip
macOS/Linux:
zip -r contoso-legal-tools.zip manifest.json color.png outline.png tools/ skills/
Using Microsoft 365 Agents Toolkit
atk package --manifest-file ./appPackage/manifest.json \
--output-package-file ./appPackage/build/appPackage.zip \
--output-folder ./appPackage/build
Step 7: Test
To test your app, upload your app package to Teams as described in Upload your app to Teams.
For personal testing, sideload the app by using the Microsoft 365 Agents Toolkit command line interface:
Install
@microsoft/m365agentstoolkit-clifromnpm:npm install -g @microsoft/m365agentstoolkit-cliVerify the installation by running:
atk --versionAuthenticate with your Microsoft 365 work account:
atk auth loginSign in to your work account and install the agent package. Replace the file path with the location of your ZIP package:
atk install --file-path "C:/Users/myuser/myPackage.zip" --scope PersonalA successful installation returns output that includes a
TitleIdandAppIdfor your account.Save these IDs for later use when you update or uninstall.
Learn more in Microsoft 365 Agents Toolkit command line interface.
Step 8: Publish to your tenant
- Open M365 admin center > Manage apps > Upload custom app.
- Select the ellipsis button (...) > Add agent.
- Upload your
.zippackage. - Open Cowork > Sources & Skills > Plugins. Your plugin appears in the Discover section.
Step 9: Publish to the public
For plugins intended for public distribution, submit your plugin to the Microsoft 365 App Store via Partner Center. Learn more in Publish agents for Microsoft 365 Copilot.
Test a connector against a local MCP server
Connectors require an HTTPS mcpServerUrl, so to test a server running on your machine you need to expose it over a public HTTPS URL. Dev tunnels provide a relay that terminates TLS for you.
devtunnel port create <tunnel> -p <port> --protocol http
Important
Use --protocol http, not https. The --protocol flag describes the local service the tunnel forwards to, not the public tunnel URL. Most local MCP servers speak plain HTTP, so if you set --protocol https while your server serves HTTP, every request through the tunnel returns a 502 error. The relay terminates TLS and serves the public URL over HTTPS regardless of this flag.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Every tunneled request returns 502 and the local server speaks HTTP |
devtunnel port create was run with --protocol https |
Recreate the port with --protocol http |
Tunneled requests return 502 on macOS even though the local server is running |
The server is bound to 0.0.0.0 (IPv4-only), but the tunnel dials localhost, which resolves to ::1 (IPv6) first |
Bind the server to :: so it accepts both IPv4 and IPv6 connections |
Upload fails with Required properties are missing from object: mcpToolDescription |
The connector is missing mcpToolDescription |
Add mcpToolDescription with a file reference and package that file in the ZIP |
Upload fails with Property '<field>' has not been defined and the schema does not allow additional properties |
The manifest includes a field the v1.28 schema doesn't allow (for example, packageName) |
Remove the field; the v1.28 schema uses additionalProperties: false |
Packaging patterns
Choose the pattern that fits your extension:
Skills only (no connector)
Best for prompt-based workflows, document analysis, and writing assistance.
my-skills-pack.zip
├── manifest.json # agentSkills only, no agentConnectors
├── color.png
├── outline.png
└── skills/
├── skill-one/SKILL.md
└── skill-two/SKILL.md
Skills + remote connector
Best for data analysis, API integrations, and enterprise systems.
my-data-skills.zip
├── manifest.json # agentSkills + agentConnectors
├── color.png
├── outline.png
├── tools/ # Tool-description file(s) for mcpToolDescription
│ └── my-connector.json
└── skills/
├── analysis-workflow/SKILL.md
└── reporting-workflow/SKILL.md
Connector only (no custom skills)
Use this option for data sources that Cowork's built-in skills can already use.
my-connector.zip
├── manifest.json # agentConnectors only, no agentSkills
├── color.png
├── outline.png
└── tools/ # Tool-description file(s) for mcpToolDescription
└── my-connector.json
Imported Claude Code or Cursor plugin
Use this option for existing plugins from other AI tools that target Cowork.
atk import openplugin --path ./claude-plugin --output ./my-plugin-project \
--privacy-url https://contoso.com/privacy \
--terms-url https://contoso.com/terms
Skill authoring best practices
Follow these guidelines to create skills that activate reliably and produce consistent results.
Write effective descriptions
The description field determines when the agent activates your skill. Be specific:
# Good-specific trigger phrases, concrete scenarios
description: |
Analyzes bond relative value using Z-spreads, ASW spreads, and butterfly analysis.
Use when user asks to "analyze bond spreads", "compare bonds",
"rich-cheap analysis", "relative value", or "Z-spread calculation".
# Bad-vague, no trigger phrases
description: Provides bond analytics capabilities.
Write effective workflows
- Be specific in the description. Include trigger phrases: "Use when user asks to..." This description is how the agent decides which skill to activate.
- Structure as a workflow. Number the steps. Each step should map to a concrete action (read a file, call a tool, generate output).
- Define output format. Show the exact table, list, or document structure users should expect. This definition improves consistency dramatically.
- Reference tools by name. If your skill depends on connector tools, name them explicitly: "Use the
search_case_lawtool to..." - Keep the main SKILL.md lean. Move detailed reference material to the
references/subdirectory. The skill body should be the workflow, not an encyclopedia.
Avoid common mistakes
- Don't embed secrets in
SKILL.mdfiles. UseagentConnectorswith auth for API credentials. - Don't duplicate built-in skills. Check the built-in skills list before building.
- Don't make skills too broad. "Do everything with legal documents" is worse than specific skills for "contract analysis", "clause extraction", and "legal research".
- Don't hardcode file paths or system commands. Skills should be portable across environments.
- Don't put everything in SKILL.md. If your body exceeds ~3,000 words, move detailed content to
references/.
Validation rules
When you submit your package, the platform validates it at multiple levels. Fix these errors before submission to avoid rejection.
Manifest-level validation
| Code | Rule | Severity |
|---|---|---|
| ASKILL-M001 | folder is required on each agentSkills entry |
Error |
| ASKILL-M002 | agentSkills array can have up to 20 items |
Error |
| ASKILL-M003 | folder path can have up to 256 characters |
Error |
Package-level validation
| Code | Rule | Common fix | Severity |
|---|---|---|---|
| ASKILL-P001 | Folder referenced in manifest exists in ZIP | Check your ZIP structure | Error |
| ASKILL-P002 | Folder contains a SKILL.md file |
Add missing SKILL.md |
Error |
| ASKILL-P003 | SKILL.md has valid YAML frontmatter between --- delimiters |
Fix YAML syntax | Error |
| ASKILL-P004 | Frontmatter includes name field |
Add name: to frontmatter |
Error |
| ASKILL-P005 | Frontmatter includes description field |
Add description: to frontmatter |
Error |
| ASKILL-P006 | name matches the folder name (last path segment) |
Rename folder or fix name: |
Error |
| ASKILL-P007 | name is kebab-case |
Use my-skill not MySkill or my_skill |
Error |
| ASKILL-P008 | No duplicate folder values in the array |
Remove duplicates | Error |
Connector validation
| Rule | Severity |
|---|---|
Each connector requires an id and displayName |
Error |
All connector id values must be unique within the manifest |
Error |
Exactly one of plugin or remoteMcpServer |
Error |
mcpServerUrl must be a valid HTTPS URL |
Error |
mcpToolDescription required on each remoteMcpServer, with a file that exists in the ZIP |
Error |
authorization.referenceId required unless type is None |
Error |
authorization.referenceId must not be present when type is None |
Error |
Companion file validation
The portal validates companion files (reference materials, scripts, and other files alongside SKILL.md) at upload and sync time:
| Rule | Severity |
|---|---|
Maximum 20 companion files per skill (excluding SKILL.md) |
Error |
| Each companion file must be 5 MB or smaller | Error |
| Total companion files must be 10 MB or smaller per skill | Error |
| File paths must be relative (no absolute paths) | Error |
No path traversal segments (..) |
Error |
| No backslashes or null bytes in file names | Error |
No hidden files (names starting with .) |
Error |
No Windows reserved names (CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9) |
Error |
File names must use safe characters only (alphanumeric, hyphens, underscores, dots, spaces, !) |
Error |
Cross-platform compatibility
Skills use the Agent Skills open standard. The same SKILL.md files work across multiple AI tools:
| Platform | Compatibility |
|---|---|
| Claude Code | Full-same SKILL.md format |
| Claude.ai Projects | Full-skills can be uploaded as project files |
| VS Code / GitHub Copilot | Full-Agent Skills supported in agent mode |
| Gemini CLI | Full-Agent Skills supported |
| JetBrains Junie | Full-Agent Skills supported |
| OpenAI Codex | Full-Agent Skills supported |
| Cursor | Full-Agent Skills supported |
If you're developing skills for both Claude Code and Cowork, start with the Claude Code plugin structure - it's the superset:
my-plugin/
├── .claude-plugin/
│ └── plugin.json # Claude plugin manifest
├── skills/
│ ├── skill-one/
│ │ ├── SKILL.md # Works in both Claude Code AND M365
│ │ └── references/
│ └── skill-two/
│ └── SKILL.md
└── .mcp.json # MCP server config (optional)
Then import it into an M365 project when you're ready to publish to the Microsoft 365 App Store:
atk import openplugin --path ./my-plugin --output ./my-plugin-project \
--privacy-url https://contoso.com/privacy \
--terms-url https://contoso.com/terms
MCP annotation and confirmation management
Copilot Cowork reads the standard MCP annotations object on tools your server returns from tools/list, and uses it to decide whether a tool call needs user confirmation and what label to show on the prompt.
Available fields
| Field | Type | Effect |
|---|---|---|
readOnlyHint |
bool | false: confirmation required before the tool runs. |
destructiveHint |
bool | true: confirmation required before the tool runs. |
title |
string | Human-readable label shown on the confirmation dialog. Falls back to the tool name when absent. |
Confirmation rules
Confirmation is required if readOnlyHint == false or destructiveHint == true.
All tools must have safety annotations specified. Tools without annotations are treated as destructive and require confirmation. Learn more in the MCP schema reference.
MCP examples
A destructive action with a friendly label:
{
"name": "send_email",
"description": "Send an email message.",
"annotations": {
"title": "Send Email",
"destructiveHint": true
},
"inputSchema": { ... }
}
A safe read that auto-runs:
{
"name": "search_docs",
"annotations": {
"title": "Search Documents",
"readOnlyHint": true
}
}
What's available now
- Microsoft tools (Graph, Dataverse, and others) are gated by Cowork's built-in policy regardless of annotations.
- For non-Microsoft MCP servers, annotation-driven confirmation is being rolled out progressively. Setting the hints now is forward-compatible, and confirmation prompts surface as the rollout expands with no developer change required.
Accept files from the Cowork workspace
A connector tool can take a file from the user's Cowork session as input—a document the user attached, an email attachment Cowork saved, or a file an earlier step produced. Declare the parameter with the standard JSON Schema keyword contentEncoding: base64 and Cowork handles the rest. No Microsoft-specific schema extension is required, and your server's API surface doesn't change.
Cowork resolves the workspace file and base64-encodes it before calling your server, so file bytes never enter the agent's context. The agent only ever sees and emits workspace file paths.
Note
Don't instruct the agent to base64-encode a file itself and paste the blob into a tool call. That loads the whole file into the model's context and depends on the model reproducing the blob exactly. It appears to work on small test files and fails on real ones.
Declare a file parameter
A string property with contentEncoding: base64 is recognized as a file input:
{
"name": "analyze_contract",
"description": "Extract key terms from a contract document.",
"annotations": {
"title": "Analyze Contract",
"readOnlyHint": true
},
"inputSchema": {
"type": "object",
"properties": {
"document": {
"type": "string",
"contentEncoding": "base64",
"description": "The contract file to analyze."
},
"jurisdiction": {
"type": "string",
"description": "Two-letter country code governing the contract."
}
},
"required": ["document"]
}
}
An array of such strings is also recognized, for tools that accept several files:
"attachments": {
"type": "array",
"items": { "type": "string", "contentEncoding": "base64" },
"description": "Receipt images to attach to the expense line."
}
What the agent sees
For file parameters declared at the top level of inputSchema.properties, Cowork replaces them in the model-facing schema with a single direct_attachment_file_paths array—the same parameter Cowork's built-in tools use, so the agent already knows how to populate it. The schema above is presented to the agent as:
{
"type": "object",
"properties": {
"direct_attachment_file_paths": {
"type": "array",
"items": { "type": "string" },
"description": "Workspace file paths to attach."
},
"jurisdiction": { "type": "string" }
}
}
If your tool declares more than one top-level file parameter, all of them collapse into that single direct_attachment_file_paths array. At call time Cowork fans the resolved files back out into your original parameter names in declaration order.
Nested file parameters
A file parameter nested inside an object or an array of objects is also supported, and is handled differently: instead of being collapsed, it's rewritten in place into a path string at its own location. This preserves the association between a file and its sibling fields—for example, one receipt per expense line:
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"amount": { "type": "number" },
"receipt": { "type": "string", "contentEncoding": "base64" }
}
}
}
The agent populates line_items[].receipt with a workspace path, and Cowork swaps each path for base64 content in place before forwarding the call.
Nesting is traversed to a depth of four levels below the top of inputSchema. $ref pointers aren't followed—define file parameters inline rather than behind a $ref.
What your server receives
Your server receives an ordinary tools/call with your original parameter names populated with base64-encoded content:
{
"method": "tools/call",
"params": {
"name": "analyze_contract",
"arguments": {
"document": "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5...",
"jurisdiction": "US"
}
}
}
Your server doesn't need to know that the agent used a path-based interface, and tools that don't declare contentEncoding: base64 parameters are unaffected.
Limits
| Limit | Value |
|---|---|
| Files per tool call | 8 |
| Size per file | 150 MiB |
| Total size per tool call | 150 MiB |
| Array file parameters per tool | 1 (combine it with any number of scalar file parameters) |
| Maximum nesting depth | 4 levels below the top of inputSchema |
A call that exceeds the file count or a size cap fails with a tool error and never reaches your server. Size your API and its timeouts with the 150 MiB ceiling in mind: base64 inflates the payload by roughly a third over the raw file size, and the encoded content is sent in the JSON-RPC request body.
Recommendations
- Describe the parameter for a human reader. The agent uses the description to decide which file belongs in which parameter. For example,
"The signed contract PDF to analyze"works better than"file". - State the formats you accept in the parameter description. Cowork passes through whatever the user attaches. Validate the content type on your side and return a clear tool error if it isn't usable.
- Set annotations. A tool that receives a file and acts on it is normally not read-only, so it prompts for confirmation. See MCP annotation and confirmation management.
- Keep file parameters inline. A parameter behind a
$ref, or nested deeper than four levels, isn't rewritten. Your server would receive a path string where it expects content. - Declare at most one array file parameter per tool. With two or more, Cowork can't tell which file belongs in which array, and the call fails with a tool error. Use one array, or several scalar parameters, or a mix of scalars and a single array.
- Expect an exact count on scalar-only tools. If your tool declares only scalar file parameters, the number of files the agent passes must match the number declared. Mark optional file parameters clearly in their descriptions so the agent doesn't under- or over-supply.
Note
This mechanism predates the Model Context Protocol's own file-input work, which is being standardized by the MCP File Uploads Working Group. Cowork might add support for the standardized form of declarative file inputs once it lands. The contentEncoding: base64 contract described here continues to work.
Identify Cowork traffic to your server
If your MCP server gates tool visibility by client, or you want to attribute the traffic it receives, you can recognize requests that come from Cowork. Cowork presents a stable software identity on two channels:
| Channel | Where it appears | Value |
|---|---|---|
User-Agent request header |
Every outbound request Cowork sends to your server | copilot-cowork/1.0 |
clientInfo in the MCP initialize handshake |
The initialize request only |
{ "name": "copilot-cowork", "version": "<version>" } |
Match on the copilot-cowork prefix
Match the copilot-cowork prefix, case-insensitive, on either channel. Don't match the exact copilot-cowork/1.0 string or a specific clientInfo.version. The version tracks the client-identity contract and is expected to change; a prefix match keeps your gate working across version bumps.
# Correct: case-insensitive prefix match
copilot-cowork
# Incorrect: exact match breaks when the version changes
copilot-cowork/1.0
Choose the right channel for your gate
The two channels have different scopes, so key on the one that matches how your server enforces its gate:
- The
User-Agentheader is present on every request, includingtools/listandtools/call. If you gate or attribute per request, key on this header. clientInfois sent only on theinitializehandshake. If you gate per session at connection time, you can read it there, but it isn't repeated on later requests.
What the identity includes and doesn't include
The identity names the software only. It's the same for every Cowork user and connection, and it never carries user identity. User identity stays in the authorization flow your connector's auth configuration defines.
| The identity includes | The identity doesn't include |
|---|---|
A stable software name (copilot-cowork) and a contract version |
Any tenant, user, session, or conversation identifier |
| The same value on every request and every connection | A per-connector qualifier |
Because there's no per-connector qualifier, you can't currently use this identity to tell which connector made a call, or to separate a published Microsoft plugin from a sideloaded server pointed at the same URL. If you need that distinction, enforce it through your connector's authorization configuration rather than the client identity.
Common questions
Can I use skills from the M365 package in Claude Code?
Yes. The skill folders contain standard Agent Skills. Copy them to .claude/skills/ in any Claude Code project, or run atk export openplugin to convert the whole project back to a Claude Code plugin.
Do I need a remote connector?
No. Skills-only packages work well for prompt-based workflows. Connectors are only needed when your skill requires live data from an external system.
How are plugin skills different from built-in skills?
Plugin skills appear with source "package" in the API. They can't override built-in skills of the same name. Admin-deployed packages show isAdminDeployed: true.
Can IT admins control which plugins are available?
Yes. Standard M365 admin controls apply: tenant-level allow/block lists, admin-managed deployments, and compliance policies.
What happens if a plugin is revoked?
On the next sync cycle, the skills and connectors from that package are removed from the user's session. Active conversations aren't interrupted, but new sessions don't have the package's capabilities.
What's the maximum number of skills per package?
Twenty (20) skills (per ASKILL-M002). For connectors, the limit is 10 per package.
Can skills reference connector tools from the same package?
Yes, and they should. Name the tools explicitly in your SKILL.md workflow (for example, "Use the search_case_law tool to..."). The agent connects them at runtime.
Can my plugin's tools accept files from the Cowork workspace?
Yes. Declare the tool parameter with contentEncoding: base64, and Cowork resolves the user's workspace file to base64 content before calling your server. The model passes file paths, not file content, so large files don't consume the model's context. For declaration details and limits, learn more in Accept files from the Cowork workspace.
How do I generate a deterministic GUID for my package?
atk import openplugin uses UUID v5 (SHA-1 based) from your plugin name. Running the import twice produces the same GUID. To set your own, pass --app-id. For manual packaging, use any GUID generator. Be sure to keep it stable across versions.