# Referencia de herramientas MCP

Herramientas, operaciones, scopes y permisos de los servidores MCP: aplicaciones externas y credenciales, CRM, Finanzas y RRHH.

Esta referencia reúne el contrato de las herramientas MCP que más usan los integradores: las cuatro del dominio de integraciones (aplicaciones externas, credenciales, ajustes de integración de la organización y estado del dominio), las ampliaciones del CRM, el servidor de Finanzas y el calendario laboral de RRHH. Para cada operación indica el permiso que necesita tu usuario, el scope que necesita el token y si exige confirmación.

Otras herramientas tienen página propia: [Marketing](https://os.utilia.ai/docs/developers/mcp/marketing), [tickets](https://os.utilia.ai/docs/developers/mcp/tickets), [AI Studio](https://os.utilia.ai/docs/developers/mcp/ai-studio-tool) y el [buscador global y los avisos](https://os.utilia.ai/docs/developers/mcp/search-and-notifications). Si es tu primera conexión, empieza por [Primeros pasos con MCP](https://os.utilia.ai/docs/developers/mcp/quickstart).

## Cómo se autoriza cada operación

Cada operación pasa por estas comprobaciones, en este orden. Las tablas de esta página dan, para cada una, el permiso, el scope y si exige `confirm`.

**Qué comprueba el servidor en cada operación**

1. **Aplicativo en el plan** (Tu organización): Ningún permiso ni scope lo sustituye. Si no: no se anuncia y se rechaza con MODULE_NOT_ENABLED o CAPABILITY_NOT_ENABLED.
2. **Scope de la operación** (El token): Ver la herramienta en tools/list no basta para ejecutar todas sus operaciones. Si no: INSUFFICIENT_SCOPE, que nombra el scope y el conjunto más pequeño que lo concede.
3. **Permiso de la operación** (Tu usuario): Si la tabla da dos permisos separados por «o», basta con uno. Si no: errorCode PERMISSION_DENIED con el nombre del permiso que falta.
4. **Confirmación** (La llamada · Opcional): Solo en las operaciones que borran, rotan secretos o no se pueden deshacer: confirm: true. Si no: se rechaza sin tocar nada.
5. **Ejecución** (Tu alcance): Sobre las filas que tu alcance te deja ver, igual que en la aplicación.

## Aplicaciones externas y credenciales

Cuatro herramientas cubren el dominio de integraciones:

| Herramienta | Servidor | Operaciones | Para qué sirve |
| - | - | - | - |
| `integrations` | `utilia-work` | 61 | Aplicaciones externas: ciclo de vida, persona responsable, renovación de claves sin cortes, OAuth saliente, webhooks, métricas de uso de la API, facturación por aplicación, los errores que reporta cada una, verificación del widget y el catálogo de aplicaciones (explorar, pedir que instalen y las instaladas) |
| `api_credentials` | `utilia-work` | 17 | Claves de API de la organización (`utk_`) y credenciales personales y de aplicación |
| `org_integrations_settings` | `utilia-crm` | 32 | Claves de API v1 de la organización, formularios de captación y otros ajustes de integración |
| `integrations_overview` | `utilia-work` | 4 | Estado del dominio en una sola llamada y el diagnóstico de cada aplicación, de solo lectura |

En la aplicación, las aplicaciones externas se gestionan en [Ajustes › Creadas por tu empresa](https://os.utilia.ai/dashboard/settings/custom-apps).

### `integrations`

Es la herramienta con la que registras tu propia integración, la suscribes a eventos y vigilas su salud. También explora el catálogo de aplicaciones y gestiona las que tu empresa tiene instaladas.

| Operación | Qué hace | Permiso | Scope | `confirm` |
| - | - | - | - | - |
| `apps_list` | Lista las aplicaciones externas de la organización, paginadas. Con `deletedOnly: true`, solo las eliminadas. | `INTEGRATIONS_APPS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:view` | no |
| `apps_get` | Detalle de una aplicación. | `INTEGRATIONS_APPS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:view` | no |
| `apps_create` | Crea una aplicación. Devuelve su clave y su secreto de webhook una sola vez. | `INTEGRATIONS_APPS_MANAGE` o `EXTERNAL_APPS_CREATE` | `integrations:apps:manage` | no |
| `apps_update` | Cambia los datos descriptivos de la aplicación. | `INTEGRATIONS_APPS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:manage` | no |
| `apps_delete` | Elimina la aplicación: deja de funcionar y desaparece de la lista, pero se conserva todo y se puede restaurar. No se puede con suscripciones o cobros programados en marcha. | `EXTERNAL_APPS_DELETE` | `integrations:apps:manage` | sí |
| `apps_restore` | Restaura una aplicación eliminada. Vuelve desactivada: actívala con `apps_toggle_active`. | `EXTERNAL_APPS_DELETE` | `integrations:apps:manage` | sí |
| `apps_delete_permanently` | Borra la aplicación para siempre y conserva lo que obliga la ley: facturas, suscripciones, autorizaciones de cobro, tickets, fichas del CRM y archivos. Exige además `confirmName`, con su nombre exacto. | `EXTERNAL_APPS_DELETE` | `integrations:apps:manage` | sí |
| `apps_toggle_active` | Activa o desactiva la aplicación sin borrarla. | `INTEGRATIONS_APPS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:manage` | sí |
| `apps_regenerate_api_key` | Renueva la clave de API de la aplicación. Con `graceDays` (1, 7 o 30) la anterior sigue valiendo esos días; con 0, el valor por defecto, deja de valer en el acto. | `INTEGRATIONS_APPS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:manage` | sí |
| `apps_api_key_retire_previous` | Retira ya la clave de API anterior: deja de valer en el acto. | `INTEGRATIONS_APPS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:manage` | sí |
| `apps_api_key_advisory_dismiss` | Descarta una recomendación de renovar la clave de API, con un motivo de 20 a 500 caracteres. No se puede deshacer. | `INTEGRATIONS_APPS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:manage` | sí |
| `apps_set_responsible` | Nombra la persona responsable de la aplicación. Solo un Super Administrador nombra a otra persona; el resto, solo a sí mismo. | `INTEGRATIONS_APPS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:manage` | sí |
| `apps_list_responsible_candidates` | Personas que se pueden nombrar responsables, con la misma regla. | `INTEGRATIONS_APPS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:manage` | no |
| `apps_update_scopes` | Sustituye el conjunto entero de scopes de la aplicación y deja el cambio en el registro de auditoría. Añadir scopes críticos de datos personales exige el rol Super Administrador y un `reason` de al menos 20 caracteres. | `INTEGRATIONS_APPS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:manage` | sí |
| `apps_get_stats` | Resumen de la aplicación: clave, webhook, usuarios externos y entregas. | `INTEGRATIONS_APPS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:view` | no |
| `apps_list_users` | Usuarios externos que ha registrado la aplicación. | `INTEGRATIONS_APPS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:view` | no |
| `apps_oauth_get` | Configuración OAuth saliente de la aplicación. | `INTEGRATIONS_APPS_VIEW`, `EXTERNAL_APPS_VIEW` u `OAUTH_APPS_VIEW` | `integrations:apps:view` | no |
| `apps_oauth_enable` | Activa el OAuth saliente. | `OAUTH_APPS_MANAGE` o `INTEGRATIONS_APPS_MANAGE` | `integrations:apps:manage` | sí |
| `apps_oauth_update` | Cambia la configuración OAuth. | `OAUTH_APPS_MANAGE` o `INTEGRATIONS_APPS_MANAGE` | `integrations:apps:manage` | no |
| `apps_oauth_disable` | Desactiva el OAuth saliente y conserva el historial de tokens. | `OAUTH_APPS_MANAGE` o `INTEGRATIONS_APPS_MANAGE` | `integrations:apps:manage` | sí |
| `apps_oauth_regenerate_secret` | Renueva el secreto de cliente OAuth. | `OAUTH_APPS_MANAGE` o `INTEGRATIONS_APPS_MANAGE` | `integrations:apps:manage` | sí |
| `apps_webhooks_get_config` | Configuración actual del webhook. | `INTEGRATIONS_APPS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:webhooks:view` | no |
| `apps_webhooks_update_config` | Cambia `webhookUrl`, `webhookEvents` y `webhookEnabled`. | `INTEGRATIONS_WEBHOOKS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:webhooks:manage` | no |
| `apps_webhooks_regenerate_secret` | Renueva la clave con la que se firman los avisos. Con `graceDays` (1, 7 o 30), cada aviso lleva también la firma de la anterior esos días. | `INTEGRATIONS_WEBHOOKS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:webhooks:manage` | sí |
| `apps_webhooks_secret_cutover` | Indica que el servidor ya usa la clave de firma nueva: `X-Utilia-Signature` pasa a ir firmada con ella. | `INTEGRATIONS_WEBHOOKS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:webhooks:manage` | sí |
| `apps_webhooks_secret_retire_previous` | Retira ya la clave de firma anterior: deja de enviarse su firma. | `INTEGRATIONS_WEBHOOKS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:webhooks:manage` | sí |
| `apps_webhooks_secret_advisory_dismiss` | Descarta una recomendación de renovar la clave de firma, con un motivo de 20 a 500 caracteres. No se puede deshacer. | `INTEGRATIONS_WEBHOOKS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:webhooks:manage` | sí |
| `apps_webhooks_test` | Envía una entrega de prueba, firmada igual que las reales. Si los webhooks están desactivados, no envía nada y lo dice. Con `secret: 'current'` prueba la clave nueva: primero un aviso con una firma falsa y después otro firmado solo con la vigente. Con `event` (por ejemplo, `INVOICE_CREATED`), envía una muestra de ese evento con su forma real, marcada con `livemode: false` y `meta.test: true`, que no queda en el historial de entregas. | `INTEGRATIONS_WEBHOOKS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:webhooks:manage` | no |
| `apps_webhooks_list_deliveries` | Historial de entregas, con filtros. | `INTEGRATIONS_APPS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:webhooks:view` | no |
| `apps_webhooks_get_delivery` | Detalle de una entrega. | `INTEGRATIONS_APPS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:webhooks:view` | no |
| `apps_webhooks_retry_delivery` | Vuelve a encolar una entrega `FAILED` o `FAILED_PERMANENTLY`. | `INTEGRATIONS_WEBHOOKS_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:webhooks:manage` | sí |
| `apps_webhooks_list_supported_events` | Catálogo de eventos a los que se puede suscribir. | `INTEGRATIONS_APPS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:webhooks:view` | no |
| `apps_metrics_requests` | Serie temporal de peticiones a la API externa. | `INTEGRATIONS_METRICS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:metrics:view` | no |
| `apps_metrics_latency` | Latencia agregada (p50, p95 y p99). | `INTEGRATIONS_METRICS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:metrics:view` | no |
| `apps_metrics_errors` | Errores por código de estado. | `INTEGRATIONS_METRICS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:metrics:view` | no |
| `apps_metrics_top_endpoints` | Endpoints más usados. | `INTEGRATIONS_METRICS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:metrics:view` | no |
| `apps_metrics_webhooks_health` | Salud de los webhooks de la aplicación. | `INTEGRATIONS_METRICS_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:apps:metrics:view` | no |
| `apps_billing_get_config` | Configuración de facturación de la aplicación. | `INTEGRATIONS_BILLING_MANAGE` o `EXTERNAL_APPS_VIEW` | `integrations:apps:billing:view` | no |
| `apps_billing_update_config` | Activa o desactiva la facturación de la aplicación. | `INTEGRATIONS_BILLING_MANAGE` o `EXTERNAL_APPS_EDIT` | `integrations:apps:billing:manage` | sí |
| `apps_billing_list_invoices` | Facturas generadas para la aplicación. | `INTEGRATIONS_BILLING_MANAGE` o `EXTERNAL_APPS_VIEW` | `integrations:apps:billing:view` | no |
| `apps_billing_list_payments` | Cobros de la aplicación. | `INTEGRATIONS_BILLING_MANAGE` o `EXTERNAL_APPS_VIEW` | `integrations:apps:billing:view` | no |
| `apps_widget_identity_get` | Modo de la verificación de identidad del widget (`OFF`, `REPORT` o `REQUIRED`) y si la aplicación tiene secreto del widget. El secreto no sale nunca por aquí. | `EXTERNAL_APPS_VIEW` | `integrations:apps:view` | no |
| `apps_widget_identity_update` | Cambia el modo de la verificación. Con `REQUIRED` y sin secreto, el widget deja de identificar a nadie. | `EXTERNAL_APPS_EDIT` | `integrations:apps:manage` | sí |
| `apps_widget_identity_regenerate_secret` | Genera o renueva el secreto del widget; el anterior deja de valer en el acto. El secreto se devuelve una sola vez. Pide confirmación en el móvil. | `EXTERNAL_APPS_EDIT` | `integrations:apps:manage` | sí |
| `catalog_list` | Fichas del catálogo de aplicaciones que puede instalar tu empresa, con búsqueda y categoría, y la dirección de cada ficha. | Ninguno: basta Integraciones en el plan | `integrations:apps:view` | no |
| `catalog_get` | Una ficha del catálogo, con sus permisos, sus aplicativos y su dirección. Las «Solo con el enlace» se abren con su dirección exacta. | Ninguno: basta Integraciones en el plan | `integrations:apps:view` | no |
| `catalog_request_install` | Pide que instalen una aplicación, con un mensaje opcional: avisa a quien puede instalar. Pedirla otra vez no es un error. | Ninguno: basta Integraciones en el plan | `integrations:apps:manage` | sí |
| `installations_list` | Las aplicaciones instaladas en tu empresa, con su estado, su persona responsable, lo que piden y, en las de pago, su suscripción (el importe, solo con `SETTINGS_ORGANIZATION_VIEW`). | `INTEGRATIONS_APPS_INSTALL` o `EXTERNAL_APPS_VIEW` | `integrations:apps:view` | no |
| `installations_get` | Detalle de una instalada, con su suscripción si es de pago (el importe, solo con `SETTINGS_ORGANIZATION_VIEW`). | `INTEGRATIONS_APPS_INSTALL` o `EXTERNAL_APPS_VIEW` | `integrations:apps:view` | no |
| `installations_uninstall` | Desinstala una aplicación, con un motivo opcional que se le cuenta al desarrollador. Pierde el acceso en el acto; lo que creó se queda. | `INTEGRATIONS_APPS_INSTALL` | `integrations:apps:manage` | sí |
| `installations_change_responsible` | Cambia la persona responsable de una instalada. Tiene que tener los permisos que usa la aplicación y no ver más datos que quien la nombra. | `INTEGRATIONS_APPS_INSTALL` | `integrations:apps:manage` | sí |
| `installations_cancel_subscription` | Cancela la suscripción de una aplicación de pago: sigue funcionando hasta el final del periodo pagado o de la prueba y no se renueva; con un pago pendiente, termina ya. No se devuelve nada. Deshacerlo se hace en la pantalla. | `INTEGRATIONS_APPS_INSTALL` o `SETTINGS_ORGANIZATION_EDIT` | `integrations:apps:manage` | sí |
| `apps_errors_list` | Grupos de errores que ha reportado una aplicación o, sin `id`, todas las activas de la organización. Por defecto, los activos. | `EXTERNAL_APPS_ERRORS_VIEW` | `integrations:apps:errors:view` | no |
| `apps_errors_get` | Detalle de un grupo de errores (`id` de la aplicación y `hash`). | `EXTERNAL_APPS_ERRORS_VIEW` | `integrations:apps:errors:view` | no |
| `apps_errors_stats` | Cifras y salud de los errores, de una aplicación o de todas. | `EXTERNAL_APPS_ERRORS_VIEW` | `integrations:apps:errors:view` | no |
| `apps_errors_resolve` | Resuelve un grupo, con una nota opcional. Si el error vuelve a ocurrir, se reabre solo. | `EXTERNAL_APPS_ERRORS_MANAGE` | `integrations:apps:errors:manage` | no |
| `apps_errors_reopen` | Reabre un grupo resuelto. | `EXTERNAL_APPS_ERRORS_MANAGE` | `integrations:apps:errors:manage` | no |
| `apps_errors_silence` | Silencia un grupo de 1 a 672 horas. | `EXTERNAL_APPS_ERRORS_MANAGE` | `integrations:apps:errors:manage` | no |
| `apps_errors_unsilence` | Le quita el silencio. | `EXTERNAL_APPS_ERRORS_MANAGE` | `integrations:apps:errors:manage` | no |
| `apps_errors_bulk` | Resuelve, reabre o silencia varios grupos de una vez. | `EXTERNAL_APPS_ERRORS_MANAGE` | `integrations:apps:errors:manage` | no |
| `apps_errors_link` | Crea un ticket de soporte o una tarea de Gestión de proyectos desde el error y los deja vinculados. Exige además poder crear en el destino. | `EXTERNAL_APPS_ERRORS_MANAGE` | `integrations:apps:errors:manage` | no |

Instalar una aplicación del catálogo no se hace por MCP: exige que una persona vea la ficha, acepte sus permisos y elija la persona responsable. Tampoco comprar una de pago, cambiar de plan, reactivar la suscripción ni pagar un cobro pendiente: piden consentimiento y pago en pantalla. Las fichas que publica una cuenta de desarrollador se gestionan en el servidor de desarrolladores, no aquí. `catalog_list` y `catalog_get` devuelven la dirección de la ficha para que el asistente te ofrezca abrirla. Lo que hace la empresa está en [Explorar e instalar aplicaciones](https://os.utilia.ai/docs/guides/integrations/explore-and-install).

Las renovaciones (`apps_regenerate_api_key`, `apps_webhooks_regenerate_secret` y `apps_widget_identity_regenerate_secret`) piden a la persona, si tiene la verificación en dos pasos, que las confirme en el móvil: la primera llamada responde `APPROVAL_REQUIRED` con un número, la persona elige ese número en la aplicación y la llamada se repite igual, con `twoFactorApprovalId`. Por MCP nunca se aceptan códigos. La aplicación de iPhone que hay hoy en la App Store todavía no aprueba estas confirmaciones; hasta que llegue la próxima versión, quien tenga la verificación activada renueva las claves desde la ficha de la aplicación, con su código. La clave nueva se devuelve una sola vez y no se guarda en ninguna parte.

El SDK de JavaScript no tiene todavía un método para `apps_update_scopes` (`appsUpdate` solo cambia los datos descriptivos): para sustituir los scopes de una aplicación, invoca la herramienta por JSON-RPC.

Ejemplo: crear una aplicación y configurar su webhook. Los SDK necesitan una sesión OAuth autorizada para el servidor `utilia-work`, como se explica en [Primeros pasos con MCP](https://os.utilia.ai/docs/developers/mcp/quickstart#llamar-a-las-herramientas-desde-tu-código).

#### JavaScript / TypeScript

```typescript
const created = await sdk.mcp.integrations.appsCreate({
  name: 'Mi integración',
  slug: 'mi-integracion',
  description: 'Integración personalizada.',
  websiteUrl: 'https://miempresa.com',
});

await sdk.mcp.integrations.appsWebhooksUpdateConfig(created.app.id, {
  webhookUrl: 'https://miempresa.com/webhooks/utilia',
  webhookEnabled: true,
  webhookEvents: ['TICKET_CREATED', 'INVOICE_PAID'],
});

const test = await sdk.mcp.integrations.appsWebhooksTest(created.app.id);
console.log(test.success ? `Entregado: ${test.statusCode}` : test.error);
```

#### Python

```python
from utilia_sdk.mcp.types import AppsCreateInput, WebhooksUpdateConfigInput

created = await sdk.mcp.integrations.apps_create(
    AppsCreateInput(
        name="Mi integración",
        slug="mi-integracion",
        description="Integración personalizada.",
        website_url="https://miempresa.com",
    )
)

await sdk.mcp.integrations.apps_webhooks_update_config(
    created.app.id,
    WebhooksUpdateConfigInput(
        webhook_url="https://miempresa.com/webhooks/utilia",
        webhook_enabled=True,
        webhook_events=["TICKET_CREATED", "INVOICE_PAID"],
    ),
)

result = await sdk.mcp.integrations.apps_webhooks_test(created.app.id)
print(f"Entregado: {result.status_code}" if result.success else result.error)
```

Cómo verificar en tu servidor la firma de las entregas se explica en [Firma HMAC de los webhooks](https://os.utilia.ai/docs/developers/webhooks/hmac-signature).

### `api_credentials`

Gestiona las claves de API de tu organización. Con ella un asistente de IA puede hacer todo lo que se hace en **Ajustes › Claves de API**, con las mismas reglas y, para lo delicado, con tu confirmación en el móvil.

**Claves de API de la organización (`utk_`, 10).** Son las de [Claves de API de la organización](https://os.utilia.ai/docs/developers/organization-api-keys/overview): cada una actúa en nombre de una persona responsable y nunca puede más que ella. Las operaciones `org_keys_*` llaman al mismo servicio que la pantalla, así que se aplican igual:

- la persona responsable;
- el techo de permisos y el plan de cada módulo;
- la regla de que solo un Super Administrador nombra responsable a otra persona;
- el modo restringido y la auditoría.

| Operación | Qué hace | Permiso | Scope | `confirm` |
| - | - | - | - | - |
| `org_keys_list` | Lista las claves, con persona responsable, permisos y estado. | `API_CREDENTIALS_VIEW` o `API_CREDENTIALS_MANAGE` | `integrations:credentials:view` | no |
| `org_keys_get` | Ficha de una clave, con lo que puede hacer hoy módulo a módulo. | `API_CREDENTIALS_VIEW` o `API_CREDENTIALS_MANAGE` | `integrations:credentials:view` | no |
| `org_keys_usage` | Peticiones de las últimas 24 horas y 7 días, por hora y por ruta. | `API_CREDENTIALS_VIEW` o `API_CREDENTIALS_MANAGE` | `integrations:credentials:view` | no |
| `org_keys_catalog` | Módulos que se pueden dar a una clave para una persona (`responsibleUserId`). | `API_CREDENTIALS_MANAGE` | `integrations:credentials:view` | no |
| `org_keys_candidates` | A quién se puede nombrar persona responsable. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:view` | no |
| `org_keys_create` | Crea una clave con `name`, `permissionMode` (`ALL`, `READ_ONLY` o `RESTRICTED` con `moduleAccess`) y, si quieres, `responsibleUserId`, `expiresAt`, `rateLimitPerHour` y `description`. La clave sale entera una sola vez. Pide confirmación en el móvil. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:manage` | sí |
| `org_keys_update` | Cambia nombre, descripción, límite, permisos, caducidad o persona responsable. Estrechar no pide nada; ampliar permisos, alargar o quitar la caducidad o cambiar de responsable piden confirmación en el móvil. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:manage` | no |
| `org_keys_rotate` | Renueva la clave. `graceDays` dice cuántos días sigue valiendo la anterior: 1, 7 o 30, o 0 para cortarla en el acto. Pide confirmación en el móvil. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:manage` | sí |
| `org_keys_end_transition` | La anterior de una renovación deja de valer ya. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:manage` | sí |
| `org_keys_revoke` | Revoca la clave y su anterior al momento, con un `reason` opcional. | `API_CREDENTIALS_MANAGE`, o ser su persona responsable | `integrations:credentials:manage` | sí |

Crear, cambiar y renovar exigen además Integraciones en el plan; ver y revocar, no. La persona responsable puede revocar su clave aunque no gestione claves, pero el asistente necesita el scope `integrations:credentials:manage`, y ese scope solo puede autorizarlo quien gestiona claves. Si no lo tiene, que la revoque desde la pantalla.

**Lo que fabrica o amplía acceso se confirma en el móvil.** Crear, renovar, ampliar permisos, alargar o quitar la caducidad y cambiar de responsable siguen el ciclo de siempre:

1. La primera llamada devuelve `errorCode: APPROVAL_REQUIRED`.
2. El móvil enseña qué clave es, a nombre de quién y con qué permisos.
3. Cuando la persona lo aprueba, repites la misma llamada, con los mismos datos y con `twoFactorApprovalId`.

La confirmación solo vale para esa clave con esos datos: una instrucción escondida en un documento no basta para fabricar una clave. Sin la verificación en dos pasos activada, estas operaciones se rechazan con `TWO_FACTOR_NOT_ENABLED`.

Las operaciones generales (`list`, `get`, `get_usage`, `rotate` y `revoke`) no tocan estas claves. `list` no las enseña y avisa de cuántas hay; las demás remiten a su operación `org_keys_*`.

**Credenciales personales y de aplicación (7).** Son las credenciales de un usuario (`PERSONAL`) y las recortadas de una aplicación externa (`EXTERNAL_APP`). Las de cuenta de servicio (`SERVICE_ACCOUNT`) ya no se crean, porque no sirven para entrar en ningún sitio. Para conectar un programa, usa `org_keys_create`. Al rotar, el secreto nuevo se muestra una sola vez y el anterior sigue valiendo 24 horas.

| Operación | Qué hace | Permiso | Scope | `confirm` |
| - | - | - | - | - |
| `list` | Lista credenciales con filtros por tipo, estado y aplicación. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:view` | no |
| `get` | Detalle de una credencial. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:view` | no |
| `get_usage` | Uso de la credencial: peticiones, último uso y veces que tocó el límite. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:view` | no |
| `create` | Crea una credencial. Devuelve el secreto una sola vez. Pide confirmación en el móvil. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:manage` | sí |
| `rotate` | Renueva el secreto; el anterior sigue valiendo 24 horas. Pide confirmación en el móvil. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:manage` | sí |
| `revoke` | Revoca la credencial al momento, sin gracia. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:manage` | sí |
| `scopes_catalog` | Catálogo de scopes v2 disponibles. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:view` | no |

Estas siete aceptan también `SETTINGS_ORGANIZATION_EDIT` en lugar de `API_CREDENTIALS_MANAGE`: basta con uno de los dos.

Reglas de `create` y `rotate`:

- **Nadie da a una credencial lo que no tiene.** Si no eres Super Administrador, solo puedes conceder scopes cuyo permiso tienes. Si pides alguno más, la llamada se rechaza con `errorCode: CREDENTIAL_SCOPE_AUTHORITY_EXCEEDED` y el mensaje dice cuáles sobran. El acceso total (`*`) solo lo concede el Super Administrador. Esto se comprueba antes de pedir la confirmación, para que nadie apruebe en el móvil algo que se va a rechazar.
- **Confirmación en el móvil**, con el mismo ciclo de `APPROVAL_REQUIRED` y `twoFactorApprovalId`. Exige tener la verificación en dos pasos activada.
- Con la organización en modo restringido no se crea ninguna credencial; consultarlas y revocarlas sigue funcionando.

#### JavaScript / TypeScript

```typescript
const { secret, previousValidUntil } = await sdk.mcp.apiCredentials.rotate(
  credentialId,
  { reason: 'Rotación trimestral' },
);

console.log('Secreto nuevo:', secret);
console.log('El anterior vale hasta:', previousValidUntil);
```

#### Python

```python
result = await sdk.mcp.api_credentials.rotate(
    credential_id,
    reason="Rotación trimestral",
)

print(f"Secreto nuevo: {result.secret}")
print(f"El anterior vale hasta: {result.previous_valid_until}")
```

### `org_integrations_settings`

Agrupa ajustes de integración de la organización anteriores al dominio de aplicaciones externas. Vive en el servidor `utilia-crm`. Si vienes de una versión antigua de los SDK, esta herramienta es la que antes se llamaba `integrations`: consulta la [guía de migración](https://os.utilia.ai/docs/developers/mcp/migration-v1-v2).

La herramienta aparece en `tools/list` con los conjuntos `crm:read`, `crm:full`, `dev:read` o `dev:full`, o con cualquier scope de lectura del CRM, pero cada operación exige además su propio scope, que es el de la tabla.

**Claves de API v1 de la organización (8).** Además del permiso, la clave tiene que haberla creado tu usuario y pertenecer a tu organización.

| Operación | Qué hace | Permiso | Scope | `confirm` |
| - | - | - | - | - |
| `api_keys_list` | Lista tus claves de API v1. | `INTEGRATIONS_APPS_VIEW` o `API_CREDENTIALS_MANAGE` | `integrations:credentials:view` | no |
| `api_keys_get` | Detalle de una clave. | `INTEGRATIONS_APPS_VIEW` o `API_CREDENTIALS_MANAGE` | `integrations:credentials:view` | no |
| `api_keys_create` | Crea una clave con `name` y, si quieres, `description`, `rateLimit` (llamadas por hora, de 10 a 10.000; 100 si lo omites) y `expiresAt`. Devuelve la clave una sola vez. Pide confirmación en el móvil. | `API_CREDENTIALS_MANAGE`, con alcance «Todo» | `integrations:credentials:manage` | sí |
| `api_keys_toggle` | Activa o desactiva una clave. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:manage` | no |
| `api_keys_delete` | Borra una clave. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:manage` | sí |
| `api_keys_regenerate` | Renueva la clave y devuelve el valor nuevo una sola vez. Pide confirmación en el móvil. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:manage` | sí |
| `api_keys_get_notifications` | Avisos configurados por departamento. | `INTEGRATIONS_APPS_VIEW` o `API_CREDENTIALS_MANAGE` | `integrations:credentials:view` | no |
| `api_keys_update_notifications` | Cambia los destinatarios de esos avisos. | `API_CREDENTIALS_MANAGE` | `integrations:credentials:manage` | no |

`api_keys_create` y `api_keys_regenerate` siguen las mismas reglas de confirmación que `api_credentials`: la primera llamada devuelve `APPROVAL_REQUIRED`, se repite con `twoFactorApprovalId` tras aprobar en el móvil, y sin la verificación en dos pasos activada se rechazan con `TWO_FACTOR_NOT_ENABLED`. Con la organización en modo restringido no se crea ninguna clave.

Por compatibilidad con las integraciones anteriores al 28 de agosto de 2026, un token con scopes del CRM o del dominio de trabajo puede leer las claves aunque no lleve `integrations:credentials:view`. Para gestionarlas hay que pedir siempre `integrations:credentials:manage`.

**Formularios de captación (11).** Se corresponden con Ajustes › CRM › [Formularios de captación](https://os.utilia.ai/dashboard/settings/lead-capture).

| Operación | Qué hace | Permiso | Scope | `confirm` |
| - | - | - | - | - |
| `lead_forms_list` | Lista los formularios de la organización. | `LEAD_FORMS_VIEW` | `crm:lead-forms:view` | no |
| `lead_forms_get` | Detalle de un formulario. | `LEAD_FORMS_VIEW` | `crm:lead-forms:view` | no |
| `lead_forms_create` | Crea un formulario. | `LEAD_FORMS_CREATE` | `crm:lead-forms:manage` | no |
| `lead_forms_update` | Cambia los campos, el vínculo con el CRM o los webhooks del formulario. | `LEAD_FORMS_EDIT` | `crm:lead-forms:manage` | no |
| `lead_forms_delete` | Borra el formulario; las respuestas recibidas se conservan. | `LEAD_FORMS_DELETE` | `crm:lead-forms:manage` | sí |
| `lead_forms_publish` | Publica el formulario. | `LEAD_FORMS_PUBLISH` | `crm:lead-forms:manage` | no |
| `lead_forms_pause` | Pausa el formulario. | `LEAD_FORMS_EDIT` | `crm:lead-forms:manage` | no |
| `lead_forms_archive` | Archiva el formulario sin borrar su historial. | `LEAD_FORMS_EDIT` | `crm:lead-forms:manage` | no |
| `lead_forms_unarchive` | Devuelve a borrador un formulario archivado y conserva su `slug`. | `LEAD_FORMS_EDIT` | `crm:lead-forms:manage` | no |
| `lead_forms_get_stats` | Estadísticas agregadas. | `LEAD_FORMS_VIEW` | `crm:lead-forms:view` | no |
| `lead_forms_get_snippet` | Fragmento HTML y JavaScript para insertar el formulario en tu web. | `LEAD_FORMS_VIEW` | `crm:lead-forms:view` | no |

### `integrations_overview`

Da el estado del dominio de integraciones en una sola llamada, pensado para un copiloto o un panel. Es de solo lectura y su respuesta se guarda en caché 30 segundos por organización y operación. `health`, `quickstart` y `app_diagnosis` leen del mismo diagnóstico que la pestaña **Pruebas** de cada aplicación y que `GET /external/v1/diagnostics`, así que no se contradicen.

| Operación | Qué devuelve | Permiso | Scope |
| - | - | - | - |
| `status` | Cifras agregadas: aplicaciones, credenciales por tipo, entregas de las últimas 24 horas y catálogo de eventos. | `INTEGRATIONS_OVERVIEW_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:overview:view` |
| `health` | Salud: cuántas aplicaciones hay en cada estado (`appsByStatus`), las que hay que revisar (`appsNeedingAttention`, hasta 10), la peor parada, las llamadas de las últimas 24 horas, errores críticos abiertos, entregas fallidas y reintentos pendientes. Su veredicto es `healthy`, `degraded` o `critical`. | `INTEGRATIONS_OVERVIEW_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:overview:view` |
| `quickstart` | La siguiente acción recomendada (`nextAction`), lo que queda pendiente (`missingConfig`) y, aparte, lo que conviene saber sin ser un paso pendiente (`recommendations`). | `INTEGRATIONS_OVERVIEW_VIEW` o `EXTERNAL_APPS_VIEW` | `integrations:overview:view` |
| `app_diagnosis` | El diagnóstico de una aplicación, con `appId` (de `integrations.apps_list`): veredicto, qué puede llamar, hallazgos con quién los arregla, pasos siguientes y recorrido. | `EXTERNAL_APPS_VIEW` | `integrations:overview:view` |

`app_diagnosis` exige `EXTERNAL_APPS_VIEW`, el mismo permiso que la ficha de la aplicación en el panel: con permisos personalizados, `INTEGRATIONS_OVERVIEW_VIEW` solo da el resumen. Busca la aplicación dentro de tu organización: si no está o está eliminada, responde `APP_NOT_FOUND`, exista o no en otra. Su respuesta tiene la forma de [`GET /external/v1/diagnostics`](https://os.utilia.ai/docs/developers/getting-started/diagnostics#el-diagnóstico-de-tu-aplicación), con la voz de quien administra la organización: «Concede el permiso…» en vez de «Pide a un administrador…». Se llama con `tools/call`; en los SDK, `appDiagnosis(appId)` (`app_diagnosis`) llega con la 4.6.0.

```json
{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "tools/call",
  "params": {
    "name": "integrations_overview",
    "arguments": { "operation": "app_diagnosis", "appId": "ckapp1234567890" }
  }
}
```

#### JavaScript / TypeScript

```typescript
const status = await sdk.mcp.integrationsOverview.status();
const health = await sdk.mcp.integrationsOverview.health();

console.log(`Aplicaciones activas: ${status.apps.active}/${status.apps.total}`);

if (health.overall !== 'healthy') {
  console.warn('La peor parada:', health.worstApp?.appName, health.worstApp?.verdict);
  for (const app of health.appsNeedingAttention) {
    console.warn(`${app.appName}: ${app.status}`);
  }
}
```

#### Python

```python
status = await sdk.mcp.integrations_overview.status()
health = await sdk.mcp.integrations_overview.health()

print(f"Aplicaciones activas: {status.apps.active}/{status.apps.total}")

if health.overall != "healthy":
    if health.worst_app:
        print(f"La peor parada: {health.worst_app.app_name} ({health.worst_app.verdict})")
    for app in health.apps_needing_attention:
        print(f"{app.app_name}: {app.status}")
```

Los campos son los de la respuesta del servidor: la peor parada va en `worstApp` (`appId`, `appName`, `errorRate`, `lastErrorAt`, `status` y `verdict`) y las que hay que revisar, en `appsNeedingAttention`. Los tipos de los SDK con estos nombres, y `app_diagnosis` como método, llegan con la 4.6.0: con una versión anterior, `worstApp.name` siempre llegaba vacío y en Python `status()` fallaba al validar ([Versiones y compatibilidad](https://os.utilia.ai/docs/developers/sdk/versions#novedades-de-la-versión-460)).

Qué cuenta `quickstart` como pendiente y qué solo recomienda:

- **`missingConfig`** (pasos que faltan): que no haya ninguna aplicación; que una aplicación activa con permisos de facturas tenga la facturación apagada, que va primero y nombra las aplicaciones, porque cada factura que intenten emitir se rechaza con `BILLING_NOT_ENABLED`; cada bloqueo del diagnóstico de las aplicaciones activas, con las aplicaciones que lo tienen; y que haya aplicaciones que envían facturas por correo sin ningún dominio de envío verificado.
- **`recommendations`** (no son pasos pendientes): lo que el plan no incluye, porque no se arregla configurando; los avisos del diagnóstico de cada aplicación; y los avisos a tu servidor, que son opcionales porque una aplicación que solo llama a la API funciona sin ellos.

`nextAction` sigue este orden: crear la primera aplicación; activar la facturación apagada, con la operación `apps_billing_update_config` de `integrations` (solo un Super Administrador, con un motivo de al menos 20 caracteres); el primer bloqueo de una aplicación, que remite a `app_diagnosis`; verificar un dominio de envío; y, con todo lo necesario hecho, los avisos a tu servidor como opcionales.

Es el mismo diagnóstico que resume en una línea el bloque Estado de la ficha de cada aplicación ([Panel de aplicaciones externas](https://os.utilia.ai/docs/developers/api/apps-panel#el-estado-de-la-aplicación)).

### Permisos por rol

De los seis roles de UTILIA OS, solo dos traen permisos de integraciones por defecto: Super Administrador, todos, y Gerente, los de consulta y los de gestionar los errores de las aplicaciones. Vendedor, Asistente, Visualizador y Comercial Externo («Otros roles» en la figura) no traen ninguno.

**Permisos de integraciones por rol**

**Aplicaciones externas**

| Scope | Super Administrador | Gerente | Otros roles |
| - | - | - | - |
| integrations:apps:view (INTEGRATIONS_APPS_VIEW o EXTERNAL_APPS_VIEW) | Sí | Sí | No |
| integrations:apps:manage (INTEGRATIONS_APPS_MANAGE o EXTERNAL_APPS_EDIT (OAuth saliente: ver el pie)) | Sí | No | No |
| integrations:apps:webhooks:view (INTEGRATIONS_APPS_VIEW o EXTERNAL_APPS_VIEW) | Sí | Sí | No |
| integrations:apps:webhooks:manage (INTEGRATIONS_WEBHOOKS_MANAGE o EXTERNAL_APPS_EDIT) | Sí | No | No |
| integrations:apps:metrics:view (INTEGRATIONS_METRICS_VIEW o EXTERNAL_APPS_VIEW) | Sí | Sí | No |
| integrations:apps:billing:view (INTEGRATIONS_BILLING_MANAGE o EXTERNAL_APPS_VIEW) | Sí | Sí | No |
| integrations:apps:billing:manage (INTEGRATIONS_BILLING_MANAGE o EXTERNAL_APPS_EDIT) | Sí | No | No |
| integrations:apps:errors:view (EXTERNAL_APPS_ERRORS_VIEW) | Sí | Sí | No |
| integrations:apps:errors:manage (EXTERNAL_APPS_ERRORS_MANAGE) | Sí | Sí | No |

**Credenciales**

| Scope | Super Administrador | Gerente | Otros roles |
| - | - | - | - |
| integrations:credentials:view (API_CREDENTIALS_MANAGE) | Sí | No | No |
| integrations:credentials:manage (API_CREDENTIALS_MANAGE) | Sí | No | No |

**Estado del dominio**

| Scope | Super Administrador | Gerente | Otros roles |
| - | - | - | - |
| integrations:overview:view (INTEGRATIONS_OVERVIEW_VIEW o EXTERNAL_APPS_VIEW) | Sí | Sí | No |

_Cada fila es un scope; la nota dice qué permisos del usuario lo cumplen (basta uno). En OAuth saliente, apps:manage pide OAUTH_APPS_MANAGE o INTEGRATIONS_APPS_MANAGE._

`API_CREDENTIALS_MANAGE` solo lo trae el rol Super Administrador: un Gerente no gestiona credenciales aunque su token lleve el scope.

Conjuntos de scopes:

| Conjunto | Incluye | Para quién |
| - | - | - |
| `integrations:full` | Los siete `integrations:apps:*` e `integrations:overview:view`. No incluye credenciales. | Clientes OAuth que gestionan aplicaciones externas |
| `api_credentials:full` | `integrations:credentials:view` e `integrations:credentials:manage` | Clientes OAuth dedicados a credenciales |
| `admin:full` | `dev:full`, `integrations:full` y `api_credentials:full` | Administración avanzada |
| `dev:full` | Todo el dominio de trabajo, soporte, integraciones y credenciales | Quien opera todo el servidor `utilia-work` |

### Eventos y conexiones con otros módulos

Tu aplicación puede suscribirse, en `webhookEvents`, a eventos de todo el ecosistema. El catálogo completo, con el contenido de cada aviso, está en [Avisos: contenido y catálogo de eventos](https://os.utilia.ai/docs/developers/webhooks/events), y `apps_webhooks_list_supported_events` lo devuelve por MCP. Algunos ejemplos:

- Leads: `LEAD_CREATED`, `LEAD_QUALIFIED`, `LEAD_DISQUALIFIED` y `LEAD_CONVERTED`. Un formulario de captación crea un lead en el CRM, y con él llega `LEAD_CREATED`.
- Clientes y contactos: `CLIENT_CREATED`, `CLIENT_UPDATED` y `CONTACT_CREATED`.
- Facturas y presupuestos: `INVOICE_PAID`, `INVOICE_PAYMENT_FAILED`, `INVOICE_REFUNDED` y `BUDGET_SIGNED`.
- Tickets de soporte: `TICKET_CREATED`, `TICKET_REPLIED`, `TICKET_RESOLVED` y `TICKET_CONVERTED`.
- Archivos: `FILE_UPLOADED`.
- Errores de tu aplicación: `ERROR_REPORTED`, `ERROR_RESOLVED` y `ERROR_REOPENED`. Tu aplicación los reporta en `POST /api/external/v1/errors/report`; se agrupan por huella, `ERROR_REPORTED` se emite la primera vez y `integrations_overview: health` los tiene en cuenta en su veredicto.

Cada aviso lleva la versión de su contenido en la cabecera `X-Utilia-Schema-Version` y en `payload.meta.schemaVersion`. Los eventos `broadcast` llegan a todas las aplicaciones suscritas; los `directed`, solo a la aplicación dueña del recurso.

La facturación por aplicación se apoya en el módulo de Finanzas: sus facturas y cobros se consultan con `apps_billing_list_invoices` y `apps_billing_list_payments`, y los eventos `INVOICE_PAID`, `INVOICE_PAYMENT_FAILED` e `INVOICE_REFUNDED` te avisan del resultado de cada cobro.

## CRM (servidor utilia-crm)

Las herramientas canónicas del CRM tienen operaciones que comparten reglas con la API externa (`/external/v1/leads` y `/external/v1/clients`). Esta sección recoge las que más usan los integradores.

### Cualificar, descartar y convertir leads; desactivar clientes

| Herramienta | Operación | Qué hace | Permiso | Scope | `confirm` |
| - | - | - | - | - | - |
| `crm_leads` | `set_qualified` | Pasa el lead a `QUALIFIED`. Idempotente. | `LEADS_EDIT` | `crm:leads:update` | no |
| `crm_leads` | `set_disqualified` | Pasa el lead a `LOST`. Es la única baja lógica de un lead por esta vía. | `LEADS_EDIT` | `crm:leads:update` | no |
| `crm_leads` | `convert_to_contact` | Crea un contacto y, si lo pides, una oportunidad, y deja el lead como `CONVERTED`. Exige un motivo de al menos 20 caracteres. | `LEADS_CONVERT` | `crm:leads:convert` o `crm:leads:update` | sí |
| `crm_clients` | `deactivate` | Pasa el cliente a `INACTIVE`: deja de poder recibir facturas y conserva su historial. No existe un borrado real. | `CLIENTS_EDIT` | `crm:clients:update` | sí |

Los conjuntos `crm:write` y `crm:full` incluyen `crm:leads:update` y `crm:clients:update`.

El motivo de `set_qualified` y de `set_disqualified` viaja en `reason`, y se guarda en las notas internas del lead. `set_qualified` acepta también `notes` como alias heredado, que es el que usan hoy los métodos `setQualified` y `set_qualified` de los SDK; si llegan los dos, manda `reason`.

### Anotar un correo sin enviarlo

`crm_interactions_quick: log_email` deja constancia de un correo saliente en el historial del cliente, pero no envía nada. Su respuesta trae `delivered: false`, que es su estado normal, no un fallo. `to` y `body` son obligatorios, y con `scheduledFor` la interacción queda programada en vez de completada.

**Anotar no es avisar.** Si un asistente usa `log_email` para «avisar al cliente», dará por comunicado algo que nadie ha recibido. Los correos de verdad salen por las herramientas que los envían: el presupuesto con `crm_budgets`, la factura con `crm_invoices`, la campaña con `marketing_campaigns`.

### Grupos de empresas

El CRM agrupa empresas bajo una matriz, en dos niveles: matriz y filiales.

`get` y `list` de `crm_clients` devuelven `parentClientId`, `hierarchyRole` (`PARENT`, `SUBSIDIARY` o `STANDALONE`) y `parent { id, name }`; el nombre de la matriz solo llega si la matriz está dentro de tu alcance. `list` acepta los filtros `parentClientId` y `hierarchyRole`, y la búsqueda por texto encuentra las filiales también por el nombre de su matriz, solo entre las matrices que puedes ver.

Operaciones de grupo en `crm_clients_info`:

| Operación | Qué hace | Permiso | Scope |
| - | - | - | - |
| `list_group` | Filiales visibles de la matriz, con sus datos básicos. | `CLIENTS_VIEW` | `crm:clients:view` |
| `get_group_stats` | Consolidado del grupo, solo sobre la matriz: contactos, proyectos, oportunidades, presupuestos, facturas y tickets de las empresas visibles. Los importes llegan solo con los permisos financieros correspondientes. | `CLIENTS_VIEW` | `crm:clients:view` |
| `link_subsidiary` | Vincula una filial a su matriz. | `CLIENTS_MANAGE_HIERARCHY` | `crm:clients:update` |
| `unlink_subsidiary` | Desvincula una filial de su matriz. | `CLIENTS_MANAGE_HIERARCHY` | `crm:clients:update` |
| `list_group_assignments` | Asignaciones del grupo (`id` es la matriz): empresas visibles y personas con su asignación en cada empresa (`{ clientId, role, isPrimary }`). Las personas desactivadas no salen. | `CLIENTS_VIEW` | `crm:clients:view` |
| `assign_group` | Asigna hasta 20 personas (`userIds`) a las empresas del grupo (`id` es la matriz). `role` es opcional y vale `ASSIGNED` por defecto; `targetClientIds` acota las empresas. Las asignaciones que ya existen conservan su rol y su condición de responsable principal, la operación es idempotente y las empresas fuera de tu alcance se omiten como `skippedOutOfScope`. Devuelve `summary` (`companiesTargeted`, `skippedOutOfScope`, `created` y `alreadyAssigned`) y el desglose por empresa. | `CLIENTS_ASSIGN` | `crm:clients:update` |

`crm_clients` acepta `parentClientId` en `create` y en `update`, y en los dos casos exige `CLIENTS_MANAGE_HIERARCHY` sobre las dos empresas. En `create`, el permiso se comprueba antes de crear y el vínculo se aplica después del alta: si el vínculo falla, el cliente queda creado y el error lo dice con su identificador. En `update`, la jerarquía se procesa primero y, si no puedes gestionarla, no se aplica ningún cambio.

`convert_to_invoice` de `crm_budgets` acepta `receiverClientId` (alias `receiver_client_id`) para emitir la factura a la matriz del cliente del presupuesto en lugar de a la propia empresa; cualquier otro receptor se rechaza. Pide el aplicativo Finanzas en el plan de la organización, los tres permisos `BUDGETS_EDIT`, `INVOICES_VIEW` e `INVOICES_CREATE` (con receptor alternativo, además acceso a la matriz) y los tres scopes `crm:budgets:update`, `crm:invoices:view` y `crm:invoices:create`; con `issueDirectly`, también `crm:invoices:issue`. Ningún scope de lectura basta.

### Campos personalizados

Cada organización define campos personalizados para clientes, contactos, leads y oportunidades. `crm_clients`, `crm_contacts`, `crm_leads` y `crm_opportunities` exponen ese catálogo y permiten escribir sus valores.

`list_custom_fields` devuelve el catálogo activo de la entidad de la herramienta. Consúltalo antes de escribir para conocer las claves válidas y sus tipos.

| Herramienta | Permiso | Scope |
| - | - | - |
| `crm_clients` | `CLIENTS_VIEW`, `CUSTOM_FIELDS_VIEW` o `CUSTOM_FIELDS_MANAGE` | `crm:clients:view` o `crm:custom-fields:view` |
| `crm_contacts` | `CONTACTS_VIEW`, `CUSTOM_FIELDS_VIEW` o `CUSTOM_FIELDS_MANAGE` | `crm:contacts:view` o `crm:custom-fields:view` |
| `crm_leads` | `LEADS_VIEW`, `CUSTOM_FIELDS_VIEW` o `CUSTOM_FIELDS_MANAGE` | `crm:leads:view` o `crm:custom-fields:view` |
| `crm_opportunities` | `OPPORTUNITIES_VIEW`, `CUSTOM_FIELDS_VIEW` o `CUSTOM_FIELDS_MANAGE` | `crm:opportunities:view` o `crm:custom-fields:view` |

Cada campo trae `key`, `label`, `type` (`TEXT`, `NUMBER`, `DATE`, `BOOLEAN`, `SELECT`, `MULTISELECT`, `CURRENCY`, `URL`, `EMAIL` o `PHONE`), `description`, `isRequired`, `isMultiline`, `sortOrder` y, en las listas, sus `options` activas (`{ value, label }`).

Para escribir valores, `create` y `update` aceptan `customFields` con la forma `{ clave: valor }`. El servicio valida el tipo, la obligatoriedad, la opción y el formato, y sus errores llegan legibles. Los valores vuelven también en `get` y `list`.

```json
{
  "operation": "update",
  "id": "cli_123",
  "customFields": {
    "sector_vertical": "retail",
    "fecha_alta_manual": "2026-07-01",
    "cliente_estrategico": true
  }
}
```

Escribir valores exige el scope de escritura de la entidad (`crm:clients:update`, `crm:contacts:create`...); `crm:custom-fields:view` solo lee el catálogo. Crear, editar, reordenar, archivar o eliminar campos se hace con `crm_settings` (apartado siguiente).

### Configuración del CRM

`crm_settings` configura lo mismo que Ajustes > CRM en la aplicación: las etapas del embudo de ventas, el catálogo de campos personalizados de clientes, contactos, leads y oportunidades, los motivos de pérdida y la puntuación de leads. Usa los mismos servicios, validaciones y registros de auditoría que la aplicación.

| Operaciones | Permiso | Scope |
| - | - | - |
| `list_pipelines` | `OPPORTUNITIES_VIEW`, `PIPELINES_VIEW` o `PIPELINES_MANAGE` | `crm:settings:view`, `crm:pipelines:view` o `crm:opportunities:view` |
| `update_pipeline`, `create_stage`, `update_stage`, `reorder_stages`, `delete_stage` | `PIPELINES_MANAGE` | `crm:settings:manage` |
| `list_custom_fields` | `CUSTOM_FIELDS_VIEW` o `CUSTOM_FIELDS_MANAGE` | `crm:settings:view` o `crm:custom-fields:view` |
| `create_custom_field`, `update_custom_field`, `reorder_custom_fields`, `delete_custom_field` | `CUSTOM_FIELDS_MANAGE` | `crm:settings:manage` |
| `list_loss_reasons` | `OPPORTUNITIES_VIEW`, `LEADS_VIEW` o `CRM_SETTINGS_MANAGE` | `crm:settings:view`, `crm:opportunities:view` o `crm:leads:view` |
| `create_loss_reason`, `update_loss_reason`, `delete_loss_reason` | `CRM_SETTINGS_MANAGE` | `crm:settings:manage` |
| `get_lead_scoring`, `preview_lead_scoring` | `CRM_SETTINGS_MANAGE` | `crm:settings:view` |
| `update_lead_scoring` | `CRM_SETTINGS_MANAGE` | `crm:settings:manage` |

Las lecturas aceptan también `crm:settings:manage`. Ningún conjunto concede `crm:settings:manage`, tampoco `crm:full`: se pide por su nombre.

Cómo usarla:

- **Plan.** Toda la herramienta pide el aplicativo CRM. Las etapas y los motivos de pérdida piden además Oportunidades, y la puntuación, Leads. Los campos de leads y de oportunidades piden la capacidad de su ficha.
- **Los borrados exigen `confirm: true`.** Sin él no se ejecuta nada y la respuesta (`errorCode: CONFIRMATION_REQUIRED`) explica qué pasará con los datos.
- **`delete_stage`**: si la etapa tiene oportunidades, `moveToStageId` es obligatorio y se mueven todas a esa etapa; las abiertas registran el cambio en su historial y su probabilidad no cambia. Un embudo conserva siempre dos etapas. `list_pipelines` da `opportunityCount` por etapa. Si no indicas `pipelineId`, se usa el embudo por defecto.
- **`delete_custom_field`**: sin valores, se borra. Activo y con valores, no se puede borrar: archívalo con `update_custom_field` e `isArchived: true`, que conserva los valores. Archivado y con valores, se borra y su valor se quita de todos los registros. Las operaciones de campos piden `entityType` (`CLIENT`, `CONTACT`, `LEAD` u `OPPORTUNITY`); `key` y `type` no cambian después de crear el campo, y una opción con valores se archiva, no se elimina.
- **`delete_loss_reason`**: un motivo en uso o del sistema se archiva y las oportunidades lo conservan; uno propio sin uso se borra.
- **`update_lead_scoring`** sustituye la configuración entera: lee antes con `get_lead_scoring`. `preview_lead_scoring` dice cuántos leads quedarían calientes, templados y fríos con unos umbrales, sin guardar nada.

### Automatizaciones del CRM

`crm_automations` gestiona las automatizaciones sobre leads y oportunidades. Solo trabaja con automatizaciones del CRM, y `marketing_automations`, solo con las de Marketing: ningún token de un lado ve ni toca las del otro.

| Operaciones | Permiso | Scope |
| - | - | - |
| `list`, `get`, `describe_catalog`, `validate`, `list_templates`, `list_runs`, `get_run`, `stats` | `CRM_AUTOMATIONS_VIEW` | `crm:automations:view` |
| `create`, `update`, `replace_steps`, `delete`, `activate`, `pause`, `bulk_set_status`, `test_trigger`, `duplicate`, `add_step`, `update_step`, `remove_step`, `create_from_template`, `retry_run`, `cancel_run` | `CRM_AUTOMATIONS_MANAGE` | `crm:automations:manage` |

Cómo usarla:

- **El orden recomendado** es `describe_catalog`, `validate`, `create` y `activate` con `confirm: true`.
- **`describe_catalog`** devuelve los disparadores y acciones disponibles con su configuración, los límites del validador y los datos reales de tu organización: etapas de cada embudo, formularios de captación activos, etiquetas y estados de lead y de oportunidad. Así no hay que adivinar claves.
- **`validate`** comprueba una definición entera sin guardar nada, con los mismos validadores que el guardado y la activación, y devuelve `valid`, `problems` y `notes` en castellano.
- **Las automatizaciones nacen en borrador.** `activate` exige `confirm: true` y revalida todos los pasos; `delete` también exige `confirm: true`.
- **`update` con `steps` y `replace_steps` sustituyen el flujo entero**: solo en borrador o en pausa, y cancelan las ejecuciones en curso. Las ramas de un paso `CONDITION` viajan en `branchTrueOrder` y `branchFalseOrder`. Para un paso suelto, `add_step`, `update_step` y `remove_step`.
- **`bulk_set_status`** activa o pausa hasta 25 automatizaciones a la vez y devuelve el resultado de cada una en `results` y `summary`: un fallo no detiene al resto y reintentar solo los fallidos es seguro. Para activar exige `confirm: true`.
- **`test_trigger`** prueba el flujo con un contacto, como el botón «Probar flujo» de la aplicación. Con `dryRun: true` (por defecto) solo simula; con `dryRun: false` crea una ejecución de prueba real, marcada como prueba y fuera de las métricas. La prueba se hace siempre sobre un contacto, aunque el disparador sea de lead o de oportunidad.
- **`duplicate`** crea una copia en borrador con el nombre «X (copia)» o el que indiques.
- En el CRM, el paso `SEND_EMAIL` exige además `EMAIL_MARKETING_VIEW`, porque usa las plantillas de Marketing.
- `create_from_template` acepta `crm-lead-cooling-alert`, `crm-form-lead-welcome`, `crm-negotiation-follow-up-task` y `crm-unresponsive-lead-reassign`, que también devuelve `list_templates`.
- Lo que cambies por MCP se refleja solo en la página de automatizaciones del CRM que tengas abierta.

`marketing_automations` tiene las mismas operaciones de eficiencia: `validate` con `EMAIL_MARKETING_VIEW` y `marketing:automations:view`; `replace_steps` y `bulk_set_status` con `EMAIL_MARKETING_EDIT`, y `duplicate` con `EMAIL_MARKETING_CREATE`, las tres con `marketing:automations:manage`.

### Analítica y cualificación de leads

`crm_leads_stats` es de solo lectura. Pide `LEADS_VIEW` y el scope `crm:leads:view`, y cada persona agrega solo las filas que vería en un listado. El período se indica con `days` (de 1 a 366, 90 por defecto) y se calcula en la zona horaria de la organización.

| Operación | Qué devuelve |
| - | - |
| `funnel` | Cifras del embudo de lead a oportunidad y a cliente: leads nuevos, contactados, cualificados y convertidos, tasas de conversión y reparto por etapa. |
| `sources` | Conversión por `source`, por `utm_source`, por campaña y por formulario de captación. El valor estimado solo llega con `LEADS_VIEW_FINANCIALS`. |
| `response_time` | Tiempo hasta la primera respuesta: media, mediana, percentil 90, porcentaje sin respuesta y tramos (menos de 1, 4, 24 y 72 horas, y más). |

En `crm_leads`:

- **`set_temperature`** fija la temperatura del lead a mano (`temperature`) o la devuelve al cálculo automático (`auto: true`, incompatible con `temperature`). Fijarla marca `temperatureIsManual`, y la puntuación deja de recalcularla. Pide `LEADS_EDIT` y `crm:leads:update`.
- `list` acepta los filtros `temperature`, `scoreMin` y `scoreMax` (de 0 a 100), `source`, `industry`, `leadFormId` y `utmSource`.
- `get` incluye `profileScore`, `behaviorScore`, `temperatureIsManual` y `firstContactedAt`.

### Motivos de pérdida y analítica de oportunidades

- `crm_opportunities: list_loss_reasons` devuelve el catálogo de motivos de pérdida de la organización (`id`, `label`, `sortOrder`, `isSystem`, `isArchived`, `requiresCompetitor`, `requiresDetail` e `inUseCount`); con `includeArchived: true` incluye los archivados. Pide `OPPORTUNITIES_VIEW` o `LEADS_VIEW` y el scope `crm:opportunities:view` o `crm:leads:view`.
- `crm_opportunities_workflow: lose` acepta `lossReasonId`, que tiene preferencia sobre el texto libre `lostReason`: el servidor valida el motivo, aplica sus exigencias de competidor y detalle y copia su etiqueta a `lostReason`. Acepta también `postMortemNotes`, para lo aprendido.

`crm_opportunities_stats` tiene tres análisis más, con `period` (`30d`, `90d` o `365d`; `90d` por defecto) y el scope `crm:opportunities:view`:

| Operación | Qué devuelve | Permiso |
| - | - | - |
| `funnel` | Embudo por etapa de las oportunidades del período: cuántas alcanzaron cada etapa, conversión respecto a la anterior, tiempo en cada etapa (las reaperturas no inflan las cifras), desenlaces y tasa de cierre. Acepta `pipelineId`. | `OPPORTUNITIES_VIEW` |
| `loss_reasons` | Pérdidas del período: motivos más frecuentes, con una fila «OTHER», desglose por etapa (`byStage`) y por comercial (`byOwner`) y competidores más citados. Los importes solo llegan con `OPPORTUNITIES_VIEW_FINANCIALS`. | `OPPORTUNITIES_VIEW` |
| `by_owner` | Clasificación de comerciales: ganadas, perdidas, abandonadas, tasa de éxito y días típicos hasta el cierre. El valor ganado y el importe medio solo llegan con permiso financiero. | `OPPORTUNITIES_VIEW_TEAM_PIPELINE` |

Los nombres antiguos `analytics_loss_reasons` y `analytics_top_contributors` son operaciones obsoletas: ya no se anuncian en `tools/list`, pero siguen respondiendo con el mismo contrato que `loss_reasons` y `by_owner` hasta el 8 de enero de 2027, con un aviso en `_notices.deprecatedOperation` que nombra la sustituta.

### Contraseñas de aplicación de otra persona

La herramienta `observability` del servidor `utilia-crm` deja ver y retirar las contraseñas de aplicación con las que una persona de tu organización conecta la unidad de red del Gestor de archivos. Es lo mismo que el bloque **Contraseñas de aplicación** de su ficha en Ajustes › Usuarios.

| Operación | Qué hace | Permiso | Scope | `confirm` |
| - | - | - | - | - |
| `webdav_app_passwords_list_user` | Lista las contraseñas de aplicación vigentes de la persona `userId`: nombre, primeros caracteres, fecha de creación y último uso. Nunca devuelve la contraseña. | `SETTINGS_SESSIONS_VIEW` | `org:observability:view` | no |
| `webdav_app_passwords_revoke_user` | Retira una contraseña de aplicación de la persona (con `appPasswordId`) o todas (sin él). Deja de valer al momento y la persona recibe un aviso. | `SETTINGS_SESSIONS_REVOKE` | `org:observability:manage` | sí |
| `sessions_revoke_user` | Cierra todas las sesiones de una persona y retira también sus contraseñas de aplicación. | `SETTINGS_SESSIONS_REVOKE` | `org:observability:manage` | sí |

Las dos primeras exigen además el Gestor de archivos en el plan y el permiso concedido sobre toda la organización (alcance **Todo**). Quien no es Super Administrador solo ve y retira las de las personas que puede administrar: un Gerente con estos permisos, las de Vendedor, Asistente, Visualizador y Comercial Externo. Una persona de otra organización responde como si no existiera.

### Seguridad y privacidad de la organización

La herramienta `org_security_privacy` del servidor `utilia-crm` hace lo mismo que Ajustes › Seguridad y Ajustes › Privacidad, con los mismos permisos y las mismas reglas. Ningún conjunto concede sus scopes: se piden por su nombre.

Cada escritura exige `confirm: true` y la confirmación en el móvil, así que la persona necesita tener activada la verificación en dos pasos: la primera llamada devuelve `APPROVAL_REQUIRED` con el número que hay que elegir en la aplicación, y después se repite la misma llamada con `twoFactorApprovalId`. La confirmación solo vale para esa operación con esos mismos datos.

| Operaciones | Qué hacen | Quién | Scope |
| - | - | - | - |
| `security_get`, `security_preview_outbound_domains` | Leer los ajustes de seguridad y ver qué webhooks y rutas de envío quedarían fuera con una lista de dominios. | Super Administrador | `org:security:view` |
| `security_get_status` | El estado que ve toda la plantilla: modo restringido y bloqueo de pantalla. | Cualquiera | `org:security:view` o `org:settings:view` |
| `security_set_two_factor_required`, `security_set_two_factor_required_advisors`, `security_set_two_factor_email_fallback`, `security_set_inactivity_lock`, `security_set_export_rate_limit`, `security_set_restricted_mode`, `security_set_outbound_domains` | Los siete interruptores de la pantalla de Seguridad. `security_set_two_factor_required` exige la verificación en dos pasos a quien administra y `security_set_two_factor_required_advisors`, a la asesoría (activado de serie). | Super Administrador | `org:security:manage` |
| `retention_get`, `retention_update` | Plazos de conservación de datos. | `PRIVACY_RETENTION_MANAGE` | `org:privacy:view` / `org:privacy:manage` |
| `portal_activity_get`, `portal_activity_update` | Registro de actividad de los contactos en el portal. Exige el Portal del cliente en el plan. | `PRIVACY_RETENTION_MANAGE` | `org:privacy:view` / `org:privacy:manage` |
| `dsar_list`, `dsar_summary`, `dsar_get` | Bandeja de solicitudes de derechos de protección de datos. | `PRIVACY_RIGHTS_VIEW` | `org:privacy:view` |
| `dsar_create`, `dsar_require_identity_check`, `dsar_verify_identity`, `dsar_start`, `dsar_extend`, `dsar_execute_export`, `dsar_execute_erasure`, `dsar_complete`, `dsar_reject` | Tramitar una solicitud, incluida la supresión de los datos de la persona, que no se puede deshacer. | `PRIVACY_RIGHTS_MANAGE` | `org:privacy:manage` |
| `incidents_list`, `incidents_summary`, `incidents_provider_notices`, `incidents_get` | Registro de incidentes de seguridad de la organización y avisos del proveedor de la plataforma. | `PRIVACY_INCIDENTS_VIEW` | `org:privacy:view` |
| `incidents_create`, `incidents_update`, `incidents_assess`, `incidents_mark_aepd_notified`, `incidents_mark_subjects_notified`, `incidents_close` | Registrar, valorar, notificar y cerrar un incidente de seguridad. | `PRIVACY_INCIDENTS_MANAGE` | `org:privacy:manage` |
| `export_request`, `export_status` | Pedir la exportación de todos los datos de la organización y seguirla. Para pedirla hace falta la verificación en dos pasos; el archivo se descarga desde Ajustes › Organización › General. | Super Administrador | `org:privacy:export` (`export_status` también con `org:privacy:view`) |

Las respuestas no llevan nunca el enlace de descarga de una exportación: indican `downloadAvailable` cuando está lista.

## Finanzas (servidor utilia-finance)

El servidor `utilia-finance` (`POST https://os.utilia.ai/api/mcp/finance`,
nombre en el intercambio inicial: `UTILIA Finance MCP`) es el **propietario canónico
de la facturación, la tesorería y la fiscalidad** de UTILIA OS. Expone
**25 herramientas**: 11 de facturación, tesorería y fiscalidad (con
nombres y scopes `crm:invoices:*`, `crm:treasury:*` y `crm:fiscal:*`
heredados de su origen en el CRM) y 14 exclusivas con scopes `finance:*`.
La autorización OAuth usa el recurso `/api/mcp/finance` (ver
[Descubrimiento de OAuth](https://os.utilia.ai/docs/developers/oauth/discovery)) y los
scopes se documentan en
[Scopes y recursos](https://os.utilia.ai/docs/developers/oauth/scopes-and-resources).

Estas 11 herramientas se publicaban también en el servidor `utilia-crm`.
Desde el 8 de julio de 2026 esas copias están obsoletas: dejan de anunciarse en
el `tools/list` del servidor CRM, aunque siguen respondiendo a
`tools/call` con el mismo nombre, esquema y permisos hasta su retirada
definitiva (prevista para el 8 de enero de 2027). Para cualquier flujo financiero
conecta este servidor.

> **Nota: Próximamente: tesorería, informes, comisiones, impuestos y contabilidad**
>
> Desde el 14 de octubre de 2026 estas herramientas están en «Próximamente»: las
> estamos validando con especialistas antes de ponerlas a tu disposición, y hasta
> entonces no están disponibles para tu organización.
>
> - Tesorería: `treasury`.
> - Impuestos: `fiscal`, salvo `config_get`, `retention_rates_get` y
>   `retention_rates_update` (configuración fiscal y retenciones, que siguen
>   disponibles), y `fiscal_capture`.
> - Contabilidad: `accounting`, `accounting_journal`, `accounting_ledger`,
>   `accounting_assets`, `accounting_close` y `suppliers: accounting_accounts`.
> - Comisiones: `commissions` y `commissions_admin`.
> - Informes: `finance_reports`, salvo `summary` (el resumen de Finanzas).
>
> Vale igual para sus copias del servidor `utilia-crm`. Una llamada a cualquiera
> de ellas responde que la función estará disponible próximamente, con el mismo
> texto que la aplicación (por ejemplo, «El apartado Tesorería estará disponible
> próximamente.»). No es un bloqueo de plan ni de permisos: no se resuelve
> ampliando el plan ni concediendo scopes, así que no la reintentes. Las tablas
> de abajo describen su contrato para cuando estén disponibles.

### Facturación

| Herramienta | Operaciones principales | Scopes |
| - | - | - |
| `crm_invoices` (canónica) | `list`, `get`, `create`, `update`, `delete`, `duplicate`, `get_stats`, `get_next_number`, `previewTaxCoherence`. `create` admite `type: "SIMPLIFIED"` (factura simplificada, con su propia serie y destinatario opcional: sin datos sale a nombre de «Consumidor final») y `list` filtra por `type` | `crm:invoices:view` (lectura), `crm:invoices:create/update/delete` |
| `crm_invoices_workflow` | `issue`, `cancel`, `mark_paid`, `mark_partial_paid`, `cancelRefund`, `refund_manual` (devolución registrada fuera de la pasarela de UTILIA, ver más abajo); y el cobro con tarjeta: `charge`, `charge_confirm`, `refund_create`, `payment_intent_cancel`, `payment_intents_list` | `crm:invoices:issue`, `crm:invoices:cancel`, `crm:invoices:refund-cancel`; `refund_manual` y el cobro con `crm:invoices:update`, y la lectura de intentos con `crm:invoices:view`. Las cinco del cobro exigen además el aplicativo Pagos con tarjeta (Stripe) |
| `crm_invoices_rectifications` | `create_rectification`, `create_credit_note`, `update_legal_metadata` | `crm:invoices:create`, `crm:invoices:update` |
| `crm_invoices_pdf` | `generate`, `get_url`, `regenerate`, `preview`, `draft_preview`, `email_preview` (devuelven URL firmada, nunca binario) | `crm:invoices:view`, `crm:invoices:export` |
| `crm_invoices_email_templates` | CRUD de plantillas, `set_default`, `preview`, `variables_list` | `crm:invoices:view`, `crm:invoices:update` |
| `crm_invoices_scheduled_charges` | `list`, `get`, `get_logs`, `schedule`, `update`, `cancel`, `retry_now` | `crm:invoices:view`, `crm:invoices:update` |
| `crm_subscriptions` | CRUD, `pause`/`resume`/`cancel`/`reactivate`, `generate_now`, `upcoming_charge` (supervisión humana); `check_readiness` (comprobación previa del cobro, solo lectura); `create` admite `billingContact` | `crm:invoices:view` (lectura y `check_readiness`, que también admite `crm:invoices:update`), `crm:invoices:update`; `billingContact` exige además `finance:billing-contacts:manage` |
| `crm_mandates` | `list`, `get`, `list_for_client`, `link_mandate`, `revoke` (supervisión humana) | `crm:invoices:view`, `crm:invoices:update` |

#### Cobros recurrentes: comprobación previa y contacto de facturación

Un cobro recurrente con `automation: "AUTO"` emite y envía cada factura solo,
pero solo puede enviarla si el cliente tiene un contacto de facturación
principal y activo. Sin él, la factura se emite y el correo se omite. Por eso
`crm_subscriptions` comprueba antes de cada cobro si la factura podrá
emitirse, llegar al cliente y cobrarse, con las mismas reglas que la ficha del
cobro recurrente en la aplicación.

**`check_readiness`** (solo lectura, no guarda nada) tiene dos formas:

- Con `id`: comprueba una suscripción guardada. Exige `RECURRING_INVOICES_VIEW`
  sobre su cliente, como `get`.
- Con `clientId` y `automation` (`AUTO` o `DRAFT`), y opcionalmente
  `invoiceType` (`STANDARD` o `SIMPLIFIED`), `paymentMethod`, `bankIban` y
  `chargeMode` (`MANUAL` o `AUTO_STRIPE`): comprueba los datos de un alta
  ANTES de crearla. Exige poder crear cobros recurrentes para ese cliente
  (`RECURRING_INVOICES_CREATE` con su alcance).

Las dos van con el scope `crm:invoices:view` o con `crm:invoices:update` (para
quien solo puede crear cobros). Devuelven `readiness` y un
`nextStep` general:

| Campo | Qué contiene |
| - | - |
| `ready` | `true` si no hay ninguna incidencia. |
| `canIssue` | `true` si ninguna incidencia impide emitir la factura. |
| `issues[]` | Las incidencias, primero las que impiden emitir. Cada una con `code`, `severity`, `message`, `missingFields` (en las de datos fiscales), `suggestedEmails` (en `BILLING_CONTACT_MISSING`) y `nextStep`, la llamada exacta que la arregla. |
| `checkedAt` | Momento de la comprobación (ISO 8601). |

`severity` es `BLOCKING` cuando la factura no podrá emitirse y `WARNING`
cuando se emitirá, pero no se enviará, no se cobrará sola o le faltará un dato
de pago. Los códigos son:

| `code` | Gravedad | Qué falta | El `nextStep` propone |
| - | - | - | - |
| `ISSUER_DATA_INCOMPLETE` | `BLOCKING` | Datos fiscales de tu organización | `organization_settings` (`update_general`, `update_addresses`) del servidor `utilia-crm` |
| `CLIENT_FISCAL_DATA_INCOMPLETE` | `BLOCKING` | Nombre o razón social, dirección, ciudad o código postal del cliente, en facturas ordinarias | `crm_clients: update` del servidor `utilia-crm` |
| `CLIENT_TAX_ID_MISSING` | `WARNING` | NIF del cliente, en facturas ordinarias | `crm_clients: update` |
| `BILLING_CONTACT_MISSING` | `WARNING` | Contacto de facturación principal, con `automation: "AUTO"` | `billing_contacts: create` con `role: "PRIMARY"`; antes de crear, `data.billingContact` |
| `BILLING_CONTACTS_OPTED_OUT` | `WARNING` | Los contactos principales desactivaron el correo de facturas en su portal | Otro contacto con `billing_contacts: create` |
| `TRANSFER_IBAN_MISSING` | `WARNING` | IBAN para el pago por transferencia | `data.bankIban` de la suscripción o `finance_settings: update_billing` |
| `AUTO_CHARGE_NOT_AUTHORIZED` | `WARNING` | Autorización del cliente para el cobro con tarjeta | `crm_mandates: request_signature` (correo real al cliente) o `link_mandate` |
| `AUTO_CHARGE_DISABLED` | `WARNING` | Cobro automático activado en la organización | `finance_settings: update_finance_comms` o pasar el cobro a `MANUAL` |

Los `nextStep` nunca inventan datos: lo que tiene que confirmar la persona va
como marcador entre ángulos (por ejemplo, `'<NIF>'`). El único valor real que
proponen es el primero de `suggestedEmails`, los correos que se pueden dar de
alta como contacto de facturación: el de la ficha del cliente, si el usuario
tiene `CLIENTS_VIEW`, y los de sus contactos del CRM, si tiene
`CONTACTS_VIEW`, cada uno con su alcance. Es una sugerencia: confírmala con
la persona antes de usarla.

**`billingContact` en `create`.** `data.billingContact` (`email` y `name`
opcional) garantiza, antes de generar la primera factura y en la misma
transacción que la suscripción, un contacto de facturación principal y activo
del cliente con ese correo: lo crea o, si ya existía, lo reactiva y lo pasa a
principal. Es la doble puerta de siempre:

- El usuario necesita `BILLING_CONTACTS_MANAGE`. Sin él, la respuesta es un
  `403` con `errorCode: "BILLING_CONTACTS_MANAGE_REQUIRED"` y no se crea nada.
- El token necesita `finance:billing-contacts:manage` (también lo concede
  `finance:full`; en el servidor `utilia-crm`, donde sigue la copia obsoleta,
  vale `crm:clients:update`). Sin él, la respuesta es `INSUFFICIENT_SCOPE`
  antes de crear nada. `crm:invoices:update` permite crear el cobro, no el
  contacto, y `finance:write` tampoco lo concede.

**Lo que devuelven las demás operaciones:**

- `create` y `update` devuelven `readiness` (con el `nextStep` de cada
  incidencia; `null` si no se pudo calcular) y `warnings`, lo que se guardó
  distinto de lo pedido. Por ejemplo, el cobro con tarjeta guardado como
  manual porque la organización tiene apagado el cobro automático, o un
  contacto de facturación que ya existía y se ha reactivado como principal.
- El `nextStep` de `create` dice qué hacer después. Si la primera factura se
  emitió pero el correo se omitió por falta de contacto, indica el orden:
  primero `billing_contacts: create` y después
  `crm_invoices_workflow: send` con `confirm: true`. Enviarla antes
  volvería a omitir el correo.
- `get` devuelve `readiness` si la suscripción está `ACTIVE` o `PAUSED`, y
  `null` en las demás.
- `list` devuelve `readinessIssueCodes` en cada fila: los códigos de la
  comprobación de las suscripciones `ACTIVE` o `PAUSED`, calculados por lote.
  Lista vacía si no falta nada o si la suscripción ya no factura.

Flujo recomendado para un alta con `automation: "AUTO"`:

```json
{ "operation": "check_readiness", "clientId": "cli_123", "automation": "AUTO" }
```

Si devuelve `BILLING_CONTACT_MISSING`, confirma el correo con la persona y
pásalo en el alta:

```json
{
  "operation": "create",
  "data": {
    "name": "Mantenimiento web",
    "clientId": "cli_123",
    "automation": "AUTO",
    "startDate": "2026-11-01",
    "lines": [{ "name": "Cuota mensual", "unitPrice": 65, "taxType": "IGIC_7" }],
    "billingContact": { "email": "administracion@cliente.es" }
  }
}
```

La misma comprobación está en la API REST interna:
`GET /api/finance/recurring-invoices/:id/readiness` y
`POST /api/finance/recurring-invoices/readiness`, y el listado
`GET /api/finance/recurring-invoices` trae `readinessIssueCodes` en cada fila.

#### Campos fiscales de las líneas (IGIC: exención y comerciante minorista)

Las líneas de `crm_invoices` (`create`/`update`) y de los presupuestos
(`crm_budgets`, servidor `utilia-crm`) aceptan campos fiscales adicionales:

- `taxType`: en el sistema IGIC admite, además de los tipos con porcentaje,
  los valores `EXEMPT` (exenta) y `NOT_SUBJECT` (no sujeta).
- `igicOperationType`: clasificación de la línea en el régimen especial del
  comerciante minorista del IGIC. En ventas: `IGIC_RETAIL` (venta minorista
  exenta; exige `taxType: "EXEMPT"` y la causa minorista la aplica el motor
  por defecto) o `IGIC_GENERAL`. Exige que la organización tenga el régimen
  declarado en su configuración fiscal (`igicRetailerMode` distinto de
  `IGIC_RETAILER_NONE`).
- `exemptionReason`: causa estructurada de la exención, obligatoria con
  `taxType: "EXEMPT"` en IGIC salvo en el régimen del pequeño empresario.
  Valores: `EXEMPT_IGIC_RETAILER`, `EXEMPT_IGIC_INTERNAL`, `EXEMPT_EXPORT`,
  `EXEMPT_OTHER` y `EXEMPT_IGIC_SMALL_BUSINESS` (régimen del pequeño
  empresario del IGIC, con la mención «Exención franquicia fiscal»). La
  causa decide la mención legal que se imprime en la factura. Dentro de un
  periodo del régimen del pequeño empresario, el motor pone esa causa en
  las líneas exentas sin causa de un documento IGIC y rechaza las líneas
  IGIC con cuota, también `IGIC_0`. El estado del régimen se consulta con
  `finance_settings: get_small_business_regime`.
- `exemptionNote`: texto explicativo de la exención (máximo 300 caracteres),
  obligatorio con `exemptionReason: "EXEMPT_OTHER"`; viaja a la mención
  legal impresa.

En presupuestos, estos campos se propagan a la factura o a la suscripción al
convertir. Las líneas de gasto (`expenses`) aceptan también
`igicOperationType` con un tercer valor propio, `IGIC_COMMON` (gasto común a
las dos actividades); en un gasto clasificado como `IGIC_RETAIL` la cuota de
IGIC soportada no es deducible.

### Tesorería y fiscalidad

| Herramienta | Operaciones principales | Scopes |
| - | - | - |
| `treasury` (canónica) | **Próximamente.** Lecturas: `cash_position`, `accounts_list`, `transactions_list`, `reconcile_suggestions`, `forecast`, `aging_report`, `cash_flow_report`, `alerts_list`, `connection_status`, `sync_logs`, `import_batches_list`, `transactions_export`. Escrituras (todas exigen `confirm: true`): cuentas (`account_create/update/delete/set_default/activate/deactivate`), movimientos manuales (`transaction_create/update/delete`; los importados del banco son inmutables), importaciones (`import_csv`, `import_norma43`, `import_batch_revert`), conciliación (`transaction_reconcile/unmatch/ratify/revert_reconciliation/exclude`), reglas (`reconciliation_rule_*`), asiento sin factura (`transaction_create_journal_entry`), `force_sync` y recurrentes de la previsión (`recurring_pattern_*`). Convención de signo: el importe de ingresos y gastos se guarda siempre en positivo (el tipo marca la dirección); solo las transferencias llevan signo (positivo entra en la cuenta, negativo sale). No mueve dinero ni conecta/desconecta bancos. | `crm:treasury:view`, `crm:treasury:reconcile`, `crm:treasury:sync`, `crm:treasury:export` |
| `fiscal_capture` | **Próximamente.** Insumos: `adjustments_*`, `modules_*`, `attributions_*`, `foreign_assets_*`, `irpf_activity_get`; rectificativas recibidas: `expense_rectification_get/save/save_many`. Bajas con `confirm` y supervisión humana. | `crm:fiscal:view/manage`; las rectificativas requieren además los scopes de gastos indicados abajo |
| `fiscal` (canónica) | **Próximamente**, salvo `config_get` y `retention_rates_get/update`. `calendar`, `pending`, `models_list`, `calculate`, `mark_filed`, `manual_file`, `pay`, `periods_*`, `coherence`, `catalog`, `config_get`, `alerts_list`, `vat_registry(_export)`, `official_file/pdf` (el fichero sigue el diseño del organismo del modelo: AEAT para los estatales, Agencia Tributaria Canaria para el 415), `investment_goods_list`, `investment_good_get/create/update/prorrata/dispose/delete`, `retention_rates_get/update`, `sii_batches_list/batch_download` | `crm:fiscal:view`, `crm:fiscal:calculate`, `crm:fiscal:file`, `crm:fiscal:manage` (bienes de inversión escritura y `retention_rates_update` exigen `crm:fiscal:manage`; `dispose`/`delete`/`retention_rates_update` con supervisión humana) |

#### Captura de rectificativas recibidas

`fiscal_capture` permite consultar y guardar los hechos fiscales de una
rectificativa recibida asociada a un gasto. La captura conserva la referencia
original, la causa, las fechas, la elección de período y las pruebas de deducción.
No modifica los importes del gasto ni el documento original, no genera pagos o
asientos y no presenta declaraciones.

| Operación | Argumentos | Permisos del usuario (todos) | Scopes del token (todos) |
| - | - | - | - |
| `expense_rectification_get` | `expenseId` | `TAX_MODELS_VIEW` + `EXPENSES_VIEW` | `crm:fiscal:view` + `finance:expenses:view` |
| `expense_rectification_save` | `expenseId`, `input` | `TAX_MODELS_CREATE` + `EXPENSES_VIEW` + `EXPENSES_EDIT` | `crm:fiscal:manage` + `finance:expenses:view` + `finance:expenses:update` |
| `expense_rectification_save_many` | `items`: 1–25 elementos con `expenseId` e `input` | Los mismos que el guardado individual, en cada elemento | Los mismos que el guardado individual |

Se requieren el aplicativo `finance` y la capacidad `finance.tax_models`. Mientras Impuestos esté en «Próximamente», `fiscal_capture` no está disponible para tu organización.
El servicio comprueba los permisos y el alcance de cada gasto y referencia;
ser Super Administrador no sustituye los scopes del token ni el plan. Las listas
OR del catálogo no reemplazan estas comprobaciones conjuntas. No se ha añadido
una excepción para tokens antiguos. `finance:read` y `finance:write` por sí
solos no conceden `crm:fiscal:manage`; solicita los scopes necesarios.

El SDK usa `POST /api/mcp/finance` con el token OAuth autorizado para ese
servidor. No hay ruta REST externa equivalente.

Ejemplo de `input` sin confirmar, conservado para revisión:

```json
{
  "expectedRevision": null,
  "cause": "RETURN",
  "causeOccurredOn": null,
  "causeKnownOn": null,
  "original": { "expenseId": "expense-original" },
  "amountRepresentation": "DIFFERENCE_SIGNED",
  "settlement": {
    "route": "UNRESOLVED",
    "targetFiscalPeriodId": null,
    "reimbursementEvidenceIds": []
  },
  "evidenceFileIds": [],
  "confirm": false
}
```

Envía `expectedRevision: null` en la primera captura; para editar, consulta
la captura y usa su revisión. Las fechas y referencias que admiten `null`
deben enviarse explícitamente: omitirlas no equivale a desconocerlas.
`confirm` es un booleano y no acepta `"false"` como texto. Confirmar requiere
fechas reales y hechos explícitos; no acredita automáticamente su efecto fiscal.

`original` acepta exactamente una referencia: `{ expenseId }` o
`{ external: { number, issuedOn, supplierTaxId, evidenceFileId } }`.
Una referencia externa puede guardarse para revisión; no prueba por sí sola
que el cálculo esté cubierto. Los archivos deben pertenecer a la organización
y ser accesibles al actor. No se aceptan `invoiceId`, autoría, organización,
sellos ni importes del gasto en esta captura recibida.

`deduction` es opcional; si lo envías debe contener `originalUse`,
`originalFiscalPeriodId`, `evidenceFileIds`, `previousAdjustmentReferences` y
`allocations`. Cada asignación lleva `originalTaxId`, `previouslyDeducted` y
`deductibleAfterCorrection`, con importes decimales como cadenas, por ejemplo
`"210.00"`. No deduzcas estos hechos de la deducibilidad actual del gasto.
En recibidas, `reimbursementEvidenceIds` debe estar vacío.

Consulta y guardado devuelven la misma vista:
`{ capture: { id, revision, confirmedAt } | null, input: objeto | null,
status: "COMPLETE" | "REVIEW_REQUIRED", reasons: string[] }`.
Sin captura, `capture` e `input` son `null`. Revisa siempre `status` y
`reasons`: conservar los hechos no habilita todas las ramas fiscales ni
supone que una declaración esté lista para exportar.

En JavaScript, usando como `input` el objeto anterior:

```typescript
const { data: actual } = await sdk.mcp.fiscalCapture.getExpenseRectification(expenseId);
const { data: guardada } = await sdk.mcp.fiscalCapture.saveExpenseRectification(
  expenseId, { ...input, expectedRevision: actual.capture?.revision ?? null },
);
const { data: lote } = await sdk.mcp.fiscalCapture.saveExpenseRectifications([
  { expenseId, input: { ...input, expectedRevision: guardada.capture?.revision ?? null } },
]);
```

En Python, `entrada` es el diccionario del ejemplo; los mismos métodos
están disponibles sin `await` en `UtiliaSDKSync`:

```python
from utilia_sdk.mcp.fiscal_capture_types import ExpenseRectificationInput, ExpenseRectificationBatchItem

actual = await sdk.mcp.fiscal_capture.get_expense_rectification(expense_id)
entrada["expectedRevision"] = actual.capture.revision if actual.capture else None
guardada = await sdk.mcp.fiscal_capture.save_expense_rectification(
    expense_id, ExpenseRectificationInput.model_validate(entrada),
)
lote = await sdk.mcp.fiscal_capture.save_expense_rectifications([
    ExpenseRectificationBatchItem(expense_id=expense_id, input=ExpenseRectificationInput.model_validate(entrada)),
])
```

El lote es secuencial y parcial: un fallo no revierte los elementos guardados
ni impide procesar los siguientes. Devuelve `succeeded`, `failed` y
`summary: { total, ok, failed }`. Cada éxito lleva `index` y
`data: { expenseId, id, revision, status, reasons }`; cada fallo lleva
`index`, `code` y `message`. Usa el índice para identificar la entrada que
falló. El resumen no incluye hechos externos, fotografías de datos ni historial.

Para un reintento, conserva los mismos hechos y `expectedRevision`.
Cada intento vuelve a comprobar el acceso y la equivalencia en el servicio;
no hay atomicidad del lote ni una cola o recibo adicional. Un cambio de hechos,
revisión o contexto fiscal puede dar conflicto y exigir una nueva consulta.
Los errores fiscales estables son `VAT_RECTIFICATION_CAPTURE_INVALID`,
`VAT_RECTIFICATION_CAPTURE_CONFLICT`, `VAT_RECTIFICATION_EFFECTS_REVIEW_REQUIRED`
y `VAT_RECTIFICATION_PERIOD_LOCKED`. En llamadas individuales viajan en
`errorCode` y en `error.code`; el SDK JS conserva `errorCode` y Python
`error_code`. En el lote viajan en `failed[].code`. Los fallos de acceso usan
`INSUFFICIENT_SCOPE`, `FORBIDDEN` o `NOT_FOUND`, sin datos de otra organización.

### Contabilidad

| Herramienta | Operaciones principales | Scopes |
| - | - | - |
| `accounting` (canónica) | **Próximamente.** `overview`, `kpis`, `alerts`, `setup`, plan de cuentas (`accounts_*`, `seed_pgc`), ejercicios (`fiscal_year*`), unidades de negocio (`business_units_list`, `business_unit_create/update/delete`) | `finance:accounting:view/create/update/delete` |
| `accounting_journal` | **Próximamente.** Asientos (`list`, `create`, `update`, `delete`), plantillas, `contabilize_invoice/expense/payment`, `post`, `cancel`, `reassign_business_unit` (reimputa la unidad de negocio de líneas de asientos contabilizados, sin tocar cuentas ni importes) | `finance:accounting:*` + `finance:accounting:post` para `post`/`cancel`; `reassign_business_unit` con `finance:accounting:update` |
| `accounting_ledger` | **Próximamente.** `account_ledger`, `trial_balance`, `balance_sheet`, `income_statement`, `export_account`, `business_unit_report` (Resultado por unidad de negocio: modos `result`/`full`, nivel `unit`/`parent`, fila «Sin asignar») | `finance:accounting:view`, `finance:accounting:export` |
| `accounting_assets` | **Próximamente.** Activos CRUD, `dispose`, `depreciation_generate/post/reverse` | `finance:accounting:*` + `finance:accounting:post` para dotaciones |
| `accounting_close` | **Próximamente.** `close_preview`, `close`, `reopen`, cuentas anuales y capturas, legalización (`legalization_get/export`) | `finance:accounting:close`, `finance:accounting:export` |

### Gastos, proveedores, pagos e informes

| Herramienta | Operaciones principales | Scopes |
| - | - | - |
| `expenses` (canónica) | CRUD, `stats`, `export`, `submit`, `approve`, `reject`, `pay`, `revert_to_draft`, `bulk_submit/approve/pay/delete` | `finance:expenses:view/create/update/delete` + `finance:expenses:approve` |
| `suppliers` | CRUD, `create_many`, `update_many`, `search_by_tax_id`, `expense_summary`, `check_duplicate`, `deduplicate_preview`, `deduplicate`, `merge` | `finance:suppliers:view/create/update/delete` (los lotes, el de su operación suelta; `merge`, el de borrado) |
| `suppliers` (personas de contacto) | `list_contacts`, `get_contact`, `contact_stats`, `create_contact`, `update_contact`, `delete_contact` | `finance:supplier-contacts:view/manage` (datos personales de terceros: fuera de `finance:read` y `finance:write`) |
| `suppliers` (actividad) | `list_interactions`, `get_interaction`, `log_interaction`, `log_interactions_many`, `update_interaction`, `delete_interaction`, `get_interaction_thread`, `list_interaction_activities`, `list_interaction_attachments`, `attach_to_interaction`, `remove_interaction_attachment`, `schedule_meeting`, `complete_meeting`, `cancel_meeting` | `finance:suppliers:interactions:read/manage` (contenido de correos: fuera de `finance:read` y `finance:write`) |
| `suppliers` (vínculos) | `list_tasks`, `list_payments`, `get_counterpart` | El scope del recurso consultado: `crm:tasks:view`, `finance:payments:view` y `crm:clients:view`. El de documentos sí está incluido en `finance:read` y `finance:full`; los dos del CRM requieren autorización aparte |
| `finance_payments` (canónica) | `list` (acepta `supplierId`), `get`, `stats`, `logs`, `check_duplicate`, `upload`, `update`, `delete`, `restore`, `promote_to_expense`, `resolve_duplicate`, `reprocess`, `export` | `finance:payments:view/update/delete` + `finance:payments:reprocess/export`; `upload` (subir un justificante ya guardado en el gestor de archivos) acepta `finance:payments:create` o `finance:payments:update`; crear un gasto desde un documento exige además los scopes de gastos indicados abajo |
| `finance_reports` (canónica, solo lectura) | `summary` (el resumen de Finanzas del periodo: ingresos, gastos, resultado, pendiente de cobro y de pago e impuestos). **Próximamente**: `cash_flow_forecast`, `burn_rate`, `recurring_patterns`, `aging`, `vat_summary`, `profitability` y `export` (balance, pérdidas y ganancias o flujo de efectivo) | `finance:reports:view` |

**Crear un gasto desde un documento digitalizado exige varios scopes a la vez.**
`promote_to_expense` requiere `finance:payments:view`,
`finance:expenses:create` y `finance:expenses:approve`.
La conversión solicita un gasto aprobado; si el documento está en otra divisa,
el gasto queda en borrador hasta confirmar su importe en euros. No registra
un pago al proveedor.

En `resolve_duplicate`, `action: 'keep_both'` conserva el documento como una
factura distinta y crea el gasto con la misma regla. Exige los tres scopes
anteriores y `finance:payments:update`. `action: 'discard'` exige
`finance:payments:view`, `finance:payments:update` y `finance:payments:delete`.
Ambas acciones requieren `confirm: true`. Estos scopes se suman a los permisos
y al alcance del usuario y al acceso de su organización a Finanzas.
Los conjuntos `finance:read` y `finance:write` no conceden el scope de aprobación.

**Los vínculos del proveedor exigen el scope del otro lado.** `list_tasks`
devuelve tareas del CRM, `list_payments` documentos de Finanzas y
`get_counterpart` la identidad de un cliente, así que un token de proveedores
no basta: hace falta además el scope de aquello que se lee. La conjunción sale
sola, porque los scopes que hacen VISIBLE la herramienta siguen siendo solo de
Finanzas. `list_tasks` exige también el aplicativo CRM en el plan, no solo
Finanzas. `get_counterpart` exige los CUATRO permisos de los dos lados
(`CLIENTS_VIEW`, `SUPPLIERS_VIEW` y los dos de datos sensibles) y devuelve
`null` sin decir por qué: el mismo `null` cubre «no tiene NIF», «ninguno casa»
y «queda fuera de tu alcance», porque distinguirlos convertiría la operación en
un comprobador de NIF ajeno. En `list_payments` y en `finance_payments` con
`supplierId`, el NIF leído del documento (`supplierNif`) solo viaja con
`SUPPLIERS_VIEW_SENSITIVE`: sin ese permiso la clave no existe en la fila.

### Cobro con tarjeta, comisiones y ajustes

| Herramienta | Operaciones principales | Scopes |
| - | - | - |
| `billing_contacts` | CRUD de destinatarios de facturación (`list`, `list_by_client`, `get`, `create`, `update`, `delete`) | `finance:billing-contacts:view/manage` |
| `payment_methods` | `list`, `stripe_readiness`, `setup_intent`, `set_default`, `delete` | `finance:payment-methods:view/manage`. **Toda la herramienta exige Finanzas y Pagos con tarjeta (Stripe)** |
| `commissions` | **Próximamente.** `list`, `by_agent`, `by_invoice`, `summary`, `profit_analysis`, `approve`, `cancel` | `finance:commissions:view/approve` |
| `commissions_admin` | **Próximamente.** Reglas y agentes CRUD, `assign_client`/`unassign_client`, liquidaciones (`settlement_generate/approve/pay`) | `finance:commissions:view/manage/liquidate` |
| `finance_settings` | `get_fiscal`, `get_billing`, `get_regime`, `get_finance_comms`, `update_billing`, `update_regime` (configuración fiscal completa: `taxpayerType`, `cashCriterionEnabled`, recargo de equivalencia, `igicRetailerMode` (régimen especial del comerciante minorista del IGIC: `IGIC_RETAILER_NONE`/`IGIC_RETAILER_MIXED`/`IGIC_RETAILER_PURE`; distinto de `NONE` exige régimen canario y es incompatible con el recargo de equivalencia), `siiEnabled` + `siiEnrollmentReason`, `invoicesToPublicAdmin`, `analyticAccountsByCounterparty`), `update_fiscal`, `update_finance_comms` (cobro automático y avisos al cliente), `get_stripe` (estado de los pagos con tarjeta, **solo lectura**); y el régimen del pequeño empresario del IGIC: `get_small_business_regime` (lectura), `open_small_business_period` y `update_small_business_period` (con `confirm: true`) | `finance:settings:view/update` |

> **Atención: El cobro con tarjeta exige su propio aplicativo**
>
> Además del scope del token y del permiso del usuario, estas operaciones
> exigen que la organización tenga contratado el aplicativo **«Pagos con
> tarjeta (Stripe)»**, que a su vez exige **Finanzas**. Sin él la respuesta
> es un error de aplicativo no habilitado, y no lo arregla ningún permiso ni
> ningún scope. Alcanza a la herramienta `payment_methods` entera, a
> `crm_invoices_workflow` en `charge`, `charge_confirm`, `refund_create`,
> `payment_intent_cancel` y `payment_intents_list`, a
> `crm_clients_info: stripe_readiness` y a `finance_settings: get_stripe`.
>
> **No lo exigen, y es deliberado**: `refund_manual` (la devolución
> registrada fuera de la pasarela), `refunds_list` y
> `cancel_refund_preview` (que listan también las manuales), y `mark_paid`
> y `mark_partial_paid` (el registro manual de un cobro). Registrar una
> devolución en efectivo no necesita pasarela y exigirla sería falso.

`refund_manual` registra una devolución hecha fuera de la pasarela de
UTILIA OS. Su `source` dice por dónde: `MANUAL_BANK` (transferencia),
`MANUAL_CASH` (efectivo) o `EXTERNAL_GATEWAY` («Pasarela de pago
externa»: la devolución ya se hizo en una pasarela ajena a UTILIA OS, por
ejemplo la cuenta de Stripe propia de la empresa). Con
`EXTERNAL_GATEWAY`:

- UTILIA OS no mueve dinero: solo registra la devolución.
- `referenceNumber` es obligatorio: el identificador de la devolución en esa pasarela (por ejemplo, `re_...`), hasta 120 caracteres sin espacios, que no puede repetirse en la organización.
- Solo se puede devolver lo que se cobró **fuera** de la pasarela de UTILIA OS. Lo cobrado con ella se devuelve con `refund_create`.
- `method` dice cómo devolvió el dinero la pasarela externa (`CARD`, por defecto, `TRANSFER` o `CASH`) y decide la cuenta de tesorería del asiento.

Con `createRectifyingInvoice: true`, tanto `refund_create` como
`refund_manual` emiten además la rectificativa por diferencias. El código
lo decide el servidor: R1 para la ordinaria y la simplificada con
destinatario identificado, R5 para la simplificada sin destinatario. Si
no se puede calcular exacta al céntimo, el reembolso se rechaza sin
registrar nada.

El régimen especial del pequeño empresario o profesional (REPEP) del
IGIC se gestiona con tres operaciones de `finance_settings`:

| Operación | Qué hace | Permiso del usuario |
| - | - | - |
| `get_small_business_regime` | Si el régimen está vigente hoy, el periodo actual y el historial, si la organización puede declararlo y por qué no, el umbral de volumen del año y del siguiente, y los trimestres en los que no se presenta el modelo 420 | El de lectura de la pantalla fiscal (facturas, gastos, presupuestos o `TAX_MODELS_VIEW`) |
| `open_small_business_period` | Declara un periodo con `startsOn`, `endsOn` opcional, `entryReason`, `endReason` y `notes`. Exige `confirm: true` | `TAX_MODELS_CREATE` |
| `update_small_business_period` | Cierra o corrige el periodo `periodId`: para cerrarlo, `endsOn` y `endReason`; `endsOn: null` lo deja vigente. Exige `confirm: true` | `TAX_MODELS_CREATE` |

Solo pueden declararlo los autónomos con un régimen fiscal canario. Los
periodos no se pueden solapar ni contradecir ventas IGIC ya emitidas
(eso se corrige con rectificativas), y las escrituras devuelven avisos no
bloqueantes. UTILIA OS nunca abre, cierra ni mueve un periodo por su
cuenta: avisa, y lo decide la persona.

`finance_settings: get_stripe` es de solo lectura. Devuelve si la pasarela está disponible, si la organización puede cobrar
y por qué no, el estado de la cuenta conectada, su país y su divisa, y
los requisitos que Stripe tiene pendientes, **ya traducidos al
castellano**. Con eso el copiloto puede responder «tu cuenta está
conectada pero le falta este documento», que antes no podía responder en
absoluto.

Lo que no hay, y no es un olvido: **ni alta ni desconexión por MCP**. El
alta exige que una persona teclee en un formulario alojado por Stripe con
verificación de identidad, así que ofrecerla sería prometer lo que no hay.
Y la desconexión es en la práctica irreversible: rehacerla obliga a repetir
el alta entera, y eso no debe estar a un mensaje de distancia. Es el mismo
criterio de `payment_methods: setup_intent`.

Divergencia de permiso dentro de la herramienta: `get_stripe` acepta
**configurar los pagos con tarjeta o verlos**, en lugar del permiso de
ajustes de Finanzas que gobierna al resto de sus operaciones.

La facturación electrónica (Facturae, UBL y envíos a FACe) está retirada desde el 6 de julio de 2026: la herramienta `invoices_einvoicing` no se publica en `tools/list` y, si se invoca por su nombre, responde que no está disponible.

### Operaciones bajo supervisión humana

Estas operaciones están anotadas con `supervisedByHuman` en el catálogo:
el cliente MCP debe pedir confirmación expresa al usuario antes de
ejecutarlas. Los scopes del token limitan el dominio finance operación a
operación: el servidor comprueba en cada llamada que el token porta el
scope granular de la operación y la deniega con un error explícito si no
lo lleva. La autorización final exige además que el usuario dueño del
token tenga el permiso correspondiente en UTILIA OS: scope y permiso son
condiciones independientes y ambas deben cumplirse. Las operaciones de
esta tabla exigen scopes que quedan fuera de los conjuntos `finance:read` y
`finance:write` (las acciones `post`, `close`, `approve`, `delete`,
`manage`, `reprocess`, `submit` y `liquidate` hay que concederlas
explícitamente o usar `finance:full`).

| Herramienta | Operaciones supervisadas | Scope que las habilita |
| - | - | - |
| `accounting_journal` | **Próximamente.** `post`, `cancel` (contabilizar y anular asientos); `reassign_business_unit` (reimputa la unidad de negocio de líneas ya contabilizadas) | `finance:accounting:post`; `reassign_business_unit` con `finance:accounting:update` |
| `accounting_assets` | **Próximamente.** `dispose`, `depreciation_post`, `depreciation_reverse` | `finance:accounting:post` |
| `accounting_close` | **Próximamente.** `close`, `reopen` (cierre y reapertura del ejercicio) | `finance:accounting:close` |
| `expenses` | `approve`, `reject`, `bulk_approve` (`finance:expenses:approve`); `bulk_delete` (`finance:expenses:delete`); `pay`, `bulk_submit`, `bulk_pay` (`finance:expenses:update`) | ver detalle por operación |
| `finance_payments` | `reprocess` (reanaliza el documento con IA) y `delete`/`restore` | `finance:payments:reprocess`, `finance:payments:delete` |
| `payment_methods` | `setup_intent`, `set_default`, `delete` | `finance:payment-methods:manage` |
| `commissions` | **Próximamente.** `approve`, `cancel` | `finance:commissions:approve` |
| `commissions_admin` | **Próximamente.** `settlement_generate`, `settlement_approve`, `settlement_pay` | `finance:commissions:liquidate` |
| `fiscal` | **Próximamente**, salvo `retention_rates_update`. `mark_filed`, `manual_file`, `pay`, `period_close`, `period_reopen` (`crm:fiscal:file`); `investment_good_dispose`, `investment_good_delete`, `retention_rates_update` (`crm:fiscal:manage`) | ver detalle por operación |
| `crm_subscriptions`, `crm_mandates` | las escrituras de las dos herramientas (cobros recurrentes y autorizaciones de cobro); sus lecturas van con `crm:invoices:view`, y `check_readiness`, con `crm:invoices:view` o `crm:invoices:update` | `crm:invoices:update` |

Las filas marcadas con **Próximamente** son de funciones en «Próximamente»: ver el aviso del principio de esta sección.

Dos operaciones mueven dinero real: `crm_invoices_workflow: charge`, que cobra con la tarjeta guardada del cliente, y `refund_create`, que devuelve por la pasarela. Las dos exigen `confirm: true`, el aplicativo de pagos con tarjeta y su permiso (`INVOICES_CHARGE` para cobrar, `STRIPE_PAYMENTS_REFUND` para devolver). Fuera de ellas, ninguna herramienta de este servidor mueve dinero ni desconecta bancos. Los presupuestos (`crm_budgets*`) son un documento comercial previo a la venta y viven solo en el servidor `utilia-crm`.

## RRHH: festivos y calendario laboral

La herramienta `attendance` del servidor `utilia-rrhh` (`POST https://os.utilia.ai/api/mcp/rrhh`) decide qué festivos cuentan para tu empresa a partir de su ámbito territorial. Las mismas operaciones siguen publicadas por compatibilidad en `workplace`, del servidor `utilia-crm`, con el prefijo `attendance_`.

Un festivo cuenta para la empresa si su clave de ámbito (`scopeKey`) está en la lista de la empresa. Sin comunidad autónoma elegida, esa lista es `['ES', 'COMPANY']`: solo cuentan los festivos nacionales y los propios de la empresa.

| Operación | Qué hace | Scope |
| - | - | - |
| `holidays_scope_get` | Comunidad autónoma, isla y municipio de la empresa, las claves de ámbito que salen de ahí y los valores admitidos para elegirlos. | `hr:attendance:view` |
| `holidays_set_scope` | Fija el ámbito. `region` es obligatoria, en formato ISO 3166-2; `island` solo vale con `ES-CN`; `locality` es el municipio tal y como se escribe. | `hr:attendance:manage` |
| `holidays_import` | Carga el calendario oficial de un año dentro del ámbito de la empresa. Es idempotente y devuelve `created`, `skipped` y `total`. | `hr:attendance:manage` |
| `holidays_list` con `applicable` | `true`: solo los festivos que cuentan para la empresa. `false`: solo los de otros ámbitos. Sin el filtro: todos. | `hr:attendance:view` |

Las cuatro exigen además el permiso `ATTENDANCE_MANAGE_HOLIDAYS` y el aplicativo RRHH contratado. Cada festivo devuelve `scopeKey`, `scopeLabel` (su rótulo en castellano) y `applicable`.

```json
{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "tools/call",
  "params": {
    "name": "attendance",
    "arguments": {
      "operation": "holidays_set_scope",
      "region": "ES-CN",
      "island": "GC",
      "locality": "Las Palmas de Gran Canaria"
    }
  }
}
```

Lo que conviene saber:

- **Los festivos municipales no se cargan**: los publica cada ayuntamiento y no hay una fuente que los cubra. `holidays_import` lo dice en el campo `notice` de su respuesta; se añaden a mano con `holidays_create`.
- **El ámbito no se pasa al cargar**: sale del que tiene declarado la empresa.
- **Una isla fuera de Canarias se rechaza** con un error `400`.
- **Elegir el ámbito y cargar el año son escrituras**: cambian qué días descuentan jornada a toda la plantilla, así que un token de solo lectura no puede hacerlas.
- **Quitar los festivos de otros ámbitos no se puede hacer por MCP.** Es un borrado irreversible y solo está en Ajustes › RRHH › [Festivos](https://os.utilia.ai/dashboard/settings/attendance/holidays), con vista previa y confirmación.

En los SDK, la superficie es `sdk.mcp.attendance.holidays`: `getScope()`, `setScope()`, `import()` y `list()` en JavaScript, y `get_scope()`, `set_scope()`, `import_year()` y `list()` en Python.

## Cambios de contrato de septiembre de 2026

> **Atención: Si tu integración es anterior al 2 de septiembre de 2026, revisa esta sección**
>
> Esos días cambió el contrato de varias herramientas: algunas llamadas que funcionaban dejan de hacerlo y algunos datos que viajaban por defecto dejan de viajar.

### Los conjuntos de escritura ya no borran en el gestor de archivos

`dev:write` y `crm:write` dejan de poder eliminar archivos, versiones y carpetas: para borrar hace falta `dev:files:full` o `crm:files:delete`. Ninguno de los dos conjuntos de escritura prometía borrar, igual que ya excluían el resto de los borrados del producto. A cambio, `dev:write` incluye ahora `dev:files:manage`, para que los tokens existentes sigan pudiendo crear carpetas y renombrar archivos.

### Los datos bancarios y fiscales tienen scope propio

| Scope | Qué autoriza | Dónde está |
| - | - | - |
| `finance:suppliers:view-sensitive` | Ver el NIF, el NIF-IVA, el IBAN, el SWIFT y el banco de los proveedores. | En `finance:full`. Fuera de `finance:read` y `finance:write`. |
| `crm:clients:view-sensitive` | Ver el NIF, el límite de crédito y el descuento pactado de los clientes. | En `crm:full`. Fuera de `crm:read` y `crm:write`. |

La pantalla de autorización los muestra con su nombre y con aviso, porque con el IBAN de un proveedor se puede desviar un pago y las condiciones pactadas con un cliente son información comercial reservada.

Sin el scope, esas claves no existen en la respuesta: no llegan vacías ni nulas, porque un nulo en un campo fiscal se leería como «esta empresa no tiene NIF». Si tu integración compara candidatos lado a lado, tiene que tolerar la clave ausente. Los tokens emitidos antes del 2 de septiembre de 2026 conservan esos datos hasta el 2 de marzo de 2027; las autorizaciones nuevas no los conceden sin nombrarlos.

El recorte afecta a:

- `suppliers`: `list`, `get`, `search_by_tax_id`, `check_duplicate`, `deduplicate_preview`, `merge`, `list_payments`, `create` y `update`.
- `finance_payments`: `list`, `get`, `check_duplicate` y `update`.
- `crm_clients`: `list`, `get`, `create`, `update`, `check_duplicate`, `deduplicate_preview` y `merge`, y el historial de `crm_clients_info`. En la previa y en la fusión de clientes se retira además el NIF-IVA, que solo se publica ahí. Los recuentos de cada ficha viajan siempre.
- El volcado del reconocimiento de documentos (`extractedData`): el recorte cubre trece nombres de clave a cualquier profundidad (el NIF, el NIF-IVA, el IBAN, la cuenta, el SWIFT y el BIC del proveedor, con sus variantes). `receiverNif`, que es el NIF de tu propia organización, no se recorta.
- `check_duplicate` de `finance_payments` y de `suppliers`: sin `SUPPLIERS_VIEW_SENSITIVE`, el NIF desaparece de `candidates` y de `expenseCandidates`. La detección de duplicados funciona igual, porque se basa en el número, la fecha, el importe y la confianza.

`create_many` y `update_many` no aparecen porque cada elemento vuelve en versión corta, sin esos campos.

### Archivos: permiso para vincular y scope por tipo de ficha

`crm_files_management` gobierna `upload`, `link`, `unlink` y `bulk_link` con `FILE_MANAGER_LINK_CRM` en lugar de `CLIENTS_EDIT`, porque admite fichas de varios módulos: las siete del CRM (`client`, `contact`, `lead`, `project`, `opportunity`, `budget` e `interaction`) y la del proveedor (`supplier`). El rol Asistente pierde esas cuatro operaciones por MCP, que tampoco podía hacer desde la aplicación.

Cada tipo de ficha exige además su propio scope, comprobado por parámetro:

| Ficha | Leer | Escribir |
| - | - | - |
| Las siete del CRM | El de la herramienta | El de la herramienta |
| `supplier` | `finance:suppliers:view` | `finance:suppliers:update` |

`get_linked_entities` recorta también la lista que devuelve: un token del CRM no ve el nombre del proveedor al que cuelga un archivo. El gestor de archivos añade `fm_virtual_folders: get_suppliers_tree`, con el árbol de la carpeta virtual «Proveedores», y admite `supplier` en `get_entity_files`, `link_files`, `unlink_file` y `unlink_files`.

`expenses: document_attach` ya no copia el justificante: recibe el identificador de un archivo del gestor al que tengas acceso y lo cuelga del gasto. Sigue exigiendo `confirm: true` porque sustituye el justificante anterior. `expenses: document_get` añade `source` (`file` o `legacy`), `fileId`, `fileName`, `mimeType` y `size`; en los justificantes históricos, el tipo y el tamaño llegan nulos. El archivo queda compartido entre el gestor y el gasto, y un justificante no se puede retirar, solo sustituir.

### Scopes nuevos

| Scope | Qué autoriza |
| - | - |
| `finance:suppliers:view-sensitive` | Datos bancarios y fiscales del proveedor. |
| `crm:clients:view-sensitive` | Datos fiscales y económicos del cliente. |
| `crm:lead-forms:view` | Ver los formularios de captación. |
| `org:notifications:view` y `org:notifications:manage` | Avisos y preferencias de aviso de la persona que autorizó el token. |
| `crm:portal:view` y `crm:portal:manage` | Ver y gestionar el acceso de terceros al portal del cliente con `portal_access`. La lectura queda fuera de `crm:read` y la gestión fuera de `crm:write`; las dos están en `crm:full`. |
| `crm:clients:merge` | Fusionar clientes duplicados: `crm_clients: deduplicate` y `merge`. |
| `crm:contacts:merge` | Fusionar contactos duplicados: `crm_contacts: deduplicate` y `merge`. |
| `crm:leads:merge` | Fusionar leads duplicados: `crm_leads: merge`. |

> **Importante: Los scopes de fusión se piden siempre por su nombre**
>
> Ninguno de los tres está dentro de `crm:read` ni de `crm:write`, y tampoco dentro de `crm:full`. Fusionar archiva o borra fichas y reescribe sus referencias sin vuelta atrás desde la aplicación, así que ninguna autorización antigua lo concede sin que el titular lo apruebe. Si tu aplicación fusiona, añade el scope a los que pide.

Las previas por reglas (`deduplicate_preview` de clientes y contactos) son lecturas y se piden con el scope de ver. La previa de `merge`, en cambio, la ejecuta la misma operación destructiva y exige el scope de fusionar.

### Operaciones nuevas

Fusión de duplicados en el CRM:

- **`crm_clients: deduplicate_preview`**: grupos de clientes duplicados según las reglas (mismo NIF; sin NIF, mismo correo, mismo dominio o mismo nombre y ciudad), con el superviviente propuesto, los recuentos de cada ficha y lo que se movería. No escribe nada: basta `CLIENTS_VIEW` y `crm:clients:view`. Si alguna ficha de un grupo queda fuera de tu alcance de fusión, el grupo entero se omite.
- **`crm_clients: deduplicate`**: fusiona esos grupos. Exige `confirm: true`, `CLIENTS_MERGE` y `crm:clients:merge`. `survivorIds` elige quién sobrevive en cada grupo (hasta 500) y `groupIds` acota los grupos (hasta 100); los que ya no son duplicados vuelven en `ignoredGroupIds` sin error.
- **`crm_clients: merge`**: fusiona las fichas que eliges a mano, de 1 a 20 duplicadas; es la salida cuando el NIF está mal escrito en una y ninguna regla las junta. Sin `confirm` devuelve la previa con `dryRun: true` y no escribe nada, pero exige igualmente el permiso y el scope de fusionar. La ficha que sobrevive recibe contactos, oportunidades, proyectos, presupuestos, tareas, facturas, autorizaciones de cobro, accesos al portal, archivos, carpetas e interacciones, y rellena sus campos vacíos con los datos de las demás.
- **Dos opciones de `merge` exigen alcance total** sobre el permiso de fusionar: `allowDifferentTaxId`, que une dos NIF distintos (nunca si las dos fichas tienen facturas emitidas), y `allowPortalAccessMerge`, que deja que los interlocutores externos de una empresa vean los documentos de la otra.
- `crm_contacts` y `crm_leads` tienen el mismo juego de operaciones con sus propios scopes. En contactos, dos fichas con usuario del portal no se pueden unir, y si alguna no puede recibir correos, la que sobrevive tampoco podrá.
- **`crm_leads: find_duplicates`** lista los grupos de leads duplicados con su tipo de coincidencia, su puntuación y el valor que los une, con `LEADS_VIEW` y `crm:leads:view`. `check_duplicate` hace lo contrario: contrasta un candidato antes de darlo de alta, y `excludeLeadId` evita que una ficha se detecte a sí misma al editarla.
- **`crm_contacts: search_by_email`** localiza un contacto activo por su correo principal o secundario, sin distinguir mayúsculas. Devuelve como mucho uno, y `found: false` tanto si no hay ninguno como si el que hay queda fuera de tu alcance.

Proveedores:

- **`suppliers: accounting_accounts`**: subcuentas contables 400 del proveedor. Exige Finanzas con la capacidad de Contabilidad, los permisos de ver contabilidad y de ver proveedores, y el scope `finance:accounting:view`. Está en «Próximamente», como la Contabilidad.
- **`suppliers: create` y `update` aceptan `logoFileId`**, el identificador de una imagen del gestor. En la edición, omitirlo deja el logotipo como está y enviarlo a nulo lo retira.
- **La actividad del proveedor** (`get_interaction_thread`, `list_interaction_activities`, `list_interaction_attachments`, `log_interactions_many`, `attach_to_interaction`, `remove_interaction_attachment`, `schedule_meeting`, `complete_meeting`, `cancel_meeting` y los filtros nuevos de `list_interactions`) se explica en [La actividad de un proveedor, sin salir del copiloto](https://os.utilia.ai/docs/developers/mcp/supplier-activity).

CRM y calendario:

- **`crm_clients: get_counterpart`**: el proveedor que comparte NIF con el cliente, o nada. Exige los dos aplicativos y los cuatro permisos de los dos lados, sensibles incluidos; si falta cualquiera, la respuesta es «no encontrado», igual que si el cliente no existiera.
- **`calendar_events_workflow: get_unified` acepta `sources`** para acotar el calendario unificado a algunas de sus catorce fuentes. `supplier_meeting` exige además el scope de la actividad del proveedor.
- **Personas de fuera de la organización en un evento.** `calendar_events` en `create` y `update`, y `calendar_events_workflow` en `add_attendees`, aceptan `externalAttendees` (lista de `{ email, name? }`, hasta 50) y `externalMessage`. Cada persona recibe por correo la fecha, la hora y el lugar del evento con el archivo de calendario y, si el evento tiene videollamada (y Meet está en el plan), el enlace para entrar. Invitar nunca crea una videollamada. En `update` se añaden, nunca sustituyen. Exige el permiso `CALENDAR_INVITE` o `CALENDAR_EDIT`, ser quien creó el evento (o administrador) y el scope `dev:calendar:invite` o `dev:calendar:update`. Si el correo de alguien no puede salir, queda invitado «por enviar» y la respuesta lo dice. Todo se comprueba antes de guardar nada. Un correo que pertenece a la organización se invita como interno, y a quien ya estaba invitado no se le vuelve a escribir.
- **`calendar_events_workflow: remove_external_attendee` y `resend_external_invitation`** retiran a esas personas (reciben el aviso de cancelación) o les reenvían la invitación. Se identifican por `email`, por `invitationId` o con una lista `externalAttendees`, varias en una llamada; el reenvío respeta el margen de cinco minutos entre correos a la misma persona. Exigen `CALENDAR_INVITE` o `CALENDAR_EDIT` y el mismo scope (y Meet en el plan para las invitaciones de un evento con videollamada). `calendar_events: get` devuelve `externalAttendees` con su estado y `hasVideoLink`, y `hasVideoCall` del evento.

Dos herramientas transversales nuevas, `search` y `notifications`, tienen [página propia](https://os.utilia.ai/docs/developers/mcp/search-and-notifications).

### Los SDK

Los SDK cubren estos cambios desde la versión 2.27.0. Dos avisos:

- **El mensaje de los errores de una herramienta ya no se pierde.** El SDK lee las tres formas de error que conviven y añade la pista al mensaje.
- **El error del SDK trae `errorCode` y `details`.** `errorCode` es el código exacto de la herramienta y `details`, su detalle: `requiredScopes` cuando falta un scope, `field` cuando falta un parámetro, `resource` y `retryAfterSeconds`. `code`, en cambio, sigue siendo `VALIDATION_ERROR` en los errores de herramienta, también en la versión actual: para distinguir un «no encontrado» de una regla de negocio, mira `errorCode`.

## Cambios de contrato de octubre de 2026

Desde el 20 de octubre de 2026, todas las herramientas comparten la misma forma de responder, de paginar y de rechazar, y se añaden operaciones que hasta ahora solo existían en la aplicación.

### Errores con motivo

Los rechazos por plan, permisos, scope o reglas de negocio llegan como resultado de la herramienta (`isError: true`), no como error del protocolo. Una llamada mal formada (una herramienta que no existe en el servidor o parámetros que no cumplen su esquema) sí puede responder con el error JSON-RPC `-32602`. En los resultados, además del mensaje, `error.details.errorCode` dice el motivo y `error.hint`, qué hacer:

| `errorCode` | Cuándo |
| - | - |
| `MODULE_NOT_ENABLED` | El plan no incluye el aplicativo. Trae el plan que lo incluye, si existe. |
| `CAPABILITY_NOT_ENABLED` | El aplicativo está contratado, pero no la capacidad. Es el mismo código que en la API REST. |
| `FEATURE_COMING_SOON` | La función está en «Próximamente». No trae plan destino. |
| `PERMISSION_DENIED` | Tu usuario no tiene el permiso. `requiredPermissions` da el código y el nombre que ves en la aplicación. |
| `INSUFFICIENT_SCOPE` | Al token le falta un scope. Trae `requiredScopes` y `grantedScopes`. |
| `APPROVAL_REQUIRED` | La operación se confirma en el móvil. Trae el reto en `approval`; repite la misma llamada con `twoFactorApprovalId`. No es un fallo. |
| `TWO_FACTOR_NOT_ENABLED` | La operación exige tener activada la verificación en dos pasos (ver más abajo). |
| `TOOL_RETIRED` | La herramienta está retirada. El mensaje dice cuál usar y con qué operación. |

### Listados más ligeros y con cursor

Los listados de las herramientas principales (clientes, contactos, leads, oportunidades, proyectos, tareas e interacciones del CRM, `calendar_events`, `tickets`, `issues`, `projects`, `ai_studio`, `chat_channels`, `chat_messages`, las de Marketing, `crm_invoices`, `crm_subscriptions` y el gestor de archivos, entre otras) aceptan:

- `response_format`: `concise` (por defecto) da los campos con los que se identifica y decide cada fila; `detailed`, la fila completa de siempre. La vista resumida nunca enseña nada que la completa no enseñaría.
- `limit` con un máximo declarado en su esquema y `cursor`. `offset` y `page` se siguen aceptando donde existían.

La respuesta trae las filas en `items` y la paginación en `pageInfo` (`nextCursor`, `hasMore`, `pageSize`, `responseFormat` y `detailHint`). `nextCursor` es nulo en la última página.

```json
{
  "name": "crm_clients",
  "arguments": { "operation": "list", "limit": 50, "response_format": "concise" }
}
```

> **Atención: Si leías la clave antigua del listado**
>
> Algunas herramientas devolvían las filas dos veces, en `items` y en una clave propia (`clients`, `contacts`, `leads`, `tickets`, `campaigns`...). Esa copia ya no llega a un asistente de IA. Si tu código la leía directamente, lee `items`. Los SDK de UTILIA no notan el cambio: se identifican con la cabecera `X-Utilia-SDK` y siguen recibiendo la fila completa y la clave antigua.

### Avisos, siguiente paso y reintentos

- **`_notices`**: avisos que no son error. `ignoredFields` lista los parámetros que la herramienta no conoce, con el nombre válido más parecido; `deprecatedOperation` avisa de que la operación es obsoleta y nombra la sustituta; `reauthorization` dice qué scope pedir al volver a autorizar cuando la llamada solo ha pasado por una compatibilidad que caduca.
- **`nextSteps`**: las creaciones y los cambios de estado de los flujos principales sugieren el paso siguiente como `tool`, `operation`, `why` y, si hace falta, `args` (presupuesto creado, generar el PDF y enviarlo; ticket de error, convertirlo en elemento de trabajo). Solo aparecen los pasos que tu plan, tus permisos y el token permiten.
- **`idempotencyKey` y `clientOperationId` son intercambiables.** Si la herramienta declara uno, el otro se acepta en su lugar, también dentro de los elementos de un lote. Si mandas los dos con valores distintos, la llamada se rechaza.

### Operaciones obsoletas

No se anuncian en `tools/list`, pero siguen respondiendo hasta el 8 de enero de 2027 con un aviso en `_notices.deprecatedOperation`. Cambia a la sustituta:

| Herramienta | Obsoleta | Sustituta |
| - | - | - |
| `crm_tasks` | `bulk_delete` | `delete_many` |
| `crm_opportunities_stats` | `analytics_loss_reasons`, `analytics_top_contributors` | `loss_reasons`, `by_owner` |
| `crm_budget_files` | `request_signature`, `generate_signing_link` | `force_send_signature` |
| `organization_settings` | `update` | La escritura de su sección: `update_general`, `update_addresses`, `update_contact`, `update_billing`, `update_branding`... |
| `organization_settings` | `get_public` | `get_branding`, `get_contact` o `get_regional` (o `get_full` con permiso de ver los ajustes) |
| `workplace` (servidor CRM) | `attendance_*` | `attendance` del servidor `utilia-rrhh` |

### Herramientas retiradas

Responden con `errorCode: TOOL_RETIRED` sin ejecutar nada, y el mensaje da la operación equivalente:

| Retirada | Usa | Operaciones que cambian de nombre |
| - | - | - |
| `crm_budgets_workflow` | `crm_budgets` | Ninguna. |
| `crm_budgets_pdf` | `crm_budgets` | `generate` → `pdf_generate`, `get_job_status` → `pdf_get_job_status`, `get_url` → `pdf_get_url`, `get_preview` → `pdf_get_preview`, `regenerate` → `pdf_regenerate` |
| `crm_reports_custom` | `crm_reports` | `execute` → `execute_custom` |
| `crm_budgets_items` | `crm_budgets_structure` | Las de líneas llevan el sufijo `_item` o `_items`: `list` → `list_items`, `add` → `add_item`, `add_many` → `add_items_many`... |
| `crm_budgets_sections` | `crm_budgets_structure` | Las de secciones llevan el sufijo `_section` o `_sections`: `list` → `list_sections`, `create` → `create_section`... |
| `crm_budget_attachments` | `crm_budget_files` | `list` → `list_attachments`, `get` → `get_attachment`, `link` → `link_attachment`, `update` → `update_attachment`, `unlink` → `unlink_attachment`, `link_many` → `link_attachments_many` |
| `crm_budget_signers` | `crm_budget_files` | `list` → `list_signers`, `get` → `get_signer`, `verify` → `verify_signature`; `request_signature` y `generate_signing_link` → `force_send_signature`. El resto se llama igual. |

### Verificación en dos pasos para lo más delicado

En la aplicación, quien no tiene la verificación en dos pasos confirma las acciones delicadas con su contraseña o con la sesión abierta. Desde un asistente de IA no hay ninguna de las dos, así que estas operaciones exigen tenerla activada y se confirman en el móvil:

- Crear y renovar credenciales y claves de API: `api_credentials` (`create`, `rotate`, `org_keys_create` y `org_keys_rotate`) y `org_integrations_settings` (`api_keys_create`, `api_keys_regenerate`). También ampliar una clave de la organización con `org_keys_update` (más permisos, más caducidad o cambiar de responsable).
- En `team_management`: dar el rol Super Administrador, conceder permisos críticos (sueltos, por lotes, al copiar los de otra persona o al aplicar una plantilla), cambiar el correo de acceso de otra persona y dar, ampliar o devolver el acceso de la asesoría (`advisors_invite`, `advisors_update` cuando amplía lo que ve o añade un responsable, y `advisors_restore`).
- Todas las escrituras de `org_security_privacy` (ver [Seguridad y privacidad de la organización](#seguridad-y-privacidad-de-la-organización)).

Sin la verificación en dos pasos, la llamada se rechaza con `TWO_FACTOR_NOT_ENABLED` antes de pedir nada al móvil. Con ella, sigue el ciclo de `APPROVAL_REQUIRED` y `twoFactorApprovalId`. La aplicación no cambia.

### Scopes nuevos de octubre

Ninguno entra en un conjunto, salvo `crm:settings:view`: se piden por su nombre y la persona los ve nombrados al autorizar.

| Scope | Qué autoriza (texto de la pantalla de consentimiento) |
| - | - |
| `org:team:permissions:manage` | Conceder y retirar permisos a las personas del equipo, también al darlas de alta, copiar los de otra persona, crear, cambiar y aplicar plantillas de permisos, y dar, ampliar o devolver el acceso de la asesoría. |
| `org:team:credentials:manage` | Cambiar la contraseña o el correo de acceso de otras personas, reenviarles el correo de alta y consultar el estado de sus enlaces para crear la contraseña. |
| `crm:settings:view` | Ver la configuración del CRM: embudos y etapas de venta, campos personalizados, motivos de pérdida y puntuación de leads. Entra en `crm:read` y `crm:full`. |
| `crm:settings:manage` | Cambiar la configuración del CRM: crear, editar, reordenar y eliminar etapas de venta, campos personalizados y motivos de pérdida, y cambiar la puntuación de leads. |
| `org:security:view` | Ver los ajustes de seguridad de la organización: verificación en dos pasos, bloqueo de pantalla, modo restringido, límite de exportaciones y dominios salientes. |
| `org:security:manage` | Cambiar los ajustes de seguridad de la organización. Exige tener activada la verificación en dos pasos y cada cambio se confirma en el móvil. |
| `org:privacy:view` | Ver los plazos de conservación de datos, las solicitudes de derechos de protección de datos, el registro de incidentes de seguridad y los ajustes de la actividad de los contactos en el portal. |
| `org:privacy:manage` | Cambiar los plazos de conservación, tramitar las solicitudes de derechos (también suprimir los datos de una persona), gestionar los incidentes de seguridad y decidir si se registra la actividad de los contactos en el portal. Exige la verificación en dos pasos y cada cambio se confirma en el móvil. |
| `org:privacy:export` | Pedir la exportación de todos los datos de la organización. Exige la verificación en dos pasos y se confirma en el móvil. |
| `finance:custom-fields:manage` | Crear, editar, reordenar y eliminar los campos personalizados de los proveedores y de sus personas de contacto. Eliminar un campo archivado quita sus valores de todas las fichas. |

> **Atención: org:team:manage ya no concede permisos ni toca contraseñas**
>
> `org:team:manage` cubre ahora solo el alta, la edición, la activación, la desactivación y el archivo de personas, y los departamentos. Conceder permisos pide `org:team:permissions:manage`, y las contraseñas y el correo de alta, `org:team:credentials:manage`. Hasta el 27 de enero de 2027, un token con `org:team:manage` puede seguir haciéndolo, con un aviso `_notices.reauthorization`: vuelve a autorizar tu aplicación con los scopes nuevos antes de esa fecha. Del mismo modo, cambiar los campos personalizados de proveedores ya no viaja en `finance:settings:update` ni en `finance:full`.

### Operaciones nuevas de octubre

Gestión del equipo (`team_management`, servidor `utilia-crm`), igual que Ajustes › Usuarios y Ajustes › Permisos:

| Operaciones | Qué hacen | Permiso | Scope |
| - | - | - | - |
| `presets_list`, `presets_get` | Plantillas de permisos de la organización. | `SETTINGS_USERS_PERMISSIONS_EDIT` | `org:team:view` o `org:team:permissions:manage` |
| `presets_create`, `presets_update`, `presets_delete`, `presets_apply` | Crear, cambiar, borrar y aplicar una plantilla a una persona. Aplicarla reemplaza sus permisos personalizados. | `SETTINGS_USERS_PERMISSIONS_EDIT` con alcance «Todo» | `org:team:permissions:manage` |
| `users_copy_permissions` | Copiar los permisos de otra persona. También reemplaza los que tenía. | `SETTINGS_USERS_PERMISSIONS_EDIT` con alcance «Todo» | `org:team:permissions:manage` |
| `roles_default_permissions` | Qué permisos trae cada rol por defecto. | `SETTINGS_PERMISSIONS_VIEW` | `org:team:view` |
| `users_get_password_tokens` | Estado de los enlaces de alta y de restablecimiento de contraseña de una persona: cuándo se generaron, si se usaron y cuándo caducan. Nunca devuelve el enlace. | `SETTINGS_USERS_VIEW` | `org:team:credentials:manage` |
| `users_resend_setup_email` | Reenviar el correo de alta, que invalida los enlaces anteriores. | `SETTINGS_USERS_EDIT` | `org:team:credentials:manage` |
| `users_offboard_preview` | Qué lleva una persona antes de darla de baja (empresas, oportunidades, tickets...), lo que se resuelve solo y los destinatarios sugeridos. | `SETTINGS_USERS_DEACTIVATE` o `SETTINGS_USERS_DELETE` | `org:team:view` |
| `users_offboard` | Dar de baja traspasando su cartera a otra persona (`reassignAllTo`) o por categorías (`assignments`). Exige `confirm: true`. | `SETTINGS_USERS_DEACTIVATE` o `SETTINGS_USERS_DELETE` | `org:team:manage` |

Nadie modifica sus propios permisos por esta vía, y los permisos protegidos solo los concede, retira o reemplaza un Super Administrador. `users_deactivate` y `users_delete` siguen dando de baja sin traspasar nada, y lo avisan.

#### Acceso de asesoría

La misma herramienta da acceso a la persona de la asesoría o gestoría de la empresa, igual que el bloque «Tu asesoría» de Ajustes › Usuarios. No ocupa usuario del plan: cuenta en los accesos de asesoría. Lo que ve se decide por áreas, cada una con su nivel y con su aplicativo en el plan:

| Área (`areas`) | Qué comparte | Niveles (`level`) |
| - | - | - |
| `finance` | Contabilidad y fiscalidad | `read` (consultar y descargar) o `manage` (también registrar) |
| `labor` | Laboral | `read` (consultar y descargar) |
| `documents` | Documentos: una carpeta compartida (`folderId`, o `newFolderName` para crearla) | `read` (ver y descargar) o `upload` (también subir) |
| `chat` | Chat de grupo con sus responsables en la empresa | `participate` (escribir en sus conversaciones) |

Dentro de las áreas que se comparten, `overrides` afina permiso a permiso (`permission` y `enabled`) con los códigos de `advisors_catalog`. Solo se comparte lo que quien da acceso tiene con alcance «Todo».

| Operaciones | Qué hacen | Permiso | Scope |
| - | - | - | - |
| `advisors_list`, `advisors_get` | Las personas de la asesoría con sus áreas y su cupo; la ficha con sus ajustes y sus permisos efectivos. | `SETTINGS_ADVISORS_MANAGE` o `SETTINGS_USERS_VIEW` | `org:team:view` |
| `advisors_catalog` | Qué áreas, niveles y ajustes puedes dar, según el plan y lo que tienes tú. | `SETTINGS_ADVISORS_MANAGE` | `org:team:view` |
| `advisors_invite` | Dar acceso: alta, responsables (`contactUserIds`, de 1 a 10, y `primaryContactUserId`, el principal; por defecto, quien invita), carpeta compartida, chat de grupo con los responsables e invitación. Se confirma en el móvil. | `SETTINGS_ADVISORS_MANAGE` | `org:team:manage` y `org:team:permissions:manage` |
| `advisors_update` | Cambiar áreas, ajustes, despacho o responsables. Las áreas, los ajustes y la lista de responsables (`contactUserIds`) se sustituyen enteros; `primaryContactUserId` cambia el principal. El campo antiguo `contactUserId` se sigue aceptando. Si amplía lo que ve o añade un responsable, se confirma en el móvil; quitar uno o cambiar el principal, no. | `SETTINGS_ADVISORS_MANAGE` | `org:team:manage`; con `areas`, `overrides` o responsables (`contactUserIds`, `primaryContactUserId` o `contactUserId`), además `org:team:permissions:manage` |
| `advisors_resend_invitation` | Reenviar la invitación a quien todavía no ha entrado. | `SETTINGS_ADVISORS_MANAGE` | `org:team:manage` |
| `advisors_revoke` | Quitar acceso: cierra sus sesiones y deja de compartir su carpeta. Exige `confirm: true`. | `SETTINGS_ADVISORS_MANAGE` | `org:team:manage` |
| `advisors_restore` | Volver a dar acceso. Se confirma en el móvil. | `SETTINGS_ADVISORS_MANAGE` | `org:team:manage` y `org:team:permissions:manage` |

`users_create` y `users_update` no dan ni quitan el rol `ASESORIA`: responden `ADVISOR_ROLE_IMMUTABLE` y remiten a `advisors_invite`. Los errores propios son los de Ajustes: `QUOTA_EXCEEDED_ADVISOR_SEATS` (cupo lleno), `MODULE_NOT_ENABLED` (área fuera del plan), `PERMISSION_NOT_ALLOWED_FOR_ADVISOR`, `GRANT_AUTHORITY_EXCEEDED`, `ADVISOR_EMAIL_EXISTS` y `ADVISOR_FOLDER_IN_TRASH`.

En `advisors_update`, la carpeta que ya tiene no hace falta reenviarla: sin `folderId` ni `newFolderName` se conserva. La misma carpeta con el mismo nivel se queda tal cual, también si está en «Eliminados recientemente»; cambiar el nivel de Documentos con la carpeta allí responde `ADVISOR_FOLDER_IN_TRASH` (409): restáurala o elige otra.

Ajustes de la organización (`organization_settings`, servidor `utilia-crm`). Cada bloque exige su aplicativo en el plan:

| Operaciones | Qué hacen | Aplicativo | Permiso |
| - | - | - | - |
| `get_invoice_brand`, `update_invoice_brand`, `update_invoice_brand_full`, `upload_invoice_watermark_image` | Diseño del PDF de facturas por tipo (ordinaria, rectificativa, nota de crédito) y su marca de agua. `update_invoice_brand` cambia un tipo; `_full` las reemplaza todas. | Finanzas | `SETTINGS_ORGANIZATION_VIEW` para leer, `SETTINGS_ORGANIZATION_EDIT` para cambiar |
| `get_billing_suggestions`, `apply_billing_suggestions` | Datos fiscales sugeridos a partir de los de facturación de tu suscripción. Aplicarlos solo rellena los vacíos. | Ninguno | `SETTINGS_ORGANIZATION_VIEW` / `SETTINGS_ORGANIZATION_EDIT` |
| `get_external_ai`, `update_external_ai` | Permitir o no el envío de datos a proveedores de IA externos. Cambiarlo es solo del Super Administrador, y desactivarlo, que pausa todas las funciones de IA, exige `confirm: true`. | Ninguno | Ninguno para leer, `SETTINGS_ORGANIZATION_EDIT` para cambiar |
| `get_meet_recording_notice`, `update_meet_recording_notice` | Párrafo propio que se añade al aviso de grabación y transcripción de las reuniones. | Meet | `MEET_MANAGE` (con alcance «Todo» para cambiarlo) |
| `get_copilot_recording_notice`, `update_copilot_recording_notice` | Párrafo propio que se añade al aviso que acepta quien usa Call Copilot. | Call Copilot | `CALL_COPILOT_MANAGE` (con alcance «Todo» para cambiarlo) |

Las lecturas piden `org:settings:view` (el aviso de Meet acepta también `crm:meet:view`) y las escrituras, `org:settings:update`.

Otras herramientas:

- **`portal_access: list_portal_permissions`** dice qué permisos del portal del cliente se pueden conceder. Pide `PORTAL_MANAGE_PERMISSIONS`, `PORTAL_INVITE_USER` o `PORTAL_MANAGE_CLIENT_ACCESS`, y `crm:portal:view`.
- **`suppliers`** (servidor `utilia-finance`) gestiona los campos personalizados de los proveedores y de sus personas de contacto: `list_custom_fields`, `create_custom_field`, `update_custom_field`, `reorder_custom_fields` y `delete_custom_field`. Leer pide el permiso de ver proveedores o de ver los campos; escribir, `CUSTOM_FIELDS_MANAGE` y `finance:custom-fields:manage`.
- **`tickets_workflow: convert_to_issue`** convierte un ticket en elemento de trabajo; los detalles, en [Herramientas MCP de tickets](https://os.utilia.ai/docs/developers/mcp/tickets).
- **`ai_studio: use`** sustituye las variables de un prompt y suma un uso; ver [Herramienta MCP de AI Studio](https://os.utilia.ai/docs/developers/mcp/ai-studio-tool).
- **`crm_settings`** y **`org_security_privacy`** son herramientas nuevas: ver [Configuración del CRM](#configuración-del-crm) y [Seguridad y privacidad de la organización](#seguridad-y-privacidad-de-la-organización).

### Gestor de archivos: Eliminados recientemente

Desde el 22 de octubre de 2026, lo que una persona elimina por MCP ya no se destruye en el acto: pasa 30 días en «Eliminados recientemente», igual que en la aplicación, y después se borra para siempre con su contenido, sus miniaturas y sus versiones. Las operaciones de borrado se llaman igual; cambia lo que responden:

- **`fm_files_manage`: `delete` y `batch_delete`** responden `outcome: "TRASHED"` con `purgeAfter` (fecha ISO del borrado definitivo); `delete` añade `retentionDays`. Si el archivo sigue en uso en un sitio que la persona no puede editar, `outcome: "IN_USE"`, como hasta ahora, con los sitios en `referencedBy` (en `batch_delete`, en el `error` de ese archivo). `physicallyDeleted` se mantiene por compatibilidad y llega a `false`.
- **`remove_from_folder` y `batch_remove_from_folder`** responden `outcome: "REMOVED"` si el archivo sigue en otros sitios y `"TRASHED"`, con `purgeAfter`, si esa carpeta era su última ubicación.
- **`fm_folders_manage`: `delete`**, con o sin `cascade`, manda la carpeta a «Eliminados recientemente» con lo que solo estaba en ella, y responde `outcome: "TRASHED"` y `purgeAfter`.
- **`fm_virtual_folders`: `unlink_file` y `unlink_files`** exigen `confirm: true` cuando quitar el vínculo deja el archivo sin ningún sitio y lo manda a «Eliminados recientemente».

Tres operaciones nuevas recorren «Eliminados recientemente» (en el servidor `utilia-crm`, por ejemplo):

| Operación | Qué hace | Permiso | Scope |
| - | - | - | - |
| `fm_files: list_trash` | Lista lo que la persona puede ver, de lo más reciente a lo más antiguo. Admite `query` (por nombre), `limit` (20 por defecto, hasta 100) y `cursor`. Devuelve `items`, `pageInfo`, `retentionDays` y `purgeableCount`. | `FILE_MANAGER_VIEW` | Cualquiera de lectura o escritura de archivos: `crm:files:view`, `crm:files:upload`, `crm:files:delete`, `dev:files:view`, `dev:files:manage` o `dev:files:full` |
| `fm_files_manage: restore` | Devuelve cada elemento a sus carpetas y fichas si siguen existiendo y la persona puede editarlas; si no, a Inicio. | `FILE_MANAGER_VIEW` | `crm:files:upload`, `crm:files:delete`, `dev:files:manage` o `dev:files:full` |
| `fm_files_manage: purge` | Lo borra para siempre. Exige `confirm: true`. | `FILE_MANAGER_DELETE_FOLDER` | `crm:files:delete` o `dev:files:full` |

`restore` y `purge` reciben un elemento (`id`, con `kind: "folder"` si es una carpeta; `file` por defecto) o un lote de hasta 50 en `items: [{ kind, id }]`. Una carpeta se restaura o se borra entera, con lo que se eliminó con ella. La respuesta trae `results` elemento a elemento, con `outcome` (`RESTORED` o `FAILED` al restaurar; `PURGED`, `QUEUED` o `FAILED` al borrar), `errorCode` y `message`, y `failedItems` si alguno falla. `restore` añade `restoredTo` (dónde ha quedado), `renamedTo` (si una carpeta cambió de nombre por estar el suyo ocupado) y `uploadedBy`. `QUEUED` cuenta como hecho: ya no se puede restaurar y termina de borrarse en cuanto responde el almacenamiento.

Qué ve y qué puede borrar cada persona lo decide el mismo servicio que la aplicación. Ve lo que eliminó y lo que subió; lo que eliminaron otras personas, solo con `FILE_MANAGER_MANAGE_TRASH` («Gestionar lo que eliminan otros») y dentro de su alcance. Borrar antes de tiempo es de quien lo subió (o creó la carpeta), del Super Administrador y de ese permiso; quien eliminó algo ajeno puede restaurarlo, pero no borrarlo. Lo que tiene bloqueado el acceso de la IA no aparece en `list_trash` y responde como inexistente. «Eliminar todo» no se publica por MCP: una destrucción masiva sin vuelta atrás se queda en la aplicación.

| `errorCode` | Cuándo |
| - | - |
| `TRASH_ITEM_NOT_FOUND` | No está en «Eliminados recientemente» o la persona no puede verlo. |
| `TRASH_ITEM_EXPIRED` | Se le acabó el plazo y se está borrando. |
| `TRASH_ITEM_INSIDE_FOLDER` | Se eliminó con su carpeta: restaura o borra la carpeta. |
| `TRASH_ITEM_CONTENT_GONE` | Su contenido ya no existe en el almacenamiento y no se puede restaurar. |
| `TRASH_RESTORE_NO_DESTINATION` | La carpeta de origen ya no existe o no se puede editar, y la persona no puede crear carpetas en Inicio. |
| `TRASH_RESTORE_CONFLICT` | Otro archivo activo usa ya el mismo contenido: restáuralo o elimínalo antes. |
| `TRASH_PURGE_FORBIDDEN` | Solo quien lo subió, el Super Administrador o quien gestiona lo que eliminan otros pueden borrarlo antes de tiempo. |

Al eliminar siguen las negativas de siempre, con su `errorCode`: `FILE_IS_WORM_PROTECTED` (la copia de lo que recibió el cliente), `FILE_IS_ISSUED_INVOICE_ORIGINAL`, `FILE_IS_VAT_ACCREDITATION_EVIDENCE` y, para carpetas, `FOLDER_IS_SYSTEM`. La guía de uso está en [Eliminar y recuperar archivos](https://os.utilia.ai/docs/guides/files/recently-deleted).

#### Si quien conecta la IA es de la asesoría

Una persona de la asesoría trabaja solo en la carpeta que la empresa le comparte, y su IA tiene las mismas reglas que ella en la aplicación y en la unidad de red:

- **Eliminar, mover, renombrar o sustituir** (`delete`, `batch_delete`, `remove_from_folder`, `batch_remove_from_folder`, `move`, `batch_move`, `rename`, `upload_version`, `restore_version`, `delete_version`, `edit_content`, `patch`): solo con el nivel «También subir» y solo lo que subió o metió ella en su carpeta compartida. Lo de la empresa es de solo lectura para ella, y nada sale de su carpeta: mover fuera, o a Inicio, se niega.
- **«Eliminados recientemente»**: `list_trash` solo trae lo que ella eliminó de su carpeta compartida; `restore` lo devuelve solo a esa carpeta y, si la carpeta no está disponible (la empresa la ha eliminado o ya no la comparte), falla con `TRASH_RESTORE_NO_DESTINATION`. `purge` nunca: responde `TRASH_PURGE_FORBIDDEN`. Con «Ver y descargar», la lista llega vacía.

| `errorCode` | Cuándo |
| - | - |
| `ADVISOR_DELETE_NOT_ALLOWED` | La asesoría intenta eliminar o sacar de una carpeta algo que no subió ni metió ella, algo de fuera de su carpeta compartida, o tiene «Ver y descargar». 403. |
| `ADVISOR_FILE_READ_ONLY` | La asesoría intenta mover, renombrar o sustituir lo que no es suyo, o mover algo fuera de su carpeta compartida. 403. |

Solo se explica la negativa sobre lo que la asesoría ve (su carpeta o lo que subió ella); un archivo de la empresa que no ve responde como siempre, sin decir que existe.

## Referencias

- [Primeros pasos con MCP](https://os.utilia.ai/docs/developers/mcp/quickstart).
- [Migración del MCP de integraciones de v1 a v2](https://os.utilia.ai/docs/developers/mcp/migration-v1-v2), si tu integración usaba la antigua herramienta `integrations`.
- [Poner al día una cartera de clientes](https://os.utilia.ai/docs/developers/mcp/client-portfolio): lotes, logotipos, fusión con previa y espejo incremental.
- [Scopes y recursos de OAuth](https://os.utilia.ai/docs/developers/oauth/scopes-and-resources).
- [Buscador global y avisos por MCP](https://os.utilia.ai/docs/developers/mcp/search-and-notifications).
- [Herramienta MCP de tickets](https://os.utilia.ai/docs/developers/mcp/tickets).
