Deep dive · Para engenheiros

Anatomia do mcp.nfe.io: stateless, 3 transportes, schemas validados.

Um MCP server não precisa ser complicado. O nosso tem zero estado em memória, usa o SDK MCP oficial da Anthropic, e entrega a mesma superfície de tools por três transportes distintos — escolhe o que fizer sentido pro seu ambiente. Código aberto, MIT, auditável linha por linha.

~180 KB
bundle gzipped
<10ms
CPU por request
~50ms
overhead p50 na borda
0 state
memória entre requests
Design · Stateless

Uma instância MCP por request. Morre no fim. Multi-tenant por natureza.

Todo request HTTP cria um McpServer fresco, instancia um NfeClient com a API key extraída do header, registra as 5 tools, processa a mensagem JSON-RPC, responde e descarta. Zero state entre requests — o servidor é um pass-through puro.

src/worker.ts · Cloudflare Workers MIT
export default {
  async fetch(request: Request): Promise<Response> {
    // 1. Extrai Bearer token do Authorization header
    const apiKey = extractApiKey(request);
    if (!apiKey) return unauthorized(...);

    // 2. Cria client NFE.io pra esse request só
    const nfe = createNfeClient({ apiKey, lookupApiKey });

    // 3. Instancia McpServer fresco e registra as 5 tools
    const server = new McpServer({ name, version });
    registerNfeTools(server, nfe);

    // 4. Conecta transport, processa, descarta
    const transport = new WebStandardStreamableHTTPServerTransport({
      sessionIdGenerator: undefined, // stateless
      enableJsonResponse: true,
    });
    await server.connect(transport);
    return transport.handleRequest(request);
  }
};

Por que stateless

Toda superfície de ataque vinha de "dados armazenados" — sessão, cache de key, pool de conexão com estado. Sem isso, a classe de bug cross-tenant leak deixa de existir por construção. Dois clientes diferentes batendo no mesmo momento nunca podem tocar no estado um do outro porque não há estado.

Efeito colateral bom: horizontal scaling sai de graça. Cada request pode ir pra qualquer instância, qualquer região, qualquer runtime.

API key viaja no header

Autenticação é Bearer puro: Authorization: Bearer <NFE_API_KEY>. Opcionalmente X-NFE-Lookup-Key pra chave separada de consulta. Ambas vivem ~150ms em memória de request e são descartadas. Nunca logadas, nunca cacheadas.

Trade-off consciente: cliente precisa mandar a key a cada request. Em troca: zero responsabilidade do servidor sobre vida/expiração da key.

Três transportes, uma superfície

Mesmas 5 tools. Três formas de entregar.

A lógica core vive em src/register-tools.ts. Cada entry point é um adapter fino: stdio pra rodar via npx, Streamable HTTP pro Cloudflare Workers, Express HTTP pro container OCI. Mudar de um pro outro não exige refatorar tool.

Transporte 1 · stdio

npx @nfe/mcp-server

O cliente MCP spawna o processo. Comunicação via stdin/stdout. Ideal pra dev local, ambiente corporativo com firewall, uso em pipeline de CI.

  • Entry: src/index.ts
  • Auth: env var NFE_API_KEY
  • Latência: zero overhead (inter-process)
  • Sem HTTP, sem TLS
Use quando: dev local, CI, firewall
Transporte 2 · HTTP · recomendado

mcp.nfe.io

Cloudflare Workers na borda global. Cliente MCP conecta direto com "type": "http". Sem instalação, sem Node local. SLA 99.99%, cold start quase zero.

  • Entry: src/worker.ts
  • Auth: header Bearer por request
  • Latência: ~50ms p50 overhead
  • Multi-tenant natural
Use quando: 95% dos casos
Transporte 3 · Container

ghcr.io/nfe/mcp-server

Imagem OCI multi-arch (amd64 + arm64). Roda em AWS Fargate, GCP Cloud Run, Azure Container Apps, Kubernetes, Fly.io, on-prem. Mesma superfície HTTP.

  • Entry: src/http-server.ts (Express)
  • Auth: header Bearer por request
  • Non-root user · HEALTHCHECK
  • Attested build provenance
Use quando: compliance, VPC, data residency
Anatomia de uma tool

Cada tool é um trio: schema, descrição, handler.

O SDK MCP oficial da Anthropic aceita registrar tools com schema Zod, descrição longa e annotations. Anotamos todas as 5 corretamente — read-only e idempotentes pra consultas, destrutivas pra emissão. O LLM lê as annotations no handshake e ajusta comportamento sem configuração extra.

