Generated banner for Declarative Agent Developer
Generated logo for Declarative Agent Developer

Declarative Agent Developer

MicrosoftOfficialPublished by Microsoft

  • Skill
  • Tool integration
  • Software development
  • MIT
  • Low risk

From the author

Create, build, deploy, and localize declarative agents for M365 Copilot and Teams. USE THIS SKILL for ANY task involving a declarative agent — including localization, scaffolding, editing manifests, adding capabilities, and deploying. Localization requires tokenized manifests and language files that only this skill knows how to produce. Triggers: "create agent", "create a declarative agent", "new declarative agent", "scaffold an agent", "new agent project", "add a capability", "add a plugin", "configure my agent", "deploy my agent", "fix my agent manifest", "edit my agent", "localize my agent", "add localization", "translate my agent", "multi-language agent", "add an API plugin", "add an MCP plugin", "add OAuth to my plugin", "review instructions", "improve instructions", "fix my instructions"
— the description by Microsoft

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The declarative-agent-developer skill provides a structured workflow for managing the lifecycle of M365 Copilot and Teams declarative agents. It utilizes the M365 Agent Toolkit (ATK) CLI to handle project scaffolding, manifest management, API plugin integration, and deployment. The skill enforces strict workspace validation, requiring specific project structures before allowing modifications, and provides standardized protocols for error handling, localization, and authentication configuration.

What it does

  • Scaffolds new declarative agent projects using the ATK CLI.
  • Manages and validates declarativeAgent.json manifests.
  • Integrates API plugins and MCP servers into existing agent projects.
  • Configures OAuth authentication for secure plugin communication.
  • Handles localization of agent manifests and language files.
  • Deploys agents to local environments for testing and validation.

When to use it

  • When creating a new declarative agent project for M365 Copilot or Teams.
  • When adding new capabilities, API plugins, or MCP servers to an existing agent.
  • When updating agent instructions or conversation starters.
  • When localizing an agent for multiple languages.
  • When troubleshooting or fixing errors in an agent manifest.

Advantages

  • Ensures consistent project structure through mandatory workspace checks.
  • Automates complex tasks like API plugin registration and OAuth setup.
  • Provides a clear, step-by-step protocol for error detection and resolution.
  • Facilitates standardized deployment and testing workflows.
  • Maintains schema compatibility by checking version requirements before feature implementation.

Usage examples

“Create a new declarative agent project.”
The agent will initialize a new project structure, including the necessary manifest files, provided the workspace is empty and valid for agent development.
“Add an API plugin to my agent using this OpenAPI spec URL.”
The agent will use the ATK CLI to integrate the specified API, map the requested operations, and prompt for necessary metadata updates.
“Localize my agent into French.”
The agent will generate the required tokenized manifests and language files to support French localization.
“Deploy my agent to the local environment.”
The agent will run the provisioning command and provide the M365 test link for the deployed agent.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/microsoft/declarative-agent-developer/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip declarative-agent-developer.zip -d .agents/skills

Documentation by Microsoft

M365 Agent Developer

Telemetry Tagging

Before running any atk CLI commands, set the session environment variable so all CLI invocations are tagged as skill-initiated:

export ATK_CLI_SKILL=true

Run this once at the start of the session. All subsequent atk commands in the same terminal will inherit it.

⛔ Workspace Check — MANDATORY FIRST STEP

Before doing ANYTHING, check the workspace files to fingerprint the project:

  1. Run npx -y --package @microsoft/m365agentstoolkit-cli atk --version to confirm ATK CLI is installed. If not found → Stop. Tell the user to install ATK.

  2. Check for m365agents.yml or teamsApp.yml at the project root.

  3. Check for appPackage/declarativeAgent.json.

  4. Check for non-agent indicators (package.json with express/react/next, src/index.js, app.py, etc.)

Then follow the decision gate:

ConditionGateAction
Non-agent project files, no appPackage/RejectText-only response. No files, no commands.
No manifest, user wants to edit/deployRejectText-only response. Explain manifest is missing.
No manifest, user wants new projectScaffold→ Scaffolding Workflow
Manifest exists with errorsFixDetect → Inform → Ask (see below). Do NOT deploy.
Valid project, user reports behavior issuesReview→ Instruction Review — run the full 5-phase review workflow
Valid agent projectEdit→ Editing Workflow

Detailed gate rules, examples, and anti-patterns: Workspace Gates

🚫 HARD REJECTION RULES — No Exceptions

These rules override ALL other instructions. If any of these apply, you MUST stop immediately.

  1. NEVER create declarativeAgent.json yourself. If the manifest is missing and the user asked to edit/modify/deploy, respond with text only: explain the manifest is missing, suggest npx -y --package @microsoft/m365agentstoolkit-cli atk new or starting from scratch. Do NOT create the file, do NOT create appPackage/, do NOT "help" by scaffolding implicitly.

  2. NEVER create files in a non-agent project. If the workspace is an Express/React/Django/etc. app without appPackage/, your response must be text-only. Do NOT create any files, do NOT run any commands.

  3. NEVER deploy when errors exist. If the agent manifest has errors, STOP. Do NOT run npx -y --package @microsoft/m365agentstoolkit-cli atk provision — not "to test", not "to demonstrate the error", not "to see what happens". Report the errors and ask the user how to proceed.

🔍 Detect → Inform → Ask (Error-Handling Protocol)

When you encounter ANY problem (missing files, malformed JSON, validation errors, incompatible features), you MUST follow this sequence in order:

  1. Detect — Identify the specific problem. For JSON issues, attempt to parse the file and report syntax errors. For missing fields, check the manifest against the Schema.

  2. Inform — Tell the user BEFORE taking any action. Describe exactly what is wrong ("declarativeAgent.json has malformed JSON: missing comma on line 12, unclosed array on line 18").

  3. Ask — Wait for the user's response before making changes. Do NOT silently fix, auto-correct, or work around the problem.

