Labsco
aixolotl logo

Microsoft Planner MCP

β˜… 6

from aixolotl

An MCP server that connects AI assistants to Microsoft Planner. Entra ID authenticated for enterprise grade on-behalf-of user authentication.

πŸ”₯πŸ”₯πŸ”₯βœ“ VerifiedFreeAdvanced setup

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

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):

{
  "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
selectstringNoComma-separated fields to include (default: id,displayName,mail,userPrincipalName). Pass *all for all fields.
searchstringNoFree text search on display name, or search by field name and value, e.g. Alice or surname:Smith
guidslist[string]NoUser object GUIDs to look up. Translated to an OData $filter expression.
emailslist[string]NoUser principal names (UPNs / e-mail addresses) to look up. Translated to an OData $filter expression.
topintegerNoMaximum 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
selectstringNoComma-separated fields to include (default: id,displayName,mail). Pass *all for all fields.
filterstringNoOData filter expression, e.g. startsWith(displayName,'Project')
searchstringNoOData 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
selectstringNoComma-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
groupIdstringYesThe object ID of the group (from list_my_groups)
selectstringNoComma-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
groupIdstringYesThe object ID of the M365 group that will own the plan
titlestringYesDisplay title for the new plan
  • Returns: The created plan object or null

delete_plan

Delete a Planner plan.

ParameterTypeRequiredDescription
planIdstringYesThe ID of the plan to delete
etagstringYesThe 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
planIdstringYesThe 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
planIdstringYesThe ID of the plan
  • Returns: List of bucket objects or null

create_bucket

Create a new bucket in a Planner plan.

ParameterTypeRequiredDescription
planIdstringYesThe ID of the plan to create the bucket in
namestringYesDisplay name for the new bucket
  • Returns: The created bucket object or null

delete_bucket

Delete a Planner bucket.

ParameterTypeRequiredDescription
bucketIdstringYesThe ID of the bucket to delete
etagstringYesThe 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
selectstringNoComma-separated fields to include (default: *all). Pass *all for all fields.
filterstringNoOData filter expression, e.g. percentComplete ne 100
searchstringNoFree-text search matched against the task title
  • Returns: List of task objects or null

list_tasks

List all tasks in a Planner plan.

ParameterTypeRequiredDescription
planIdstringYesThe ID of the plan
selectstringNoComma-separated fields to include (default: *all). Pass *all for all fields.
filterstringNoOData filter expression, e.g. percentComplete eq 0
searchstringNoFree-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
taskIdstringYesThe ID of the task
  • Returns: Task details object (description, checklist, references) or null

create_task

Create a new task in a Planner plan.

ParameterTypeRequiredDescription
planIdstringYesThe ID of the plan
bucketIdstringYesThe ID of the bucket to place the task in
titlestringYesTitle of the task
startDateTimestringNoISO 8601 start date (e.g. 2026-05-01T00:00:00)
dueDateTimestringNoISO 8601 due date (e.g. 2026-05-31T00:00:00)
percentCompleteintegerNoCompletion percentage, 0–100
assignUserIdslist[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
taskIdstringYesThe ID of the task
etagstringYesThe current @odata.etag of the task (retries once if stale)
titlestringNoNew title
percentCompleteintegerNoCompletion percentage, 0–100
dueDateTimestringNoISO 8601 due date
bucketIdstringNoID of the bucket to move the task to
priorityintegerNoPriority 0 (urgent)–10 (low). Planner maps: 1=urgent, 3=important, 5=medium, 9=low
assigneePrioritystringNoOrder hint for sorting within the assignee's task list
appliedCategoriesobjectNoCategories keyed by category slot (e.g. {"category3": true}). Use list_plan_categories for label names
assignUserIdslist[string]NoUser IDs to assign
unassignUserIdslist[string]NoUser IDs to remove
descriptionstringNoPlain-text description (up to 2000 characters) β€” detail field
previewTypestringNoPreview style: automatic, noPreview, checklist, description, or reference β€” detail field
checklistItemsobjectNoDict keyed by checklist item GUID. Pass null for a key to delete that item β€” detail field
referencesobjectNoDict keyed by URL-encoded reference URL. Pass null for a key to delete that reference β€” detail field
etagDetailsstringNoThe @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
taskIdstringYesThe ID of the task
etagstringYesThe 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

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

uv sync --dev

Running Tests

uv run pytest

With verbose output:

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
ErrorHandlingMiddlewareCatches unhandled exceptions, logs full traces server-side, returns clean MCP errors
SlidingWindowRateLimitingMiddleware60 req/min per client to protect the Microsoft Graph quota
TimingMiddlewareRecords wall-clock duration for every MCP operation
StructuredLoggingMiddlewareEmits one JSON log line per request (method, status, duration, client info)
ResponseLimitingMiddlewareTruncates 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:

# 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:

    uv run uvicorn src.server:app --host 0.0.0.0 --port 8000
  2. In a separate terminal:

    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.