# Subir archivos a una ficha por MCP

La herramienta file_uploads: sube un archivo de tu disco a un gasto, una factura, una tarea o un presupuesto, con el permiso y el plan de ese módulo.

`file_uploads` («Subir archivos») sube un archivo del disco de la persona a la ficha que lo pide: el justificante de un gasto, un adjunto de una factura o de un presupuesto, la imagen de un proyecto. Pasa por el permiso y el plan del módulo de esa ficha, igual que la subida de la aplicación en esa pantalla, y no necesita el Gestor de archivos.

Úsala cuando otra herramienta de tu servidor pida un `fileId` y el archivo esté en el disco. Antes había que subirlo con `fm_files_manage` del servidor `utilia-crm`, que exige el Gestor de archivos en el plan y su permiso. Para guardar un archivo en una carpeta del Gestor, esa sigue siendo la herramienta.

## Requisitos

- **Servidor.** Está en `utilia-finance`, `utilia-work`, `utilia-crm`, `utilia-marketing` y `utilia-rrhh`, cada uno con sus propios destinos. No está en el servidor de Chat, que no tiene fichas a las que subir, ni en el de desarrolladores, ni en los conectores de los directorios de Claude y ChatGPT.
- **Scope.** No hay scopes nuevos. La conexión necesita el scope de escritura de la operación que va a usar el archivo, o un conjunto que lo incluya: con `finance:payments:create` subes un documento para digitalizar, pero no el justificante de un gasto, que pide `finance:expenses:update`. La herramienta aparece en `tools/list` si la conexión puede abrir alguno de los destinos de ese servidor.
- **Permiso.** Los del destino, todos a la vez, con el alcance de editar esa ficha: los mismos que pide la aplicación para subir ahí. Están en [Permisos y scopes de cada destino](#permisos-y-scopes-de-cada-destino).
- **Plan.** El aplicativo del destino: Finanzas, Gestión de proyectos, CRM, Marketing o RRHH. El Gestor de archivos no hace falta.
- **Cuenta en pausa.** Si la cuenta de la empresa está en pausa, solo funcionan `list` y `abort_upload`.

## Cómo se sube

Son tres pasos, reservar, enviar el archivo y confirmar, y después el `fileId` se pasa a la operación del destino. El archivo nunca viaja dentro de la llamada.

**Subir el justificante de un gasto**

Participantes: Tu asistente, UTILIA OS y Almacén de archivos.

1. Tu asistente → UTILIA OS: request_upload. Con destination y el nombre y el tamaño exacto de cada archivo.
2. UTILIA OS (por su cuenta): Comprueba el plan, los permisos, el scope y la ficha.
3. UTILIA OS → Tu asistente (respuesta): fileId, uploadUrl y relayUploadUrl. Las dos direcciones valen 15 minutos.
4. Tu asistente → Almacén de archivos: `PUT del archivo a uploadUrl`. Con el Content-Type de la reserva.
5. Tu asistente → UTILIA OS: confirm_upload. Con los fileId de la reserva.
6. UTILIA OS → Tu asistente (respuesta): confirmed y nextStep. Mide el tamaño y el tipo reales y dice qué operación usa el fileId.
7. Tu asistente → UTILIA OS: expenses document_attach con el fileId. El archivo pasa a ser el justificante del gasto.

_Las puertas del destino se vuelven a comprobar al confirmar y al usar el archivo._

La herramienta recibe siempre `operation` y, según la operación, estos parámetros:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `operation` | `string` | Sí | request_upload, confirm_upload, abort_upload, create_upload_link o list. |
| `destination` | `object` | No | En request_upload y create_upload_link, obligatorio; en list, filtra. type: el destino. id: el identificador de la ficha, salvo en los destinos que no la llevan. |
| `files` | `object[]` | No | request_upload: de 1 a 20 archivos, cada uno con fileName, fileSize (los bytes exactos) y, si lo sabes, mimeType. |
| `fileIds` | `string[]` | No | confirm_upload y abort_upload: de 1 a 20 fileId de request_upload. |

### Reservar la subida

```json
{
  "name": "file_uploads",
  "arguments": {
    "operation": "request_upload",
    "destination": { "type": "expense_document", "id": "ckexp0142example" },
    "files": [
      { "fileName": "ticket-gasolinera.pdf", "fileSize": 184233, "mimeType": "application/pdf" }
    ]
  }
}
```

La respuesta trae el destino tal como lo verá la persona y, por cada archivo, su `fileId` y sus dos direcciones:

```json
{
  "success": true,
  "destination": {
    "type": "expense_document",
    "id": "ckexp0142example",
    "label": "Justificante del gasto",
    "recordLabel": "GAS-2026-00142",
    "acceptedMimeTypes": ["application/pdf", "image/jpeg", "image/png", "image/webp"],
    "maxBytes": 10485760
  },
  "files": [
    {
      "fileId": "ckfile0001example",
      "fileName": "ticket-gasolinera.pdf",
      "method": "PUT",
      "uploadUrl": "https://…",
      "relayUploadUrl": "https://os.utilia.ai/api/files/relay-upload/…",
      "headers": { "Content-Type": "application/pdf" },
      "mimeType": "application/pdf",
      "expiresInSeconds": 900
    }
  ],
  "nextStep": "Envía cada fichero con un PUT a su uploadUrl, con su Content-Type (…). Si el PUT a uploadUrl falla por la red, repítelo a relayUploadUrl; si tu entorno no puede hacer peticiones, pide a la persona que lo suba con create_upload_link. Después llama a file_uploads(operation: \"confirm_upload\", fileIds: [...]) con los fileId de files."
}
```

Cada archivo va por su cuenta: el que no cabe en el destino por su tipo o su tamaño sale en `rejected`, con su `errorCode`, y no impide reservar los demás. `success` llega a `false` solo si no se ha reservado ninguno. `recordLabel` es el número de la ficha, nunca un título libre, y llega a `null` si no lo tiene, como una factura en borrador.

### Enviar el archivo

Haz un `PUT` del archivo a su `uploadUrl`, con las cabeceras de `headers`, antes de que pasen los 15 minutos:

```bash
curl --fail -sS -T "./ticket-gasolinera.pdf" \
  -H "Content-Type: application/pdf" \
  "<uploadUrl>"
```

Si el `PUT` no llega al almacén, tienes dos salidas, que se explican en [Si el PUT no llega](#si-el-put-no-llega).

### Confirmar y usar el fileId

`confirm_upload` comprueba que el archivo ha llegado y mide su tamaño y su tipo reales. Si no caben en el destino, lo borra y lo devuelve en `failed` con su `errorCode`. Lo que cabe sale en `confirmed`, y `nextStep` nombra la operación que recibe ese `fileId`:

```json
{
  "success": true,
  "confirmed": [
    {
      "fileId": "ckfile0001example",
      "fileName": "ticket-gasolinera.pdf",
      "size": 184233,
      "mimeType": "application/pdf",
      "destination": { "type": "expense_document", "id": "ckexp0142example" }
    }
  ],
  "nextStep": "Pasa cada fileId a expenses(operation: \"document_attach\") con la ficha \"ckexp0142example\"; si esa operación pide confirm, ponlo a true solo cuando la persona lo haya confirmado. Cada fileId vale 24 horas y solo para ese destino."
}
```

La operación del destino recibe el `fileId` con su contrato de siempre, también su `confirm` si lo pide. Con un archivo subido así, se salta solo las puertas del Gestor de archivos (verlo, tenerlo en el plan y el scope de lectura de Archivos) y aplica todas las suyas.

## Destinos de cada servidor

`destination.type` solo admite los destinos del servidor al que llamas: el `enum` de cada copia de la herramienta trae los suyos. Uno de otro servidor responde `UPLOAD_DESTINATION_NOT_IN_SERVER`, con el servidor donde vive.

| Servidor | `destination.type` | Para qué | `destination.id` | Lo usa |
| - | - | - | - | - |
| `utilia-finance` | `payment_document` | Facturas de proveedores para digitalizar, como en «Subir facturas de proveedores» | No lleva | `finance_payments` `upload` |
| `utilia-finance` | `expense_document` | El justificante principal de un gasto, que sustituye al anterior | El gasto | `expenses` `document_attach` |
| `utilia-finance` | `expense_attachment` | Otro justificante del gasto, sin tocar el principal | El gasto | `expenses` `attachment_link` |
| `utilia-finance` | `invoice_attachment` | Un adjunto de una factura, en cualquier estado | La factura | `crm_invoices` `attachment_link` |
| `utilia-finance` | `supplier_interaction_attachment` | Un adjunto de una interacción con un proveedor | La interacción | `suppliers` `attach_to_interaction` |
| `utilia-work` | `issue_attachment` | Un adjunto de un elemento de trabajo | El elemento | `issues_workflow` `add_attachment` |
| `utilia-work` | `project_image` | El logotipo o la portada de un proyecto | El proyecto | `projects` `update`, en `logoFileId` o `coverFileId` |
| `utilia-crm` | `budget_attachment` | Un adjunto de un presupuesto | El presupuesto | `crm_budget_files` `link_attachment` o `link_attachments_many` |
| `utilia-marketing` | `marketing_image` | Una imagen para los correos de campaña | No lleva | `marketing_assets` `upload` |
| `utilia-rrhh` | `disconnection_policy` | La política de desconexión digital de la empresa | No lleva | `attendance` `disconnection_policy_upload` |

`destination.id` es el identificador interno de la ficha, el que devuelven `get` y `list`: el número de un gasto no sirve. En `issue_attachment` y `project_image` vale también la clave de la tarea o del proyecto. Si falta en un destino que lo lleva, o sobra en uno que no, la respuesta es `UPLOAD_DESTINATION_RECORD_REQUIRED`. En los destinos de Gestión de proyectos, `label` usa los nombres del perfil de trabajo: en Agencia, «Adjunto de un entregable» o «Logotipo o portada de la cuenta».

## Permisos y scopes de cada destino

Cada destino pasa sus puertas en este orden al reservar, al confirmar, al crear el enlace y otra vez cuando la operación usa el archivo:

**Las puertas de un destino**

1. **¿Está su aplicativo en el plan?** (Tu organización): El del destino; el Gestor de archivos no cuenta. Si no: MODULE_NOT_ENABLED.
2. **¿Tiene la persona sus permisos?** (Tu usuario): Todos los de la fila, no uno de ellos. Si no: PERMISSION_DENIED, con los que faltan.
3. **¿Lleva la conexión su scope?** (El token): El de escritura de la operación que usará el archivo. Si no: INSUFFICIENT_SCOPE, con el enlace para concederlo.
4. **¿Está la ficha a tu alcance?** (La ficha): Con el alcance de editarla. Si no: UPLOAD_DESTINATION_NOT_FOUND.

La ficha se mira al final, así que una conexión sin el scope no llega a saber si existe. Una ficha que no existe y una que queda fuera de tu alcance dan la misma respuesta.

| `destination.type` | Permisos de la persona | Scope de la conexión |
| - | - | - |
| `payment_document` | `PAYMENTS_CREATE` | `finance:payments:create` o `finance:payments:update` |
| `expense_document` y `expense_attachment` | `EXPENSES_EDIT` | `finance:expenses:update` |
| `invoice_attachment` | `INVOICES_EDIT` | `crm:invoices:update` |
| `supplier_interaction_attachment` | `SUPPLIERS_INTERACTIONS_MANAGE` | `finance:suppliers:interactions:manage` |
| `issue_attachment` | `DEV_ISSUES_EDIT` | `dev:issues:update` |
| `project_image` | `DEV_PROJECTS_EDIT` y `FILE_MANAGER_UPLOAD` | `dev:projects:update` |
| `budget_attachment` | `BUDGETS_EDIT` | `crm:budgets:update` |
| `marketing_image` | `EMAIL_MARKETING_EDIT` | `marketing:campaigns:update` |
| `disconnection_policy` | `ATTENDANCE_MANAGE_POLICIES`, con alcance de toda la empresa | `hr:attendance:manage` |

`project_image` pide `FILE_MANAGER_UPLOAD` porque es el permiso con el que la aplicación sube esa imagen, pero no el Gestor de archivos en el plan. `confirm_upload`, `abort_upload` y `list` piden los mismos scopes: solo ves, confirmas y liberas subidas de los destinos que tu conexión puede abrir, y las demás responden como si no existieran. `abort_upload` no pide el permiso del destino, porque solo libera lo tuyo.

## Usar el fileId en su destino

Un `fileId` de `file_uploads` vale solo para el destino y la ficha para los que se reservó, una sola vez y durante 24 horas. En otra operación o con otra ficha, la respuesta es `UPLOAD_DESTINATION_MISMATCH` (409), aunque la persona tenga permiso sobre el Gestor de archivos: hay que subirlo otra vez con el destino que toca. Un `fileId` del Gestor sigue igual que siempre, con sus puertas.

Cada operación guarda el archivo como lo hace la aplicación en esa pantalla:

- **Mismo archivo.** El justificante del gasto, el documento para digitalizar, la imagen del proyecto y el adjunto del presupuesto se guardan tal cual, sin copia.
- **Copia.** En los demás, la operación guarda una copia con el mismo servicio que la subida de la aplicación y la subida de paso se borra. Un adjunto de una factura o de un gasto queda como subido a la ficha, no como un archivo del Gestor enlazado: al quitarlo pasa 30 días por «Eliminados recientemente». El de un elemento de trabajo se adjunta siempre como copia, nunca vinculado.
- **Presupuesto.** El adjunto nace solo para el equipo: el cliente no lo ve hasta que se comparte, desde la ficha del presupuesto en la aplicación o con `crm_budget_files` `update_attachment` (`visibility` y `confirm: true`; sin `confirm` es un ensayo que no cambia nada). Por MCP, todo `link_attachment` nace así, también con un archivo del Gestor, y `list_attachments` y `get_attachment` devuelven `visibility`. La respuesta lo recuerda en `notice`.
- **Proyecto.** La imagen se pone con `projects` `update`, con el mismo proyecto en el destino; el mismo `fileId` vale para `logoFileId` o para `coverFileId`.
- **Política de desconexión.** Se copia a la carpeta del protocolo de RRHH y pasa a ser la vigente.

Lo que no se usa no se queda guardado:

**Vida de una subida a una ficha**

1. **Reservada** (`reserved`). Tiene fileId y direcciones; falta el archivo o la confirmación.
   - **Liberada** (estado final). Se llega desde Reservada cuando: abort_upload. Se borra sin pasar por la papelera.
2. **Confirmada** (`confirmed`). Se llega desde Reservada cuando: confirm_upload mide el tamaño y el tipo. Lista para la operación de su destino.
   - **Borrada sola** (estado final). Se llega desde Confirmada cuando: Nadie la usa en 25 horas. Desde las 24 horas ya no se puede usar.
3. **Usada** (estado final). Se llega desde Confirmada cuando: La operación del destino recibe el fileId. Pasa a la ficha y deja de salir en list.

Otras transiciones:

- Confirmada → Liberada: abort_upload también libera una confirmada que no se ha usado.
- Reservada → Borrada sola: Una reserva sin confirmar se retira igual.

`list` devuelve tus subidas sin usar de las últimas 24 horas, como mucho 50, con `fileId`, `fileName`, `size`, `mimeType`, `status` (`reserved` o `confirmed`), `via` (`mcp` o `link`), `destination` y `createdAt`. Admite `destination` para filtrar. Sirve para recuperar una reserva olvidada y para conocer lo que la persona subió con un enlace.

## Si el PUT no llega

Algunos entornos, como el de código de Claude, bloquean la dirección del almacén, y otros, como ChatGPT en la web, no pueden hacer peticiones con un archivo. Para los dos casos hay salida:

**¿Cómo llega el archivo?**

¿Puede tu entorno hacer peticiones HTTP con el archivo?

- Sí: ¿Llega el PUT a uploadUrl?
  - Sí: **Subida directa**. El PUT a uploadUrl y, después, confirm_upload.
  - No: **relayUploadUrl** (`PUT`). El mismo PUT, con las mismas cabeceras, a través de os.utilia.ai.
- No: **create_upload_link**. Un enlace de 24 horas para que la persona suba el archivo desde su navegador.

_La confirmación y el uso del fileId no cambian._

**`relayUploadUrl`** llega con cada archivo de `request_upload`. Es el mismo `PUT`, con el mismo método y las mismas cabeceras, a `https://os.utilia.ai/api/files/relay-upload/…`. Vale una sola vez y para ese archivo, caduca con la reserva a los 15 minutos y exige que `Content-Length` sea el tamaño exacto reservado. Después se confirma como siempre. Sus errores están en [Subir un archivo cuando el PUT no llega](https://os.utilia.ai/docs/developers/mcp/reference#subir-un-archivo-cuando-el-put-no-llega).

**`create_upload_link`** recibe `destination`, con su `id` si lo lleva, y devuelve `uploadLink`: `url`, `expiresAt`, `destination`, `maxFiles` y `maxFileBytes`. La persona abre el enlace con su sesión de UTILIA OS, ve para qué ficha es y sube los archivos, con los tipos y el tamaño del destino; cada archivo pasa las mismas puertas. Cuando diga que lo ha subido, `list` con el mismo `destination` te da los `fileId`, ya confirmados y con `via: "link"`, y los pasas a la operación del destino.

Solo abre el enlace quien lo pidió, y cada persona puede tener 10 enlaces vivos. Si tu conexión no puede enviar archivos y pides una reserva, la respuesta trae en `hint` la pista de liberarla con `abort_upload` y pedir el enlace con el mismo destino.

## Límites

**Límites de la subida a una ficha**

- Archivos por llamada: 20. Algunos destinos admiten menos.
- Vida de uploadUrl y relayUploadUrl: 15 minutos
- Vida de un fileId sin usar: 24 horas. A las 25 horas se borra solo.
- Enlaces de subida vivos por persona: 10

Cada destino tiene además su tope de archivos por llamada, de tamaño y de tipos, que es el mismo en `request_upload` y en el enlace para la persona:

| `destination.type` | Archivos por llamada | Tamaño por archivo | Tipos |
| - | - | - | - |
| `payment_document` | 20 | 25 MB | PDF, PNG, JPEG o WebP |
| `expense_document` | 1 | 10 MB | PDF, JPEG, PNG o WebP |
| `expense_attachment` | 20 | 50 MB | Cualquiera |
| `invoice_attachment` | 20 | 50 MB | Cualquiera |
| `supplier_interaction_attachment` | 10 | 10 MB | Cualquiera |
| `issue_attachment` | 20 | 10 MB | Cualquiera |
| `project_image` | 1 | 5 MB | PNG, JPEG, WebP, GIF o SVG |
| `budget_attachment` | 20 | 50 MB | Cualquiera |
| `marketing_image` | 20 | 5 MB | PNG, JPEG, GIF o WebP |
| `disconnection_policy` | 1 | 10 MB | PDF, JPEG, PNG, WebP o HEIC |

El tipo se comprueba dos veces: al reservar, con el que declaras, y al confirmar, con el del archivo que ha llegado.

## Errores

Todos llegan con `isError: true`, el código en `details.errorCode` y una pista para el asistente en `hint`. En `request_upload`, `confirm_upload` y `abort_upload`, un archivo que falla no tumba la llamada: sale en `rejected`, `failed` o `notReleased` con su `errorCode`.

| `errorCode` | Cuándo | Qué hacer |
| - | - | - |
| `UPLOAD_DESTINATION_UNKNOWN` | `destination.type` no es ningún destino. | Usa uno de tu servidor: el mensaje los nombra. |
| `UPLOAD_DESTINATION_NOT_IN_SERVER` | El destino es de otro servidor. | Llama a `file_uploads` del servidor que dice el mensaje. |
| `UPLOAD_DESTINATION_RECORD_REQUIRED` | Falta `destination.id` en un destino con ficha, o sobra en uno sin ella. | Añádelo o quítalo. |
| `MODULE_NOT_ENABLED` | El aplicativo del destino no está en el plan. | No es un fallo: díselo a la persona. Repetir la llamada da lo mismo. |
| `PERMISSION_DENIED` | A la persona le falta un permiso del destino. Trae `requiredPermissions`. | Que se lo conceda quien administra la organización. |
| `INSUFFICIENT_SCOPE` | A la conexión le falta el scope del destino. Trae `requiredScopes` y, en OAuth, `grantUrl`. | Dale el enlace a la persona; si es una clave personal, crea otra. |
| `UPLOAD_DESTINATION_NOT_FOUND` | La ficha no existe o queda fuera de tu alcance. | Búscala con `search` y repite con su `id`. No digas que no existe: puede estar fuera de tu alcance. |
| `UPLOAD_DESTINATION_TYPE_NOT_ACCEPTED` | El destino no admite ese tipo de archivo. | Pide el archivo en uno de los tipos que dice el mensaje. |
| `UPLOAD_DESTINATION_FILE_TOO_LARGE` | El archivo pasa del tope del destino. Trae `maxFileBytes`. | Pide uno más ligero. |
| `UPLOAD_DESTINATION_TOO_MANY_FILES` | Más archivos de los que admite el destino de una vez. | Repártelos en varias llamadas. |
| `TOO_MANY_UPLOAD_RESERVATIONS` | Demasiadas reservas sin terminar. Trae `retryAfterSeconds`. | Confirma o libera con `abort_upload` las que tienes, y espera. |
| `UPLOAD_NOT_CONFIRMED` | La operación del destino recibe un `fileId` sin confirmar. | Llama antes a `confirm_upload` y repite. |
| `UPLOAD_DESTINATION_MISMATCH` | El archivo se subió para otro destino u otra ficha, o su contenido cambió después de confirmarlo; en ese caso se ha borrado. | Súbelo otra vez con el destino y la ficha de esa operación. |
| `UPLOAD_DESTINATION_EXPIRED` | Han pasado las 24 horas. | Reserva otra subida con el mismo destino. |
| `UPLOAD_DESTINATION_IN_USE` | El archivo ya se está usando o ya se ha usado. | No repitas a ciegas: mira cómo ha quedado la ficha. |
| `UPLOAD_DESTINATION_FILE_NOT_FOUND` | No hay ninguna subida tuya con ese `fileId` para ese destino. | Mira tus subidas con `list`. |
| `UPLOAD_DESTINATION_UNAVAILABLE` | No se puede comprobar el destino ahora mismo. No se ha reservado ni usado nada. | Pide a la persona que lo suba desde la aplicación. |
| `UPLOAD_DESTINATION_READ_BUSY` | Hay otros archivos grandes leyéndose a la vez. Trae `retryAfterSeconds`. | Repite la misma llamada pasados esos segundos. |
| `UPLOAD_LINK_NOT_IN_DIRECTORY` | `create_upload_link` desde un conector de un directorio. | Pide a la persona que lo suba desde la aplicación. |

Para subir a una carpeta del Gestor de archivos, a un lead, un cliente, un contacto o una oportunidad, las herramientas son `fm_files_manage` y `crm_files_management` del servidor `utilia-crm`, con las mismas dos salidas; lo explica [Subir un archivo cuando el PUT no llega](https://os.utilia.ai/docs/developers/mcp/reference#subir-un-archivo-cuando-el-put-no-llega).