nfeio_lookup_cnpj

name
"nfeio_lookup_cnpj" — prefixo nfeio_ evita colisão com outros MCP servers instalados no mesmo cliente.
inputSchema
z.object({ cnpj, updateAddress }).strict() — Zod com .strict(): qualquer campo extra é rejeitado antes de tocar na API.
description
Bloco de ~40 linhas que o LLM lê: resumo, "Use when", "Do NOT use when", lista de args, shape do retorno, erros possíveis. É o prompt que convence o agente a chamar essa tool no momento certo.
annotations
readOnlyHint: true, idempotentHint: true, destructiveHint: false, openWorldHint: true — o Claude Desktop usa isso pra decidir se pergunta confirmação.
handler
async (nfe, params) => { ... } — recebe o NfeClient injetado, chama UMA operação do SDK, retorna via jsonResult(). Try/catch delega erros a handleNfeError().
Error mapping

O agente precisa de erros acionáveis, não stack traces.

O SDK nfe-io tem hierarquia de erros bem definida. src/errors.ts mapeia cada classe pra uma mensagem MCP que o agente entende e consegue agir em cima — sem vazar detalhe interno, sem ambiguidade.

src/errors.ts · error → mensagem pro agente excerto
if (error instanceof AuthenticationError) {
  return formatError(
    "NFE.io authentication failed. Verify that NFE_API_KEY " +
    "is valid and has not been revoked."
  );
}

if (error instanceof ValidationError) {
  return formatError(
    `NFE.io rejected the request due to invalid data: ${error.message}`
  );
}

if (error instanceof PollingTimeoutError) {
  return formatError(
    `Invoice is still processing after polling timeout. Some 
municipalities take 3-5 min. Use nfeio_get_invoice_status 
with the returned invoiceId to check again later.`
  );
}

// ...8 outras classes tratadas com mensagens específicas
Perfil de performance

O que esperar em produção.

Números medidos em staging — o overhead do servidor MCP é marginal; o gargalo real é a latência de saída pra api.nfe.io e, de lá, pra cada prefeitura.

Métrica Valor Contexto
CPU time por request < 10ms Tempo de execução JS ativa no Worker (sem contar I/O wait).
Overhead MCP · p50 ~50ms Da chegada do request na borda até resposta, descontando a chamada pra API NFE.io.
Overhead MCP · p99 ~150ms Cauda longa absorvida por variação de rede e retry transiente.
Cold start ≈ 0ms V8 isolates — Workers sobem o código em 5-10ms. Sem container pra aquecer.
SLA (Cloudflare Workers) 99,99% Quatro noves. Em 2 anos de histórico público, a plataforma já entrega acima disso.
Bundle size 183 KB gzip 999 KB raw — bem dentro do limite de 10 MB do Workers.
Requests simultâneos ilimitado Stateless + edge = cada request vai pro isolate mais próximo, sem contenção.
Open source, auditável

Não é caixa preta. Clona, lê, modifica, forka.

Código em github.com/nfe/mcp-server, licença MIT, Dockerfile incluído, Cloudflare Workers config incluído. Você pode rodar a sua própria instância. A nossa em mcp.nfe.io é só a oficial, não é a única possível.

Build provenance

Cada release vem com attestation assinada pelo GitHub Actions via Sigstore. A imagem Docker em ghcr.io também. Verificável com gh attestation verify — você prova, sem confiar, que o binário veio daquele commit específico.

CI completo em cada PR

Matrix Node 18/20/22, typecheck em duas configs (stdio + worker), build, npm pack --dry-run, smoke test que sobe o servidor e valida as 5 tools, wrangler deploy --dry-run, build Docker + boot test. Se o PR merge, algo foi realmente testado.

Release com gate humano

npm publish só roda depois que um reviewer aprova o GitHub Environment production-publish. Elimina release acidental de tag mal-criada. Pre-releases (v0.2.0-rc.1) vão automaticamente pro dist-tag next, não latest.

Tipagem end-to-end

TypeScript 5.7 + strict mode + noUncheckedIndexedAccess. Zero any no código. Inputs Zod com .strict(), descriminated unions pros retornos do SDK. Se você contribuir com um PR, o tsc te protege de 80% dos regressões.

Para quem constrói

Lê o código. Forca, melhora, manda PR. Usa em produção.

É um MCP server pequeno, bem estruturado, open source. Ideal pra quem quer entender como um bom MCP server é construído — ou pra quem quer self-hostar uma versão própria.