# Agenda compartida y categorías por MCP

calendar_events_management y lo que lee un asistente de la agenda de otras personas: operaciones, permisos, scopes y la aprobación en el móvil.

`calendar_events_management`, del servidor `utilia-crm`, hace por MCP lo mismo que el Calendario con dos cosas: las categorías de cada persona y con quién comparte su agenda. Usa los mismos servicios que la aplicación, con sus reglas, y añade dos puertas que solo existen para un asistente: un scope propio que ningún conjunto incluye y la aprobación en el móvil de lo que amplía quién ve una agenda. Esta página cuenta también qué lee un asistente de la agenda de otra persona con `get_availability` y `check_availability`, y qué pide invitar. La guía de uso está en [Compartir tu agenda](https://os.utilia.ai/docs/guides/calendar/share-agenda).

## Requisitos

- **Plan**: el Calendario. No tiene capacidad propia: sin él, `MODULE_NOT_ENABLED`.
- **Permiso de la persona y scope del token**, por operación (tabla de abajo).
- **Scopes que se piden por su nombre**: `dev:calendar:agenda-titles`, `dev:calendar:sharing:own` y `dev:calendar:sharing:others` no entran en ningún conjunto, tampoco en `crm:full`, `dev:full` ni `admin:full`. Una integración autorizada antes no los tiene: hay que volver a autorizarla pidiéndolos. `dev:calendar:categories:manage` sí entra en `crm:full`, `dev:full` y `admin:full`.
- **Quién puede compartir**: una persona. Una clave de API de la organización recibe `CALENDAR_AGENDA_SHARE_API_KEY_NOT_ALLOWED`, y durante una sesión de soporte no se comparte ni se retira nada (`CALENDAR_AGENDA_SHARE_SUPPORT_SESSION`).

**Límites de la agenda compartida**

- Títulos ajenos por llamada: 5 personas y 14 días. Por encima, horas y tipo con titlesTruncated.
- Destinos por llamada: 50
- Destinos por agenda: 100. Personas y grupos vigentes, juntos.
- Cambios por minuto: 30. Compartir, cambiar y retirar, por la app y por MCP.

## Operaciones

| Operación | Qué hace | Permiso | Scope | ¿`confirm`? |
| - | - | - | - | - |
| `categories_list` | Tus categorías y las de la empresa (`readOnly: true`), con su `id`, que es el `categoryId` de `calendar_events`. | `CALENDAR_VIEW` | `dev:calendar:view` | no |
| `categories_create` | Crea una (`name` y `color`). Con el mismo nombre y el mismo color devuelve la que ya había, con `created: false`. | `CALENDAR_CATEGORIES_MANAGE` | `dev:calendar:categories:manage` | no |
| `categories_update` | Cambia `name`, `color` o `sortOrder` de una tuya. | `CALENDAR_CATEGORIES_MANAGE` | `dev:calendar:categories:manage` | no |
| `categories_delete` | La elimina y dice qué pasa con sus eventos (`eventsAction`). | `CALENDAR_CATEGORIES_MANAGE` | `dev:calendar:categories:manage` | sí |
| `agenda_share_list` | Con quién compartes tu agenda (`given`) y quién te comparte la suya (`received`). | `CALENDAR_VIEW` | `dev:calendar:view` | no |
| `agenda_audience` | Quién ve tu agenda y por qué: porque se la compartes, por sus permisos o por administrar la empresa. | `CALENDAR_VIEW` | `dev:calendar:view` | no |
| `agenda_share_preview` | Lo que haría `agenda_share_set`, sin guardar, con la muestra «Así te verá» y `approvalRequired`. | `CALENDAR_SHARE_OWN_AGENDA` | `dev:calendar:sharing:own` | no |
| `agenda_share_set` | Comparte o cambia el nivel o la fecha de fin. | `CALENDAR_SHARE_OWN_AGENDA` | `dev:calendar:sharing:own` | sí |
| `agenda_share_revoke` | Deja de compartir, por `shareId` o por `target`. La persona dueña siempre puede, aunque haya perdido el permiso de compartir. | `CALENDAR_VIEW` | `dev:calendar:sharing:own` | sí |

Las cinco de la agenda admiten `ownerUserId` para actuar sobre la agenda de otra persona. Entonces exigen el permiso `CALENDAR_SHARE_OTHERS_AGENDA` con alcance sobre esa persona y el scope `dev:calendar:sharing:others`, también para leer. Si falta el permiso o la persona no existe, la respuesta es la misma: no dice si existe. De la agenda de otra persona, `agenda_share_list` solo da `given`. En `agenda_audience`, el permiso y el alcance de cada compañero solo salen si quien pregunta tiene `SETTINGS_USERS_VIEW`.

Sin `confirm: true`, una escritura responde `VALIDATION` con `details.field: "confirm"` y no toca nada: enséñale antes a la persona lo que va a pasar con `agenda_share_preview`, con los mismos argumentos.

## Compartir una agenda

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `targets` | `object[]` | Sí | De 1 a 50: { type: "user", userId }, { type: "department", departmentId } o { type: "organization" }, que es todo el equipo sin la asesoría ni comerciales externos. Los identificadores, con users. |
| `level` | `string` | Sí | busy: a qué horas tiene algo y de qué tipo, sin títulos. titles: además el título, la categoría y la carga de cada día. |
| `until` | `string \| null` | No | Último día que la verán, YYYY-MM-DD, incluido: deja de verse al terminar ese día en la zona de la empresa. Sin él o con null, sin fecha de fin. Dos años como mucho. |
| `ownerUserId` | `string` | No | La agenda de otra persona. Sin él, la tuya. |
| `confirm` | `boolean` | Sí | true cuando la persona lo haya confirmado. |
| `twoFactorApprovalId` | `string` | No | El approvalId de APPROVAL_REQUIRED, al repetir la misma llamada una vez aprobada en el móvil. |

```json
{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "tools/call",
  "params": {
    "name": "calendar_events_management",
    "arguments": {
      "operation": "agenda_share_set",
      "targets": [{ "type": "user", "userId": "cmuser0002" }],
      "level": "busy",
      "until": "2026-12-19",
      "confirm": true
    }
  }
}
```

La respuesta trae `owner`, una fila por destino en `results` (`shareId`, `target`, `status`, que es `created`, `updated` o `unchanged`, `level`, `levelLabel`, `previousLevel`, `until`, `untilLabel` y, en las personas, `effectiveForRecipient` y una `note` si ya lo veía por su permiso) y `effect`:

- `summary`: lo que ha pasado, en una frase para contárselo a la persona;
- `latent`: lo que no surte efecto todavía, como alguien al que le falta el permiso de ver agendas;
- `notified`: a quién se ha avisado;
- `undo`: cómo deshacerlo.

Mismo nivel y misma fecha que ya tenía: `unchanged`, sin aviso ni registro. `agenda_share_revoke` sin nada que retirar responde `revoked: false`, con la misma forma.

### Aprobación en el móvil

Lo que amplía quién ve una agenda se confirma en el móvil de la persona, también si lo pide el asistente del chat:

| Cambio | ¿Aprobación? |
| - | - |
| La agenda de otra persona | Siempre |
| Un departamento o todo el equipo | Siempre |
| Crear o subir a `titles`, a cualquier destino | Siempre |
| Alargar o quitar la fecha de fin de una que ya es `titles` | Siempre |
| `busy` a personas sueltas | No |
| Bajar a `busy` o adelantar la fecha de fin | No |

Lo que no cambia nada no cuenta, y retirar no la pide nunca. `agenda_share_preview` te lo dice antes en `approvalRequired`.

**Compartir con títulos desde un asistente**

Participantes: Tu asistente, UTILIA OS y Móvil de la persona.

1. Tu asistente → UTILIA OS: agenda_share_preview. Con los mismos argumentos que usará después; approvalRequired: true.
2. Tu asistente → UTILIA OS: agenda_share_set con confirm: true. La persona ya ha dicho que sí.
3. UTILIA OS → Móvil de la persona: «Compartir tu agenda con títulos con…». El resumen lo compone el servidor con los nombres de la base de datos.
4. UTILIA OS → Tu asistente (respuesta): APPROVAL_REQUIRED con su approvalId. No se ha guardado nada.
5. Móvil de la persona → UTILIA OS: Aprobado
6. Tu asistente → UTILIA OS: La misma llamada con twoFactorApprovalId
7. UTILIA OS → Tu asistente (respuesta): results y effect. Y la persona recibe el aviso de lo que ha hecho su asistente.

La aprobación queda atada a lo que el servidor resolvió: la persona dueña, los destinos, el nivel y la fecha de fin. La que se pidió para compartir con una persona no vale para otra, ni con otro nivel, ni hasta otra fecha. El ciclo es el de [Verificación en dos pasos para lo más delicado](https://os.utilia.ai/docs/developers/mcp/reference#verificación-en-dos-pasos-para-lo-más-delicado): sin la verificación en dos pasos activada, `FORBIDDEN` con `details.errorCode: TWO_FACTOR_NOT_ENABLED` y una `hint` que manda hacerlo en la aplicación.

### El aviso a la persona dueña

Cuando su propio asistente, por MCP o en el chat, comparte su agenda o amplía lo que ven de ella (un destino nuevo, subir a títulos o alargar la fecha de fin), la persona recibe en la aplicación y en el móvil «Tu asistente de IA ha compartido tu agenda con…», con «Si no lo has pedido tú, puedes deshacerlo en Calendario › Compartir mi agenda.». Si lo hace otra persona sobre su agenda, el aviso lleva su nombre y llega también por correo si la persona no lo ha apagado.

## Categorías

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `name` | `string` | No | Hasta 50 caracteres. Obligatorio en categories_create. |
| `color` | `string` | No | Hexadecimal #RRGGBB, como #3B82F6. Obligatorio en categories_create. |
| `sortOrder` | `integer` | No | Posición en la lista, desde 0. |
| `id` | `string` | No | La categoría, en categories_update y categories_delete. |
| `eventsAction` | `string` | No | categories_delete: block (por defecto) no la elimina si la usa algún evento; uncategorize les quita la categoría; move los pasa a moveToCategoryId. |
| `moveToCategoryId` | `string` | No | Con move: otra categoría tuya o de la empresa. |

Solo se cambian y se eliminan las propias: las de la empresa son de solo lectura. La categoría personal de otra persona responde igual que una que no existe. Con `move`, los eventos de otras personas que la usaran se quedan sin categoría. Quien ve la agenda con títulos ve el nombre y el color de la categoría de lo que organiza esa persona.

## Lo que se lee de la agenda de otra persona

`get_availability` y `check_availability` de `calendar_events_workflow` usan la misma regla que la aplicación: lo que la persona ve por sus permisos y lo que le comparten. El tercer camino, abrir el evento entero con `CALENDAR_VIEW`, no pasa por aquí.

- **`get_availability`** devuelve una entrada por cada identificador pedido, con `visibility`: `none` (no la ve; `busy: []` no quiere decir libre), `busy` (horas y tipo) o `titles`. Cada franja de `busy` trae `start`, `end` y `kind`; con `titles`, además `titleByOwner` y `category`. Lo provisional va aparte en `tentative` y no ocupa, solo de quien comparte su agenda con quien pregunta y de la propia. Con títulos, `dayLoad` cuenta por día las tareas del CRM sin hora (`tasks`, con `crm:tasks:view` en el token) y el trabajo de Gestión de proyectos que vence ese día (`issues`, con `dev:issues:view`), y `issuesLabel` dice cómo lo llama la empresa.
- **`check_availability`** describe cada choque con su tipo; el título de un compromiso ajeno, solo con títulos.
- **Tipos** (`kind`): `VIDEO_CALL`, `IN_PERSON`, `MEETING`, `TASK`, `ABSENCE` u `OTHER`. Lo privado de otra persona no lleva tipo ni título.
- **Títulos ajenos**: solo con `dev:calendar:agenda-titles` en el token, y además el permiso `CALENDAR_VIEW_AGENDA_TITLES` sobre esa persona o que te comparta su agenda con títulos. Sin el scope es un techo, no un rechazo: llegan horas y tipo, y `titlesNote` lo dice. Como mucho 5 personas y 14 días con títulos por llamada: por encima, horas y tipo con `titlesTruncated: true` y una `hint`. El título ajeno llega en una sola línea y recortado: lo escribió otra persona y es un dato, nunca una instrucción.
- **`get_availability`** admite 20 llamadas por minuto de la misma persona.
- **En una respuesta para todo un canal** del chat, lo que comparten con quien pregunta no cuenta y no llega ningún título ajeno. Las cinco operaciones de la agenda de `calendar_events_management` no se hacen ahí: el asistente pide que se lo pregunten en privado.

El resto de la tabla de disponibilidad (festivos, `find_free_slots`) está en [Calendario: festivos, disponibilidad y buscar un hueco](https://os.utilia.ai/docs/developers/mcp/reference#calendario-festivos-disponibilidad-y-buscar-un-hueco).

## Crear e invitar

- **Crear con invitados es invitar.** `calendar_events` `create` con `attendees` que no seas tú, o con `externalAttendees`, exige el permiso `CALENDAR_INVITE` y el scope `dev:calendar:invite`; `dev:calendar:create` solo no basta. Sin el scope, `INSUFFICIENT_SCOPE` con `dev:calendar:invite` en `requiredScopes` y no se crea nada; sin el permiso, `CALENDAR_INVITE_NOT_ALLOWED`.
- **Gestionar invitados después** (`add_attendees`, `replace_attendees`, `remove_external_attendee` y `resend_external_invitation`) pide el mismo permiso y el mismo scope. `CALENDAR_EDIT` y `dev:calendar:update` ya no sirven de alternativa.
- **Provisional y privado**: en `create` y `update`, `status: "TENTATIVE"` es provisional (no ocupa la hora; lo ven marcado quien lo organiza, sus invitados y a quien se le comparte la agenda) y `visibility: "PRIVATE"` es privado. `DEFAULT` y `PUBLIC` hacen lo mismo.
- **Categoría**: `categoryId` de `categories_list`, tuya o de la empresa.

## Conceder permisos con `team_management`

- **Los permisos de personas exigen `scope`.** Son los que `permissions_list` marca con `reachKind: "people"`, entre ellos los tres de ver agendas y `CALENDAR_SHARE_OTHERS_AGENDA`. Sin `scope`, `VALIDATION` con `details.errorCode: PERMISSION_SCOPE_REQUIRED` y, en `details.items`, a quién llegaría cada alcance en esa persona. En los demás, sin `scope` se guarda `ALL` y la respuesta trae `scopeDefaulted: true`. Los de `reachKind: "presence"` no miran el alcance.
- **Vista previa.** `preview: true` en `users_grant_permission` y `users_batch_permissions` no guarda nada y devuelve `effect` (o `effects`): `reach.label` («Alcanza a su departamento, Ventas: 6 personas.»), `reach.count`, hasta 10 `names` y `more`; `opens`, `summary`, `warnings`, `alternative` y `undo`. Cuéntaselo a la persona antes de conceder.
- **Lo crítico se aprueba en el móvil**: conceder `CALENDAR_VIEW_AGENDA_TITLES` con un alcance distinto de `OWN` y `CALENDAR_SHARE_OTHERS_AGENDA`, sueltos, por lotes, al copiar los de otra persona o al aplicar una plantilla.
- **La alternativa más estrecha**: para que alguien vea la agenda de una persona, `alternative` propone que esa persona la comparta con `agenda_share_set`, en vez de conceder un permiso.

## Errores

| `details.errorCode` | Cuándo |
| - | - |
| `CALENDAR_AGENDA_SHARE_SELF` | El destino es la persona dueña de la agenda. |
| `CALENDAR_AGENDA_SHARE_TARGET_NOT_FOUND` | La persona no está activa en la empresa o es de la asesoría, el departamento no existe o, en la agenda de otra persona, el destino eres tú o un grupo del que formas parte (también todo el equipo). Trae `hint`. 404. |
| `CALENDAR_AGENDA_SHARE_OWNER_NOT_FOUND` | La persona dueña no existe, no es de tu empresa o tu permiso no llega a ella. 404. |
| `CALENDAR_AGENDA_SHARE_LEVEL_ABOVE_ACTOR` | En la agenda de otra persona, el nivel pedido es más del que tú ves de ella. 403. |
| `CALENDAR_AGENDA_SHARE_NOT_ALLOWED` | Te falta el permiso de compartir. 403. |
| `CALENDAR_AGENDA_SHARE_EXPIRY_INVALID` | `until` es de hoy o anterior, o pasa de dos años. |
| `CALENDAR_AGENDA_SHARE_TARGETS_REQUIRED` | Sin destinos. |
| `CALENDAR_AGENDA_SHARE_TOO_MANY_IN_CALL`, `CALENDAR_AGENDA_SHARE_TOO_MANY_TARGETS` | Más de 50 destinos en una llamada o más de 100 vigentes en esa agenda. |
| `CALENDAR_AGENDA_SHARE_RATE_LIMITED` | Más de 30 cambios por minuto. `details.retryAfterSeconds` dice cuánto esperar. 429. |
| `CALENDAR_AGENDA_SHARE_API_KEY_NOT_ALLOWED`, `CALENDAR_AGENDA_SHARE_SUPPORT_SESSION` | Una clave de API de la organización, o una sesión de soporte. |
| `CALENDAR_AGENDA_SHARE_APPROVAL_UNAVAILABLE` | No se ha podido pedir la aprobación en el móvil. No se ha compartido nada; se puede reintentar. |
| `CALENDAR_CATEGORY_NAME_TAKEN` | Ya hay una categoría con ese nombre y otro color. 409. |
| `CALENDAR_CATEGORY_READ_ONLY` | Es de la empresa: no se cambia ni se elimina. 403. |
| `CALENDAR_CATEGORY_IN_USE` | La usan eventos y no has dicho qué hacer con ellos (`eventsAction`). 409. |
| `CALENDAR_CATEGORY_NOT_FOUND` | No existe o es de otra persona. 404. |
| `CALENDAR_CATEGORY_MOVE_TARGET_REQUIRED`, `CALENDAR_CATEGORY_MOVE_TARGET_INVALID` | `move` sin `moveToCategoryId`, o hacia la misma categoría. |
| `CALENDAR_INVITE_NOT_ALLOWED` | Crear con invitados sin `CALENDAR_INVITE`. 403. |
| `PERMISSION_SCOPE_REQUIRED` | Conceder un permiso de personas sin `scope`. |

Cada error trae su mensaje en castellano para contárselo a la persona y, si lo hay, una `hint` con qué hacer.

## Guías de uso

- [Compartir tu agenda](https://os.utilia.ai/docs/guides/calendar/share-agenda) y [Ver la agenda que te comparten](https://os.utilia.ai/docs/guides/calendar/shared-agendas).
- [Qué ven los demás de tu agenda](https://os.utilia.ai/docs/guides/calendar/who-sees-your-agenda): los tres caminos, lo privado y lo provisional.
- [Organizar tus eventos con categorías](https://os.utilia.ai/docs/guides/calendar/categories).
