Metadata-Version: 2.5
Name: foundry-boards-mcp
Version: 0.1.0
Summary: MCP para administrar los tableros Kanban de Foundry desde agentes de IA.
Project-URL: Homepage, https://foundry.collie.digital
Author: Collie Valley
License-Expression: MIT
License-File: LICENSE
Keywords: agents,agile,claude,foundry,kanban,mcp,model-context-protocol
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: Spanish
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Bug Tracking
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: keyring>=25
Requires-Dist: mcp<2,>=1.12
Requires-Dist: python-dotenv>=1.0
Description-Content-Type: text/markdown

# Foundry Boards — MCP para agentes de IA

Servidor **MCP** (Model Context Protocol) que le da a **Claude Code, Codex, OpenCode, Gemini**
y otros clientes compatibles herramientas para
**administrar los tableros de Foundry** con un **modelo de gestión de trabajo ágil** (ADR-0014):
listar/crear tableros, crear/mover/editar tarjetas (work items), **transicionar por estados**,
gestionar **jerarquía** (épicas/historias/subtareas), **etiquetas**, **vínculos**, **sprints**
(Scrum), leer el **historial estructurado** y **gestionar adjuntos** (leer imágenes, subir y
borrar) — además de mover un tablero personal a un Área.

No forma parte del backend ni lo modifica: habla el **mismo contrato REST público** que el
frontend, autenticándose como una **cuenta de usuario** (JWT). Corre en **tu máquina**, igual que
`integrations/discord-porrin` y `integrations/porrin-meet`.

```
Agente IA  ──stdio──►  foundry-boards (este MCP)  ──HTTP + JWT──►  Foundry API (/api/v1)
                              tools de tablero                         (permisos por rol)
```

## Por qué un MCP (y no llamar al backend a mano)
Con el MCP conectado, le pides al agente cosas en lenguaje natural y opera el tablero:
- *"Lista mis tableros y muéstrame las tarjetas del de Marketing."*
- *"Crea la tarea 'Migrar auth a OAuth' en En progreso, prioridad alta, asígnala a Ana."*
- *"Mueve todas las tarjetas de En revisión que ya estén aprobadas a Hecho."*

Los **permisos los valida el backend por rol**: dueño/colaborador pueden mutar; **observador solo
lee** (una mutación devuelve 403 y Claude te lo dice).

## Requisitos
- **Python 3.10+** (lo gestiona `uv`; no necesitas python/pip en el PATH).
- **Un backend con el que hablar.** Por omisión, el Foundry público
  (`https://foundry.collie.digital`): no hace falta levantar nada. Para apuntar a otro —el tuyo en
  local, o uno propio— se dice con `FOUNDRY_API_BASE` (ver *Configuración*).
- Una **cuenta de Foundry**; para administrar tableros de un Área, esa cuenta debe ser
  **miembro colaborador/dueño** de ese Área. Puede ser tu propia cuenta o una dedicada.

## Instalación (con `uv`)
```powershell
cd integrations/foundry-mcp
.\run.ps1            # crea .venv, instala deps y hace un chequeo de humo (no pide credenciales)
```
Manual (o en Linux/Mac):
```bash
cd integrations/foundry-mcp
uv sync                        # crea .venv e instala deps desde pyproject.toml/uv.lock (portable)
uv run python -m foundry_mcp   # (opcional) arranque manual por stdio; normalmente lo lanza Claude Code
```
No necesitas `.env`: **no se configuran credenciales**. Como mucho, un `.env` con
`FOUNDRY_API_BASE` para apuntar a otro backend, `FOUNDRY_AGENT_SOURCE` para corregir la identidad
de un cliente que no se anuncie bien o `FOUNDRY_AGENT_MODEL` para declarar el modelo usado (tu
`.env` local **gana** sobre el default del `.mcp.json`).

## Identidad del agente en tareas, comentarios e historial

Cada descripción, comentario, avance, bloqueo, verificación o cambio de historial creado por el
MCP conserva la procedencia `ia` y guarda dos datos separados:

- `agent_source`: `claude_code`, `codex`, `opencode`, `gemini`, `other` o `unknown`.
- `agent_model`: nombre abierto del modelo concreto, por ejemplo `gpt-5`, `claude-sonnet-*` o
  `gemini-2.5-pro`.

