# 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____`: ≤ 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-', 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*