Microsoft Planner MCP
Un servidor MCP que conecta asistentes de IA con Microsoft Planner. Autenticado con Entra ID para autenticación empresarial en nombre del usuario.
Documentación
Servidor MCP para Microsoft Planner
Un servidor MCP no oficial que conecta asistentes de IA a Microsoft Planner. Pídele a tu asistente de IA que cree tareas, organice planes, gestione buckets y más, todo mediante lenguaje natural.
Este es un servidor MCP no oficial, comunitario/de código abierto para Microsoft Planner.
No está afiliado, respaldado ni patrocinado por Microsoft.
Construido con FastMCP y autenticado mediante Microsoft Entra ID (Azure AD) usando el flujo On-Behalf-Of (OBO) para llamar a Microsoft Graph.
Tabla de Contenidos
- ¿Qué Puede Hacer?
- Requisitos Previos
- Configuración de Azure Entra ID
- Instalación
- Configuración
- Ejecutar el Servidor
- Conectar un Cliente MCP
- Herramientas Disponibles
- Desarrollo
- Contribuciones
- Licencia
- Seguridad
¿Qué Puede Hacer?
Una vez conectado, puedes pedirle a tu asistente de IA cosas como:
- "Muéstrame todas mis tareas de Planner que están vencidas"
- "Crea una tarea llamada 'Preparar informe Q3' en el plan de Marketing"
- "Mueve todas las tareas incompletas del bucket Sprint al bucket Backlog"
- "Marca la tarea 'Actualizar documentos' como completada"
El asistente de IA traduce tu solicitud en las llamadas de herramienta apropiadas automáticamente.
Nota: Este MCP solo admite tareas y planes básicos de Planner
Requisitos Previos
- Una cuenta de Microsoft 365 con acceso a Microsoft Planner
- Un registro de aplicación de Azure Entra ID (Azure AD) (ver Configuración de Azure abajo)
- Un cliente compatible con MCP — por ejemplo:
Para ejecutar el servidor localmente también necesitas:
- Python 3.12+
- uv (gestor de paquetes de Python)
O, para ejecutar con Docker:
Configuración de Azure Entra ID
Se requiere un registro de aplicación de Azure para que el servidor pueda autenticar usuarios y llamar a Microsoft Graph en su nombre. Necesitas acceso de administrador a un tenant de Azure Entra ID (o pregúntale a tu administrador de TI).
Paso 1 — Registrar la Aplicación
- Ve al Portal de Azure → Microsoft Entra ID → Registros de aplicaciones → Nuevo registro
- Ingresa un nombre (p. ej.
Microsoft Planner MCP) - En Tipos de cuenta admitidos, elige la opción apropiada para tu organización
- Establece la URI de redirección a Web →
http://localhost:8000/auth/callback - Haz clic en Registrar
Paso 2 — Configurar Permisos de API
- En tu nuevo registro de aplicación, ve a Permisos de API → Agregar un permiso → Microsoft Graph → Permisos delegados
- Agrega estos permisos:
Tasks.ReadWrite— leer y escribir tareas de PlannerUser.Read— leer el perfil del usuario que ha iniciado sesiónUser.ReadBasic.All— resolver nombres para mostrar de usuarios desde los GUID en las asignaciones de tareas (requerido solo para la herramientalist_users)
- Haz clic en Conceder consentimiento de administrador para tu organización
Paso 3 — Exponer un Alcance de API
- Ve a Exponer una API
- Establece la URI de ID de aplicación (acepta el valor predeterminado
api://<client-id>o personalízalo) - Haz clic en Agregar un alcance:
- Nombre del alcance:
mcp-access - Quién puede consentir: Administradores y usuarios (o Solo administradores si lo prefieres)
- Completa el nombre para mostrar y la descripción
- Establece el estado en Habilitado
- Nombre del alcance:
Paso 4 — Establecer la Versión del Token
- Ve a Manifiesto (o Autenticación → Configuración avanzada en versiones más nuevas del portal)
- Establece
"requestedAccessTokenVersion"a2 - Guarda
Paso 5 — Crear un Secreto de Cliente
- Ve a Certificados y secretos → Nuevo secreto de cliente
- Agrega una descripción y elige un período de expiración
- Copia el Valor inmediatamente (solo se muestra una vez)
Paso 6 — Anota Tus IDs
Necesitarás estos tres valores para la configuración:
| Valor | Dónde Encontrarlo |
|---|---|
| ID de aplicación (cliente) | Registro de aplicación → Información general |
| ID de directorio (tenant) | Registro de aplicación → Información general |
| Secreto de cliente | El valor copiado en el Paso 5 |
Instalación
Opción A — Local (Python + uv)
git clone https://github.com/aixolotl/microsoft-planner-mcp
cd microsoft-planner-mcp
uv sync
Opción B — Docker
No se necesita instalación de Python. Ver Ejecutar con Docker abajo.
Configuración
Copia el archivo de entorno de ejemplo y completa tus credenciales de Azure:
cp .env.example .env
Edita .env:
# 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
# Optional: keep sign-ins in Redis so they survive restarts (see below)
# REDIS_URL=rediss://:<access-key>@<name>.redis.cache.windows.net:6380/0
# JWT_SIGNING_KEY=<long random string>
# STORAGE_ENCRYPTION_KEY=<Fernet key>
REQUIRE_AUTHORIZATION_CONSENT controla si FastMCP solicita a los usuarios aprobar explícitamente cada nuevo cliente MCP. Mantén esto en true (predeterminado) en producción para prevenir ataques de diputado confundido; establécelo en false solo durante el desarrollo local con clientes desechables.
RATE_LIMIT_MAX_REQUESTS y RATE_LIMIT_WINDOW_MINUTES son opcionales y deben permanecer comentados a menos que necesites anular los valores predeterminados para tu entorno.
Mantener sesiones iniciadas entre reinicios (Redis)
De forma predeterminada, el servidor mantiene el estado de OAuth — registros de clientes y tokens de Entra de los usuarios — en un almacén de archivos cifrado dentro del contenedor. Ese estado se pierde cuando el contenedor se recrea o una Azure Container App se escala a cero, y cada réplica tiene su propia copia. Los clientes entonces fallan al actualizar con 401 invalid_client y los usuarios deben reconectarse.
Establece REDIS_URL cuando ejecutes más de una réplica, escales a cero o recrees contenedores:
| Variable | Predeterminado | Propósito |
|---|---|---|
REDIS_URL | no establecido (almacén de archivos) | redis:// o rediss:// (TLS — requerido por Azure Redis). La ruta es el índice de la base de datos (p. ej. /1). Codifica en porcentaje la contraseña si contiene / (las claves de acceso de Azure pueden): python -c "import urllib.parse; print(urllib.parse.quote('<key>', safe=''))". Se requiere un Redis no agrupado (Redis Cluster solo admite DB 0 y no es compatible). Contiene una credencial: mantenla en un secreto. |
JWT_SIGNING_KEY | derivado de CLIENT_SECRET | Secreto fijo (al menos 32 caracteres) para firmar los tokens del servidor. Establécelo para que rotar el secreto de cliente de Entra ya no cierre la sesión de todos los usuarios. Usa el mismo valor en cada réplica. |
STORAGE_ENCRYPTION_KEY | derivado como el predeterminado de FastMCP | Clave Fernet que cifra el almacén de Redis (generar: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"). Requiere REDIS_URL — el servidor se niega a iniciar sin él. Establécelo explícitamente en producción: el valor predeterminado derivado sigue el esquema de clave interno de FastMCP, por lo que solo es tan estable como ese esquema. |
Todo lo escrito en Redis está cifrado con Fernet. Los tokens y las transacciones de inicio de sesión expiran con las vidas útiles de FastMCP (tokens de actualización 30 días, transacciones minutos), pero los registros de clientes nunca expiran — se agrega uno por registro de cliente MCP, y las entradas escritas bajo una clave antigua quedan como basura ilegible. Dale al almacén su propio índice de base de datos (o su propio Redis) en lugar de compartir una base de datos con una caché, y usa una política de desalojo que no lo elimine silenciosamente (noeviction, o volatile-* con suficiente memoria). Un REDIS_URL, JWT_SIGNING_KEY o STORAGE_ENCRYPTION_KEY inválido falla al inicio; un valor vacío (JWT_SIGNING_KEY=) cuenta como no establecido. Si Redis no es accesible en tiempo de ejecución, las solicitudes de OAuth y las llamadas de herramientas fallan con 500 en aproximadamente 4 segundos y se recuperan por sí solas una vez que Redis vuelve.
Habilitar estas configuraciones cierra la sesión de todos una vez. Activar
REDIS_URL, establecerJWT_SIGNING_KEYo cambiar cualquiera de las claves más tarde invalida los inicios de sesión existentes: los clientes conectados reciben un401en su próxima llamada o actualización y deben reconectarse una vez. El estado del almacén de archivos existente no se migra. Planifica el cambio.
Ejecutar el Servidor
Local
uv run uvicorn src.server:app --host 0.0.0.0 --port 8000
El endpoint de MCP está disponible en http://localhost:8000/mcp. Un endpoint de verificación de salud está en http://localhost:8000/health.
Ejecutar con Docker
# 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
Esto inicia el servidor MCP en el puerto 8000.
Para anular los límites de tasa, también pasa -e RATE_LIMIT_MAX_REQUESTS=<value> y -e RATE_LIMIT_WINDOW_MINUTES=<minutes> (predeterminados: 120 y 1).
Para mantener las sesiones iniciadas entre recreaciones de contenedores, también pasa -e REDIS_URL=... (ver Mantener sesiones iniciadas entre reinicios).
Ejecutar con Docker compose
docker compose up
Esto inicia el servidor MCP en el puerto 8000. La configuración de Docker Compose también incluye una instancia de Jaeger para la visualización de trazas (ver Trazado con OpenTelemetry).
Conectar un Cliente MCP
Una vez que el servidor esté ejecutándose, configura tu cliente MCP para conectarse a él.
VS Code
Agrega lo siguiente a tu configuración de VS Code (.vscode/settings.json en tu proyecto, o tu configuración de usuario):
{
"mcp": {
"servers": {
"planner": {
"type": "http",
"url": "http://localhost:8000/mcp"
}
}
}
}
Luego usa Copilot Chat en modo Agente y pídele que interactúe con tus tareas de Planner. Copilot descubrirá las herramientas disponibles automáticamente.
Otros Clientes MCP
Cualquier cliente que admita el transporte Streamable HTTP puede conectarse apuntando a http://localhost:8000/mcp. El servidor anuncia los metadatos de OAuth automáticamente — el cliente maneja el flujo de autenticación.
Herramientas Disponibles
Todas las herramientas están disponibles para tu asistente de IA automáticamente una vez conectado. No necesitas llamarlas directamente — solo describe lo que quieres en lenguaje natural. Los detalles de parámetros a continuación se proporcionan como referencia y para desarrolladores de clientes.
Las herramientas de solo lectura están anotadas con readOnlyHint: true para que los clientes puedan omitir las indicaciones de confirmación. Las herramientas destructivas (eliminaciones) están anotadas con destructiveHint: true.
Usuario
get_me
Devuelve el perfil del usuario autenticado desde Microsoft Graph.
- Parámetros: Ninguno
- Devuelve: Objeto de perfil de usuario (id, displayName, mail, etc.) o
null
list_users
Recupera usuarios de Microsoft 365 por GUID, dirección de correo electrónico o búsqueda de texto libre. Útil para resolver los GUID de usuario devueltos en los objetos de asignación de tareas a nombres para mostrar.
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
select | string | No | Campos separados por comas a incluir (predeterminado: id,displayName,mail,userPrincipalName). Pasa *all para todos los campos. |
search | string | No | Búsqueda de texto libre en el nombre para mostrar, o búsqueda por nombre de campo y valor, p. ej. Alice o surname:Smith |
guids | list[string] | No | GUID de objetos de usuario a buscar. Traducidos a una expresión OData $filter. |
emails | list[string] | No | Nombres principales de usuario (UPNs / direcciones de correo electrónico) a buscar. Traducidos a una expresión OData $filter. |
top | integer | No | Número máximo de usuarios a devolver (predeterminado: 10). Se ignora cuando se proporcionan guids o emails. |
- Devuelve: Lista de objetos de usuario o
null
Nota: Cuando se proporcionan tanto
guids/emailscomosearch, el filtro de GUID/correo electrónico tiene prioridad. Las listas muy grandes de GUID o direcciones de correo electrónico se truncan silenciosamente para mantenerse dentro del límite de URL de Graph de 2 048 caracteres.
Grupos
list_my_groups
Lista todos los grupos de Microsoft 365 de los que el usuario autenticado es miembro.
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
select | string | No | Campos separados por comas a incluir (predeterminado: id,displayName,mail). Pasa *all para todos los campos. |
filter | string | No | Expresión de filtro OData, p. ej. startsWith(displayName,'Project') |
search | string | No | Cadena de búsqueda OData, p. ej. "displayName:Project" |
- Devuelve: Lista de objetos de grupo o
null
Nota: La herramienta Grupos no devolverá detalles como nombre o correo de los grupos con los permisos estándar
Tasks.ReadWritedocumentados aquí, sin embargo la búsqueda y el filtrado aún funcionan, por lo que puedes encontrar un grupo con un nombre específico usando esta herramienta.
Planes
list_my_plans
Lista los planes de Planner compartidos con el usuario autenticado.
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
select | string | No | Campos separados por comas a incluir (predeterminado: id,title,owner,createdBy,createdDateTime). Pasa *all para todos los campos. |
- Devuelve: Lista de objetos de plan o
null
list_group_plans
Lista todos los planes de Planner que pertenecen a un grupo de Microsoft 365.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
groupId | string | Sí | El ID de objeto del grupo (de list_my_groups) |
select | string | No | Campos separados por comas a incluir (predeterminado: id,title,owner,createdBy,createdDateTime). Pase *all para todos los campos. |
- Devuelve: Lista de objetos de plan o
null
create_plan
Crea un nuevo plan de Planner para un grupo de Microsoft 365.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
groupId | string | Sí | El ID de objeto del grupo de M365 que será propietario del plan |
title | string | Sí | Título para mostrar del nuevo plan |
- Devuelve: El objeto de plan creado o
null
delete_plan
Elimina un plan de Planner.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
planId | string | Sí | El ID del plan a eliminar |
etag | string | Sí | El @odata.etag actual del plan (reintenta una vez si está obsoleto) |
- Devuelve: Mensaje de confirmación
list_plan_categories
Obtiene las definiciones de etiquetas de categoría para un plan de Planner. Devuelve las 25 ranuras de categoría con su clave (p. ej., category1) y nombre para mostrar.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
planId | string | Sí | El ID del plan |
- Devuelve: Lista de objetos de categoría (
key,display_name) onull
Cubos
list_buckets
Lista todos los cubos en un plan de Planner.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
planId | string | Sí | El ID del plan |
- Devuelve: Lista de objetos de cubo o
null
create_bucket
Crea un nuevo cubo en un plan de Planner.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
planId | string | Sí | El ID del plan en el que se creará el cubo |
name | string | Sí | Nombre para mostrar del nuevo cubo |
- Devuelve: El objeto de cubo creado o
null
delete_bucket
Elimina un cubo de Planner.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
bucketId | string | Sí | El ID del cubo a eliminar |
etag | string | Sí | El @odata.etag actual del cubo (reintenta una vez si está obsoleto) |
- Devuelve: Mensaje de confirmación
Tareas
list_my_tasks
Lista todas las tareas de Planner asignadas al usuario autenticado en todos los planes.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
select | string | No | Campos separados por comas a incluir (predeterminado: *all). Pase *all para todos los campos. |
filter | string | No | Expresión de filtro OData, p. ej., percentComplete ne 100 |
search | string | No | Búsqueda de texto libre que coincide con el título de la tarea |
- Devuelve: Lista de objetos de tarea o
null
list_tasks
Lista todas las tareas en un plan de Planner.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
planId | string | Sí | El ID del plan |
select | string | No | Campos separados por comas a incluir (predeterminado: *all). Pase *all para todos los campos. |
filter | string | No | Expresión de filtro OData, p. ej., percentComplete eq 0 |
search | string | No | Búsqueda de texto libre que coincide con el título de la tarea |
- Devuelve: Lista de objetos de tarea o
null
get_task_details
Obtiene los detalles completos de una tarea: descripción, elementos de lista de verificación y referencias externas.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
taskId | string | Sí | El ID de la tarea |
- Devuelve: Objeto de detalles de la tarea (descripción, lista de verificación, referencias) o
null
create_task
Crea una nueva tarea en un plan de Planner.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
planId | string | Sí | El ID del plan |
bucketId | string | Sí | El ID del cubo donde se colocará la tarea |
title | string | Sí | Título de la tarea |
startDateTime | string | No | Fecha de inicio ISO 8601 (p. ej., 2026-05-01T00:00:00) |
dueDateTime | string | No | Fecha de vencimiento ISO 8601 (p. ej., 2026-05-31T00:00:00) |
percentComplete | integer | No | Porcentaje de finalización, 0–100 |
assignUserIds | list[string] | No | IDs de objeto de usuario para asignar a la tarea |
- Devuelve: El objeto de tarea creado o
null
update_task
Actualiza los campos estándar de una tarea y/o los campos de detalle (descripción, lista de verificación, referencias). Solo se cambian los campos proporcionados. Cuando se especifican campos de detalle, una llamada API separada actualiza automáticamente el recurso de detalles de la tarea.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
taskId | string | Sí | El ID de la tarea |
etag | string | Sí | El @odata.etag actual de la tarea (reintenta una vez si está obsoleto) |
title | string | No | Nuevo título |
percentComplete | integer | No | Porcentaje de finalización, 0–100 |
dueDateTime | string | No | Fecha de vencimiento ISO 8601 |
bucketId | string | No | ID del cubo al que se moverá la tarea |
priority | integer | No | Prioridad 0 (urgente)–10 (baja). Planner asigna: 1=urgente, 3=importante, 5=media, 9=baja |
assigneePriority | string | No | Sugerencia de orden para ordenar dentro de la lista de tareas del asignado |
appliedCategories | object | No | Categorías clave por ranura de categoría (p. ej., {"category3": true}). Use list_plan_categories para nombres de etiquetas |
assignUserIds | list[string] | No | IDs de usuario para asignar |
unassignUserIds | list[string] | No | IDs de usuario para eliminar |
description | string | No | Descripción de texto plano (hasta 2000 caracteres) — campo de detalle |
previewType | string | No | Estilo de vista previa: automatic, noPreview, checklist, description o reference — campo de detalle |
checklistItems | object | No | Dict clave por GUID de elemento de lista de verificación. Pase null como clave para eliminar ese elemento — campo de detalle |
references | object | No | Dict clave por URL de referencia codificada. Pase null como clave para eliminar esa referencia — campo de detalle |
etagDetails | string | No | El @odata.etag del recurso de detalles de la tarea. Obligatorio solo al actualizar campos de detalle; se actualiza automáticamente desde Graph si se omite. |
- Devuelve: El objeto de tarea actualizado, el objeto de detalles de tarea actualizado o ambos (
{ "task": ..., "details": ... }) según los campos proporcionados. Devuelvenullsi el resultado está vacío.
Nota: La herramienta
update_task_detailsse ha eliminado. Useupdate_taskcon parámetros de campo de detalle (description,checklistItems,references,previewType) en su lugar. Los recursos de tarea y detalles tienen ETags separados: proporcioneetagDetailscuando esté disponible para evitar un viaje de ida y vuelta adicional, u omítalo para que la herramienta se actualice automáticamente.
list_task_fields
Devuelve metadatos para cada campo en una tarea de Planner, incluido su tipo de datos, descripción, si es editable y si requiere una llamada get_task_details separada.
- Parámetros: Ninguno
- Devuelve: Lista de objetos de metadatos de campo (
name,type,description,writable,detailed)
Nota: La lista de campos es estática ya que Planner Standard no admite campos personalizados. Pero este endpoint espera las futuras actualizaciones de Microsoft a la API para admitir completamente Planner Premium.
delete_task
Elimina una tarea de Planner.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
taskId | string | Sí | El ID de la tarea |
etag | string | Sí | El @odata.etag actual de la tarea (reintenta una vez si está obsoleto) |
- Devuelve: Mensaje de confirmación
Desarrollo
Esta sección cubre la compilación, las pruebas y la contribución al proyecto.
Estructura del Proyecto
src/
├── server.py # FastMCP app, middleware, route mounting
├── config.py # Settings via pydantic-settings
├── auth_provider.py # Azure OAuth provider (OBO flow) + optional Redis state store
├── 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
└── users.py # list_users
tests/
├── conftest.py
├── test_auth_provider.py
├── test_buckets_tool.py
├── test_config.py
├── test_groups_tool.py
├── test_planner_service.py
├── test_plans_tool.py
├── test_redis_storage_integration.py # needs a real Redis (REDIS_TEST_URL)
├── test_tasks_tool.py
└── test_users_tool.py
Instalación de Dependencias de Desarrollo
uv sync --dev
Ejecución de Pruebas
uv run pytest
Con salida detallada:
uv run pytest -v
tests/test_redis_storage_integration.py se ejecuta contra un Redis real y se omite a menos que REDIS_TEST_URL esté configurado (CI proporciona un servicio redis:7-alpine). Apúntelo a una base de datos de prueba: las pruebas escriben y eliminan sus propias claves:
docker run -d --rm --name redis-test -p 6379:6379 redis:7-alpine
REDIS_TEST_URL=redis://localhost:6379/15 uv run pytest tests/test_redis_storage_integration.py -v
Arquitectura
El servidor está construido con FastMCP y utiliza estos patrones clave:
Composición del servidor — Las herramientas se dividen en cinco enrutadores de dominio (me, groups, plans, tasks, buckets), cada uno una instancia FastMCP independiente montada en la aplicación principal. Esto mantiene las herramientas, importaciones y pruebas de cada dominio aisladas.
Autenticación — El servidor utiliza el patrón OAuthProxy de FastMCP a través de un AzureProvider personalizado. Azure Entra ID no admite el Registro Dinámico de Clientes (DCR), por lo que el proveedor actúa como un proxy compatible con DCR frente a los clientes MCP mientras usa las credenciales de la aplicación pre-registrada con Azure. Cuando llega una llamada de herramienta, el servidor intercambia el token de sesión MCP por un token de Microsoft Graph mediante el flujo On-Behalf-Of, con ámbito Tasks.ReadWrite, User.Read y User.ReadBasic.All. El estado del proxy (registros de clientes, tokens de Entra de los usuarios) vive en el almacén de archivos cifrado de FastMCP de forma predeterminada, o en un almacén Redis cifrado cuando REDIS_URL está configurado — consulte Mantener los inicios de sesión entre reinicios.
Middleware — Cinco capas de middleware integrado se apilan en el servidor (primero el más externo):
| Middleware | Propósito |
|---|---|
ErrorHandlingMiddleware | Captura excepciones no controladas, registra trazas completas en el servidor, devuelve errores MCP limpios |
SlidingWindowRateLimitingMiddleware | 60 req/min por cliente para proteger la cuota de Microsoft Graph |
TimingMiddleware | Registra la duración de tiempo real para cada operación MCP |
StructuredLoggingMiddleware | Emite una línea de registro JSON por solicitud (método, estado, duración, información del cliente) |
ResponseLimitingMiddleware | Trunca las respuestas de herramientas por encima de 500 KB para evitar desbordar las ventanas de contexto de LLM |
Registro del cliente — Cada herramienta envía mensajes de progreso en tiempo real al cliente MCP mediante get_optional_context(), para que los usuarios vean actualizaciones de estado como "Obteniendo tareas…" y "Encontradas 12 tarea(s)" en su cliente.
Trazado OpenTelemetry
El servidor incluye instrumentación OpenTelemetry nativa con cero sobrecarga cuando no se usa. Para habilitar la exportación de trazas:
# 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
Las trazas siguen las convenciones semánticas de MCP y funcionan con cualquier backend compatible con OTLP (Jaeger, Grafana Tempo, Datadog, New Relic, etc.). La configuración de Docker Compose incluye Jaeger con una interfaz de usuario en http://localhost:16686.
Pruebas con MCP Inspector
MCP Inspector le permite probar herramientas de forma interactiva en un navegador:
-
Inicie el servidor:
uv run uvicorn src.server:app --host 0.0.0.0 --port 8000 -
En una terminal separada:
npx @modelcontextprotocol/inspector -
Abra la URL impresa en la terminal (p. ej.,
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=...) -
Establezca Tipo de Transporte en
Streamable HTTPy URL enhttp://localhost:8000/mcp, luego haga clic en Conectar.
Contribuciones
¡Las contribuciones son bienvenidas! Abra un problema o envíe una solicitud de extracción. Consulte las plantillas de problemas para informes de errores, solicitudes de funciones y tareas.
Licencia
Este proyecto está licenciado bajo la Licencia MIT.