This protocol applies to:

  • Missing declarativeAgent.json → Detect (file not found) → Inform ("no manifest found") → Ask ("would you like to create a new agent?")

  • Malformed JSON → Detect (parse errors) → Inform (list specific syntax issues) → Ask ("should I fix these syntax errors?")

  • Validation errors → Detect (parse and check manifest) → Inform (list all errors) → Ask ("how would you like to fix these?")

  • Version incompatibility → Detect (feature requires newer version) → Inform ("this feature requires v1.6, your agent is v1.4") → Ask ("should I upgrade?")


Phase Routing

ScenarioWorkflow Reference
Creating a NEW project from scratchScaffolding Workflow
Working with existing .json manifestsEditing Workflow
Adding an API pluginAPI Plugins
Adding an MCP serverMCP Plugin
Adding OAuth to an MCP or API pluginAuthentication
Reviewing or improving existing agent instructionsInstruction Review
User reports agent gives generic/wrong answersInstruction Review
Localizing an agent into multiple languagesLocalization
Adding a new language to an already-localized agentLocalization
Writing agent instructionsConversation Design

ATK CLI Setup

Before running any ATK commands, check if the ATK CLI is available by running npx -y --package @microsoft/m365agentstoolkit-cli atk --version. If not found, STOP and tell the user — do NOT attempt to install it yourself.

All commands use the npx -y --package @microsoft/m365agentstoolkit-cli atk prefix (e.g., npx -y --package @microsoft/m365agentstoolkit-cli atk provision --env local).


Critical Rules

1. Deploy After EVERY Edit

After ANY change to files in appPackage/, you MUST deploy and show the test link before responding:

npx -y --package @microsoft/m365agentstoolkit-cli atk provision --env local --interactive false

Then read M365_TITLE_ID from env/.env.local and ALWAYS present the review UX:

✅ Agent deployed successfully!

🚀 Test Your Agent in M365 Copilot:
🔗 https://m365.cloud.microsoft/chat/?titleId={M365_TITLE_ID}

⛔ Never respond without this link. If you deployed, the test link MUST appear in your response. This is not optional — it is how the user tests their agent.

  • If the manifest has errors → STOP. Fix errors. Do NOT deploy.

  • Exception: user explicitly asks you not to deploy

2. Never Invent Content or Create Missing Files
  • Do NOT invent placeholder names, descriptions, or instructions

  • Do NOT create declarativeAgent.json or appPackage/ if they don't exist — this is a REJECT scenario, not a "help by creating" scenario

  • If required fields are missing, report the gaps, and ASK the user

  • If JSON is malformed, follow Detect → Inform → Ask: parse the file first, tell the user what's broken, then ask before fixing. Use surgical edits (not rewrites)

  • ⛔ NEVER set placeholder values for environment variables that are populated by automation (e.g., <PREFIX>_MCP_AUTH_ID, TEAMS_APP_ID). Leave them empty (VAR_NAME=). Placeholders will be treated as real values and will NOT be overwritten by provisioning.

3. Schema Version Compatibility

Before adding ANY feature, read the version field in declarativeAgent.json and check the Schema feature matrix. If the feature isn't supported in that version, refuse and offer to upgrade.

Key version gates:

  • sensitivity_label, worker_agents, EmbeddedKnowledge → v1.6 only

  • Meetings → v1.5+

  • ScenarioModels, behavior_overrides, disclaimer → v1.4+

  • Dataverse, TeamsMessages, Email, People → v1.3+

4. Use npx -y --package @microsoft/m365agentstoolkit-cli atk add action for API Plugins — NEVER Create Plugin Files Manually

You are forbidden from manually creating ai-plugin.json, OpenAPI specs, adaptive cards, or editing the actions array. Use the CLI:

# ⛔ Always list ALL operations in a single call — NEVER run separate calls per operation
npx -y --package @microsoft/m365agentstoolkit-cli atk add action --api-plugin-type api-spec --openapi-spec-location URL --api-operation "GET /path,POST /path,PATCH /path/{id},DELETE /path/{id}" -i false

Run a single npx -y --package @microsoft/m365agentstoolkit-cli atk add action call per OpenAPI spec, listing all operations as a comma-separated list in --api-operation. Never run separate npx -y --package @microsoft/m365agentstoolkit-cli atk add action calls for different operations from the same spec — this creates multiple plugins instead of one. If npx -y --package @microsoft/m365agentstoolkit-cli atk add action fails, report the error; do NOT fall back to manual creation.

Exception: MCP servers are not supported by npx -y --package @microsoft/m365agentstoolkit-cli atk add action. Use the MCP Plugin workflow instead.

5. MCP Server Integration

When the user mentions an MCP server URL, follow the MCP Plugin workflow. You MUST discover tools via the MCP protocol handshake (initialize → notifications/initialized → tools/list) — NEVER fabricate tool names/descriptions. For authenticated MCP servers, follow the authentication guide to configure OAuth.

6. Always Update Instructions & Starters After Changes

Adding a capability or plugin without updating instructions is incomplete. After ANY change:

  1. Update instructions to describe the new/changed functionality — every data source should have clear intent coverage (WHEN and WHY to use it) per the Instruction Review quality bar. Built-in capabilities don't need exact names; actions/plugins should be named.

  2. Do NOT list tool names, descriptions, or parameters in instructions — these are already in the plugin metadata (ai-plugin.json, MCP manifests, capability config). Instructions should contain decision logic only: WHEN to use each tool, chaining rules, and failure handling.

  3. Stay within the 8,000-character instruction limit — if close to the limit, cut tool descriptions first

  4. Add at least 1 conversation starter per added capability/plugin

  5. Remove starters that reference removed capabilities

  6. Run the Diagnostic Checklist against the updated instructions to verify quality

