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
241 lines
6.4 KiB
Markdown
241 lines
6.4 KiB
Markdown
# MCP Templates - Scaffolding TypeScript (SDK v2, HTTP obrigatório)
|
|
|
|
> Templates de código prontos a usar. Substituir `<nome>` pelo nome real do MCP.
|
|
> SDK oficial: [ts.sdk.modelcontextprotocol.io/v2](https://ts.sdk.modelcontextprotocol.io/v2/).
|
|
> **Transporte HTTP é obrigatório** — o template stdio no fim deste ficheiro só se aplica a uma excepção documentada (ver `SKILL.md` §Regra de Ouro).
|
|
|
|
---
|
|
|
|
## package.json
|
|
|
|
```json
|
|
{
|
|
"name": "mcp-<nome>",
|
|
"version": "1.0.0",
|
|
"type": "module",
|
|
"main": "dist/http.js",
|
|
"scripts": {
|
|
"build": "tsc",
|
|
"start": "node dist/http.js",
|
|
"dev": "tsx src/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/server": "^2.0.0",
|
|
"@modelcontextprotocol/express": "^2.0.0",
|
|
"@modelcontextprotocol/node": "^2.0.0",
|
|
"express": "^4.19.0",
|
|
"zod": "^4.2.0"
|
|
},
|
|
"devDependencies": {
|
|
"@modelcontextprotocol/client": "^2.0.0",
|
|
"@types/node": "^20.0.0",
|
|
"@types/express": "^4.17.0",
|
|
"typescript": "^5.7.0",
|
|
"tsx": "^4.19.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
Node.js **20+** obrigatório. `type: module` — o SDK v2 é ESM-only na sua API pública (ainda que publique um build CJS de compatibilidade).
|
|
|
|
---
|
|
|
|
## tsconfig.json
|
|
|
|
```json
|
|
{
|
|
"compilerOptions": {
|
|
"target": "ES2022",
|
|
"module": "NodeNext",
|
|
"moduleResolution": "NodeNext",
|
|
"outDir": "./dist",
|
|
"rootDir": "./src",
|
|
"strict": true,
|
|
"esModuleInterop": true,
|
|
"skipLibCheck": true,
|
|
"declaration": true
|
|
},
|
|
"include": ["src/**/*"],
|
|
"exclude": ["node_modules", "dist", "eval"]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## src/server.ts — factory partilhada (única fonte da definição de tools)
|
|
|
|
A `McpServer` do v2 deriva capabilities automaticamente de `registerTool`/`registerResource`/`registerPrompt` — nunca declarar `capabilities: {...}` à mão (isso é a API de baixo nível v1, proibida).
|
|
|
|
```typescript
|
|
/**
|
|
* MCP <Nome> — factory partilhada por HTTP e (excepcionalmente) stdio.
|
|
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
|
*/
|
|
|
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
import * as z from 'zod/v4';
|
|
|
|
export function createServer(): McpServer {
|
|
const server = new McpServer({ name: 'mcp-<nome>', version: '1.0.0' });
|
|
|
|
server.registerTool(
|
|
'example_get_item', // {serviço}_{acção}_{recurso}, snake_case, <=40 chars
|
|
{
|
|
description: 'Obtém um item pelo ID',
|
|
inputSchema: z.object({
|
|
param: z.string().min(1).describe('ID do item'),
|
|
}),
|
|
outputSchema: z.object({
|
|
id: z.string(),
|
|
status: z.string(),
|
|
}),
|
|
annotations: {
|
|
readOnlyHint: true,
|
|
destructiveHint: false,
|
|
idempotentHint: true,
|
|
openWorldHint: false,
|
|
},
|
|
},
|
|
async ({ param }) => {
|
|
// ... lógica aqui — nunca interpolar `param` num comando shell/SQL;
|
|
// se este tool fizer ponte SSH/CLI, ver best-practices.md §Segurança.
|
|
return {
|
|
content: [{ type: 'text', text: `Item: ${param}` }],
|
|
structuredContent: { id: param, status: 'found' },
|
|
};
|
|
},
|
|
);
|
|
|
|
return server;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## src/http.ts — entry point HTTP (obrigatório)
|
|
|
|
```typescript
|
|
/**
|
|
* MCP <Nome> — HTTP Server (Streamable HTTP)
|
|
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
|
*/
|
|
|
|
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
|
import { toNodeHandler } from '@modelcontextprotocol/node';
|
|
import { createMcpHandler } from '@modelcontextprotocol/server';
|
|
import { createServer } from './server.js';
|
|
|
|
const PORT = parseInt(process.env.MCP_HTTP_PORT || '32XX', 10);
|
|
const HOST = process.env.MCP_HTTP_HOST || '127.0.0.1';
|
|
|
|
const handler = createMcpHandler(createServer);
|
|
const node = toNodeHandler(handler);
|
|
|
|
// createMcpExpressApp já valida Host/Origin contra DNS rebinding no bind 127.0.0.1
|
|
const app = createMcpExpressApp();
|
|
|
|
app.all('/mcp', (req, res) => void node(req, res, req.body));
|
|
|
|
app.get('/health', (_req, res) => {
|
|
res.json({ status: 'ok', name: 'mcp-<nome>', version: '1.0.0' });
|
|
});
|
|
|
|
const httpServer = app.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`);
|
|
});
|
|
|
|
async function shutdown(): Promise<void> {
|
|
await handler.close();
|
|
httpServer.close(() => process.exit(0));
|
|
}
|
|
process.on('SIGINT', shutdown);
|
|
process.on('SIGTERM', shutdown);
|
|
```
|
|
|
|
Binding a `0.0.0.0` (exposição fora de localhost) exige passar `allowedHosts` a `createMcpExpressApp({ host: '0.0.0.0', allowedHosts: [...] })` — nunca desligar a validação de Host/Origin.
|
|
|
|
---
|
|
|
|
## ~/.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/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 (HTTP — obrigatório)
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"<nome>": {
|
|
"type": "http",
|
|
"url": "http://127.0.0.1:32XX/mcp"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## src/stdio.ts — apenas para excepção documentada
|
|
|
|
Usar **só** quando o host tem de lançar o processo directamente e não existe gateway (justificar no README do MCP). Mesma factory `createServer()` de `src/server.ts` — nunca duplicar a definição de tools.
|
|
|
|
```typescript
|
|
/**
|
|
* MCP <Nome> — stdio (EXCEPÇÃO — ver README §Transporte para a justificação)
|
|
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
|
*/
|
|
|
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
import { createServer } from './server.js';
|
|
|
|
void serveStdio(createServer);
|
|
console.error('MCP <nome> running on stdio');
|
|
```
|
|
|
|
::: warning
|
|
stdout é o canal do protocolo — logar sempre com `console.error`; um único `console.log` corrompe o stream JSON-RPC.
|
|
:::
|
|
|
|
Configuração `~/.claude.json` correspondente:
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"<nome>": {
|
|
"command": "node",
|
|
"args": ["/home/ealmeida/mcp-servers/<nome>/dist/stdio.js"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
*templates.md v2.0 | 2026-08-19*
|