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
284 lines
8.8 KiB
Markdown
284 lines
8.8 KiB
Markdown
# MCP Evaluation Guide
|
|
|
|
> Guia para criar e executar evaluations de servidores MCP.
|
|
> Baseado nos padrões do mcp-builder Anthropic.
|
|
|
|
---
|
|
|
|
## O que são Evaluations MCP?
|
|
|
|
Evaluations são testes estruturados que validam o comportamento de um MCP em cenários reais. Não testam apenas se as ferramentas existem — testam se produzem **resultados correctos, seguros e previsíveis**.
|
|
|
|
**Objectivos:**
|
|
- Garantir que cada tool faz o que a descrição promete
|
|
- Detectar regressões após alterações
|
|
- Validar edge cases e erros esperados
|
|
- Confirmar que annotations correspondem ao comportamento real
|
|
|
|
---
|
|
|
|
## Estrutura de uma Evaluation
|
|
|
|
Cada evaluation é definida em XML e contém:
|
|
|
|
```xml
|
|
<evaluation>
|
|
<name>get_customer_basic</name>
|
|
<description>Verifica que get_customer retorna dados completos de um cliente existente</description>
|
|
<tool>get_customer</tool>
|
|
<input>
|
|
<customer_id>1</customer_id>
|
|
</input>
|
|
<expected>
|
|
<contains>name</contains>
|
|
<contains>email</contains>
|
|
<not_error>true</not_error>
|
|
</expected>
|
|
</evaluation>
|
|
```
|
|
|
|
---
|
|
|
|
## 10 Perguntas de Evaluation por MCP
|
|
|
|
Para cada MCP novo, responder a estas 10 perguntas criando um teste para cada uma:
|
|
|
|
```xml
|
|
<!-- 1. A tool mais básica funciona? -->
|
|
<evaluation id="1">
|
|
<question>A tool {principal} retorna dados quando recebe input válido?</question>
|
|
<scenario>Input mínimo válido</scenario>
|
|
<assert>Resposta não é erro, contém campos esperados</assert>
|
|
</evaluation>
|
|
|
|
<!-- 2. Erros de input são tratados graciosamente? -->
|
|
<evaluation id="2">
|
|
<question>O que acontece com input inválido (ID negativo, campo obrigatório em falta)?</question>
|
|
<scenario>Input inválido ou em falta</scenario>
|
|
<assert>Resposta tem isError=true, mensagem é accionável</assert>
|
|
</evaluation>
|
|
|
|
<!-- 3. O recurso inexistente é tratado correctamente? -->
|
|
<evaluation id="3">
|
|
<question>O que retorna quando o recurso pedido não existe (ID=99999)?</question>
|
|
<scenario>Recurso não encontrado</scenario>
|
|
<assert>Mensagem clara "não encontrado", não retorna null silencioso</assert>
|
|
</evaluation>
|
|
|
|
<!-- 4. Operações de escrita são idempotentes quando declaradas? -->
|
|
<evaluation id="4">
|
|
<question>Operações com idempotentHint=true produzem o mesmo resultado em chamadas repetidas?</question>
|
|
<scenario>Chamar a mesma tool create/update duas vezes com os mesmos parâmetros</scenario>
|
|
<assert>Segundo resultado idêntico ao primeiro ou erro controlado</assert>
|
|
</evaluation>
|
|
|
|
<!-- 5. Operações destrutivas têm confirmação ou são irreversíveis como declarado? -->
|
|
<evaluation id="5">
|
|
<question>Tools com destructiveHint=true apagam realmente dados? O utilizador é avisado?</question>
|
|
<scenario>Executar delete em recurso existente</scenario>
|
|
<assert>Dados apagados. Mensagem confirma acção irreversível</assert>
|
|
</evaluation>
|
|
|
|
<!-- 6. Tools read-only não alteram estado? -->
|
|
<evaluation id="6">
|
|
<question>Tools com readOnlyHint=true não modificam dados?</question>
|
|
<scenario>Executar get/list e verificar estado antes e depois</scenario>
|
|
<assert>Estado da BD/sistema idêntico antes e depois da chamada</assert>
|
|
</evaluation>
|
|
|
|
<!-- 7. Paginação e listas grandes funcionam? -->
|
|
<evaluation id="7">
|
|
<question>Tools de listagem com muitos resultados retornam dados paginados correctamente?</question>
|
|
<scenario>Lista com 1000+ registos, pedir página 2</scenario>
|
|
<assert>Retorna subset correcto, metadados de paginação presentes</assert>
|
|
</evaluation>
|
|
|
|
<!-- 8. Autenticação/autorização é validada? -->
|
|
<evaluation id="8">
|
|
<question>Chamadas sem credenciais válidas são rejeitadas?</question>
|
|
<scenario>Remover variável de ambiente com API key e chamar tool</scenario>
|
|
<assert>Erro claro de autenticação, não crash do servidor</assert>
|
|
</evaluation>
|
|
|
|
<!-- 9. structuredContent é consistente com content textual? -->
|
|
<evaluation id="9">
|
|
<question>O campo structuredContent contém os mesmos dados que o texto Markdown?</question>
|
|
<scenario>Comparar campos em structuredContent com conteúdo do text</scenario>
|
|
<assert>Dados idênticos em ambos os formatos</assert>
|
|
</evaluation>
|
|
|
|
<!-- 10. Performance é aceitável? -->
|
|
<evaluation id="10">
|
|
<question>A tool responde em menos de 5 segundos em condições normais?</question>
|
|
<scenario>Medir tempo de resposta em 10 chamadas consecutivas</scenario>
|
|
<assert>p95 < 5000ms, sem memory leaks após 100 chamadas</assert>
|
|
</evaluation>
|
|
```
|
|
|
|
---
|
|
|
|
## Como Executar Evaluations
|
|
|
|
### Método Manual (MCP Inspector)
|
|
|
|
```bash
|
|
# Instalar MCP Inspector
|
|
npx @modelcontextprotocol/inspector
|
|
|
|
# Conectar ao MCP local
|
|
# Interface web em http://localhost:5173
|
|
# Testar cada tool manualmente
|
|
```
|
|
|
|
### Método Automatizado (Script TypeScript, SDK v2 — in-process, sem porta/socket)
|
|
|
|
`handler.fetch` do `createMcpHandler` serve pedidos em processo — o transporte de teste nunca liga de facto ao URL, por isso não há porta a reservar nem servidor a arrancar/parar à volta de cada corrida. Ver [testing.md](https://ts.sdk.modelcontextprotocol.io/v2/testing.md).
|
|
|
|
```typescript
|
|
// eval/run-evals.ts
|
|
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
|
|
import { createMcpHandler } from '@modelcontextprotocol/server';
|
|
import { createServer } from '../src/server.js'; // a mesma factory usada em src/http.ts
|
|
|
|
interface EvalResult {
|
|
id: string;
|
|
passed: boolean;
|
|
duration: number;
|
|
error?: string;
|
|
}
|
|
|
|
async function runEval(
|
|
client: Client,
|
|
toolName: string,
|
|
input: Record<string, unknown>,
|
|
assertions: {
|
|
notError?: boolean;
|
|
containsFields?: string[];
|
|
isError?: boolean;
|
|
}
|
|
): Promise<EvalResult> {
|
|
const start = Date.now();
|
|
|
|
try {
|
|
const result = await client.callTool({ name: toolName, arguments: input });
|
|
const duration = Date.now() - start;
|
|
|
|
if (assertions.notError && result.isError) {
|
|
return { id: toolName, passed: false, duration, error: 'Esperava sucesso, recebeu erro' };
|
|
}
|
|
|
|
if (assertions.isError && !result.isError) {
|
|
return { id: toolName, passed: false, duration, error: 'Esperava erro, recebeu sucesso' };
|
|
}
|
|
|
|
if (assertions.containsFields) {
|
|
const text = (result.content?.[0] as { text?: string })?.text || '';
|
|
for (const field of assertions.containsFields) {
|
|
if (!text.includes(field)) {
|
|
return { id: toolName, passed: false, duration, error: `Campo "${field}" não encontrado` };
|
|
}
|
|
}
|
|
}
|
|
|
|
return { id: toolName, passed: true, duration };
|
|
} catch (err) {
|
|
return {
|
|
id: toolName,
|
|
passed: false,
|
|
duration: Date.now() - start,
|
|
error: err instanceof Error ? err.message : String(err)
|
|
};
|
|
}
|
|
}
|
|
|
|
// Executar todas as evaluations
|
|
async function main() {
|
|
const handler = createMcpHandler(createServer);
|
|
const transport = new StreamableHTTPClientTransport(new URL('http://test.local/mcp'), {
|
|
fetch: (url, init) => handler.fetch(new Request(url, init)),
|
|
});
|
|
|
|
const client = new Client({ name: 'eval-client', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
|
|
await client.connect(transport);
|
|
|
|
const results: EvalResult[] = [];
|
|
|
|
// Eval 1: Tool básica
|
|
results.push(await runEval(client, 'get_customer', { customer_id: 1 }, {
|
|
notError: true,
|
|
containsFields: ['name', 'email']
|
|
}));
|
|
|
|
// Eval 2: Input inválido
|
|
results.push(await runEval(client, 'get_customer', { customer_id: -1 }, {
|
|
isError: true
|
|
}));
|
|
|
|
// Eval 3: Recurso inexistente
|
|
results.push(await runEval(client, 'get_customer', { customer_id: 99999 }, {
|
|
isError: true
|
|
}));
|
|
|
|
// Sumário
|
|
const passed = results.filter(r => r.passed).length;
|
|
const total = results.length;
|
|
console.log(`\nEvaluations: ${passed}/${total} passou`);
|
|
results.filter(r => !r.passed).forEach(r => {
|
|
console.log(` FALHOU ${r.id}: ${r.error}`);
|
|
});
|
|
|
|
await client.close();
|
|
await handler.close();
|
|
process.exit(passed === total ? 0 : 1);
|
|
}
|
|
|
|
main().catch(console.error);
|
|
```
|
|
|
|
**Adicionar ao package.json:**
|
|
```json
|
|
{
|
|
"scripts": {
|
|
"eval": "tsx eval/run-evals.ts",
|
|
"eval:ci": "npm run build && npm run eval"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Checklist de Evaluations (pré-deploy)
|
|
|
|
- [ ] Eval 1 passa: tool principal com input válido
|
|
- [ ] Eval 2 passa: input inválido retorna erro accionável
|
|
- [ ] Eval 3 passa: recurso inexistente retorna erro claro
|
|
- [ ] Eval 4 passa: idempotência verificada (se aplicável)
|
|
- [ ] Eval 5 passa: operações destrutivas confirmadas
|
|
- [ ] Eval 6 passa: read-only não altera estado
|
|
- [ ] Eval 7 passa: paginação funciona (se aplicável)
|
|
- [ ] Eval 8 passa: auth inválida rejeitada graciosamente
|
|
- [ ] Eval 9 passa: structuredContent consistente
|
|
- [ ] Eval 10 passa: p95 < 5000ms
|
|
|
|
---
|
|
|
|
## Integração CI/CD
|
|
|
|
```yaml
|
|
# .gitea/workflows/eval.yml
|
|
name: MCP Evaluations
|
|
on: [push, pull_request]
|
|
|
|
jobs:
|
|
eval:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
- run: npm ci
|
|
- run: npm run eval:ci
|
|
```
|
|
|
|
---
|
|
|
|
*evaluation-guide.md v2.0 | 2026-08-19*
|