7. App Name Requirement

Always update the app name and description to something meaningful. Never leave defaults like "My Agent".


References

Shared
  • Authentication — OAuth discovery, credentials, oauth/register lifecycle, OAuthPluginVault

  • Best Practices — Security, performance, testing, compliance

  • Conversation Design — Authoring instructions and conversation starters from scratch

  • Instruction Review — Auditing, diagnosing, and improving existing instructions; anti-pattern detection; before/after rewrites

  • Deployment — ATK CLI workflows, environments, CI/CD

  • Localization — Multi-language support, tokenized manifests, language files

  • Workspace Gates — Detailed gate rules, examples, anti-patterns

Scaffolding
JSON Development
  • Editing Workflow — Step-by-step JSON development instructions

  • Schema — Official JSON schema for agent manifests

  • API Plugins — OpenAPI integration for JSON agents

  • MCP Plugin — MCP server integration with RemoteMCPServer, OAuth, response semantics, logo handling

  • Examples — JSON manifest examples

Files

references/mcp-plugin.md

MCP Server Plugin Integration

This guide explains how to integrate Model Context Protocol (MCP) servers as actions in your Microsoft 365 Copilot agent using JSON manifests. It covers both unauthenticated and OAuth-authenticated MCP servers.

⛔ SINGLE FILE ONLY: MCP plugins require exactly ONE file — the plugin manifest ({name}-plugin.json). Tool descriptions are inlined directly in the manifest's mcp_tool_description.tools array. Do NOT create a separate {name}-mcp-tools.json file. There is no "file" property — only "tools": [...].

Overview

MCP servers expose tools that can be consumed by your agent. Unlike OpenAPI-based plugins, MCP plugins use a RemoteMCPServer runtime type and embed the tool descriptions directly in the plugin manifest.

⚠️ IMPORTANT: npx -y --package @microsoft/m365agentstoolkit-cli atk add action does NOT support MCP servers — it only supports --api-plugin-type api-spec for OpenAPI plugins. MCP plugins MUST be created manually following the steps below. This is NOT a violation of the "Always Use npx -y --package @microsoft/m365agentstoolkit-cli atk add action" rule — that rule applies only to OpenAPI/REST API plugins.

Prerequisites

  • MCP server URL (must be accessible via HTTP/HTTPS)

  • Node.js installed (for mcp-remote authentication helper)

  • Logo images for the agent (color.png 192×192 and outline.png 32×32) — optional, see Step 5: Logo Images


Scaffold the Agent Project First

Before adding an MCP plugin, you must have a scaffolded agent project. Run npx -y --package @microsoft/m365agentstoolkit-cli atk new if you haven't already:

npx -y --package @microsoft/m365agentstoolkit-cli atk new \
  -n my-agent \
  -c declarative-agent \
  -i false

This creates m365agents.yml (and m365agents.local.yml) with the 5 required lifecycle steps:

StepLifecycle ActionWhat it does
1teamsApp/createRegisters the Teams app
2teamsApp/zipAppPackagePackages manifest + icons into a zip
3teamsApp/validateAppPackageValidates the package (icons, schema, etc.)
4teamsApp/updateUploads the package to Teams
5teamsApp/extendToM365Extends the app to M365 Copilot — generates M365_TITLE_ID

What breaks without extendToM365: If this step is missing, npx -y --package @microsoft/m365agentstoolkit-cli atk provision will register the Teams app and generate TEAMS_APP_ID, but the agent will never appear in Copilot Chat because no M365_TITLE_ID is generated. This is the most common reason for "provision succeeded but agent not found" failures.

If you already have a project but are missing teamsApp/extendToM365, add it to the provision lifecycle in m365agents.yml after teamsApp/update. See deployment.md for the full provisioning reference.


Step-by-Step Integration

Step 1: Get MCP Server URL

Ask the user for the MCP server URL. Example: https://learn.microsoft.com/api/mcp

Derive the server root (scheme + host only): e.g., https://learn.microsoft.com

Step 2: Detect Authentication Requirements

Before discovering tools, determine if the MCP server requires OAuth authentication.

Probe both well-known endpoints in parallel:

curl -s <SERVER_ROOT>/.well-known/oauth-authorization-server
curl -s <SERVER_ROOT>/.well-known/openid-configuration

Decision:

  • OAuth metadata found (either endpoint returns valid JSON with authorization_endpoint) → the server requires authentication. Follow authentication.md Steps 1-3 to discover endpoints, obtain credentials, and configure oauth/register in both m365agents.yml and m365agents.local.yml. Then continue to Step 3 below for authenticated tool discovery.

  • No OAuth metadata (both return 404 or non-JSON) → the server is unauthenticated. Skip directly to Step 3 for unauthenticated tool discovery.

Step 3: Discover MCP Tools (MANDATORY)

🚨 THIS STEP IS MANDATORY — DO NOT SKIP

You MUST discover tools via the MCP protocol directly. Tool discovery uses HTTP POST requests to the MCP server URL.

3a. Authenticate (OAuth servers only)

If the server requires OAuth (detected in Step 2), perform a one-time authentication:

Tell the user:

"I need to authenticate with [name]'s MCP server. A browser window will open — please sign in."

Run the command interactively (NOT backgrounded — do NOT append & or redirect to files):

npx -p mcp-remote@latest mcp-remote-client <MCP_SERVER_URL> --port 3334

Wait for it to complete. The command will open a browser for OAuth sign-in and then exit once authentication succeeds.

WSL / headless environments: mcp-remote starts a local HTTP server for the OAuth callback and tries to open a browser. In WSL, the browser opens on the Windows host but the http://127.0.0.1:3334/... callback URL may not route back to WSL. If the browser opens but authentication seems stuck:

  1. After signing in, copy the full callback URL from the browser (it will show an error or blank page)

  2. Run curl '<callback-url>' inside WSL to deliver the auth code to mcp-remote

  3. Alternatively, run export BROWSER=wslview before the command so WSL's browser opener is used, which handles the redirect correctly

