New plugins: core-tools New skills: auto-expense, ticket-triage, design, security-check, aiktop-tasks, daily-digest, imap-triage, index-update, mindmap, notebooklm, proc-creator, tasks-overview, validate-component, perfex-module, report, calendar-manager New agents: design-critic, design-generator, design-lead, design-prompt-architect, design-researcher, compliance-auditor, metabase-analyst, gitea-integration-specialist Updated: all plugin configs, knowledge datasets, existing skills Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1150 lines
28 KiB
Markdown
1150 lines
28 KiB
Markdown
---
|
|
name: mcp-dev
|
|
description: MCP server development and testing. Creates new MCP servers following
|
|
Descomplicar standards with SSE transport. Use when user mentions "mcp development",
|
|
"novo mcp", "criar mcp", "mcp server", "mcp testing".
|
|
author: Descomplicar® Crescimento Digital
|
|
version: 1.3.0
|
|
quality_score: 75
|
|
user_invocable: true
|
|
desk_task: 1476
|
|
allowed-tools: Grep
|
|
---
|
|
|
|
# /mcp-dev - Desenvolvimento de MCPs
|
|
|
|
Skill para criação, configuração e gestão de servidores MCP customizados.
|
|
|
|
> **Regra #48:** Novos MCPs devem ser desenvolvidos no **container dev** (`server:"dev"`, path `/root/Dev/<nome-mcp>`). O path `/home/ealmeida/mcp-servers/` é para MCPs já em produção. Desenvolvimento inicial -> `/root/Dev/` -> depois mover para `mcp-servers/` no deploy final.
|
|
|
|
---
|
|
|
|
## Comandos
|
|
|
|
| Comando | Descrição |
|
|
|---------|-----------|
|
|
| `/mcp-dev create <nome>` | Criar novo MCP (scaffold TypeScript) |
|
|
| `/mcp-dev config <nome>` | Configurar MCP em ~/.claude.json |
|
|
| `/mcp-dev test <nome>` | Testar conexão e ferramentas |
|
|
| `/mcp-dev docs <nome>` | Gerar documentação Obsidian |
|
|
| `/mcp-dev list` | Listar MCPs instalados |
|
|
| `/mcp-dev status` | Estado dos MCPs activos |
|
|
|
|
---
|
|
|
|
## Estrutura MCP Padrão
|
|
|
|
```
|
|
~/mcp-servers/<nome>/
|
|
├── package.json
|
|
├── tsconfig.json
|
|
├── src/
|
|
│ ├── index.ts # Entry point
|
|
│ ├── tools/ # Ferramentas MCP
|
|
│ │ └── example.ts
|
|
│ └── types.ts # Tipos TypeScript
|
|
├── README.md
|
|
├── .env.example
|
|
└── .gitignore
|
|
```
|
|
|
|
---
|
|
|
|
## /mcp-dev create <nome>
|
|
|
|
Cria scaffold completo para novo MCP.
|
|
|
|
### Execução
|
|
|
|
```
|
|
1. Criar pasta ~/mcp-servers/<nome>/
|
|
2. Gerar package.json com dependências MCP
|
|
3. Gerar tsconfig.json
|
|
4. Criar src/index.ts com estrutura base
|
|
5. Criar README.md
|
|
6. Criar .env.example
|
|
7. npm install
|
|
8. Sugerir configuração
|
|
```
|
|
|
|
### Template package.json
|
|
|
|
```json
|
|
{
|
|
"name": "mcp-<nome>",
|
|
"version": "1.0.0",
|
|
"type": "module",
|
|
"main": "dist/index.js",
|
|
"scripts": {
|
|
"build": "tsc",
|
|
"start": "node dist/index.js",
|
|
"dev": "tsx src/index.ts"
|
|
},
|
|
"dependencies": {
|
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
"zod": "^3.22.0"
|
|
},
|
|
"devDependencies": {
|
|
"@types/node": "^20.0.0",
|
|
"typescript": "^5.0.0",
|
|
"tsx": "^4.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Template index.ts
|
|
|
|
```typescript
|
|
#!/usr/bin/env node
|
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import {
|
|
CallToolRequestSchema,
|
|
ListToolsRequestSchema,
|
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
|
|
const server = new Server(
|
|
{
|
|
name: 'mcp-<nome>',
|
|
version: '1.0.0',
|
|
},
|
|
{
|
|
capabilities: {
|
|
tools: {},
|
|
},
|
|
}
|
|
);
|
|
|
|
// List available tools
|
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
tools: [
|
|
{
|
|
name: 'example_tool',
|
|
description: 'Descrição da ferramenta',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
param: {
|
|
type: 'string',
|
|
description: 'Parâmetro exemplo',
|
|
},
|
|
},
|
|
required: ['param'],
|
|
},
|
|
},
|
|
],
|
|
}));
|
|
|
|
// Handle tool calls
|
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
const { name, arguments: args } = request.params;
|
|
|
|
switch (name) {
|
|
case 'example_tool':
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: `Resultado: ${args?.param}`,
|
|
},
|
|
],
|
|
};
|
|
default:
|
|
throw new Error(`Unknown tool: ${name}`);
|
|
}
|
|
});
|
|
|
|
// Start server
|
|
async function main() {
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
console.error('MCP Server running on stdio');
|
|
}
|
|
|
|
main().catch(console.error);
|
|
```
|
|
|
|
---
|
|
|
|
## /mcp-dev config <nome>
|
|
|
|
Adiciona MCP ao ficheiro de configuração.
|
|
|
|
### Claude Code (~/.claude.json)
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"<nome>": {
|
|
"command": "node",
|
|
"args": ["/home/ealmeida/mcp-servers/<nome>/dist/index.js"],
|
|
"env": {}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Claude Desktop (~/.config/claude/claude_desktop_config.json)
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"<nome>": {
|
|
"command": "node",
|
|
"args": ["/home/ealmeida/mcp-servers/<nome>/dist/index.js"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Verificação
|
|
|
|
```bash
|
|
# Testar se compila
|
|
cd ~/mcp-servers/<nome> && npm run build
|
|
|
|
# Testar manualmente
|
|
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node dist/index.js
|
|
```
|
|
|
|
---
|
|
|
|
## /mcp-dev test <nome>
|
|
|
|
Testa conexão e lista ferramentas disponíveis.
|
|
|
|
### Output Esperado
|
|
|
|
```
|
|
🔧 MCP Test: <nome>
|
|
──────────────────
|
|
Status: ✅ Conectado
|
|
Versão: 1.0.0
|
|
Transport: stdio
|
|
|
|
Ferramentas (3):
|
|
• tool_1 - Descrição
|
|
• tool_2 - Descrição
|
|
• tool_3 - Descrição
|
|
|
|
Recursos (0):
|
|
(nenhum)
|
|
|
|
Prompts (0):
|
|
(nenhum)
|
|
```
|
|
|
|
---
|
|
|
|
## /mcp-dev docs <nome>
|
|
|
|
Gera documentação Obsidian para o MCP.
|
|
|
|
### Output
|
|
|
|
Cria `Stack/Claude Code/MCPs/MCP-<Nome>.md`:
|
|
|
|
```markdown
|
|
---
|
|
title: MCP <Nome>
|
|
date: YYYY-MM-DD
|
|
type: mcp
|
|
status: active
|
|
tags: [mcp, <nome>]
|
|
---
|
|
|
|
# MCP <Nome>
|
|
|
|
Descrição do MCP.
|
|
|
|
## Ferramentas
|
|
|
|
| Tool | Descrição | Parâmetros |
|
|
|------|-----------|------------|
|
|
| tool_1 | ... | param1, param2 |
|
|
|
|
## Configuração
|
|
|
|
...
|
|
|
|
## Exemplos
|
|
|
|
...
|
|
```
|
|
|
|
---
|
|
|
|
## /mcp-dev list
|
|
|
|
Lista todos os MCPs em ~/mcp-servers/.
|
|
|
|
### Output
|
|
|
|
```
|
|
📋 MCPs Instalados
|
|
──────────────────
|
|
~/mcp-servers/
|
|
├── desk-crm-v3/ ✅ Configurado
|
|
├── memory-supabase/ ✅ Configurado
|
|
├── google-workspace/ ✅ Configurado
|
|
├── novo-mcp/ ⚠️ Não configurado
|
|
└── teste/ ❌ Não compila
|
|
|
|
Total: 5 | Activos: 3 | Pendentes: 2
|
|
```
|
|
|
|
---
|
|
|
|
## /mcp-dev status
|
|
|
|
Estado dos MCPs activos na sessão.
|
|
|
|
### Output
|
|
|
|
```
|
|
🔌 MCPs Activos
|
|
───────────────
|
|
✅ desk-crm-v3 (15 tools)
|
|
✅ memory-supabase (5 tools)
|
|
✅ google-workspace (42 tools)
|
|
✅ filesystem (8 tools)
|
|
⚠️ youtube-uploader (auth required)
|
|
|
|
Total: 5 | OK: 4 | Problemas: 1
|
|
```
|
|
|
|
---
|
|
|
|
## Padrões de Desenvolvimento
|
|
|
|
### Limites de Nomes de Tools
|
|
|
|
> **REGRA 31/01/2026:** Nomes de tools MCP têm limite de caracteres.
|
|
|
|
| Limite | Valor | Cálculo |
|
|
|--------|-------|---------|
|
|
| Tool name | **≤40 chars** | Nome da função |
|
|
| Total com prefixo | **≤64 chars** | `mcp__<server>__<tool_name>` |
|
|
|
|
**Exemplo:**
|
|
```
|
|
mcp__desk-crm-v3__get_customer_notes
|
|
└─────────┘ └────────────────┘
|
|
server=12 tool_name=18 = 30 chars ✅
|
|
|
|
mcp__desk-crm-v3__get_customer_alternate_addresses_with_details
|
|
└─────────┘ └──────────────────────────────────────────┘
|
|
server=12 tool_name=46 = 58 chars ❌ (tool > 40)
|
|
```
|
|
|
|
**Validação recomendada:**
|
|
```typescript
|
|
function validateToolName(name: string): void {
|
|
if (name.length > 40) {
|
|
throw new Error(`Tool name "${name}" exceeds 40 char limit (${name.length})`);
|
|
}
|
|
}
|
|
|
|
// Aplicar a todas as tools no array
|
|
allTools.forEach(t => validateToolName(t.name));
|
|
```
|
|
|
|
---
|
|
|
|
### Error Handling
|
|
|
|
```typescript
|
|
try {
|
|
// operação
|
|
} catch (error) {
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Erro: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
```
|
|
|
|
### Validação com Zod
|
|
|
|
```typescript
|
|
import { z } from 'zod';
|
|
|
|
const InputSchema = z.object({
|
|
param: z.string().min(1),
|
|
optional: z.number().optional(),
|
|
});
|
|
|
|
// No handler
|
|
const validated = InputSchema.parse(args);
|
|
```
|
|
|
|
### Logging
|
|
|
|
```typescript
|
|
// Usar console.error (não console.log - interfere com stdio)
|
|
console.error(`[MCP] Processing: ${param}`);
|
|
```
|
|
|
|
---
|
|
|
|
## Transporte HTTP (StreamableHTTP) - RECOMENDADO
|
|
|
|
**Padrão oficial MCP SDK.** Usar para todos os novos MCPs.
|
|
|
|
### Vantagens HTTP vs SSE
|
|
|
|
| Aspecto | HTTP (StreamableHTTP) | SSE (deprecated) |
|
|
|---------|----------------------|------------------|
|
|
| Padrão | ✅ Oficial MCP SDK | ⚠️ Deprecated |
|
|
| Stateless | ✅ Suportado | ❌ Sempre stateful |
|
|
| Simplicidade | ✅ Um endpoint | ❌ /sse + /message |
|
|
| Gate ready | ✅ REST-like | ❌ Streaming complexo |
|
|
|
|
### Template index-http.ts (PADRÃO)
|
|
|
|
```typescript
|
|
#!/usr/bin/env node
|
|
/**
|
|
* MCP <Nome> - HTTP Server Mode
|
|
* StreamableHTTP transport for web/remote access
|
|
* @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,
|
|
} 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 || '32XX', 10);
|
|
const HOST = process.env.MCP_HTTP_HOST || '127.0.0.1';
|
|
const STATEFUL = process.env.MCP_STATEFUL !== 'false';
|
|
|
|
// Track active sessions (stateful mode)
|
|
const sessions = new Map<string, { transport: StreamableHTTPServerTransport }>();
|
|
|
|
// Define tools array
|
|
const allTools = [
|
|
{
|
|
name: 'example_tool',
|
|
description: 'Descrição da ferramenta',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
param: { type: 'string', description: 'Parâmetro exemplo' }
|
|
},
|
|
required: ['param']
|
|
},
|
|
handler: async (args: any) => ({
|
|
content: [{ type: 'text', text: `Resultado: ${args.param}` }]
|
|
})
|
|
}
|
|
];
|
|
|
|
function createMcpServer(): Server {
|
|
const server = new Server(
|
|
{ name: 'mcp-nome', version: '1.0.0' },
|
|
{ capabilities: { tools: {} } }
|
|
);
|
|
|
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
tools: allTools.map(t => ({
|
|
name: t.name,
|
|
description: t.description,
|
|
inputSchema: t.inputSchema
|
|
}))
|
|
}));
|
|
|
|
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 not found: ${name}` }] };
|
|
}
|
|
|
|
try {
|
|
return await tool.handler(args);
|
|
} catch (error) {
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
}],
|
|
isError: true
|
|
};
|
|
}
|
|
});
|
|
|
|
return server;
|
|
}
|
|
|
|
async function main() {
|
|
const httpServer = http.createServer(async (req, res) => {
|
|
// CORS headers
|
|
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}`);
|
|
|
|
// Health check endpoint
|
|
if (url.pathname === '/health') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
status: 'ok',
|
|
transport: 'streamable-http',
|
|
version: '1.0.0',
|
|
sessions: sessions.size,
|
|
stateful: STATEFUL,
|
|
tools: allTools.length
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// Stats endpoint
|
|
if (url.pathname === '/stats') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
totalTools: allTools.length,
|
|
activeSessions: sessions.size
|
|
}, null, 2));
|
|
return;
|
|
}
|
|
|
|
// MCP endpoint
|
|
if (url.pathname === '/mcp') {
|
|
try {
|
|
const transport = new StreamableHTTPServerTransport({
|
|
sessionIdGenerator: STATEFUL ? () => randomUUID() : undefined
|
|
});
|
|
|
|
const server = createMcpServer();
|
|
|
|
if (STATEFUL && transport.sessionId) {
|
|
sessions.set(transport.sessionId, { transport });
|
|
transport.onclose = () => {
|
|
if (transport.sessionId) {
|
|
sessions.delete(transport.sessionId);
|
|
}
|
|
};
|
|
}
|
|
|
|
await server.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;
|
|
}
|
|
|
|
// 404
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
});
|
|
|
|
httpServer.listen(PORT, HOST, () => {
|
|
console.log(`MCP HTTP Server v1.0.0`);
|
|
console.log(` Endpoint: http://${HOST}:${PORT}/mcp`);
|
|
console.log(` Health: http://${HOST}:${PORT}/health`);
|
|
console.log(` Mode: ${STATEFUL ? 'Stateful' : 'Stateless'}`);
|
|
console.log(` Tools: ${allTools.length}`);
|
|
});
|
|
|
|
// Graceful shutdown
|
|
const shutdown = () => {
|
|
httpServer.close(() => process.exit(0));
|
|
};
|
|
process.on('SIGINT', shutdown);
|
|
process.on('SIGTERM', shutdown);
|
|
}
|
|
|
|
main().catch(console.error);
|
|
```
|
|
|
|
### Configuração HTTP
|
|
|
|
```json
|
|
// ~/.claude.json - HTTP transport
|
|
{
|
|
"mcpServers": {
|
|
"meu-mcp": {
|
|
"type": "http",
|
|
"url": "http://127.0.0.1:32XX/mcp"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Portas HTTP Reservadas
|
|
|
|
| Porta | MCP | Estado |
|
|
|-------|-----|--------|
|
|
| 3200 | outline-postgresql | ✅ HTTP |
|
|
| 3201+ | disponíveis | - |
|
|
|
|
### Scripts package.json (HTTP)
|
|
|
|
```json
|
|
{
|
|
"scripts": {
|
|
"start:http": "node dist/index-http.js",
|
|
"dev:http": "tsx src/index-http.ts",
|
|
"reload:http": "npm run build && systemctl --user restart mcp-nome && sleep 2 && curl -s http://127.0.0.1:32XX/health"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Transporte SSE (Server-Sent Events) - DEPRECATED
|
|
|
|
⚠️ **DEPRECATED:** Usar HTTP (StreamableHTTP) para novos MCPs. SSE mantido apenas para retrocompatibilidade.
|
|
|
|
Para MCPs que precisam de conexao persistente (ex: Claude Code com multiplos terminais).
|
|
|
|
**IMPORTANTE:** Verificar MCPs existentes (desk-crm-v3, moloni) antes de implementar!
|
|
|
|
### Configuracao SSE
|
|
|
|
```json
|
|
// ~/.claude.json
|
|
{
|
|
"mcpServers": {
|
|
"meu-mcp": {
|
|
"type": "sse",
|
|
"url": "http://127.0.0.1:31XX/sse"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Template index-sse.ts (http nativo - RECOMENDADO)
|
|
|
|
```typescript
|
|
#!/usr/bin/env node
|
|
/** @author Descomplicar | @link descomplicar.pt | @copyright 2026 */
|
|
|
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
import {
|
|
ListToolsRequestSchema,
|
|
CallToolRequestSchema,
|
|
ListResourcesRequestSchema,
|
|
ListPromptsRequestSchema
|
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
import * as http from 'http';
|
|
import { URL } from 'url';
|
|
|
|
const PORT = parseInt(process.env.MCP_SSE_PORT || '31XX');
|
|
const HOST = process.env.MCP_SSE_HOST || '127.0.0.1';
|
|
|
|
// Track active sessions
|
|
const sessions = new Map<string, SSEServerTransport>();
|
|
|
|
// Define tools array
|
|
const allTools = [
|
|
// ... your tools
|
|
];
|
|
|
|
// Create MCP server for each session
|
|
function createMcpServer(): Server {
|
|
const server = new Server({
|
|
name: 'mcp-nome',
|
|
version: '1.0.0'
|
|
});
|
|
|
|
// Set capabilities
|
|
(server as any)._capabilities = {
|
|
tools: {},
|
|
resources: {},
|
|
prompts: {}
|
|
};
|
|
|
|
// List tools
|
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
tools: allTools.map((t) => ({
|
|
name: t.name,
|
|
description: t.description,
|
|
inputSchema: t.inputSchema
|
|
}))
|
|
}));
|
|
|
|
// List resources (empty)
|
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
resources: []
|
|
}));
|
|
|
|
// List prompts (empty)
|
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
prompts: []
|
|
}));
|
|
|
|
// Call tool
|
|
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 not found' }] };
|
|
}
|
|
|
|
try {
|
|
return await tool.handler(args);
|
|
} catch (error) {
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
}]
|
|
};
|
|
}
|
|
});
|
|
|
|
return server;
|
|
}
|
|
|
|
async function main() {
|
|
const httpServer = http.createServer(async (req, res) => {
|
|
// CORS headers
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.writeHead(200);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(req.url || '/', `http://${HOST}:${PORT}`);
|
|
|
|
// Health check
|
|
if (url.pathname === '/health') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
status: 'ok',
|
|
sessions: sessions.size,
|
|
tools: allTools.length
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// SSE endpoint - new session
|
|
if (url.pathname === '/sse' && req.method === 'GET') {
|
|
const transport = new SSEServerTransport('/message', res);
|
|
const sessionId = transport.sessionId; // USAR sessionId do transport!
|
|
sessions.set(sessionId, transport);
|
|
|
|
// Criar NOVO server por sessao
|
|
const server = createMcpServer();
|
|
|
|
transport.onclose = () => {
|
|
sessions.delete(sessionId);
|
|
};
|
|
|
|
await server.connect(transport);
|
|
return;
|
|
}
|
|
|
|
// Message endpoint
|
|
if (url.pathname === '/message' && req.method === 'POST') {
|
|
const sessionId = url.searchParams.get('sessionId');
|
|
|
|
if (!sessionId) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Missing sessionId' }));
|
|
return;
|
|
}
|
|
|
|
const transport = sessions.get(sessionId);
|
|
if (!transport) {
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Session not found' }));
|
|
return;
|
|
}
|
|
|
|
await transport.handlePostMessage(req, res);
|
|
return;
|
|
}
|
|
|
|
// 404
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
});
|
|
|
|
httpServer.listen(PORT, HOST, () => {
|
|
console.log(`MCP SSE Server running at http://${HOST}:${PORT}/sse`);
|
|
});
|
|
|
|
// Graceful shutdown
|
|
process.on('SIGTERM', () => httpServer.close(() => process.exit(0)));
|
|
process.on('SIGINT', () => httpServer.close(() => process.exit(0)));
|
|
}
|
|
|
|
main().catch(console.error);
|
|
```
|
|
|
|
### Pontos Criticos SSE
|
|
|
|
| Aspecto | Correcto | Errado |
|
|
|---------|----------|--------|
|
|
| sessionId | `transport.sessionId` | `Date.now().toString()` |
|
|
| Server | Novo por sessao | Reutilizar mesmo |
|
|
| HTTP | `http.createServer` | Express (overhead) |
|
|
| /message | Com `?sessionId=` | Sem sessionId |
|
|
|
|
### Template systemd Service
|
|
|
|
Criar em `~/.config/systemd/user/mcp-nome.service`:
|
|
|
|
```ini
|
|
[Unit]
|
|
Description=MCP Nome SSE 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.js --sse --port 31XX
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
Environment=NODE_ENV=production
|
|
Environment=LOG_LEVEL=error
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
```
|
|
|
|
### Comandos systemd
|
|
|
|
```bash
|
|
# Activar
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable mcp-nome.service
|
|
systemctl --user start mcp-nome.service
|
|
|
|
# Ver estado
|
|
systemctl --user status mcp-nome
|
|
|
|
# Ver logs
|
|
journalctl --user -u mcp-nome -f
|
|
|
|
# Reiniciar apos alteracoes
|
|
systemctl --user restart mcp-nome
|
|
```
|
|
|
|
### Portas SSE Reservadas
|
|
|
|
| Porta | MCP |
|
|
|-------|-----|
|
|
| 3100 | desk-crm-v3 |
|
|
| 3101 | memory-supabase |
|
|
| 3102 | wikijs |
|
|
| 3103 | ssh-unified |
|
|
| 3104 | dify-kb |
|
|
| 3105 | n8n |
|
|
| 3106 | cwp |
|
|
| 3107 | youtube-research |
|
|
| 3108 | moloni |
|
|
| 3109+ | disponiveis |
|
|
|
|
### Script de Desenvolvimento (reload:sse)
|
|
|
|
Adicionar ao `package.json`:
|
|
|
|
```json
|
|
{
|
|
"scripts": {
|
|
"start:sse": "node dist/index.js --sse",
|
|
"reload:sse": "npm run build && systemctl --user restart mcp-nome && sleep 2 && curl -s http://127.0.0.1:31XX/health"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Padrões SQL (Perfex CRM)
|
|
|
|
Para MCPs que integram com Perfex CRM:
|
|
|
|
### Campos FK (Foreign Keys)
|
|
|
|
```typescript
|
|
// CORRECTO: Perfex usa 0 como "não definido", NÃO NULL
|
|
client_id || 0,
|
|
project_id || 0,
|
|
country || 0,
|
|
assigned || 0,
|
|
|
|
// EXCEPÇÃO: source em leads precisa ID válido (buscar da BD)
|
|
let leadSource = source;
|
|
if (!leadSource) {
|
|
const [defaultSource] = await client.query(
|
|
'SELECT id FROM tblleads_sources ORDER BY id ASC LIMIT 1'
|
|
);
|
|
leadSource = defaultSource?.id || 1;
|
|
}
|
|
```
|
|
|
|
### Padrão de Fallback
|
|
|
|
```typescript
|
|
// Para campos que aceitam 0 como default
|
|
field || 0
|
|
|
|
// Para campos que precisam valor válido da BD
|
|
const defaultValue = await getDefaultFromDB();
|
|
field || defaultValue
|
|
```
|
|
|
|
### Tabelas Perfex (prefixo tbl)
|
|
|
|
```
|
|
tblclients, tblprojects, tbltasks, tblinvoices,
|
|
tblleads, tblleads_sources, tblleads_status,
|
|
tblstaff, tbltaskstimers, tblexpenses
|
|
```
|
|
|
|
---
|
|
|
|
## Checklist de Segurança MCP
|
|
|
|
**Baseado em audit MCP Outline PostgreSQL (164 tools, 7 bugs corrigidos).**
|
|
|
|
### Antes de Commit
|
|
|
|
- [ ] **SQL Injection:** Todos os inputs validados antes de entrar em queries?
|
|
```typescript
|
|
// ERRADO: interpolação directa
|
|
const query = `ORDER BY ${field} ASC`;
|
|
|
|
// CORRECTO: validar primeiro
|
|
validateFieldName(field); // rejeita keywords SQL
|
|
const query = `ORDER BY ${field} ASC`;
|
|
```
|
|
|
|
- [ ] **Transações:** Operações multi-query relacionadas em transação?
|
|
```typescript
|
|
// ERRADO: queries separadas
|
|
await query('DELETE FROM children WHERE parentId = $1', [id]);
|
|
await query('DELETE FROM parents WHERE id = $1', [id]);
|
|
|
|
// CORRECTO: transação
|
|
await withTransaction(async (client) => {
|
|
await client.query('DELETE FROM children WHERE parentId = $1', [id]);
|
|
await client.query('DELETE FROM parents WHERE id = $1', [id]);
|
|
});
|
|
```
|
|
|
|
- [ ] **Recursos:** Aquisição tem `finally` com release?
|
|
```typescript
|
|
// ERRADO: release pode não executar
|
|
const client = await pool.connect();
|
|
await client.query('SELECT 1');
|
|
client.release();
|
|
|
|
// CORRECTO: finally garante release
|
|
let client = null;
|
|
try {
|
|
client = await pool.connect();
|
|
await client.query('SELECT 1');
|
|
} finally {
|
|
if (client) client.release();
|
|
}
|
|
```
|
|
|
|
- [ ] **Cleanup:** Código de cleanup tem try-catch próprio?
|
|
```typescript
|
|
// ERRADO: ROLLBACK pode falhar e mascarar erro
|
|
catch (error) {
|
|
await client.query('ROLLBACK');
|
|
throw error;
|
|
}
|
|
|
|
// CORRECTO: ROLLBACK com try-catch
|
|
catch (error) {
|
|
try {
|
|
await client.query('ROLLBACK');
|
|
} catch (rollbackError) {
|
|
console.error('Rollback failed', rollbackError);
|
|
}
|
|
throw error;
|
|
}
|
|
```
|
|
|
|
- [ ] **Random:** Usando `crypto.randomBytes()` e não `Math.random()`?
|
|
- [ ] **Versão:** Sincronizada com package.json?
|
|
|
|
### Grep de Validação
|
|
|
|
```bash
|
|
# Verificar interpolação SQL perigosa
|
|
grep -rn '`.*\${.*}`' src/ | grep -i 'select\|insert\|update\|delete\|order'
|
|
|
|
# Verificar Math.random em produção
|
|
grep -rn 'Math.random' src/
|
|
|
|
# Verificar .connect() sem finally
|
|
grep -rn '\.connect()' src/
|
|
```
|
|
|
|
---
|
|
|
|
## MCPs Existentes (Referência)
|
|
|
|
| MCP | Path | Tools |
|
|
|-----|------|-------|
|
|
| desk-crm-v3 | ~/mcp-servers/desk-crm-v3/ | 150+ |
|
|
| memory-supabase | ~/mcp-servers/memory-supabase/ | 5 |
|
|
| google-workspace | (npm package) | 42 |
|
|
| filesystem | (npm package) | 8 |
|
|
|
|
---
|
|
|
|
## Integração com Agente
|
|
|
|
O agente `mcp-protocol-developer` é invocado para:
|
|
- Desenvolvimento complexo de ferramentas
|
|
- Debug de problemas MCP
|
|
- Optimização de performance
|
|
- Implementação de recursos avançados
|
|
|
|
---
|
|
|
|
## Datasets Dify
|
|
|
|
```
|
|
mcp__notebooklm__notebook_query, mcp__dify-kb__dify_kb_retrieve_segments dataset:"MCP Servers" query:"..."
|
|
mcp__dify-kb__dify_kb_retrieve_segments dataset:"Claude Code" query:"..."
|
|
mcp__dify-kb__dify_kb_retrieve_segments dataset:"Desenvolvimento de Software" query:"..."
|
|
```
|
|
|
|
---
|
|
|
|
## Referências e Documentação
|
|
|
|
### Procedimentos Obrigatórios (D7-Tecnologia)
|
|
|
|
**SEMPRE consultar antes de criar/modificar MCPs:**
|
|
|
|
- **[PROC-MCP-Desenvolvimento.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-MCP-Desenvolvimento.md)** - Guia oficial v2.3: Regra de Ouro MCP, capabilities obrigatórias, validação pre-deploy, fallback enterprise
|
|
- **[PROC-MCP-Troubleshooting-Erro-471.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-MCP-Troubleshooting-Erro-471.md)** - Debug erro 471 (capabilities incompletas, too many requests)
|
|
- **[PROC-MCP-Google-Auth.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-MCP-Google-Auth.md)** - Autenticação OAuth Google Workspace, refresh tokens, troubleshooting
|
|
- **[PROC-MCP-Session-Recovery.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-MCP-Session-Recovery.md)** - Recuperação de sessões MCP após crash
|
|
- **[PROC-MCP-Desk-Timer.md](file:///media/ealmeida/Dados/Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-MCP-Desk-Timer.md)** - Workflow timer Desk CRM (atribuição e status obrigatórios)
|
|
|
|
### Quick Reference (ver PROC-MCP-Desenvolvimento.md)
|
|
|
|
- **ESLint + Prettier:** Ver PROC secção "Scripts package.json"
|
|
- **Husky pre-commit:** Ver PROC secção "Husky + Lint-Staged"
|
|
- **Checklist novo MCP:** Ver PROC secção "Checklist Desenvolvimento MCP"
|
|
- **Schema BD:** Ver PROC secção "Documentação Schema BD"
|
|
- **Validação pre-deploy:** Ver PROC secção "Validação Obrigatória"
|
|
|
|
### Agente Especializado
|
|
|
|
- **Agent:** `mcp-protocol-developer` - Desenvolvimento complexo, debug, optimização, recursos avançados
|
|
|
|
### Documentação Técnica
|
|
|
|
- [MCP SDK Documentation](https://modelcontextprotocol.io/)
|
|
- [[Stack/Claude Code/MCPs/|MCPs Documentados]]
|
|
- [[D7-Tecnologia/INDEX|Ver Todos Procedimentos D7]]
|
|
|
|
---
|
|
|
|
---
|
|
|
|
## Changelog
|
|
|
|
### v1.3.0 (2026-01-31)
|
|
- **HTTP StreamableHTTP** como padrão oficial (template completo)
|
|
- SSE marcado como DEPRECATED (retrocompatibilidade)
|
|
- **Checklist de Segurança MCP** (baseado em audit 164 tools)
|
|
- SQL injection prevention
|
|
- Transacções para operações multi-query
|
|
- Resource release em finally blocks
|
|
- Cleanup error handling
|
|
- crypto.randomBytes() vs Math.random()
|
|
- Grep de validação para code review
|
|
- Portas HTTP reservadas (3200+)
|
|
|
|
### v1.2.0 (2026-01-27)
|
|
- Template SSE completo com http nativo (nao Express)
|
|
- Pontos criticos SSE documentados (sessionId, Server por sessao)
|
|
- Template systemd service para MCPs SSE
|
|
- Tabela de portas SSE reservadas
|
|
- Comandos systemd documentados
|
|
|
|
### v1.1.0 (2026-01-27)
|
|
- Adicionada seccao SSE (Server-Sent Events)
|
|
- Script reload:sse para desenvolvimento
|
|
- Padroes SQL Perfex CRM (FKs usam 0, nao NULL)
|
|
- Workflow de desenvolvimento documentado
|
|
|
|
### v1.0.0 (2026-01-27)
|
|
- Versao inicial
|
|
|
|
---
|
|
|
|
*Skill v1.3 | Descomplicar®*
|
|
|
|
---
|
|
|
|
## Instrumentação Automática
|
|
|
|
Esta skill grava métricas automaticamente para análise PDCA.
|
|
|
|
### Query para Gravar (executar no final)
|
|
|
|
```sql
|
|
INSERT INTO tblskill_agent_metrics (
|
|
type, name, duration_ms, status, staff_id,
|
|
kb_consulted, kb_cache_hit, tool_calls, project_id
|
|
) VALUES (
|
|
'skill', '/$SKILL_NAME', {DURACAO_MS}, '{STATUS}', 25,
|
|
{KB_CONSULTADO}, {CACHE_HIT}, {TOOL_CALLS}, {PROJECT_ID}
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
**Instrumentação**: Activa | **Data**: 2026-02-03
|
|
|
|
---
|
|
|
|
|
|
## Quando NÃO Usar
|
|
|
|
- Para tarefas fora do domínio de especialização desta skill
|
|
- Quando outra skill mais específica está disponível
|
|
- Para operações que requerem confirmação manual do utilizador
|
|
|
|
|
|
## Protocolo
|
|
|
|
1. Analisar requisitos da tarefa
|
|
2. Verificar disponibilidade de ferramentas necessárias
|
|
3. Executar operações de forma incremental
|
|
4. Validar resultados antes de concluir
|
|
5. Reportar status e próximos passos
|