mozak.tech Engenharia de IA Corporativa 24%

Parte I — Conectividade Padronizada entre Sistemas e Modelos

1.9 — MCP em Produção: HTTP com SSE, Health Check e Contêineres (MCP em Produção)

Objetivo da Aula

Ao concluir esta aula, você será capaz de:

Implementar um MCP server com transport HTTP+SSE usando SSEServerTransport

Configurar endpoints de health check e métricas (Prometheus-compatible)

Gerenciar múltiplas conexões SSE simultâneas com Map de sessões

Containerizar um MCP server com Dockerfile de boas práticas

Conectar um MCP Inspector externo para debug e verificação do servidor

Por que isso importa

Desenvolvimento local usa InMemoryTransport ou stdio — simples, sem rede. Mas para disponibilizar um MCP server para múltiplos usuários simultâneos, você precisa de HTTP+SSE.

Por que SSE e não WebSocket? - MCP especifica SSE como transport para HTTP - SSE é unidirecional do servidor ao cliente (mensagens MCP chegam via SSE) - Mensagens do cliente chegam via POST HTTP normal - SSE funciona com load balancers, proxies e CDNs sem configuração especial - WebSocket precisaria de configuração adicional no nginx/ALB

O starter desta unidade implementa um MCP server de produção pronto para container:

curl http://localhost:3001/health  →  {"status":"ok"}

curl http://localhost:3001/metrics  →  métricas Prometheus

GET  http://localhost:3001/sse  →  SSE stream (MCP connection)

POST http://localhost:3001/messages  →  mensagens do cliente

Conceitos Fundamentais

Transport HTTP+SSE vs InMemoryTransport

Aspecto

InMemoryTransport

HTTP+SSE

Uso

Testes, labs

Produção

Processo

Mesmo processo

Processos separados

Clientes

1 por vez

Múltiplos simultâneos

Latência

~0ms

1-10ms (local)

Deploy

Não

Sim

Debug MCP Inspector

Não

Sim

Fundamento: Sessão SSE do MCP

O transporte HTTP+SSE do MCP usa dois canais separados para comunicação bidirecional. O cliente faz um GET /sse que fica aberto como Server-Sent Events — o servidor envia respostas por esse stream. Para enviar comandos, o cliente faz POST /messages com um header correlacionando à sessão SSE aberta. É diferente de um request/response REST: o canal de saída (SSE) fica persistente, e o canal de entrada (POST) é stateless mas correlacionado pelo session-id. Na prática, é um padrão análogo ao WebSocket porém em HTTP puro, compatível com proxies e load balancers que não suportam upgrade de protocolo. Para o arquiteto: configure o timeout do load balancer para o GET /sse como "sem timeout" (ou muito longo) — conexões SSE que caem por timeout do LB interrompem todas as sessões ativas.

SSEServerTransport — Como Funciona

// Endpoint SSE: cliente MCP conecta aqui

app.get('/sse', async (req, res) => {

  const sessionId = `session-${Date.now()}-${Math.random().toString(36).substring(2)}`

  

  // SSEServerTransport recebe res (response stream) e o path POST

  const transport = new SSEServerTransport('/messages', res)

  const server = criarMCPServer()

  

  // Registrar para receber POST /messages

  conexoes.set(sessionId, transport)

  res.on('close', () => conexoes.delete(sessionId))

  

  // Conectar server ao transport SSE

  await server.connect(transport)

})



// Endpoint POST: mensagens do cliente chegam aqui

app.post('/messages', async (req, res) => {

  const sessionId = req.headers['x-session-id'] as string

  const transport = conexoes.get(sessionId)

  if (!transport) return res.status(404).json({ erro: 'Sessão não encontrada' })

  await transport.handlePostMessage(req, res)

})

O fluxo é: 1. Cliente MCP faz GET /sse → recebe SSE stream e sessionId 2. Para enviar mensagem, cliente faz POST /messages com header x-session-id 3. transport.handlePostMessage() processa e envia response via SSE stream

Health Check para Kubernetes

app.get('/health', (_, res) => {

  res.json({ status: 'ok', timestamp: new Date().toISOString() })

})

Simples, mas crítico para: - Kubernetes liveness probe: mata e reinicia pod se /health falhar - Kubernetes readiness probe: para de enviar tráfego se o pod não estiver pronto - Load balancer health check: remove instâncias não saudáveis do pool

