Skip to main content
Glama
RafaOPato

mcp-obras

by RafaOPato

mcp-obras

Servidor MCP pequeno (Node.js, stdio) em que a permissão de cada agente é aplicada em código, não no prompt.

Problema

Quando um agente de IA tem ferramentas (consultar obras, criar pedido de compra, aprovar pagamento), a regra "o agente de campo não mexe na obra dos outros" costuma morar no prompt. Prompt é pedido, não trava: o modelo pode errar, ser enganado por um texto malicioso que veio num dado, ou simplesmente ignorar a instrução. Quando a ferramenta mexe em dinheiro, a trava precisa ficar onde o modelo não alcança.

Related MCP server: ProcureFlow MCP Server

Solução

Um servidor MCP com 6 ferramentas sobre dados fictícios de obras, pedidos de compra, cotações e fornecedores. Quem é o agente vem de uma variável de ambiente (AGENTE) que quem configura o cliente define, e uma única função, authorize(), decide antes de qualquer ferramenta rodar:

  • nega por padrão: agente desconhecido, ferramenta fora da lista, tudo é "não";

  • cada agente tem uma lista de ferramentas, uma lista de obras e a flag de escrita (agentes.json);

  • aprovar_pedido exige o papel gestor, mesmo que alguém liste a ferramenta para outro papel por engano;

  • criar_pedido nunca vale na hora: o pedido nasce aguardando_aprovacao e não entra no orçamento até um gestor aprovar;

  • toda chamada de ferramenta que chega ao handler vira uma linha em um log JSONL só de acréscimo (permitido, negado, invalido ou erro, este último quando o arquivo de estado está ilegível); se o log não grava, a chamada é recusada;

  • argumentos são validados com zod e as respostas têm tamanho máximo.

Esta é uma versão pequena, inspirada em um sistema real de uma construtora. O código foi escrito de novo e nenhum dado de lá foi copiado; a ideia de devolver o resultado das ferramentas como dado, e não como instrução, dentro de um envelope com rótulo aleatório, vem desse sistema.

Diagrama

flowchart LR
    C["Claude Desktop ou Claude Code<br/>env AGENTE=compras"] -- "JSON-RPC por stdio" --> S["src/server.js"]
    S --> A{"authorize()"}
    AG[["agentes.json"]] -.-> A
    A -- "negado" --> L[("logs/auditoria.jsonl")]
    A -- "permitido" --> V["validação zod"]
    V -- "inválido" --> L
    V -- "ok" --> W["grava a decisão no log"]
    W --> L
    W -- "gravou" --> T["corpo da ferramenta"]
    T --> E[("estado: pedidos")]

Detalhes, sequência de aprovação e códigos de negação em docs/arquitetura.md.

Stack

Node.js 22+ (declarado em engines; testado só em 24.19.0) · @modelcontextprotocol/sdk 1.32.2 (stdio) · zod 4.6.7 · node:test (sem biblioteca de teste).

Agentes

Agente

Papel

Escrita

Obras

Ferramentas

compras

operador

sim

Aurora, Horizonte

listar_obras, resumo_obra, listar_pedidos, buscar_fornecedor, criar_pedido

financeiro

gestor

sim

Aurora, Horizonte

listar_obras, resumo_obra, listar_pedidos, aprovar_pedido

campo

operador

sim

só Aurora

listar_obras, resumo_obra, listar_pedidos, buscar_fornecedor, criar_pedido

leitura

leitor

não

Aurora, Horizonte

listar_obras, resumo_obra, listar_pedidos, buscar_fornecedor

Resultado

