feat: refactor 30+ skills to Anthropic progressive disclosure pattern
- All SKILL.md files now <500 lines (avg reduction 69%) - Detailed content extracted to references/ subdirectories - Frontmatter standardised: only name + description (Anthropic standard) - New skills: brand-guidelines, spec-coauthor, report-templates, skill-creator - Design skills: anti-slop guidelines, premium-proposals reference - Removed non-standard frontmatter fields (triggers, version, author, category) Plugins affected: infraestrutura, marketing, dev-tools, crm-ops, gestao, core-tools, negocio, perfex-dev, wordpress, design-media Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
287
infraestrutura/skills/mcp-dev/references/best-practices.md
Normal file
287
infraestrutura/skills/mcp-dev/references/best-practices.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# 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*
|
||||
280
infraestrutura/skills/mcp-dev/references/evaluation-guide.md
Normal file
280
infraestrutura/skills/mcp-dev/references/evaluation-guide.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# 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)
|
||||
|
||||
```typescript
|
||||
// eval/run-evals.ts
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
|
||||
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;
|
||||
|
||||
// Verificar assertions
|
||||
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]?.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 transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
args: ['dist/index.js']
|
||||
});
|
||||
|
||||
const client = new Client({ name: 'eval-client', version: '1.0.0' });
|
||||
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();
|
||||
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 v1.0 | 2026-03-10*
|
||||
341
infraestrutura/skills/mcp-dev/references/templates.md
Normal file
341
infraestrutura/skills/mcp-dev/references/templates.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# MCP Templates - Scaffolding TypeScript
|
||||
|
||||
> Templates de código prontos a usar. Substituir `<nome>` pelo nome real do MCP.
|
||||
|
||||
---
|
||||
|
||||
## package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mcp-<nome>",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"start:http": "node dist/index-http.js",
|
||||
"dev": "tsx src/index.ts",
|
||||
"dev:http": "tsx src/index-http.ts",
|
||||
"eval": "tsx eval/run-evals.ts",
|
||||
"eval:ci": "npm run build && npm run eval",
|
||||
"reload:http": "npm run build && systemctl --user restart mcp-<nome> && sleep 2 && curl -s http://127.0.0.1:32XX/health"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"zod": "^3.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"tsx": "^4.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## tsconfig.json
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "eval"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## src/index.ts (stdio — padrão local)
|
||||
|
||||
```typescript
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* MCP <Nome>
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListPromptsRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
// --- Schemas de validação ---
|
||||
const ExampleSchema = z.object({
|
||||
param: z.string().min(1, 'param é obrigatório'),
|
||||
});
|
||||
|
||||
// --- Definição de tools ---
|
||||
const allTools = [
|
||||
{
|
||||
name: 'example_get_item', // {serviço}_{acção}_{recurso}
|
||||
description: 'Obtém um item pelo ID',
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: false,
|
||||
},
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
param: { type: 'string', description: 'ID do item' },
|
||||
},
|
||||
required: ['param'],
|
||||
},
|
||||
handler: async (args: unknown) => {
|
||||
const { param } = ExampleSchema.parse(args);
|
||||
// ... lógica aqui
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Item: ${param}` }],
|
||||
structuredContent: { id: param, status: 'found' },
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// --- Servidor ---
|
||||
const server = new Server(
|
||||
{ name: 'mcp-<nome>', version: '1.0.0' },
|
||||
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: allTools.map(t => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
annotations: t.annotations,
|
||||
inputSchema: t.inputSchema,
|
||||
})),
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));
|
||||
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
const tool = allTools.find(t => t.name === name);
|
||||
|
||||
if (!tool) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Tool desconhecida: ${name}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return await tool.handler(args);
|
||||
} 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 em ${name}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('MCP <nome> running on stdio');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## src/index-http.ts (StreamableHTTP — padrão remoto)
|
||||
|
||||
```typescript
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* MCP <Nome> - HTTP Server Mode
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import {
|
||||
ListToolsRequestSchema,
|
||||
CallToolRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListPromptsRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
const PORT = parseInt(process.env.MCP_HTTP_PORT || '3200', 10);
|
||||
const HOST = process.env.MCP_HTTP_HOST || '127.0.0.1';
|
||||
const STATEFUL = process.env.MCP_STATEFUL !== 'false';
|
||||
|
||||
const sessions = new Map<string, { transport: StreamableHTTPServerTransport }>();
|
||||
|
||||
// Importar allTools do mesmo local que stdio
|
||||
import { allTools } from './tools/index.js';
|
||||
|
||||
function createMcpServer(): Server {
|
||||
const server = new Server(
|
||||
{ name: 'mcp-<nome>', version: '1.0.0' },
|
||||
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: allTools.map(t => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
annotations: t.annotations,
|
||||
inputSchema: t.inputSchema,
|
||||
})),
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));
|
||||
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
const tool = allTools.find(t => t.name === name);
|
||||
if (!tool) {
|
||||
return { content: [{ type: 'text', text: `Tool desconhecida: ${name}` }], isError: true };
|
||||
}
|
||||
try {
|
||||
return await tool.handler(args);
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Erro: ${error instanceof Error ? error.message : String(error)}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const httpServer = http.createServer(async (req, res) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
|
||||
|
||||
if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
|
||||
|
||||
const url = new URL(req.url || '/', `http://${HOST}:${PORT}`);
|
||||
|
||||
if (url.pathname === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok', version: '1.0.0', sessions: sessions.size, tools: allTools.length }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/mcp') {
|
||||
try {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: STATEFUL ? () => randomUUID() : undefined,
|
||||
});
|
||||
const srv = createMcpServer();
|
||||
if (STATEFUL && transport.sessionId) {
|
||||
sessions.set(transport.sessionId, { transport });
|
||||
transport.onclose = () => { if (transport.sessionId) sessions.delete(transport.sessionId); };
|
||||
}
|
||||
await srv.connect(transport);
|
||||
await transport.handleRequest(req, res);
|
||||
} catch (error) {
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Internal server error' }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Not found' }));
|
||||
});
|
||||
|
||||
httpServer.listen(PORT, HOST, () => {
|
||||
console.log(`MCP <nome> HTTP v1.0.0`);
|
||||
console.log(` Endpoint: http://${HOST}:${PORT}/mcp`);
|
||||
console.log(` Health: http://${HOST}:${PORT}/health`);
|
||||
console.log(` Mode: ${STATEFUL ? 'Stateful' : 'Stateless'}`);
|
||||
});
|
||||
|
||||
const shutdown = () => httpServer.close(() => process.exit(0));
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ~/.config/systemd/user/mcp-<nome>.service
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=MCP <Nome> HTTP Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/ealmeida/mcp-servers/mcp-<nome>
|
||||
ExecStart=/home/ealmeida/.nvm/versions/node/v22.18.0/bin/node dist/index-http.js
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=NODE_ENV=production
|
||||
Environment=MCP_HTTP_PORT=32XX
|
||||
Environment=LOG_LEVEL=error
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuração ~/.claude.json
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"<nome>": {
|
||||
"type": "http",
|
||||
"url": "http://127.0.0.1:32XX/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*templates.md v1.0 | 2026-03-10*
|
||||
Reference in New Issue
Block a user