Em produção, expanda:

app.get('/health', async (_, res) => {

  const checks = {

    server: 'ok',

    conexoes_ativas: conexoes.size,

    memoria_mb: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1),

  }

  

  // Adicionar checks de dependências (DB, Redis, etc)

  // try { await redis.ping(); checks.redis = 'ok' } catch { checks.redis = 'error' }

  

  const saudavel = Object.values(checks).every(v => v !== 'error')

  res.status(saudavel ? 200 : 503).json({ status: saudavel ? 'ok' : 'degraded', checks })

})

Endpoint de Métricas Prometheus

app.get('/metrics', (_, res) => {

  res.type('text/plain').send([

    `mcp_uptime_seconds ${process.uptime().toFixed(0)}`,

    `mcp_memory_bytes ${process.memoryUsage().heapUsed}`,

    `mcp_connections_active ${conexoes.size}`,

  ].join('\n'))

})

Formato Prometheus (nome_metrica{labels} valor). Com um scraper Prometheus e Grafana, você visualiza essas métricas em tempo real.

Aprofundamento Técnico

Gerenciamento de Sessões e Memory Leaks

const conexoes = new Map<string, SSEServerTransport>()



app.get('/sse', async (req, res) => {

  const sessionId = `session-${Date.now()}-${Math.random().toString(36).substring(2)}`

  const transport = new SSEServerTransport('/messages', res)

  const server = criarMCPServer()

  

  conexoes.set(sessionId, transport)

  

  // CRÍTICO: limpar sessão quando cliente desconectar

  res.on('close', () => {

    conexoes.delete(sessionId)

    console.log(`Conexão encerrada: ${sessionId}`)

  })

  

  await server.connect(transport)

})

res.on('close') é o listener de desconexão. Sem ele, o Map conexoes cresce indefinidamente (memory leak). Em produção, adicione também um TTL por sessão:

// Após 30 minutos sem atividade, limpar sessão

const TIMEOUT_MS = 30 * 60 * 1000

const sessionTimers = new Map<string, NodeJS.Timeout>()



function resetTimer(sessionId: string) {

  clearTimeout(sessionTimers.get(sessionId))

  sessionTimers.set(sessionId, setTimeout(() => {

    conexoes.delete(sessionId)

    sessionTimers.delete(sessionId)

  }, TIMEOUT_MS))

}

Containerização com Docker

# Etapa 1: build

FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./

RUN npm ci --only=production

COPY tsconfig.json .

COPY *.ts .

RUN npx tsc



# Etapa 2: runtime (imagem menor)

FROM node:20-alpine AS runtime

WORKDIR /app



# Usuário não-root (segurança)

RUN addgroup -S mcp && adduser -S mcp -G mcp

USER mcp



# Apenas o necessário para runtime

COPY --from=builder /app/node_modules ./node_modules

COPY --from=builder /app/dist ./dist



# Healthcheck integrado no container

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \

  CMD wget -q --spider http://localhost:${PORT:-3001}/health || exit 1



EXPOSE 3001

ENV PORT=3001

ENV NODE_ENV=production



CMD ["node", "dist/starter.js"]

Boas práticas neste Dockerfile: 1. Multi-stage build: imagem final sem devDependencies e arquivos TypeScript (menor e mais segura) 2. node:20-alpine: alpine = imagem mínima (~5MB vs ~1GB para node:20) 3. Usuário não-root: USER mcp — se o container for comprometido, o atacante não tem acesso root 4. HEALTHCHECK: Kubernetes e Docker Compose usam isso para verificar saúde 5. ENV NODE_ENV=production: ativa otimizações do Node.js

Conectando MCP Inspector ao Server

O MCP Inspector é uma ferramenta oficial para testar servidores MCP:

# Terminal 1: rodar o server

npx ts-node starter.ts



# Terminal 2: conectar inspector

npx @modelcontextprotocol/inspector http://localhost:3001/sse

O Inspector permite: - Listar tools disponíveis - Chamar tools com argumentos custom - Ver o JSON-RPC 2.0 trafegado - Debug de respostas

Exemplos Anotados

Exemplo 1: Server Completo com Autenticação Bearer

import express from 'express'

import { Server } from '@modelcontextprotocol/sdk/server/index.js'

import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'

import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'



const app = express()

app.use(express.json())



const API_KEY = process.env.MCP_API_KEY ?? 'dev-key-insegura'