⛔ IMPORTANT: Do NOT look for tokens in /tmp/ or log files. Tokens are ONLY stored at ~/.mcp-auth/.

Read the cached access token from ~/.mcp-auth:

ls ~/.mcp-auth/mcp-remote-*/

Find the token file (pattern: {url-hash}_tokens.json), read it, and extract access_token.

⛔ Security: Do NOT print the token value in your output. Extract it silently and use it only in subsequent HTTP calls. Do NOT write it to any file or create copies.

3b. MCP Session Handshake

Run three sequential HTTP calls to discover tools.

⛔ Security: Suppress raw HTTP responses that may contain tokens. Only extract the fields you need (mcp-session-id, tool definitions). Do NOT display Authorization headers or token values to the user.

Call 1 — Initialize:

curl -s -X POST <MCP_SERVER_URL> \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  [-H "Authorization: Bearer <access_token>"] \
  -D - \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"m365-agent-skill","version":"1.0.0"}}}'

Extract mcp-session-id from the response headers. Omit the Authorization header for unauthenticated servers.

Call 2 — Initialized notification:

curl -s -X POST <MCP_SERVER_URL> \
  -H "Content-Type: application/json" \
  -H "mcp-session-id: <session_id>" \
  [-H "Authorization: Bearer <access_token>"] \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

Call 3 — List tools (with pagination):

curl -s -X POST <MCP_SERVER_URL> \
  -H "Content-Type: application/json" \
  -H "mcp-session-id: <session_id>" \
  [-H "Authorization: Bearer <access_token>"] \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

If the response contains nextCursor, repeat with {"params":{"cursor":"<nextCursor>"}} until no cursor remains. Collect all tools.

Extracting tools from the response:

Save the raw tools/list response to a file, then use this script to extract the tools array:

python3 << 'EXTRACT_TOOLS'
import json, sys

with open("/tmp/mcp-tools-response.json") as f:
    data = json.load(f)

tools = data.get("result", {}).get("tools", [])
with open("/tmp/mcp-tools.json", "w") as out:
    json.dump(tools, out, indent=2)

print(f"Extracted {len(tools)} tools")
for t in tools:
    print(f"  - {t['name']}: {t.get('description', '')[:80]}")
EXTRACT_TOOLS

⛔ Do NOT use inline Python inside command substitutions (e.g., $(python3 -c '...')). The shell security policy blocks nested command substitutions. Always use heredoc scripts (<< 'EOF') or standalone .py files instead.

Expected output structure:

{
  "result": {
    "tools": [
      {
        "name": "tool_name",
        "description": "Tool description",
        "inputSchema": {
          "type": "object",
          "properties": { ... },
          "required": [...]
        },
        "annotations": {
          "readOnlyHint": true
        },
        "execution": {
          "taskSupport": "forbidden"
        },
        "_meta": {
          "ui": {
            "resourceUri": "ui://namespace/view-name.html"
          }
        }
      }
    ]
  }
}

⚠️ IMPORTANT: The example above shows commonly seen fields (annotations, execution, _meta), but MCP servers may return any additional properties on tool objects. You MUST preserve every property returned by tools/list — copy each tool object in its entirety into mcp_tool_description.tools[]. Do NOT cherry-pick known fields; treat the tools/list output as the source of truth and inline it verbatim.

3c. Use All Discovered Tools

Include ALL tools returned by tools/list in the plugin manifest. Do NOT filter or exclude tools unless the developer explicitly asks to limit the tool set.

Copy each tool object verbatim — every property the server returns (name, description, inputSchema, annotations, execution, _meta, outputSchema, title, or any other field) must be preserved in mcp_tool_description.tools[]. Do NOT maintain a hardcoded list of "known" fields — the MCP protocol evolves and servers may return new properties at any time.

Tell the user how many tools were discovered and confirm they will all be included.

Step 4: Create the Plugin Manifest

Create {name}-plugin.json in the appPackage folder:

{
  "$schema": "https://developer.microsoft.com/json-schemas/copilot/plugin/v2.4/schema.json",
  "schema_version": "v2.4",
  "name_for_human": "{NAME-FOR-HUMAN}",
  "description_for_human": "{DESCRIPTION-FOR-HUMAN}",
  "namespace": "simplename",
  "functions": [],
  "runtimes": []
}

Required fields:

FieldDescription
name_for_humanDisplay name shown to users (max 20 characters)
description_for_humanBrief description of the plugin (max 100 characters)
namespaceUnique identifier, lowercase alphanumeric only (no hyphens, no underscores)
Step 4a: Add Functions from Discovered Tools

For EACH discovered tool from Step 3, add a function entry with name, description, and capabilities only. Do NOT duplicate parameters/inputSchema in the function — all tool schema data lives exclusively in mcp_tool_description.tools[] (see Step 6).

{
  "functions": [
    {
      "name": "microsoft_docs_search",
      "description": "Search official Microsoft/Azure documentation to find the most relevant content for a user's query."
    }
  ]
}

🚨 CRITICAL: Preserve ALL tool properties when creating function entries:

MCP tools/list OutputPlugin Manifest (functions[])
namename — copy EXACTLY, do not rename
descriptiondescription — use the full description text, do NOT abbreviate or summarize
inputSchemaDo NOT add to functions[] — this goes in mcp_tool_description.tools[] only
Any other property (annotations, execution, _meta, outputSchema, title, or any future field)Do NOT add to functions[] — this goes in mcp_tool_description.tools[] only

🔑 Full-fidelity rule: Every tool object in mcp_tool_description.tools[] must be a verbatim copy of the corresponding tool from the tools/list response. Copy the entire object as-is — do NOT strip, rename, reorder, or omit any property. The MCP protocol evolves and servers may return fields not listed above. If the server returns it, the plugin must include it.

