trackmcp
Back to directory
aixolotl

microsoft-planner-mcp

View on GitHub

Unofficial MCP server for Microsoft Planner using Microsoft Graph and Entra ID delegated authentication

11 stars PythonOthers Updated Aug 17, 2026
mcpmcp-servermsgraphsdkmicrosoft-plannerfastmcpmodel-context-protocol

Documentation

MCP Server for Microsoft Planner

An unofficial MCP server that connects AI assistants to Microsoft Planner. Ask your AI assistant to create tasks, organise plans, manage buckets, and more, all through natural language.

> This is an unofficial, community/open-source MCP server for Microsoft Planner.

> It is not affiliated with, endorsed by, or sponsored by Microsoft.

Built with FastMCP and authenticated via Microsoft Entra ID (Azure AD) using the On-Behalf-Of (OBO) flow to call Microsoft Graph.

Table of Contents

What Can It Do?

Once connected, you can ask your AI assistant things like:

  • *"Show me all my Planner tasks that are overdue"*
  • *"Create a task called 'Prepare Q3 report' in the Marketing plan"*
  • *"Move all incomplete tasks in the Sprint bucket to the Backlog bucket"*
  • *"Mark the 'Update docs' task as complete"*

The AI assistant translates your request into the appropriate tool calls automatically.

*Note: This MCP only supports Planner basic tasks and plans*

Prerequisites

  • A Microsoft 365 account with access to Microsoft Planner
  • An Azure Entra ID (Azure AD) app registration (see Azure Setup below)
  • An MCP-compatible client — for example:

To run the server locally you also need:

Or, to run via Docker:

Azure Entra ID Setup

An Azure app registration is required so the server can authenticate users and call Microsoft Graph on their behalf. You need admin access to an Azure Entra ID tenant (or ask your IT administrator).

Step 1 — Register the Application

1. Go to the Azure PortalMicrosoft Entra IDApp registrationsNew registration

2. Enter a name (e.g. `Microsoft Planner MCP`)

3. Under Supported account types, choose the option appropriate for your organisation

4. Set the Redirect URI to Web → `http://localhost:8000/auth/callback`

5. Click Register

Step 2 — Configure API Permissions

1. In your new app registration, go to API permissionsAdd a permissionMicrosoft GraphDelegated permissions

