knowledge-base/records/plugin-dev/KB-PLUGIN-022-mcprotocol-tool-input-patterns.md
Rodolpho Lopes 200cd6c2ce kb: KB-PLUGIN-031 runbook de workflow dev/deploy de plugins + commit dos registros 021-030 pendentes
- Novo runbook KB-PLUGIN-031: nascimento do plugin ate validacao E2E no
  GLPI dev (scaffold, Forgejo local, deploy CT100, console, bootstrap
  Kernel para testes CLI). Validado de ponta a ponta com o assetinherit.
- Registros KB-PLUGIN-021..030 existiam apenas no disco (drift) e foram
  incluidos no versionamento; index.json sincronizado via kb-fix.
- Ignora lixo AppleDouble/.DS_Store do macOS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:59:47 +00:00

167 lines
5.7 KiB
Markdown

---
id: KB-PLUGIN-022
title: mcprotocol — Padrões de validação e ergonomia em tool inputs
domain: plugin-dev
tags:
- mcp
- tools
- validation
- ergonomics
- llm
- patterns
status: active
severity: medium
created_at: 2026-05-26
updated_at: 2026-05-26
applies_to:
- glpi-11
- mcprotocol-plugin
related_records:
- KB-PLUGIN-019
- KB-PLUGIN-020
---
# mcprotocol — Padrões de validação e ergonomia em tool inputs
## Contexto
Tools do MCP são chamadas por LLMs que **não conhecem IDs internos** do GLPI nem o formato exato esperado pelos campos. Sem padrões de entrada bem desenhados, o LLM precisa fazer várias chamadas exploratórias (listar estados → encontrar ID → criar projeto), gastando turns e tokens. Pior: quando erra, recebe mensagens de erro do ORM crípticas (`SQL error`, `boolean false`) e não consegue se autocorrigir.
Este registro documenta os padrões implementados no `ProjectTools::handleCreate` que resolvem esses problemas, **aplicáveis a qualquer tool de criação/atualização** no plugin.
## Padrões implementados
### 1. `resolveReference` — Aceita ID ou nome
**Problema:** LLM não sabe que `projectstates_id=2` é "Processing".
**Solução:** helper que aceita int (validando existência) ou string (resolvendo por nome).
```php
private static function resolveReference(
string $table, // ex: 'glpi_projectstates'
$value, // int OU string
string $field, // nome do campo (pra mensagem de erro)
array &$errors // acumulador de erros
): ?int
```
**Caso int / string numérica:**
- Valida existência via `SELECT id FROM {$table} WHERE id = ?`
- Se não existe: `"{$field}: ID {$id} não existe em {$table}."`
**Caso string (nome):**
- Busca via `SELECT id FROM {$table} WHERE name = ?`
- Se não encontra: lista até 20 nomes disponíveis na mensagem de erro
**Exemplo de erro útil pra LLM:**
```
projectstates_id: nome 'AbacaxiDoido' não encontrado em glpi_projectstates.
Disponíveis: New, Processing, Closed.
```
A LLM lê isso e **se autocorrige** na próxima chamada — sem precisar de uma tool separada `glpi_projectstate_list`.
### 2. `normalizeDate` — Normaliza formato
**Problema:** Datas vêm em formatos variados (`2026-06-01`, `2026-06-01 00:00:00`, ISO 8601, etc.). GLPI espera `Y-m-d H:i:s`.
**Solução:** helper aceita os 2 formatos mais comuns e normaliza:
```php
private static function normalizeDate(
string $value,
string $field,
array &$errors
): ?string
```
- `YYYY-MM-DD` → preenche `00:00:00`
- `YYYY-MM-DD HH:MM:SS` → mantém
- Qualquer outro: erro claro com formato esperado
### 3. Validação prévia agregada (fail-loud-once)
**Problema:** Validar campo por campo lançando exception faz a LLM corrigir 1 erro por chamada — N chamadas pra N erros.
**Solução:** acumula erros num array e lança UMA exception no final:
```php
$errors = [];
// ... várias validações que appendam em $errors
if (!empty($errors)) {
throw new \Exception("Falha na validação: " . implode(" | ", $errors));
}
```
A LLM recebe **todos os problemas de uma vez**, corrige tudo e tenta de novo.
### 4. Response inclui `applied` (debug & confiança)
**Problema:** depois de resolver nomes → IDs, normalizar datas, etc., a LLM não sabe o que efetivamente foi gravado.
**Solução:** resposta de sucesso inclui o input final aplicado no ORM:
```php
return [
'status' => 'success',
'id' => $newId,
'message' => 'Projeto criado com sucesso.',
'applied' => $input // mostra o que realmente entrou no DB
];
```
A LLM confirma `projectstates_id: 2` (resolvido de "Processing"), `plan_start_date: 2026-06-01 00:00:00` (normalizado), e segue confiante.
### 5. Schema usa `oneOf` pra polimorfismo
Campos que aceitam int ou string declaram isso explicitamente no `inputSchema`:
```php
'projectstates_id' => [
'description' => 'Estado. Aceita ID (int) ou nome (ex: "New", "Processing", "Closed").',
'oneOf' => [
['type' => 'integer'],
['type' => 'string']
]
]
```
A descrição já lista exemplos comuns — a LLM começa certo na primeira tentativa.
## Quando aplicar
**Sempre que uma tool tem campo que referencia outra tabela GLPI:**
- `users_id``glpi_users` (login ou ID)
- `groups_id``glpi_groups` (nome ou ID)
- `entities_id``glpi_entities` (nome ou ID)
- `itilcategories_id``glpi_itilcategories`
- `locations_id``glpi_locations`
- `tickets_states`, `priority`, `urgency`, `impact` (esses já são enums numéricos GLPI — pode aceitar string como atalho: "Alta"=4)
**Sempre que aceitar data:** usar `normalizeDate`.
**Sempre que houver múltiplas validações:** agregar erros.
**Sempre na resposta de sucesso:** retornar `applied`.
## Anti-padrões a evitar
| Anti-padrão | Por quê é ruim |
|---|---|
| Aceitar só ID int | LLM precisa fazer N chamadas exploratórias |
| Mensagem de erro do tipo "Erro ao criar projeto" | LLM não sabe o que corrigir |
| Lançar exception no primeiro erro | LLM corrige 1 problema por vez |
| Não validar antes do ORM | Erros de SQL/ORM são incompreensíveis pra LLM |
| Não retornar input aplicado | LLM perde rastreabilidade do que foi gravado |
## Referências de implementação
- `src/ProjectTools.php` — handler `handleCreate` (referência canônica deste padrão)
- `src/ToolRegistry.php` — registro central de tools
- Spec MCP — Tool Input Schemas: https://spec.modelcontextprotocol.io/specification/server/tools
## Próxima evolução
Extrair os helpers (`resolveReference`, `normalizeDate`) para uma classe utilitária `src/InputValidation.php` quando o segundo handler precisar do mesmo padrão (provavelmente `glpi_project_update` ou `glpi_ticket_create` com `users_id`/`itilcategories_id`).