Why this matters: The model uses description from functions to decide when to invoke each tool. The runtime uses the full tool definitions from mcp_tool_description.tools[] to actually call the MCP server. These definitions include inputSchema (parameters), annotations (read-only/destructive hints), execution (task support behavior), _meta (UI widget resources), and potentially other properties. Stripping any of them breaks runtime behavior or loses capabilities. Do not duplicate schema data in both places — only name and description go into functions[].

Step 4b: Add Response Semantics

ALWAYS add capabilities.response_semantics to every function — even if no title or URL fields can be identified. Never omit it.

For each tool:

  1. Check the tool's outputSchema field (optional in MCP — present on some servers). If present, read field names from it directly.

  2. If outputSchema is absent (common), reason from the tool's description text to identify which fields are returned. Look for mentions of URL fields (url, link, href) and title fields (title, name, label).

  3. If you can confidently identify BOTH a title-like field AND a navigable URL field → use the rich pattern.

  4. Otherwise → use the default pattern.

Rich pattern (when title + URL field are identified):

{
  "name": "tool_name",
  "description": "...",
  "capabilities": {
    "response_semantics": {
      "data_path": "$.items",
      "properties": {
        "title": "$.title",
        "url": "$.url"
      },
      "static_template": {
        "type": "AdaptiveCard",
        "$schema": "https://adaptivecards.io/schemas/adaptive-card.json",
        "version": "1.6",
        "body": [
          {
            "type": "TextBlock",
            "text": "[${title}](${url})",
            "wrap": true,
            "maxLines": 2
          }
        ]
      }
    }
  }
}

Default pattern (when title or URL cannot be confidently identified):

{
  "name": "tool_name",
  "description": "...",
  "capabilities": {
    "response_semantics": {
      "data_path": "$",
      "properties": {},
      "static_template": {
        "type": "AdaptiveCard",
        "$schema": "https://adaptivecards.io/schemas/adaptive-card.json",
        "version": "1.6",
        "body": [
          {
            "type": "TextBlock",
            "text": "${if(title, title, description)}",
            "wrap": true
          }
        ]
      }
    }
  }
}

Response semantics rules:

  • $schema: always https://adaptivecards.io/schemas/adaptive-card.json (not http://)

  • version: always "1.6"

  • Rich template body is always "[${title}](${url})" — the title IS the hyperlink

  • Source name comes from name_for_human automatically — do NOT add it as a TextBlock

  • data_path and field paths are connector-specific — derive them from the tool's actual response structure

Step 5: Logo Images (Optional)

Logos are not mandatory. The default logos from npx -y --package @microsoft/m365agentstoolkit-cli atk new work fine. Ask the user casually:

"Would you like to use a custom logo for [name], or is the default fine?"

If the user does not have a logo or says to skip → move on. Do NOT block the workflow for logos.

If the user does want a custom logo, they need two PNG files (no JPG, SVG, or other formats):

  • color.png — 192×192 px, full colour

  • outline.png — 32×32 px, white-on-transparent

Resolving logo inputs — check in this order:

  1. URL: If the user provides a URL, download the image with curl -L -o <tempfile> <url>.

  2. Local file path: If the user provides a path, use it directly.

If the provided image is not PNG, convert it to PNG before processing.

Handling missing formats:

  • If the user provides only one image, ask: "I have your [color/outline] logo. For the [other format], would you like to provide it, or shall I generate it automatically?"

  • If the user says to generate it, derive it from the provided image using jimp.

  • If the user says the provided images already meet the size requirements, skip processing and use them directly.

Processing with jimp (only when resizing or conversion is needed):

// Install: npm install jimp (in a temp directory)
// Import: const { Jimp } = require('jimp');

// color.png: resize to 192x192
// outline.png: resize to 32x32, convert all non-transparent pixels to white on transparent background

Output files: appPackage/color.png (192×192) and appPackage/outline.png (32×32 white-on-transparent).

Show the resulting icon(s) to the user for approval before proceeding. If the user rejects, ask them to provide their own images and do NOT proceed until approved.

Step 6: Configure the Runtime

Add the RemoteMCPServer runtime with the tools inlined in mcp_tool_description.tools:

For authenticated servers (see authentication.md):

{
  "runtimes": [
    {
      "type": "RemoteMCPServer",
      "auth": {
        "type": "OAuthPluginVault",
        "reference_id": "${{<PREFIX>_MCP_AUTH_ID}}"
      },
      "spec": {
        "url": "{MCP_SERVER_URL}",
        "mcp_tool_description": {
          "tools": [
            {
              "name": "function_name_1",
              "description": "Full tool description from tools/list output",
              "inputSchema": {
                "type": "object",
                "properties": { "..." : "..." },
                "required": ["..."]
              },
              "annotations": { "readOnlyHint": true },
              "execution": { "taskSupport": "forbidden" },
              "_meta": { "ui": { "resourceUri": "ui://namespace/view.html" } }
            }
          ]
        }
      },
      "run_for_functions": [
        "function_name_1",
        "function_name_2"
      ]
    }
  ]
}

For unauthenticated servers:

{
  "runtimes": [
    {
      "type": "RemoteMCPServer",
      "auth": {
        "type": "None"
      },
      "spec": {
        "url": "{MCP_SERVER_URL}",
        "mcp_tool_description": {
          "tools": [ ... ]
        }
      },
      "run_for_functions": [ ... ]
    }
  ]
}

⚠️ IMPORTANT:

  • The mcp_tool_description.tools array must be a verbatim copy of the tools from the tools/list output (Step 3). Copy every property on each tool object exactly as returned — inputSchema, annotations, execution, _meta, and any other field the server includes. Do NOT strip, rename, or omit any property. Do NOT use a file reference — inline the tools directly.

  • Do NOT fabricate properties that the server did not return. Only include what tools/list actually gives you.

  • For authenticated servers, both m365agents.yml and m365agents.local.yml must include the oauth/register step — see authentication.md.