// Middleware de autenticação

function autenticar(req: express.Request, res: express.Response, next: express.NextFunction) {

  const auth = req.headers.authorization

  if (!auth?.startsWith('Bearer ') || auth.slice(7) !== API_KEY) {

    return res.status(401).json({ erro: 'Não autorizado' })

  }

  next()

}



function criarMCPServer(): Server {

  const server = new Server(

    { name: 'producao-server', version: '1.0.0' },

    { capabilities: { tools: {} } }

  )



  server.setRequestHandler(ListToolsRequestSchema, async () => ({

    tools: [

      {

        name: 'ping',

        description: 'Verifica se o server está respondendo',

        inputSchema: { type: 'object', properties: {} },

      },

      {

        name: 'obter_info_sistema',

        description: 'Retorna informações do sistema: versão, uptime, memória',

        inputSchema: { type: 'object', properties: {} },

      },

    ],

  }))



  server.setRequestHandler(CallToolRequestSchema, async ({ params: { name } }) => {

    if (name === 'ping') {

      return { content: [{ type: 'text', text: `pong — ${new Date().toISOString()}` }] }

    }

    if (name === 'obter_info_sistema') {

      return {

        content: [{

          type: 'text',

          text: JSON.stringify({

            versao: '1.0.0',

            uptime_s: process.uptime().toFixed(0),

            memoria_mb: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1),

            node_version: process.version,

            timestamp: new Date().toISOString(),

          }, null, 2),

        }],

      }

    }

    throw new Error(`Tool desconhecida: ${name}`)

  })



  return server

}



const conexoes = new Map<string, SSEServerTransport>()



// Saúde (sem auth — load balancer não envia token)

app.get('/health', (_, res) => {

  res.json({

    status: 'ok',

    conexoes_ativas: conexoes.size,

    uptime_s: process.uptime().toFixed(0),

    timestamp: new Date().toISOString(),

  })

})



// Métricas Prometheus (sem auth — Prometheus scraper interno)

app.get('/metrics', (_, res) => {

  res.type('text/plain').send([

    `mcp_uptime_seconds ${process.uptime().toFixed(0)}`,

    `mcp_memory_bytes ${process.memoryUsage().heapUsed}`,

    `mcp_connections_active ${conexoes.size}`,

  ].join('\n'))

})



// Endpoint SSE com autenticação

app.get('/sse', autenticar, async (req, res) => {

  const sessionId = `session-${Date.now()}-${Math.random().toString(36).substring(2)}`

  console.log(`Nova conexão MCP: ${sessionId}`)



  const transport = new SSEServerTransport('/messages', res)

  const server = criarMCPServer()



  conexoes.set(sessionId, transport)

  res.on('close', () => {

    conexoes.delete(sessionId)

    console.log(`Conexão encerrada: ${sessionId} (total: ${conexoes.size})`)

  })



  await server.connect(transport)

})



// Endpoint POST com autenticação

app.post('/messages', autenticar, async (req, res) => {

  const sessionId = req.headers['x-session-id'] as string

  const transport = conexoes.get(sessionId)

  if (!transport) return res.status(404).json({ erro: 'Sessão não encontrada' })

  await transport.handlePostMessage(req, res)

})



const PORT = parseInt(process.env.PORT ?? '3001')

app.listen(PORT, () => {

  console.log(`MCP Server rodando em http://localhost:${PORT}`)

  console.log(`Health: http://localhost:${PORT}/health`)

  console.log(`SSE:    http://localhost:${PORT}/sse  (requer Authorization: Bearer ${API_KEY})`)

})

Exemplo 2: Cliente que Conecta ao Server HTTP

import Anthropic from '@anthropic-ai/sdk'

import { Client } from '@modelcontextprotocol/sdk/client/index.js'

import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'