Números medidos em 28/10/2026 (Windows 10, Node 24.19.0, npm 11.17.0):

  • 6 ferramentas, 4 agentes e 5 códigos de negação (tabela em docs/arquitetura.md).

  • npm test: 18 testes, 18 passando (entre 30 e 50 s num PC com Windows 10; é lento porque 17 dos 18 testes sobem o servidor de verdade como processo filho e falam com ele pelo cliente do SDK; em Linux ou Mac não foi medido).

  • Para ver se os testes pegam falha de verdade, desliguei de propósito, numa cópia, a checagem de papel (1 teste falhou) e a de allowlist de obras (2 falharam).

  • 366 linhas em src/ (wc -l src/*.js), 327 em test/.

  • npm audit logo após o npm install: 0 vulnerabilidades. O SDK está em 1.32.2 porque, nas versões até 1.31.0, npm audit aponta 4 avisos (2 de severidade alta e 2 moderada; o resumo do comando conta 1 vulnerabilidade alta), todos de transporte HTTP, OAuth ou tarefas experimentais, que este projeto nem usa; não vale começar com versão que já vem com aviso.

Saída real de npm run demo:

leitura    listar_obras    {}                                  -> ok
leitura    criar_pedido    {"obra":"obra-aurora",...           -> NEGADO [FERRAMENTA_NAO_PERMITIDA]
campo      resumo_obra     {"obra":"obra-horizonte"}           -> NEGADO [OBRA_FORA_DO_ESCOPO]
compras    criar_pedido    {"obra":"obra-aurora",...           -> ok
compras    aprovar_pedido  {"pedido_id":"ped-005"}             -> NEGADO [FERRAMENTA_NAO_PERMITIDA]
financeiro aprovar_pedido  {"pedido_id":"ped-005"}             -> ok
fantasma   listar_obras    {}                                  -> NEGADO [AGENTE_DESCONHECIDO]

(O script imprime a mensagem de negação completa e depois o log de auditoria; aqui encurtei as colunas.)

Não publico aqui números do sistema real (quantidade de agentes ou ferramentas); este repositório mede só o que está nele.

Como rodar

Precisa de Node 22 ou mais novo (engines do package.json; só foi testado no 24.19.0).

# depois de clonar o repositório
cd mcp-obras
npm install
npm test        # 18 testes (30 a 50 s num PC com Windows 10: 17 deles sobem o servidor)
npm run demo    # passeio de 7 chamadas + o log de auditoria

Para usar no Claude Desktop ou no Claude Code (um servidor por agente, cada um com seu AGENTE), veja docs/conectar.md. O servidor não lê .env; as variáveis vão no bloco env da configuração do cliente:

Variável

Para quê

Padrão

AGENTE

quem está chamando; precisa existir em agentes.json

(vazio: tudo negado)

OBRAS_AGENTES

caminho do arquivo de agentes

agentes.json na raiz do projeto

OBRAS_ESTADO

onde guardar os pedidos criados

data/estado.local.json na raiz do projeto

OBRAS_AUDITORIA

onde gravar o log

logs/auditoria.jsonl na raiz do projeto

Os padrões valem a partir da raiz do projeto, de qualquer pasta de onde o cliente suba o servidor. Se você definir uma dessas três variáveis, use caminho absoluto: um caminho relativo passa a valer a partir da pasta em que o cliente rodou o comando.

Estrutura de pastas

mcp-obras/
├── README.md
├── LICENSE               # MIT
├── .gitignore
├── package.json          # e package-lock.json (versões fixas)
├── agentes.json          # quem pode o quê (ferramentas, obras, escrita, papel)
├── data/dados.json       # dados fictícios (obras, fornecedores, pedidos, cotações)
├── src/
│   ├── server.js         # liga tudo: authorize -> validação -> auditoria -> ferramenta
│   ├── authorize.js      # a função que decide, e a validação do agentes.json
│   ├── ferramentas.js    # as 6 ferramentas (schema zod + regra de negócio)
│   ├── auditoria.js      # log JSONL de acréscimo
│   └── estado.js         # leitura e gravação do "banco" (um JSON)
├── test/mcp-obras.test.js
├── scripts/demo.js
└── docs/
    ├── arquitetura.md    # diagramas, decisões, o que falta
    └── conectar.md       # Claude Desktop e Claude Code

Decisões de projeto

  • Nega por padrão. A função authorize() só devolve "pode" se todas as regras passarem. Ferramenta que não existe e ferramenta não liberada recebem a mesma resposta (FERRAMENTA_NAO_PERMITIDA); agente desconhecido recebe AGENTE_DESCONHECIDO. Nos três casos o resultado é negar.

  • Trava em código, não no prompt. O prompt (a description das ferramentas) ajuda o modelo a se comportar, mas quem impede é o authorize(). O teste "mesmo listada por engano, o papel operador continua negado" existe para provar isso.

  • stdio. É o transporte mais simples, sem porta aberta na rede, e o Claude Desktop e o Claude Code falam com ele. O custo: a identidade é só uma variável de ambiente, sem login.

  • Aprovação em duas camadas. O pedido só vale depois de aprovar_pedido, que é de outro agente com papel gestor; além disso, os clientes MCP normalmente pedem confirmação da pessoa antes de rodar uma ferramenta (comportamento do cliente, que não testei aqui; o servidor só marca as de escrita com readOnlyHint: false). O servidor não consegue provar que uma pessoa clicou; veja Limitações.

  • Server de baixo nível do SDK, não McpServer. Assim nenhuma validação de argumentos nem corpo de ferramenta roda antes do authorize() (só a leitura do estado, ver docs/arquitetura.md), e até chamada com argumentos inválidos cai no log (requisição sem name em texto, ou com arguments que não é objeto, o SDK recusa antes do handler, e ela fica fora do log). O JSDoc do SDK 1.32.2 marca Server como @deprecated (recomenda McpServer; Server só para uso avançado), então o editor pode mostrá-lo riscado. É uma escolha consciente: o McpServer valida o schema antes de chamar o callback, e o authorize() precisa vir antes.

Mais detalhes e o raciocínio em docs/arquitetura.md.

Limitações / o que ficou de fora

  • A identidade não é autenticada. Quem edita a configuração do cliente escolhe o AGENTE. Serve para uma máquina de confiança, não para vários usuários.

  • A aprovação humana não é verificável pelo servidor. Se o financeiro for um agente de IA que chama aprovar_pedido sozinho, o servidor aprova. Quem garante que há uma pessoa no meio é o pedido de confirmação do cliente MCP, ou, melhor, não expor aprovar_pedido a um agente autônomo.

  • "Banco" é um arquivo JSON, sem trava entre processos: dois agentes gravando ao mesmo tempo, vale o último.

  • O log registra a decisão, não o resultado da execução, e é de acréscimo por construção do código, não por garantia do sistema de arquivos.

  • Nem toda requisição chega ao log. Um tools/call sem name em texto, ou com arguments que não é objeto, é recusado pelo SDK (erro JSON-RPC -32603) antes do handler: não passa pelo authorize(), nenhuma ferramenta roda e não sai linha em logs/auditoria.jsonl. Conferido mandando essas requisições na mão, como em docs/conectar.md.

  • Sem OAuth, sem limite de taxa, sem banco de dados de verdade: o que cada um seria numa versão de produção está em "O que falta" de docs/arquitetura.md.

  • Não foi testado dentro do Claude Desktop nem do Claude Code; o npm test usa o mesmo comando e as mesmas variáveis de ambiente, pelo cliente do SDK.

Como foi feito

Rodei npm test e npm run demo para conferir o resultado.

Licença

MIT. Veja LICENSE. Rafael M. da Silva Santos.

Related MCP Connectors

Related MCP Servers