Step 7: Register Plugin in Agent Manifest

Add the plugin to your declarative-agent.json:

{
  "actions": [
    {
      "id": "mcpPlugin",
      "file": "{name}-plugin.json"
    }
  ]
}

Complete Workflow Checklist

□ Step 0: Scaffold agent project with `npx -y --package @microsoft/m365agentstoolkit-cli atk new` (if not already scaffolded)      ← MANDATORY
□ Step 1: Get MCP server URL from user
□ Step 2: Detect authentication requirements (probe well-known endpoints)
□       → If OAuth: follow authentication.md (discover endpoints, get creds, configure oauth/register)
□ Step 3: Discover tools via MCP protocol (initialize → tools/list)               ← MANDATORY
□       → Include ALL tools (do not filter unless developer explicitly requests it)
□ Step 4: Create {name}-plugin.json with functions + response_semantics
□ Step 5: Ask user about custom logo (optional — skip if user declines)
□ Step 6: Add runtime with RemoteMCPServer type (OAuthPluginVault or None)
□ Step 7: Register plugin in declarativeAgent.json
□ Step 8: Run npx -y --package @microsoft/m365agentstoolkit-cli atk provision --env local --interactive false

Complete Example — Unauthenticated Server

For the Zava Insurance MCP server at https://zava-insurance-mcp.azurewebsites.net/mcp:

appPackage/zava-plugin.json
{
  "$schema": "https://developer.microsoft.com/json-schemas/copilot/plugin/v2.4/schema.json",
  "schema_version": "v2.4",
  "name_for_human": "Zava Insurance",
  "description_for_human": "Manage insurance claims, inspections, contractors, and purchase orders",
  "namespace": "zavainsurance",
  "functions": [
    {
      "name": "show-claims-dashboard",
      "description": "Displays the Zava Insurance claims dashboard showing all claims with status overview, filters, and summary metrics. Supports filtering by status and/or policy holder name. When the user mentions a person's name, first name, last name, or partial name, always pass it as the policyHolderName parameter. The name filter is case-insensitive and supports partial matches.",
      "capabilities": { "response_semantics": { "data_path": "$", "properties": {}, "static_template": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [{ "type": "TextBlock", "text": "${if(title, title, description)}", "wrap": true }] } } }
    },
    {
      "name": "show-claim-detail",
      "description": "Displays detailed information about a specific insurance claim including related inspections, purchase orders, and contractor assignments. Use claim ID (e.g. '1', '2') or claim number (e.g. 'CN202504990').",
      "capabilities": { "response_semantics": { "data_path": "$", "properties": {}, "static_template": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [{ "type": "TextBlock", "text": "${if(title, title, description)}", "wrap": true }] } } }
    },
    {
      "name": "show-contractors",
      "description": "Displays the list of contractors available for insurance repair work. Optionally filter by specialty or preferred status.",
      "capabilities": { "response_semantics": { "data_path": "$", "properties": {}, "static_template": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [{ "type": "TextBlock", "text": "${if(title, title, description)}", "wrap": true }] } } }
    },
    {
      "name": "update-claim-status",
      "description": "Updates the status of an insurance claim. Use claim ID (e.g. '1', '2').",
      "capabilities": { "response_semantics": { "data_path": "$", "properties": {}, "static_template": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [{ "type": "TextBlock", "text": "${if(title, title, description)}", "wrap": true }] } } }
    },
    {
      "name": "update-inspection",
      "description": "Updates an inspection record — status, findings, recommended actions, property, or inspector assignment.",
      "capabilities": { "response_semantics": { "data_path": "$", "properties": {}, "static_template": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [{ "type": "TextBlock", "text": "${if(title, title, description)}", "wrap": true }] } } }
    },
    {
      "name": "update-purchase-order",
      "description": "Updates a purchase order status (e.g. approve, reject, complete).",
      "capabilities": { "response_semantics": { "data_path": "$", "properties": {}, "static_template": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [{ "type": "TextBlock", "text": "${if(title, title, description)}", "wrap": true }] } } }
    },
    {
      "name": "get-claim-summary",
      "description": "Returns a text summary for a specific claim with key details. Use claim ID or claim number.",
      "capabilities": { "response_semantics": { "data_path": "$", "properties": {}, "static_template": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [{ "type": "TextBlock", "text": "${if(title, title, description)}", "wrap": true }] } } }
    },
    {
      "name": "create-inspection",
      "description": "Creates a new inspection record. Only claimNumber is required. ID is auto-generated, status defaults to 'open'. claimId is optional.",
      "capabilities": { "response_semantics": { "data_path": "$", "properties": {}, "static_template": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [{ "type": "TextBlock", "text": "${if(title, title, description)}", "wrap": true }] } } }
    },
    {
      "name": "list-inspectors",
      "description": "Lists all available inspectors with their specializations.",
      "capabilities": { "response_semantics": { "data_path": "$", "properties": {}, "static_template": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [{ "type": "TextBlock", "text": "${if(title, title, description)}", "wrap": true }] } } }
    }
  ],
  "runtimes": [
    {
      "type": "RemoteMCPServer",
      "auth": { "type": "None" },
      "spec": {
        "url": "https://zava-insurance-mcp.azurewebsites.net/mcp",
        "mcp_tool_description": {
          "tools": [
            {
              "name": "show-claims-dashboard",
              "description": "Displays the Zava Insurance claims dashboard showing all claims with status overview, filters, and summary metrics. Supports filtering by status and/or policy holder name. When the user mentions a person's name, first name, last name, or partial name, always pass it as the policyHolderName parameter. The name filter is case-insensitive and supports partial matches.",
              "inputSchema": { "type": "object", "properties": { "status": { "type": "string", "description": "Filter claims by status keyword (e.g. 'Open', 'Approved', 'Pending', 'Denied', 'Closed')" }, "policyHolderName": { "type": "string", "description": "Filter claims by policy holder name. Supports partial, case-insensitive matching." } }, "additionalProperties": false },
              "annotations": { "readOnlyHint": true },
              "execution": { "taskSupport": "forbidden" },
              "_meta": { "ui": { "resourceUri": "ui://zava/claims-dashboard.html" } }
            },
            {
              "name": "show-claim-detail",
              "description": "Displays detailed information about a specific insurance claim including related inspections, purchase orders, and contractor assignments. Use claim ID (e.g. '1', '2') or claim number (e.g. 'CN202504990').",
              "inputSchema": { "type": "object", "properties": { "claimId": { "type": "string", "description": "The claim ID or claim number to look up" } }, "required": ["claimId"], "additionalProperties": false },
              "annotations": { "readOnlyHint": true },
              "execution": { "taskSupport": "forbidden" },
              "_meta": { "ui": { "resourceUri": "ui://zava/claim-detail.html" } }
            },
            {
              "name": "show-contractors",
              "description": "Displays the list of contractors available for insurance repair work. Optionally filter by specialty or preferred status.",
              "inputSchema": { "type": "object", "properties": { "specialty": { "type": "string", "description": "Filter by contractor specialty (e.g. 'Roofing', 'Water Damage', 'Fire')" }, "preferredOnly": { "type": "boolean", "description": "Show only preferred contractors" } }, "additionalProperties": false },
              "annotations": { "readOnlyHint": true },
              "execution": { "taskSupport": "forbidden" },
              "_meta": { "ui": { "resourceUri": "ui://zava/contractors-list.html" } }
            },
            {
              "name": "update-claim-status",
              "description": "Updates the status of an insurance claim. Use claim ID (e.g. '1', '2').",
              "inputSchema": { "type": "object", "properties": { "claimId": { "type": "string", "description": "The claim ID" }, "status": { "type": "string", "description": "New status (e.g. 'Approved', 'Denied', 'Closed', 'Open - Under Investigation')" }, "note": { "type": "string", "description": "Optional note to add to the claim" } }, "required": ["claimId", "status"], "additionalProperties": false },
              "execution": { "taskSupport": "forbidden" }
            },
            {
              "name": "update-inspection",
              "description": "Updates an inspection record — status, findings, recommended actions, property, or inspector assignment.",
              "inputSchema": { "type": "object", "properties": { "inspectionId": { "type": "string", "description": "The inspection ID (e.g. 'insp-001')" }, "status": { "type": "string", "description": "New status (e.g. 'completed', 'scheduled', 'in-progress', 'cancelled')" }, "findings": { "type": "string", "description": "Updated findings text" }, "recommendedActions": { "type": "array", "items": { "type": "string" }, "description": "Updated recommended actions" }, "property": { "type": "string", "description": "Updated property address" }, "inspectorId": { "type": "string", "description": "Inspector ID to assign (e.g. 'inspector-003')" } }, "required": ["inspectionId"], "additionalProperties": false },
              "execution": { "taskSupport": "forbidden" }
            },
            {
              "name": "update-purchase-order",
              "description": "Updates a purchase order status (e.g. approve, reject, complete).",
              "inputSchema": { "type": "object", "properties": { "purchaseOrderId": { "type": "string", "description": "The purchase order ID (e.g. 'po-001')" }, "status": { "type": "string", "description": "New status (e.g. 'approved', 'rejected', 'completed', 'in-progress')" }, "note": { "type": "string", "description": "Optional note to add" } }, "required": ["purchaseOrderId", "status"], "additionalProperties": false },
              "execution": { "taskSupport": "forbidden" }
            },
            {
              "name": "get-claim-summary",
              "description": "Returns a text summary for a specific claim with key details. Use claim ID or claim number.",
              "inputSchema": { "type": "object", "properties": { "claimId": { "type": "string", "description": "Claim ID or claim number" } }, "required": ["claimId"], "additionalProperties": false },
              "execution": { "taskSupport": "forbidden" }
            },
            {
              "name": "create-inspection",
              "description": "Creates a new inspection record. Only claimNumber is required. ID is auto-generated, status defaults to 'open'. claimId is optional.",
              "inputSchema": { "type": "object", "properties": { "claimNumber": { "type": "string", "description": "The claim number (e.g. 'CN202504990')" }, "claimId": { "type": "string", "description": "Optional claim ID" }, "taskType": { "type": "string", "description": "Type of inspection: 'initial', 're-inspection', 'final'. Defaults to 'initial'" }, "priority": { "type": "string", "description": "Priority: 'low', 'medium', 'high'. Defaults to 'medium'" }, "status": { "type": "string", "description": "Status. Defaults to 'open'" }, "scheduledDate": { "type": "string", "description": "Scheduled date (ISO string)" }, "inspectorId": { "type": "string", "description": "Inspector ID to assign" }, "property": { "type": "string", "description": "Property address" }, "instructions": { "type": "string", "description": "Inspection instructions" } }, "required": ["claimNumber"], "additionalProperties": false },
              "execution": { "taskSupport": "forbidden" }
            },
            {
              "name": "list-inspectors",
              "description": "Lists all available inspectors with their specializations.",
              "inputSchema": { "type": "object", "properties": {} },
              "execution": { "taskSupport": "forbidden" }
            }
          ]
        }
      },
      "run_for_functions": [
        "show-claims-dashboard",
        "show-claim-detail",
        "show-contractors",
        "update-claim-status",
        "update-inspection",
        "update-purchase-order",
        "get-claim-summary",
        "create-inspection",
        "list-inspectors"
      ]
    }
  ]
}