Foundry los muestra como `IA · Codex · gpt-*` junto a la descripción, en cada aporte y en cada
evento del historial; no hace falta firmar el texto. El MCP adjunta la identidad como cabeceras a
sus peticiones, de modo que también cubre movimientos, etiquetas, vínculos, sprints y otras
mutaciones que dejan `ChangeLog`.

- `FOUNDRY_AGENT_SOURCE=auto` (recomendado) usa `clientInfo` de la sesión MCP.
- Si el cliente se identifica con un nombre genérico, fija un override, por ejemplo
  `FOUNDRY_AGENT_SOURCE=codex`.
- MCP no estandariza el modelo en `clientInfo`: fija `FOUNDRY_AGENT_MODEL` si el proceso usa uno
  estable, o pasa `agent_model` en la herramienta si cambia dentro de la misma sesión. El servidor
  también reconoce, si existen, `CODEX_MODEL`, `CLAUDE_MODEL`, `ANTHROPIC_MODEL`,
  `OPENCODE_MODEL` y `GEMINI_MODEL`.
- Los clientes antiguos siguen funcionando: sus descripciones y eventos de agente aparecen como
  `IA · Desconocido · Modelo desconocido`.
- La identidad es **declarada por el cliente o por la configuración local**, no una certificación
  criptográfica. No debe usarse como control de autorización; los permisos siguen dependiendo del
  JWT y del rol de la cuenta.

## Autenticación (interactiva — te la pide Claude Code)
No hay credenciales en archivos ni en variables de entorno. La **primera acción** sobre un tablero
(o la tool `login`) abre un **diálogo de Claude Code** que te pide **email + contraseña** (o pegar un
**token**). Con eso el MCP obtiene un JWT y lo usa durante la sesión.

- El **token** se cachea en **memoria** para la sesión. Si marcas **"recordar"** en el diálogo, se
  guarda en el **gestor de credenciales del sistema operativo** (Windows Credential Manager / macOS
  Keychain / Secret Service) — **nunca** en `.env`, en un archivo ni en el repo. La **contraseña no
  se guarda en ningún lado**: solo se usa para obtener el token.
- El token de Foundry dura **24 h** y no hay renovación automática (la contraseña no se guarda).
  Al caducar, el MCP **relee primero el gestor de credenciales**: si otra sesión ya renovó (y
  guardó con "recordar"), lo reutiliza **sin preguntar** — con varias ventanas de Claude Code
  abiertas, solo la primera pide login. Si el guardado también caducó, vuelve el diálogo (una vez
  al día como mucho).
- `login` inicia sesión por adelantado o cambia de cuenta; `logout` olvida el token (memoria + SO).
- **Automatización/CI (sin diálogo):** define `FOUNDRY_TOKEN`, o `FOUNDRY_EMAIL` + `FOUNDRY_PASSWORD`
  en el entorno y el MCP se autentica sin preguntar. Es opcional; para uso normal, no lo definas.

> Se usa *elicitation* en **modo formulario**. Para un backend local que tú controlas es lo práctico;
> como solo se persiste el JWT (nunca la contraseña), el riesgo es acotado.

## Registrar en Claude Code
El registro solo necesita **cómo lanzar** el server (sin secretos: la sesión se pide en runtime).
El repo ya trae un **`.mcp.json` en la raíz** (scope de **proyecto**, compartido con el equipo) que
lanza el server con `uv run` — **portable** (Windows/Linux/Mac, sin rutas de `.venv`) — y fija el
backend de prod:

```json
{
  "mcpServers": {
    "foundry-boards": {
      "type": "stdio",
      "command": "uv",
      "args": ["run", "--directory", "${CLAUDE_PROJECT_DIR:-.}/integrations/foundry-mcp", "python", "run_server.py"],
      "env": {
        "FOUNDRY_API_BASE": "https://foundry.collie.digital/api/v1",
        "FOUNDRY_AGENT_SOURCE": "auto"
      }
    }
  }
}
```
Si ese proceso usa un modelo fijo, añade
`"FOUNDRY_AGENT_MODEL": "<modelo-actual>"` dentro de `env`. Si permite cambiar de modelo durante
la sesión, deja la configuración sin ese valor y usa el argumento `agent_model` de cada aporte,
creación o edición de tarea.
Claude Code **pide aprobarlo** la primera vez (`claude mcp reset-project-choices` para reelegir).
Requiere `uv` en el PATH (el mismo que usa el backend).

