Regra de Ouro adicionada ao topo da skill: SDK v2 (@modelcontextprotocol/server), API de alto nível McpServer+registerTool (API de baixo nível Server+setRequestHandler proibida), transporte HTTP obrigatório (stdio só como excepção documentada), e proibição explícita de interpolação de dados em comandos shell/SSH. Motivado por revisão de segurança ao mcp-kivicare (19-08-2026): 8 bugs reais corrigidos em produção, incluindo injecção de comandos shell crítica e hooks nativos disparados com o tipo errado. Regras adicionadas para nunca mais acontecer em silêncio. - SKILL.md: nova secção Regra de Ouro, API/annotations/capabilities actualizadas para v2, transportes HTTP obrigatório, changelog v3.0.0, healing log com os 3 bugs mais graves do incidente. - references/templates.md: reescrito para createMcpHandler + createMcpExpressApp (HTTP) e serveStdio (excepção), package.json com pacotes v2. - references/best-practices.md: capabilities/annotations/error handling actualizados para API v2, checklist de segurança com regra anti-injecção de comandos shell, tabela de transportes com HTTP obrigatório. - references/evaluation-guide.md: script de evaluations reescrito para Client+StreamableHTTPClientTransport in-process (sem porta/socket). - infraestrutura: 1.2.2 -> 1.3.0
282 lines
8.9 KiB
Markdown
282 lines
8.9 KiB
Markdown
# 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 |
|
|
|
|
**v2 não tem `inferAnnotations()`** (era só v1) — declarar sempre `annotations` inline na config de `registerTool`, como no exemplo acima. Nunca inventar um passo de inferência que o SDK actual não tem.
|
|
|
|
---
|
|
|
|
## Capabilities — derivadas automaticamente (nunca declarar à mão)
|
|
|
|
`McpServer` (SDK v2) deriva `capabilities` de `registerTool`/`registerResource`/`registerPrompt` automaticamente — **não existe** `capabilities: {...}` nem `setRequestHandler(...)` na API de alto nível obrigatória.
|
|
|
|
```typescript
|
|
// ERRADO — API de baixo nível v1, proibida nesta casa
|
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
const server = new Server({...}, { capabilities: { tools: {} } }); // causava erro 471 em v1
|
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({...}));
|
|
|
|
// CORRECTO — API de alto nível v2, a única aceite
|
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
const server = new McpServer({ name: 'mcp-<nome>', version: '1.0.0' });
|
|
server.registerTool('get_customer', { description: '...', inputSchema: z.object({...}) }, async (args) => {...});
|
|
```
|
|
|
|
O erro 471 (capabilities incompletas) era uma armadilha da API de baixo nível v1 — deixa de poder acontecer ao usar `McpServer`/`registerTool` do v2. Se algum código ainda usa `Server` + `setRequestHandler(Schema, ...)`, é sinal de que está a reintroduzir o v1 — migrar antes de continuar (ver `SKILL.md` §Regra de Ouro).
|
|
|
|
---
|
|
|
|
## 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 * as z from 'zod/v4';
|
|
|
|
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(),
|
|
});
|
|
|
|
server.registerTool(
|
|
'get_customer',
|
|
{ description: 'Obtém dados de um cliente', inputSchema: GetCustomerSchema },
|
|
async ({ customer_id, include_invoices, date_from }) => {
|
|
// O SDK v2 já validou e rejeitou argumentos inválidos antes de este handler correr —
|
|
// nunca chamar `.parse()`/`.safeParse()` outra vez aqui, é redundante e esconde a
|
|
// mensagem de erro accionável que o SDK já produziu.
|
|
// ... lógica aqui
|
|
},
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
No v2, um handler que atira (`throw`) é convertido automaticamente num resultado `isError: true` — não existe `setRequestHandler(CallToolRequestSchema, ...)` para envolver manualmente (isso é API de baixo nível v1). Ver [errors.md](https://ts.sdk.modelcontextprotocol.io/v2/servers/errors.md) para o detalhe completo (`ProtocolError` para resources/prompts, subclasses tipadas).
|
|
|
|
```typescript
|
|
server.registerTool(
|
|
'get_customer',
|
|
{ description: 'Obtém dados de um cliente pelo ID', inputSchema: z.object({ customer_id: z.number() }) },
|
|
async ({ customer_id }) => {
|
|
const customer = await db.getCustomer(customer_id);
|
|
if (!customer) {
|
|
// Devolver isError explicitamente dá controlo total sobre o `content`
|
|
return {
|
|
content: [{ type: 'text', text: `Cliente ${customer_id} não encontrado. Verificar: 1) ID existe na BD 2) Permissões de acesso` }],
|
|
isError: true,
|
|
};
|
|
}
|
|
return { content: [{ type: 'text', text: `Cliente: ${customer.name}` }], structuredContent: customer };
|
|
},
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Logging
|
|
|
|
```typescript
|
|
// SEMPRE usar console.error (não console.log — interfere com o framing stdio quando o
|
|
// MCP corre em modo stdio excepcional; em HTTP não framing-crítico mas mantém-se a
|
|
// convenção para consistência entre os dois entry points)
|
|
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
|
|
- [ ] **Comandos shell/SSH: nunca interpolar dados do utilizador num comando (`ssh ... "$var"`, `exec(\`cmd ${var}\`)`); dados sempre por STDIN (texto ou base64)** — regra escrita depois de uma injecção de comandos real corrigida em produção (`mcp-kivicare`, 19-08-2026)
|
|
- [ ] 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
|
|
- [ ] Se o MCP faz ponte para um sistema nativo (WP-CLI, API externa, …): o formato exacto de escrita (ex.: JSON vs PHP serialize, hooks disparados com objecto vs escalar) foi confirmado por leitura do código-fonte real do sistema alvo, nunca assumido
|
|
|
|
**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/
|
|
|
|
# Comandos shell/ssh interpolados — qualquer resultado exige revisão manual
|
|
grep -rn 'exec(\|execSync(\|spawn(.*sh\b' src/ | grep -i '\${'
|
|
```
|
|
|
|
---
|
|
|
|
## 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 |
|
|
|------------|--------|-----|
|
|
| Streamable HTTP (SDK v2) | **Obrigatório** | Todos os MCPs novos — acesso remoto, gateway |
|
|
| stdio (SDK v2, `serveStdio`) | Excepção documentada | Só quando o host lança o processo directamente e não há gateway — justificar no README |
|
|
| SSE | Deprecated | Retrocompatibilidade apenas — nunca em MCP novo |
|
|
|
|
**Portas HTTP reservadas (3200+):**
|
|
|
|
| Porta | MCP |
|
|
|-------|-----|
|
|
| 3200 | outline-postgresql |
|
|
| 3201+ | disponíveis |
|
|
|
|
**Portas SSE reservadas (3100+):**
|
|
|
|
| Porta | MCP |
|
|
|-------|-----|
|
|
| 3100 | desk-crm-v3 |
|
|
| 3101 | mem0 |
|
|
| 3102 | wikijs |
|
|
| 3103 | ssh-unified |
|
|
| 3105 | n8n |
|
|
| 3106 | cwp |
|
|
| 3107 | youtube-research |
|
|
| 3108 | moloni |
|
|
| 3109+ | disponíveis |
|
|
|
|
---
|
|
|
|
*best-practices.md v3.0 | 2026-08-19*
|