# Tool Implementation Patterns

Standard patterns for MCP tool development.

## Tool Description Format

```
Tool summary.

**Actions**: action1, action2
**Params**: param1 (required), param2
**Response**: {format}
**Note**: important notes

📚 modyo://mcp-{module}/tools/{tool-name}
```

## Action Patterns

### CRUD Actions
| Action | Purpose |
|--------|---------|
| `manage` | Get/create/update (default) |
| `list` | List with pagination |
| `delete` | Delete resource |

### Lifecycle Actions
| Action | Purpose |
|--------|---------|
| `publish` | Publish draft |
| `unpublish` | Remove from published |
| `archive` | Archive resource |
| `restore` | Restore archived |

## Parameter Patterns

### Smart Identifier
```typescript
identifier: z.union([z.string(), z.number()])
```
- **Number** → Direct ID lookup
- **String** → Search by name/slug

### Pagination
```typescript
page: z.number().min(1).default(1)
per_page: z.number().min(1).max(100).default(30)
```

### Verification
```typescript
verify: z.boolean().default(false)
```
Returns before/after comparison.

## Validation

### Use Zod Refinements
```typescript
const schema = z.object({...})
  .refine(
    (data) => condition,
    { message: "Error message", path: ["field"] }
  );
```

Prefer refinements over `throw` in execute.

## Response Patterns

> Contrato completo y su razonamiento: [`response-contract.md`](response-contract.md).
> Es **obligatorio** para toda tool nueva; lo verifican las forcing functions de `tests/forcing-functions/`.

Toda tool responde con `structuredContent` bajo un envelope común y declara su `outputSchema`. `content[].text` lleva el mismo payload, serializado compacto.

```jsonc
{
  "platform": { "name": "langflow", "host": "langflow.modyo.cloud" },  // siempre (#172)
  "data":     { /* el resultado, propio de cada tool */ },             // siempre
  "resource": { "type": "space", "id": 636, "name": "Beneficios" },    // entidad resuelta/afectada
  "state":    { "returned": 100, "total": 314, "complete": false },    // cobertura o post-condición
  "control":  { "cancelled": true, "message": "…" }                    // solo si no se ejecutó
}
```

**No hay `message` de la operación en `data`.** La prohibición de #104 es **profunda**: ni en la raíz ni enlatado dentro de una colección — un `message` en `results[]` narra la operación igual que uno arriba. Lo humano-legible de la operación vive en `content[].text`, y `control.message` queda reservado a cancelaciones.

**La única excepción, y es por regla, no por lista:** un objeto puede declarar `message` si declara también un **`code`** en el mismo objeto. Un `message` de diagnóstico —"Step 'kyc' referencia una tarea inexistente"— es un dato, no narración; lo que #104 combate es la prosa *como único asidero*, que obliga al consumidor a ramificar sobre texto que cambia de redacción sin aviso. Con un `code` estable al lado hay sobre qué ramificar. El `code` sale de lo que el productor ya sabe (`customers-originations-validate` publica el `checkType` del validador); si hiciera falta inventar un catálogo de códigos, eso es una decisión de contrato y no se resuelve en la tool. Lo verifica `published-schema-contract.test.ts`.

### En el código

```typescript
protected override getOutputSchema() {
  // Solo el schema del `data`. El envelope lo agrega ToolBase.
  return z.object({ entries: z.array(z.looseObject({ uuid: z.string() })) });
}

return this.structured({ entries }, {
  resource: { type: "space", id: spaceId },
  state: { returned, total, page, per_page, complete: returned >= total },
});
```

Reglas que no se negocian:

- **Estricto lo nuestro, `looseObject` lo ajeno.** Lo que se ecoa de la API de Modyo va `z.looseObject`: un schema estricto sobre datos ajenos convierte cualquier cambio upstream en caída de disponibilidad.
- **Sin uniones de más de una rama estructurada en la salida**, a ninguna profundidad y sin whitelist. Una tool multi-acción declara **un solo objeto con todos los campos opcionales**.
- **Post-condición releída (#167):** el `state` y los valores persistidos de una escritura salen de la respuesta de la API o de un refetch, **nunca** de los parámetros de entrada. Ecoar el pedido certifica el pedido, no el hecho — y es lo que convierte el `"549895"` de #163 de corrupción silenciosa en error visible.
- **Cobertura declarada (#181):** una búsqueda informa `searched` (tamaño del corpus recorrido) y toda respuesta que dejó algo afuera pone `complete: false`. Un resultado vacío tiene que ser autoexplicativo sin una segunda llamada.
- **Eco de la entidad resuelta (#181 ítem 20):** cuando el identificador entra como nombre o slug y el servidor lo resuelve por búsqueda, `resource` dice qué resolvió.
- **El estado de dominio no es `state.status`.** El estado de una página o de un job va en `data` (`page_status`, `job_status`); `state.status` es el resultado de **esta** llamada.

### Cancelaciones y rechazos

```typescript
return this.controlResponse({ cancelled: true, message: "…" });
```

Una respuesta `isError: false` sin `structuredContent` en una tool con `outputSchema` es rechazada por el SDK con `-32602`. `controlResponse()` es el único camino correcto para una operación que no se ejecutó y no es un error.

## File Structure

```
src/tools/{category}/
├── Manage.ts           # Main CRUD tool
├── Copy.ts             # Cross-site copy
├── UpdateOrCreate.ts   # Idempotent operations
└── handlers/           # Action handlers (optional)
    ├── index.ts
    ├── listHandler.ts
    └── manageHandler.ts
```