**Alternativa personal** (sin `.mcp.json`, solo para ti): `claude mcp add-json foundry-boards '<json>'`
con la misma estructura pero **ruta absoluta** en `--directory` (en `~/.claude.json` no se expande
`${CLAUDE_PROJECT_DIR}`). Si no vas a tocar el código del MCP, es más simple instalarlo: el
[`mcp.json.example`](./mcp.json.example) trae esa forma, la de *[Instalarlo sin el
repo](#instalarlo-sin-el-repo-desde-pypi)*, que no depende de dónde tengas el proyecto.

Verifica: `/mcp` debe mostrar `foundry-boards` con sus tools. Prueba *"usa whoami de foundry-boards"*.

## Compartir con el equipo (onboarding)
Todo lo necesario ya vive en el repo (el `.mcp.json` y el código); **no hay secretos que repartir** —
cada quien entra con **su** cuenta. Cada compañero, **una vez**:
1. Tener **`uv`** en el PATH y el repo al día (`git pull`).
2. **Preparar el entorno** para que el primer arranque no expire:
   `cd integrations/foundry-mcp && uv sync` (o `.\run.ps1` en Windows).
3. Abrir **Claude Code** en la carpeta del repo → **aprobar** el `.mcp.json` cuando lo pregunte.
4. `/mcp` → `foundry-boards` (74 tools: tablero + notas). La primera acción abre el **diálogo de login**: entra con
   **tu** cuenta (debes ser miembro colaborador/dueño de los espacios que quieras editar).

- **Backend:** por defecto prod (fijado en el `.mcp.json`). Para apuntar a otro (p. ej. local), crea
  un `.env` en `integrations/foundry-mcp/` con `FOUNDRY_API_BASE=...` — tu `.env` local **gana** sobre
  el default del `.mcp.json`.
- **Si ya lo tenías registrado a mano** (scope local/usuario), quítalo para que mande el de proyecto:
  `claude mcp remove foundry-boards -s local`.

## Instalarlo sin el repo (desde PyPI)
Quien **no** tiene el monorepo —ni tiene por qué tenerlo— instala el paquete publicado. Basta con
`uv` en el PATH: `uvx` lo baja, lo cachea y lo ejecuta sin dejar nada en tu carpeta de trabajo.

```powershell
claude mcp add-json foundry-boards '{ "type":"stdio", "command":"uvx", "args":["foundry-boards-mcp"] }'
```

Y ya está: **no hay que configurar ningún backend**, porque por omisión habla con el Foundry
público (`https://foundry.collie.digital`). Lo demás es igual que para el equipo — la primera
herramienta abre el **diálogo de login** y entras con **tu** cuenta.

> **Aún no publicado.** La primera versión todavía no está en PyPI; hasta entonces, usa la vía
> desde git de aquí abajo. El nombre ya está decidido: `foundry-mcp` está cogido por un proyecto
> ajeno, así que el paquete se llama **`foundry-boards-mcp`** y el comando, igual.

- Para clavar una versión concreta: `"args": ["foundry-boards-mcp@0.1.0"]`.
- Para apuntar a otro backend —el tuyo en local, o uno propio—, añade
  `"env": {"FOUNDRY_API_BASE": "http://localhost:8000/api/v1"}`.
- Necesitas una **cuenta de Foundry**; para editar los tableros de un Área, ser miembro
  colaborador o dueño de ella. No hay nada más que repartir: ni claves, ni tokens.

**Desde git, sin esperar a una publicación** (requiere acceso al repositorio, que es privado):

```powershell
claude mcp add-json foundry-boards '{ "type":"stdio", "command":"uvx", "args":["--from","git+https://github.com/gepres/foundry.git#subdirectory=integrations/foundry-mcp","foundry-boards-mcp"] }'
```

- Se registra en tu scope **local/usuario** (ruta git absoluta; `${CLAUDE_PROJECT_DIR}` no aplica
  sin repo).
- Para fijar rama o etiqueta: `...foundry.git@<tag-o-rama>#subdirectory=integrations/foundry-mcp`
  (sin `@`, usa la rama por defecto = último `main`).
- La primera vez uv **clona el repo una vez** (en su caché, no en tu workspace) para construir el
  paquete; los arranques siguientes son rápidos.

## Herramientas
**Sesión**
- `login` — inicia sesión (abre el diálogo de credenciales) o cambia de cuenta.
- `logout` — olvida el token (memoria + gestor de credenciales del SO).

**Lectura**
- `whoami` — la cuenta con la que actúa el MCP (verifica la sesión).
- `list_spaces` — Áreas de trabajo visibles.
- `list_boards(space_id?)` — tableros personales (sin `space_id`) o los de un Área.
- `list_all_boards` — panorama: personales + los de cada Área.
- `get_board(initiative_id)` — tablero completo: columnas con sus tarjetas.

**Tableros**
- `list_board_presets()` — las **plantillas de columnas** para el alta: `key`, `label`, `template`
  (`kanban`/`scrum`), `is_default` y las columnas de cada una con su categoría. Léelas antes de
  crear: las claves no se adivinan.
- `create_board(title, space_id?, preset?, columns?)` — tablero nuevo (personal o de un Área).
  `preset` es la clave de una plantilla; `columns` monta las columnas **a medida**
  (`[{"name": "Ideas", "category": "todo", "color": "#5b9bd5"}, …]`, entre 2 y 10, con al menos una
  `todo` y una `done`). Con columnas a medida manda también el `preset` del que partes: es quien
  fija la plantilla del tablero y, con ella, si hay sprints.
- `move_board_to_space(initiative_id, space_id)` — comparte un tablero personal con un Área.

**Columnas del tablero** (FNV-267; el servidor aplica el tope de 10, los nombres sin repetir y el
invariante de ≥1 de entrada y ≥1 de cierre)
- `add_column(initiative_id, name, category="in_progress", color?)` — al final del tablero.
- `rename_column(initiative_id, column, name)` — el id no cambia: **el historial no se toca**.
- `set_column_category(initiative_id, column, category)` — `todo` · `in_progress` · `done`; es lo
  que decide dónde entra una tarjeta y qué la da por cerrada.
- `set_column_color(initiative_id, column, color?)` — `#rrggbb`, o vacío para heredar el de su tipo.
- `reorder_columns(initiative_id, columns)` — **todas** las columnas en el orden que quedan.
- `delete_column(initiative_id, column, reassign_to?)` — si tiene tarjetas, `reassign_to` dice a
  dónde se mudan (se avisa antes con cuántas son). Marcada como **destructiva** para el cliente.

**Tarjetas (work items)**
- `create_task(initiative_id, title, column="Backlog", description?, priority?, kind?, assignee?, issue_type?, parent_id?, story_points?)`
- `move_task(initiative_id, task_id, column, order=0)`
- `transition_task(initiative_id, task_id, status, order=0)` — mueve por **estado** del workflow
  (la columna destino); deja rastro (status_id/column_id).
- `edit_task(initiative_id, task_id, title?, description?, priority?, kind?, assignee?)` — **parcial**.
- `set_task_hierarchy(initiative_id, task_id, issue_type, parent_id?)` — tipo + padre (jerarquía).
- `set_task_details(initiative_id, task_id, story_points?, start_date?, due_date?, flagged?)` —
  **PATCH**: solo cambia lo que pases, conserva lo demás. Fechas ISO; `""` borra una fecha.
- `set_task_labels(initiative_id, task_id, labels[])` — etiquetas por **nombre** (crea las que falten).
- `link_tasks(initiative_id, task_id, target_id, link_type)` — `blocks|is_blocked_by|relates_to`.
- `get_task_links(initiative_id, task_id)` — vínculos de la tarjeta (id, tipo, la otra tarjeta).
- `unlink_tasks(initiative_id, task_id, target_id, link_type?)` — quita el/los vínculo(s) hacia esa
  tarjeta (resuelve el id del vínculo solo; sin `link_type` borra todos los que apunten a ella).
- `link_repo(initiative_id, task_id, url, kind?, title?)` — vincula la tarjeta a un recurso de
  repositorio (`kind`: `repository|branch|pull_request|commit`); ideal para asociar el repo/branch/PR
  donde trabajas (ADR-0016, Opción A).
- `get_repo_links(initiative_id, task_id)` — recursos de repo de la tarjeta (id, url, kind, title).
- `unlink_repo(initiative_id, task_id, url)` — quita el vínculo de repo cuya URL coincide.
- `delete_task(initiative_id, task_id)`
- `create_tasks_from_seeds(initiative_id, seed_ids)` — Vivero→Tablero (tarjetas propuestas).

**Épicas**
- `create_epic(initiative_id, title, column?, description?, priority?)` — work item raíz.
- `list_epics(initiative_id)` — tarjetas con `issue_type=epic`.

**Sprints (Scrum)**
- `list_sprints(initiative_id)` — sprints del tablero (state future|active|closed).
- `create_sprint(initiative_id, name, goal?, start_date?, end_date?)` — sprint planificado.
- `start_sprint(initiative_id, sprint_id)` — future → active (uno activo por tablero).
- `complete_sprint(initiative_id, sprint_id)` — active → closed (incompletas vuelven al backlog).
- `assign_task_to_sprint(initiative_id, task_id, sprint_id?)` — a un sprint, o al backlog si se omite.

**Historial de la tarjeta**
- `add_task_entry(initiative_id, task_id, content, kind="avance", meta=None)` — registra un **aporte
  tipado** (ADR-0017) en el hilo, para que el output quede estructurado, no aplanado en texto.
  `kind`: `avance` · `criterio` · `decision` · `bloqueo` (marca la bandera de impedimento) ·
  `verificacion` · `pregunta` · `nota`. `meta` es JSON libre (p. ej. `{"commit": "abc123"}`). Queda
  como aporte del agente (procedencia `ia`).
- `add_task_comment(initiative_id, task_id, content)` — deja una **nota** libre (equivale a
  `add_task_entry` con `kind="nota"`). Queda marcada como nota del agente.
- `get_task_comments(initiative_id, task_id)` — lee el hilo (autor, tipo human|agent, procedencia,
  tipo de aporte, meta, texto y fecha).
- `get_task_history(initiative_id, task_id)` — historial **estructurado** (ADR-0014, E4): qué campo
  cambió, de qué valor a qué valor, quién y cuándo (created·status·priority·assignee·sprint·label·…).

**Datos con gate humano (ADR-0017 F2)** — la IA **propone**, una persona acepta/descarta. El gate es
**simétrico**: se ve y se completa por los **dos lados** (el tablero y estas tools). Si la sesión de
Claude se cierra, la decisión se toma en la app; al reabrir, `list_*` te muestra qué se decidió.
- `propose_criterion(initiative_id, task_id, text)` — propone un **criterio de aceptación** ("para
  darla por hecha debe cumplirse Z"). Nace `proposed` (procedencia `ia`).
- `record_decision(initiative_id, task_id, title, rationale="", repo_url="")` — registra una
  **decisión** de diseño (ADR-lite). Nace `proposed`. `repo_url` la ata a un commit/PR (ADR-0016)
  para la trazabilidad IA→código.
- `list_criteria(initiative_id, task_id)` / `list_decisions(initiative_id, task_id)` — **leen** los
  criterios/decisiones con su `status` (`proposed|accepted|rejected`), procedencia y datos del gate.
  Úsalas para confirmar qué se aceptó/descartó (incluso si se decidió desde la app).
- `review_criterion(initiative_id, task_id, criterion_id, accept)` /
  `review_decision(initiative_id, task_id, decision_id, accept)` — **gate humano** desde Claude:
  `accept=True` lo vuelve oficial, `False` lo descarta. Es la decisión de una persona (igual que el
  botón del tablero): úsalas solo a pedido explícito; **no auto-apruebes** lo que tú propusiste.
- `review_task_proposal(initiative_id, task_id, accept)` — gate humano de una **tarjeta** propuesta
  por la IA (`status_agent="proposed"`): la acepta (tarjeta oficial) o la rechaza (queda `rejected`,
  oculta pero con traza). Su estado se lee en `get_board`.

**Adjuntos**
- `list_task_attachments(initiative_id, task_id)` — metadatos de los adjuntos de una tarjeta
  (id, filename, content_type, size, quién los subió y cuándo).
- `read_task_attachment(initiative_id, task_id, attachment_id)` — lee un adjunto: las **imágenes** se
  devuelven como imagen (Claude las **ve**: mockups, capturas, diagramas), el **texto** (text/*, json,
  csv, yaml…) como texto, y otros binarios como metadatos + aviso. Límite 10 MB (backend).
- `upload_task_attachment(initiative_id, task_id, path? | text? | base64_data?, filename?)` — **sube**
  un archivo: desde una ruta del disco, desde un texto que el agente tiene en la mano o desde bytes
  en base64 (estas dos exigen `filename`, del que sale el tipo de contenido). Una sola de las tres.
- `delete_task_attachment(initiative_id, task_id, attachment_id)` — **borra** un adjunto.
  Irreversible; devuelve qué se borró (nombre y tamaño) en vez de un OK mudo.

**Prompts** (los invoca el usuario desde su cliente, como un comando; FNV-270)
- `trabajar_tarjeta(initiative_id, task_id)` — el ciclo completo: enterarse, hacer visible el plan,
  construir, dejar avance con su commit, dejar evidencia y entregar. Con lo que no se negocia:
  **«Hecho» lo decide una persona**.
- `dejar_avance(initiative_id, task_id)` — qué se construyó, qué quedó fuera, el commit y la
  evidencia con números (`parcial` si falta la prueba de fuego).
- `repasar_tablero(initiative_id)` — qué espera a una persona, qué dice estar hecho sin
  demostrarlo, qué lleva parado. Solo lee: no mueve nada.

**Qué declara cada herramienta** (FNV-268): todas dicen si **solo leen** (23), si **crean o
actualizan** sin destruir (45) o si **borran** (6). El cliente lo usa para saber qué puede aprobar
solo y dónde pararse — y ojo, lo no declarado se asume destructivo, así que callar no era neutral.
Las que borran (tarjeta, nota, adjunto, columna) además **preguntan antes**, diciendo qué se pierde
(FNV-269); si el cliente no sabe preguntar, siguen adelante en vez de bloquearse.

**Ergonomía pensada para el agente:**
- **Columnas/estados por nombre** (tolerante a acentos/mayúsculas) o por id. Cada tablero tiene las
  suyas —siete plantillas o las que haya montado el equipo (FNV-256)—, así que se leen con
  `get_board` y **nunca se suponen**: mira la `category` de cada columna (`todo`/`in_progress`/
  `done`), que es lo único estable. Al crear una tarjeta sin `column`, nace en la primera `todo`.
- **Responsable por nombre/email**: en tableros de Área resuelve el `assignee` contra los miembros;
  en tableros personales acepta un id. Para quitarlo: `edit_task(..., assignee="")`.
- `priority` ∈ `highest|high|medium|low|lowest` (también `baja|media|alta`); `kind` ∈
  `tarea|bug|incidencia`; `issue_type` ∈ `epic|historia|tarea|bug|subtarea`.

## Límites (a propósito, para no inventar API)
Este MCP envuelve lo que necesita del backend. **No** hace (aún):
- crear/renombrar/borrar/reordenar **columnas** y editar el **workflow** (el backend ya lo soporta,
  ADR-0014 E2; el MCP aún no lo envuelve — al crear un tablero toma el preset por defecto);
- **borrar** un tablero/iniciativa.

Si hace falta, se agregan como tools nuevas aquí.

## Seguridad
- Las mutaciones las autoriza el **servidor** por rol; el MCP no elude permisos.
- **No hay credenciales en archivos ni en variables de entorno** (salvo que actives el override de
  automatización). La sesión se pide en runtime; solo el **token** se cachea (memoria, u opcional
  gestor de credenciales del SO). La **contraseña no se persiste**.
- Da a la cuenta usada **solo** el acceso que necesite (invítala como colaborador únicamente a los
  espacios que deba administrar).

## Solución de problemas
- **Las tools piden login cada vez** → marca **"recordar"** en el diálogo para guardar el token en el
  gestor de credenciales del SO (si no, la sesión es solo en memoria y se pide en cada arranque).
- **`/mcp` muestra el server pero las tools fallan** → confirma que el backend esté arriba en
  `FOUNDRY_API_BASE`. Prueba `whoami`; si pide login, complétalo.
- **403 al mutar** → la cuenta es observador, o no es miembro de ese Área.
- **404 en un tablero/tarea** → id equivocado, o la cuenta no ve ese tablero.
- **El server no conecta** → verifica la ruta de `command` (Windows `.venv/Scripts/python.exe` vs
  POSIX `.venv/bin/python`) y que `run.ps1` haya creado el `.venv`.
- **"Este cliente no puede pedir credenciales…"** → el cliente MCP no soporta *elicitation*; usa el
  override de entorno (`FOUNDRY_TOKEN` o `FOUNDRY_EMAIL`/`FOUNDRY_PASSWORD`).
