# MCP Best Practices - Referência Completa

> Extraído de auditorias a 27+ projectos MCP (500+ ferramentas).
> Ver também: [PROC-MCP-Desenvolvimento.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-MCP-Desenvolvimento.md)

---

## Nomenclatura de Tools

**Padrão:** `{serviço}_{acção}_{recurso}` em snake_case

```
# Correcto
get_customer_notes
create_project_task
list_invoice_items
delete_session_token

# Errado
getCustomerNotes     (camelCase)
customer-notes-get   (kebab-case)
get_customer_notes_with_all_billing_details_and_history  (>40 chars)
```

**Limites obrigatórios:**
- Tool name: ≤ 40 caracteres
- Total com prefixo `mcp__<servidor>__<tool>`: ≤ 64 caracteres

**Validação em código:**
```typescript
function validateToolName(name: string): void {
  if (name.length > 40) {
    throw new Error(`Tool "${name}" excede limite de 40 chars (${name.length})`);
  }
}
allTools.forEach(t => validateToolName(t.name));
```

---

## Annotations Obrigatórias

Cada tool deve declarar as suas annotations para orientar o modelo:

```typescript
{
  name: 'get_customer',
  description: 'Obtém dados de um cliente pelo ID',
  annotations: {
    readOnlyHint: true,       // Não modifica estado
    destructiveHint: false,   // Não é destrutiva
    idempotentHint: true,     // Mesmo resultado em chamadas repetidas
    openWorldHint: false,     // Opera em dados internos (fechado)
  },
  inputSchema: { ... }
}
```

**Referência rápida:**

| Annotation | Tipo | Significado |
|------------|------|-------------|
| `readOnlyHint` | boolean | Leitura sem efeitos secundários |
| `destructiveHint` | boolean | Pode apagar/substituir dados |
| `idempotentHint` | boolean | Resultado idêntico em múltiplas chamadas |
| `openWorldHint` | boolean | Interage com sistemas externos/web |

**Inferência automática com `inferAnnotations()`:**

```typescript
import { inferAnnotations } from '@modelcontextprotocol/sdk/server/utils.js';

// Inferir automaticamente a partir do nome e descrição da tool
const annotated = inferAnnotations(tool);
```

---

## Capabilities Obrigatórias (Regra de Ouro)

```typescript
// ERRADO: capabilities incompletas -> erro 471
capabilities: { tools: {} }

// CORRECTO: sempre declarar as três, mesmo vazias
capabilities: {
  tools: {},
  resources: {},
  prompts: {}
}
```

**Handlers mínimos obrigatórios:**
```typescript
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...] }));
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
server.setRequestHandler(CallToolRequestSchema, async (req) => { ... });
```

---

## Formatos de Resposta

### Dual Format (JSON + Markdown)

Para máxima compatibilidade, devolver ambos:

```typescript
return {
  content: [
    {
      type: 'text',
      text: `# Cliente ${data.name}\n\n**ID:** ${data.id}\n**Email:** ${data.email}`
    }
  ],
  structuredContent: {
    id: data.id,
    name: data.name,
    email: data.email,
    status: data.status
  }
};
```

### Erros Accionáveis

```typescript
// ERRADO: mensagem genérica
throw new Error('Falha ao obter dados');

// CORRECTO: mensagem accionável com contexto e próximos passos
return {
  content: [{
    type: 'text',
    text: [
      `Erro ao obter cliente ID ${customerId}.`,
      `Causa: ${error.message}`,
      `Verificar: 1) ID existe na BD  2) Permissões de acesso  3) Conexão à BD`
    ].join('\n')
  }],
  isError: true
};
```

---

## Validação com Zod

```typescript
import { z } from 'zod';

const GetCustomerSchema = z.object({
  customer_id: z.number().int().positive(),
  include_invoices: z.boolean().optional().default(false),
  date_from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
});

// No handler
const validated = GetCustomerSchema.parse(args);
// Zod lança ZodError automaticamente se inválido
```

---

## Error Handling

```typescript
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    const result = await handleTool(name, args);
    return result;
  } catch (error) {
    if (error instanceof z.ZodError) {
      return {
        content: [{
          type: 'text',
          text: `Parâmetros inválidos:\n${error.errors.map(e => `  - ${e.path.join('.')}: ${e.message}`).join('\n')}`
        }],
        isError: true
      };
    }

    return {
      content: [{
        type: 'text',
        text: `Erro: ${error instanceof Error ? error.message : String(error)}`
      }],
      isError: true
    };
  }
});
```

---

## Logging

```typescript
// SEMPRE usar console.error (não console.log — interfere com stdio)
console.error(`[MCP:${toolName}] Início — params: ${JSON.stringify(args)}`);
console.error(`[MCP:${toolName}] Concluído em ${duration}ms`);
```

---

## Segurança (Checklist Pré-Commit)

- [ ] SQL injection: inputs validados antes de entrar em queries?
- [ ] Interpolação directa em SQL proibida sem validação
- [ ] Transacções em operações multi-query relacionadas
- [ ] Recursos (pool.connect) têm `finally` com release
- [ ] Cleanup (ROLLBACK) tem try-catch próprio
- [ ] `crypto.randomBytes()` em vez de `Math.random()`
- [ ] Secrets em variáveis de ambiente, nunca hardcoded
- [ ] `pnpm audit` sem vulnerabilidades críticas

**Grep de validação:**
```bash
# Interpolação SQL perigosa
grep -rn '`.*\${.*}`' src/ | grep -i 'select\|insert\|update\|delete\|order'

# Math.random em produção
grep -rn 'Math.random' src/

# connect() sem finally
grep -rn '\.connect()' src/
```

---

## Padrões SQL (Perfex CRM)

```typescript
// Perfex usa 0 como "não definido", NÃO NULL
client_id || 0,
project_id || 0,

// Excepção: source em leads precisa ID válido
const [defaultSource] = await db.query(
  'SELECT id FROM tblleads_sources ORDER BY id ASC LIMIT 1'
);
leadSource = source || defaultSource?.id || 1;
```

**Tabelas Perfex (prefixo tbl):**
```
tblclients, tblprojects, tbltasks, tblinvoices,
tblleads, tblleads_sources, tblstaff, tbltaskstimers, tblexpenses
```

---

## Transportes

| Transporte | Estado | Uso |
|------------|--------|-----|
| StreamableHTTP | Recomendado | Novos MCPs, acesso remoto |
| stdio | Válido | Claude Code local, scripts |
| SSE | Deprecated | Retrocompatibilidade apenas |

**Portas HTTP reservadas (3200+):**

| Porta | MCP |
|-------|-----|
| 3200 | outline-postgresql |
| 3201+ | disponíveis |

**Portas SSE reservadas (3100+):**

| Porta | MCP |
|-------|-----|
| 3100 | desk-crm-v3 |
| 3101 | memory-supabase |
| 3102 | wikijs |
| 3103 | ssh-unified |
| 3105 | n8n |
| 3106 | cwp |
| 3107 | youtube-research |
| 3108 | moloni |
| 3109+ | disponíveis |

---

*best-practices.md v2.0 | 2026-03-10*