Note how tools with UI widgets (e.g., show-claims-dashboard, show-claim-detail, show-contractors) include annotations, execution, AND _meta with resourceUri — all copied verbatim from the tools/list response. Tools without UI (e.g., update-claim-status) still include execution when the server returned it, but omit annotations and _meta since the server didn't provide them.

Register in declarative-agent.json: { "actions": [{ "id": "zavaPlugin", "file": "zava-plugin.json" }] }


Complete Example — Authenticated Server

For an OAuth-protected MCP server at https://mcp.example.com/mcp. See authentication.md for the full oauth/register template (must be added to both m365agents.yml and m365agents.local.yml).

appPackage/example-plugin.json
{
  "$schema": "https://developer.microsoft.com/json-schemas/copilot/plugin/v2.4/schema.json",
  "schema_version": "v2.4",
  "name_for_human": "Example Service",
  "description_for_human": "Search and browse Example Service content",
  "namespace": "example",
  "functions": [
    {
      "name": "search",
      "description": "Search Example Service for content matching a query.",
      "capabilities": { "response_semantics": { "data_path": "$.items", "properties": { "title": "$.title", "url": "$.url" }, "static_template": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [{ "type": "TextBlock", "text": "[${title}](${url})", "wrap": true, "maxLines": 2 }] } } }
    }
  ],
  "runtimes": [
    {
      "type": "RemoteMCPServer",
      "auth": { "type": "OAuthPluginVault", "reference_id": "${{<PREFIX>_MCP_AUTH_ID}}" },
      "spec": {
        "url": "https://mcp.example.com/mcp",
        "mcp_tool_description": {
          "tools": [
            { "name": "search", "description": "Search Example Service for content matching a query.", "inputSchema": { "type": "object", "properties": { "query": { "description": "Search query", "type": "string" } }, "required": ["query"] } }
          ]
        }
      },
      "run_for_functions": ["search"]
    }
  ]
}

Multiple MCP Servers

You can integrate multiple MCP servers by adding multiple runtimes, each with its own auth type. Each runtime has its own mcp_tool_description.tools and run_for_functions:

{
  "functions": [
    { "name": "docs_search", "description": "Search Microsoft docs.", "capabilities": { "response_semantics": { "data_path": "$", "properties": {}, "static_template": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [{ "type": "TextBlock", "text": "${if(title, title, description)}", "wrap": true }] } } } },
    { "name": "search", "description": "Search authenticated service.", "capabilities": { "response_semantics": { "data_path": "$", "properties": {}, "static_template": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [{ "type": "TextBlock", "text": "${if(title, title, description)}", "wrap": true }] } } } }
  ],
  "runtimes": [
    {
      "type": "RemoteMCPServer",
      "auth": { "type": "None" },
      "spec": { "url": "https://learn.microsoft.com/api/mcp", "mcp_tool_description": { "tools": [{ "name": "docs_search", "description": "Search Microsoft docs.", "inputSchema": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] } }] } },
      "run_for_functions": ["docs_search"]
    },
    {
      "type": "RemoteMCPServer",
      "auth": { "type": "OAuthPluginVault", "reference_id": "${{<PREFIX>_MCP_AUTH_ID}}" },
      "spec": { "url": "https://mcp.example.com/mcp", "mcp_tool_description": { "tools": [{ "name": "search", "description": "Search authenticated service.", "inputSchema": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] } }] } },
      "run_for_functions": ["search"]
    }
  ]
}

Common Issues

IssueSolution
Plugin fails to loadVerify {name}-plugin.json exists and has correct mcp_tool_description.tools array
Tools not recognizedVerify function names match exactly between functions[] and mcp_tool_description.tools[]
Runtime errorsCheck that run_for_functions includes all functions using that runtime
OAuth token errorsRe-authenticate with mcp-remote — cached tokens may have expired
<PREFIX>_MCP_AUTH_ID emptyCheck oauth/register step in both m365agents.yml and m365agents.local.yml and verify credentials
"Invalid redirect URI"Ensure redirect URI in DCR is https://teams.microsoft.com/api/platform/v1.0/oAuthRedirect

Best Practices

  1. Always discover tools via MCP protocol — run the full handshake (initialize → notifications/initialized → tools/list) before writing the plugin manifest. NEVER fabricate tool names or descriptions.

  2. Full-fidelity tool copying in mcp_tool_description.tools — each tool object must be a verbatim copy of the tools/list output. Copy every property exactly as returned (inputSchema, annotations, execution, _meta, outputSchema, title, and any other field). The MCP protocol evolves — do NOT maintain a hardcoded allowlist of known fields. If the server returns it, the plugin must include it. Never abbreviate, omit, or rename properties. Do NOT duplicate inputSchema or other properties in functions[].

  3. Inline tools in mcp_tool_description.tools — do NOT use a separate tools file; embed the tools array directly in the runtime spec

  4. Match function names exactly — copy tool names directly from the tools/list output

  5. Always add response semantics — every function must have capabilities.response_semantics, even if using the default (empty body) pattern

  6. Include all tools by default — inline every tool from tools/list unless the developer explicitly asks to limit the set; for all included tools always keep the complete tool object with all properties

  7. Logos are optional — ask the user if they want a custom logo; if not, use the defaults from npx -y --package @microsoft/m365agentstoolkit-cli atk new. Logos must be PNG only (no JPG, SVG, etc.)

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an MCP server, Needs Node.js

Security

What our scanner noticed· Low risk

  • Phrasing that asks for actions without confirmation
  • Instructions that use the network

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Microsoft
Publisher
Microsoft (Official)
Source
microsoft/skills · .github/plugins/microsoft-365-agents-toolkit/skills/declarative-agent-developer · commit 0696600
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Microsoft or Microsoft.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download declarative-agent-developer.zip