async function consultarServerHTTP(query: string) {

  const anthropic = new Anthropic()



  // Conectar ao server HTTP (em vez de InMemoryTransport)

  const transport = new SSEClientTransport(

    new URL('http://localhost:3001/sse'),

    {

      requestInit: {

        headers: { Authorization: `Bearer ${process.env.MCP_API_KEY}` }

      }

    }

  )



  const client = new Client({ name: 'cliente-http', version: '1.0.0' }, { capabilities: {} })

  await client.connect(transport)



  const { tools: mcpTools } = await client.listTools()

  const tools: Anthropic.Tool[] = mcpTools.map(t => ({

    name: t.name,

    description: t.description ?? '',

    input_schema: t.inputSchema as Anthropic.Tool['input_schema'],

  }))



  const mensagens: Anthropic.MessageParam[] = [{ role: 'user', content: query }]



  while (true) {

    const r = await anthropic.messages.create({

      model: 'claude-haiku-4-5-20251001',

      max_tokens: 300,

      tools,

      messages: mensagens,

    })

    mensagens.push({ role: 'assistant', content: r.content })



    if (r.stop_reason === 'end_turn') {

      const text = r.content.find(b => b.type === 'text')

      console.log('Resposta:', text?.type === 'text' ? text.text : '')

      break

    }



    const results: Anthropic.ToolResultBlockParam[] = []

    for (const b of r.content) {

      if (b.type !== 'tool_use') continue

      const res = await client.callTool({ name: b.name, arguments: b.input as Record<string, unknown> })

      results.push({

        type: 'tool_result', tool_use_id: b.id,

        content: res.content.filter(c => c.type === 'text').map(c => c.text).join('')

      })

    }

    mensagens.push({ role: 'user', content: results })

  }



  await client.close()

}



await consultarServerHTTP('O server está funcionando? Quais informações você tem?')

Padrões e Armadilhas

Padrões

Padrão 1: Um server MCP por instância Express (não singleton)

// O starter cria um novo server por conexão SSE:

app.get('/sse', async (req, res) => {

  const server = criarMCPServer()  // novo server por conexão

  await server.connect(transport)

})

Isso é correto: cada conexão SSE tem seu próprio server MCP isolado. State de uma conexão não vaza para outra.

Padrão 2: Logs estruturados com sessionId

// Inclua sessionId em todos os logs para correlação

console.log(JSON.stringify({

  level: 'info',

  sessionId,

  event: 'connection_open',

  timestamp: new Date().toISOString(),

}))

Em produção, esses logs vão para Datadog, CloudWatch ou Loki. Sem sessionId, é impossível rastrear problemas.

Padrão 3: /health sem autenticação, /metrics e /sse com autenticação Load balancers e probes Kubernetes chamam /health sem token. Metrics scrapers internos também. Mas /sse e /messages devem exigir autenticação.

Armadilhas

⚠️ Armadilha 1: Não limpar sessões = memory leak

// SEM cleanup (ERRADO):

conexoes.set(sessionId, transport)

// O Map cresce para sempre



// COM cleanup (CORRETO):

conexoes.set(sessionId, transport)

res.on('close', () => conexoes.delete(sessionId))

⚠️ Armadilha 2: PORT hardcoded

// ERRADO: não funciona em container

const PORT = 3001



// CORRETO: sempre leia de env

const PORT = parseInt(process.env.PORT ?? '3001')

Em Kubernetes, o PORT pode ser configurado externamente. process.env.PORT é padrão universal.

⚠️ Armadilha 3: Rodar como root no container

# ERRADO: container roda como root

FROM node:20-alpine

CMD ["node", "dist/server.js"]



# CORRETO: usuário não-root

FROM node:20-alpine

RUN addgroup -S app && adduser -S app -G app

USER app

CMD ["node", "dist/server.js"]

Se o container for comprometido via vulnerabilidade, usuário não-root limita o blast radius.

⚗ Laboratório prático — mozak.tech
Se não for realizar o laboratório, pule para o próximo capítulo.

Ponte para o Lab

O starter desta unidade é um MCP server HTTP+SSE completo — sem TODOs de código.

Parte 1: Verificar o server

npx ts-node starter.ts

# Em outro terminal:

curl http://localhost:3001/health

curl http://localhost:3001/metrics

Parte 2: Conectar MCP Inspector

npx @modelcontextprotocol/inspector http://localhost:3001/sse

Use o Inspector para chamar as tools ping e obter_info_sistema manualmente.

Parte 3: Adicionar autenticação Bearer Implemente o middleware autenticar do Exemplo 1. Verifique que /sse sem token retorna 401, e com token retorna a conexão SSE.

Parte 4: Dockerfile (opcional) Crie o Dockerfile do Aprofundamento Técnico. Construa e rode o container:

docker build -t mcp-server .

docker run -p 3001:3001 -e MCP_API_KEY=minha-chave mcp-server

Agora você está pronto para o lab.