2. Add these permissions:

    3. Click Grant admin consent for your organisation

    Step 3 — Expose an API Scope

    1. Go to Expose an API

    2. Set the Application ID URI (accept the default `api://` or customise it)

    3. Click Add a scope:

      Step 4 — Set Token Version

      1. Go to Manifest (or AuthenticationAdvanced settings in newer portal versions)

      2. Set `"requestedAccessTokenVersion"` to `2`

      3. Save

      Step 5 — Create a Client Secret

      1. Go to Certificates & secretsNew client secret

      2. Add a description and choose an expiry period

      3. Copy the Value immediately (it is only shown once)

      Step 6 — Note Your IDs

      You will need these three values for configuration:

      ValueWhere to Find It
      Application (client) IDApp registration → Overview
      Directory (tenant) IDApp registration → Overview
      Client secretThe value copied in Step 5

      Installation

      Option A — Local (Python + uv)

      bash
      git clone https://github.com/aixolotl/microsoft-planner-mcp
      cd microsoft-planner-mcp
      uv sync

      Option B — Docker

      No Python installation needed. See Running with Docker below.

      Configuration

      Copy the example environment file and fill in your Azure credentials:

      bash
      cp .env.example .env

      Edit `.env`:

      ini
      # Azure App Registration (required)
      CLIENT_ID=your-app-client-id
      CLIENT_SECRET=your-app-client-secret
      TENANT_ID=your-azure-tenant-id
      
      # Public URL of this server (used for OAuth redirect URI)
      BASE_URL=http://localhost:8000
      
      # JSON-encoded list of allowed CORS origins
      # Include http://localhost:6274 if using MCP Inspector for testing
      ALLOWED_ORIGINS=["http://localhost:8000","http://localhost:6274"]
      
      # Require FastMCP's extra client consent prompt per MCP client. (default: true)
      # Prevents confused-deputy attacks by requiring users to explicitly approve each new
      # client. Keep true in production; set false only for local dev with throwaway
      # clients.
      # REQUIRE_AUTHORIZATION_CONSENT=true
      
      # Optional rate-limit overrides (per client sliding window)
      # Defaults are 120 requests per 1 minute
      # RATE_LIMIT_MAX_REQUESTS=120
      # RATE_LIMIT_WINDOW_MINUTES=1

      `REQUIRE_AUTHORIZATION_CONSENT` controls whether FastMCP prompts users to explicitly approve each new MCP client. Keep this `true` (default) in production to prevent confused-deputy attacks; set it to `false` only during local development with throwaway clients.

      `RATE_LIMIT_MAX_REQUESTS` and `RATE_LIMIT_WINDOW_MINUTES` are optional and should stay commented out unless you need to override the defaults for your environment.

      Running the Server

      Local

      bash
      uv run uvicorn src.server:app --host 0.0.0.0 --port 8000

      The MCP endpoint is available at `http://localhost:8000/mcp`. A health check endpoint is at `http://localhost:8000/health`.

      Running with Docker

      bash
      # Pull the latest image
      docker pull ghcr.io/aixolotl/microsoft-planner-mcp:latest
      
      # Run with environment variables
      docker run --rm -i \
        -e BASE_URL=https://localhost:8000 \
        -e CLIENT_ID=your_client_id \
        -e CLIENT_SECRET=your_api_token \
        -e TENANT_ID=your_tenant_id \
        -e ALLOWED_ORIGINS=["http://localhost:8000","http://localhost:6274", "http://localhost:3000"] \
        -e REQUIRE_AUTHORIZATION_CONSENT=true \
        ghcr.io/aixolotl/microsoft-planner-mcp:latest

      This starts the MCP server on port 8000.

      To override rate limits, also pass `-e RATE_LIMIT_MAX_REQUESTS=` and `-e RATE_LIMIT_WINDOW_MINUTES=` (defaults: `120` and `1`).

      Running with Docker compose

      bash
      docker compose up

      This starts the MCP server on port 8000. The Docker Compose configuration also includes a Jaeger instance for trace visualisation (see OpenTelemetry Tracing).

      Connecting an MCP Client

      Once the server is running, configure your MCP client to connect to it.

      VS Code

      Add the following to your VS Code settings (`.vscode/settings.json` in your project, or your user settings):

      json
      {
        "mcp": {
          "servers": {
            "planner": {
              "type": "http",
              "url": "http://localhost:8000/mcp"
            }
          }
        }
      }

      Then use Copilot Chat in Agent mode and ask it to interact with your Planner tasks. Copilot will discover the available tools automatically.

      Other MCP Clients

      Any client that supports the Streamable HTTP transport can connect by pointing to `http://localhost:8000/mcp`. The server advertises OAuth metadata automatically — the client handles the authentication flow.

      Available Tools

      All tools are available to your AI assistant automatically once connected. You don't need to call them directly — just describe what you want in natural language. The parameter details below are provided for reference and for client developers.

      Read-only tools are annotated with `readOnlyHint: true` so clients can skip confirmation prompts. Destructive tools (deletes) are annotated with `destructiveHint: true`.

      User

      `get_me`

      Return the authenticated user's profile from Microsoft Graph.

      • Parameters: None
      • Returns: User profile object (id, displayName, mail, etc.) or `null`

      `list_users`

      Retrieve Microsoft 365 users by GUID, e-mail address, or free-text search. Useful for resolving the user GUIDs returned in task assignment objects to display names.

      ParameterTypeRequiredDescription
      `select`stringNoComma-separated fields to include (default: `id,displayName,mail,userPrincipalName`). Pass `*all` for all fields.
      `search`stringNoFree text search on display name, or search by field name and value, e.g. `Alice` or `surname:Smith`
      `guids`list[string]NoUser object GUIDs to look up. Translated to an OData `$filter` expression.
      `emails`list[string]NoUser principal names (UPNs / e-mail addresses) to look up. Translated to an OData `$filter` expression.
      `top`integerNoMaximum number of users to return (default: `10`). Ignored when `guids` or `emails` are provided.
      • Returns: List of user objects or `null`

      > Note: When both `guids`/`emails` and `search` are supplied, the GUID/email filter takes priority. Very large lists of GUIDs or e-mail addresses are silently truncated to stay within the 2 048-character Graph URL limit.

      Groups

      `list_my_groups`

      List all Microsoft 365 groups the authenticated user is a member of.

      ParameterTypeRequiredDescription
      `select`stringNoComma-separated fields to include (default: `id,displayName,mail`). Pass `*all` for all fields.
      `filter`stringNoOData filter expression, e.g. `startsWith(displayName,'Project')`
      `search`stringNoOData search string, e.g. `"displayName:Project"`
      • Returns: List of group objects or `null`

      > Note: The Groups tool will not return details like name or mail of groups with the standard permissions `Tasks.ReadWrite` documented here, *however* searching and filtering still works, so you can find a group with a specific name using this tool.

      Plans

      `list_my_plans`

      List Planner plans shared with the authenticated user.

      ParameterTypeRequiredDescription
      `select`stringNoComma-separated fields to include (default: `id,title,owner,createdBy,createdDateTime`). Pass `*all` for all fields.
      • Returns: List of plan objects or `null`

      `list_group_plans`

      List all Planner plans belonging to a Microsoft 365 group.

      ParameterTypeRequiredDescription
      `groupId`stringYesThe object ID of the group (from `list_my_groups`)
      `select`stringNoComma-separated fields to include (default: `id,title,owner,createdBy,createdDateTime`). Pass `*all` for all fields.
      • Returns: List of plan objects or `null`

      `create_plan`

      Create a new Planner plan for a Microsoft 365 group.

      ParameterTypeRequiredDescription
      `groupId`stringYesThe object ID of the M365 group that will own the plan
      `title`stringYesDisplay title for the new plan
      • Returns: The created plan object or `null`

      `delete_plan`

      Delete a Planner plan.

      ParameterTypeRequiredDescription
      `planId`stringYesThe ID of the plan to delete
      `etag`stringYesThe current `@odata.etag` of the plan (retries once if stale)
      • Returns: Confirmation message

      `list_plan_categories`

      Get category label definitions for a Planner plan. Returns all 25 category slots with their key (e.g. `category1`) and display name.

      ParameterTypeRequiredDescription
      `planId`stringYesThe ID of the plan
      • Returns: List of category objects (`key`, `display_name`) or `null`

      Buckets

      `list_buckets`

      List all buckets in a Planner plan.

      ParameterTypeRequiredDescription
      `planId`stringYesThe ID of the plan
      • Returns: List of bucket objects or `null`

      `create_bucket`

      Create a new bucket in a Planner plan.

      ParameterTypeRequiredDescription
      `planId`stringYesThe ID of the plan to create the bucket in
      `name`stringYesDisplay name for the new bucket
      • Returns: The created bucket object or `null`

      `delete_bucket`

      Delete a Planner bucket.

      ParameterTypeRequiredDescription
      `bucketId`stringYesThe ID of the bucket to delete
      `etag`stringYesThe current `@odata.etag` of the bucket (retries once if stale)
      • Returns: Confirmation message

      Tasks

      `list_my_tasks`

      List all Planner tasks assigned to the authenticated user across all plans.

      ParameterTypeRequiredDescription
      `select`stringNoComma-separated fields to include (default: `*all`). Pass `*all` for all fields.
      `filter`stringNoOData filter expression, e.g. `percentComplete ne 100`
      `search`stringNoFree-text search matched against the task title
      • Returns: List of task objects or `null`

      `list_tasks`

      List all tasks in a Planner plan.

      ParameterTypeRequiredDescription
      `planId`stringYesThe ID of the plan
      `select`stringNoComma-separated fields to include (default: `*all`). Pass `*all` for all fields.
      `filter`stringNoOData filter expression, e.g. `percentComplete eq 0`
      `search`stringNoFree-text search matched against the task title
      • Returns: List of task objects or `null`

      `get_task_details`

      Get the full details for a task: description, checklist items, and external references.

      ParameterTypeRequiredDescription
      `taskId`stringYesThe ID of the task
      • Returns: Task details object (description, checklist, references) or `null`

      `create_task`

      Create a new task in a Planner plan.

      ParameterTypeRequiredDescription
      `planId`stringYesThe ID of the plan
      `bucketId`stringYesThe ID of the bucket to place the task in
      `title`stringYesTitle of the task
      `startDateTime`stringNoISO 8601 start date (e.g. `2026-05-01T00:00:00`)
      `dueDateTime`stringNoISO 8601 due date (e.g. `2026-05-31T00:00:00`)
      `percentComplete`integerNoCompletion percentage, 0–100
      `assignUserIds`list[string]NoUser object IDs to assign to the task
      • Returns: The created task object or `null`

      `update_task`

      Update a task's standard fields and/or detail fields (description, checklist, references). Only provided fields are changed. When detail fields are specified, a separate API call updates the task details resource automatically.

      ParameterTypeRequiredDescription
      `taskId`stringYesThe ID of the task
      `etag`stringYesThe current `@odata.etag` of the task (retries once if stale)
      `title`stringNoNew title
      `percentComplete`integerNoCompletion percentage, 0–100
      `dueDateTime`stringNoISO 8601 due date
      `bucketId`stringNoID of the bucket to move the task to
      `priority`integerNoPriority 0 (urgent)–10 (low). Planner maps: 1=urgent, 3=important, 5=medium, 9=low
      `assigneePriority`stringNoOrder hint for sorting within the assignee's task list
      `appliedCategories`objectNoCategories keyed by category slot (e.g. `{"category3": true}`). Use `list_plan_categories` for label names
      `assignUserIds`list[string]NoUser IDs to assign
      `unassignUserIds`list[string]NoUser IDs to remove
      `description`stringNoPlain-text description (up to 2000 characters) — detail field
      `previewType`stringNoPreview style: `automatic`, `noPreview`, `checklist`, `description`, or `reference` — detail field
      `checklistItems`objectNoDict keyed by checklist item GUID. Pass `null` for a key to delete that item — detail field
      `references`objectNoDict keyed by URL-encoded reference URL. Pass `null` for a key to delete that reference — detail field
      `etagDetails`stringNoThe `@odata.etag` of the task details resource. Required only when updating detail fields; auto-refreshed from Graph if omitted.
      • Returns: The updated task object, the updated task details object, or both (`{ "task": ..., "details": ... }`) depending on which fields were provided. Returns `null` if the result is empty.

      > Note: The `update_task_details` tool has been removed. Use `update_task` with detail field parameters (`description`, `checklistItems`, `references`, `previewType`) instead. The task and details resources have separate ETags — provide `etagDetails` when available to avoid an extra round-trip, or omit it to let the tool auto-refresh.

      `list_task_fields`

      Return metadata for every field on a Planner task, including its data type, description, whether it is writable, and whether it requires a separate `get_task_details` call.

      • Parameters: None
      • Returns: List of field metadata objects (`name`, `type`, `description`, `writable`, `detailed`)

      > Note: The list of fields is static as Planner Standard doesn't support custom fields. But this endpoint awaits Microsoft's future updates to the API to fully support Planner Premium.

      `delete_task`

      Delete a Planner task.

      ParameterTypeRequiredDescription
      `taskId`stringYesThe ID of the task
      `etag`stringYesThe current `@odata.etag` of the task (retries once if stale)
      • Returns: Confirmation message

      Development

      This section covers building, testing, and contributing to the project.

      Project Structure

      code
      src/
      ├── server.py                  # FastMCP app, middleware, route mounting
      ├── config.py                  # Settings via pydantic-settings
      ├── auth_provider.py           # Azure OAuth provider (OBO flow)
      ├── deps.py                    # Shared dependency helpers
      ├── graph_client_manager.py    # Singleton GraphClientManager with per-user OBO clients
      ├── telemetry.py               # OpenTelemetry setup
      ├── types.py                   # Shared structural types
      ├── services/
      │   └── planner_service.py     # Business logic wrapping Graph SDK calls
      └── tools/
          ├── me.py                  # get_me
          ├── groups.py              # list_my_groups
          ├── plans.py               # plan tools + list_plan_categories
          ├── tasks.py               # task tools + list_task_fields
          └── buckets.py             # bucket tools
      tests/
      ├── conftest.py
      ├── test_buckets_tool.py
      ├── test_groups_tool.py
      ├── test_planner_service.py
      ├── test_plans_tool.py
      └── test_tasks_tool.py

      Installing Dev Dependencies

      bash
      uv sync --dev

      Running Tests

      bash
      uv run pytest

      With verbose output:

      bash
      uv run pytest -v

      Architecture

      The server is built with FastMCP and uses these key patterns:

      Server composition — Tools are split into five domain routers (`me`, `groups`, `plans`, `tasks`, `buckets`), each a standalone `FastMCP` instance mounted on the main app. This keeps each domain's tools, imports, and tests isolated.

      Authentication — The server uses FastMCP's `OAuthProxy` pattern via a custom `AzureProvider`. Azure Entra ID does not support Dynamic Client Registration (DCR), so the provider acts as a DCR-compliant proxy facing MCP clients while using the pre-registered app credentials with Azure. When a tool call arrives, the server exchanges the MCP session token for a Microsoft Graph token via the On-Behalf-Of flow, scoped to `Tasks.ReadWrite` and `User.Read`.

      Middleware — Five built-in middleware layers are stacked on the server (outermost first):

      MiddlewarePurpose
      `ErrorHandlingMiddleware`Catches unhandled exceptions, logs full traces server-side, returns clean MCP errors
      `SlidingWindowRateLimitingMiddleware`60 req/min per client to protect the Microsoft Graph quota
      `TimingMiddleware`Records wall-clock duration for every MCP operation
      `StructuredLoggingMiddleware`Emits one JSON log line per request (method, status, duration, client info)
      `ResponseLimitingMiddleware`Truncates tool responses above 500 KB to prevent overflowing LLM context windows

      Client logging — Every tool sends real-time progress messages to the MCP client via `get_optional_context()`, so users see status updates like *"Fetching tasks…"* and *"Found 12 task(s)"* in their client.

      OpenTelemetry Tracing

      The server includes native OpenTelemetry instrumentation with zero overhead when unused. To enable trace export:

      bash
      # Install the optional OTEL dependency group
      uv sync --group otel
      
      # Set the OTLP endpoint and start the server
      export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
      export OTEL_SERVICE_NAME=microsoft-planner-mcp
      uv run uvicorn src.server:app --host 0.0.0.0 --port 8000

      Traces follow the MCP semantic conventions and work with any OTLP-compatible backend (Jaeger, Grafana Tempo, Datadog, New Relic, etc.). The Docker Compose setup includes Jaeger with a UI at `http://localhost:16686`.

      Testing with MCP Inspector

      MCP Inspector lets you test tools interactively in a browser:

      1. Start the server:

      bash
      uv run uvicorn src.server:app --host 0.0.0.0 --port 8000

      2. In a separate terminal:

      bash
      npx @modelcontextprotocol/inspector

      3. Open the URL printed in the terminal (e.g. `http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=...`)

      4. Set Transport Type to `Streamable HTTP` and URL to `http://localhost:8000/mcp`, then click Connect.

      Contributing

      Contributions are welcome! Please open an issue or submit a pull request. See the issue templates for bug reports, feature requests, and tasks.

      License

      This project is licensed under the MIT License.

      Frequently asked questions

      What is microsoft-planner-mcp?

      microsoft-planner-mcp is Unofficial MCP server for Microsoft Planner using Microsoft Graph and Entra ID delegated authentication

      How do I install microsoft-planner-mcp?

      Open the GitHub repository and follow its README. Most MCP servers are added to your client's MCP config, then called by your agent.

      Is microsoft-planner-mcp open source?

      Yes — it is hosted on GitHub at https://github.com/aixolotl/microsoft-planner-mcp and has 11 stars.

      Related MCP tools

      Run your own MCP server? See who uses it and what to fix.

      Measure it with TrackMCP