Skip to main content
Glama
adventureandre

@expertcustom/funilaria-mcp

@expertcustom/funilaria-mcp

MCP (Model Context Protocol) server with the typed tools that Aurora's AI uses to write and read on the Funilaria & Pintura portal.

It replaces mcp-fetch by building the HTTP request by hand with the secret written in the system prompt: here each operation is a tool with schema, description, and error in Portuguese.

IA do Aurora ──stdio──> npx @expertcustom/funilaria-mcp ──HTTPS──> backend NestJS

Per ADR-001, this package is an adapter: no business rules live here. Every tool calls an endpoint that already exists, and the backend service remains the owner of the decision.

Tools — the backend defines them

This package does not know which tools exist. On startup it fetches GET /mcp/catalogo and publishes whatever comes back; execution goes through POST /mcp/executar, and the backend resolves the name to the service.

subida  ──> GET  /mcp/catalogo   → lista publicada em tools/list
chamada ──> POST /mcp/executar   → { name, args }

The reason is operational: before, a new tool cost editing this package, building, committing, publishing, and reinstalling the MCP in Aurora — five steps to expose an endpoint that already existed in the backend. Now it's a single module commit in backend/src/mcp.

The current list comes from GET /mcp/catalogo. There is deliberately no copy of it here: a copy ages and starts lying.

When the catalog does not respond, the server publishes a single tool, funilaria_catalogo_indisponivel, whose description explains what to check. Without it, the symptom would be "the tools disappeared" and the diagnosis would start in the wrong place.

The catalog is fetched once, on startup: the MCP client reads tools/list right after connecting and does not ask again. A tool added in the backend goes live on the next server reinstall in Aurora.

Related MCP server: Telegram Bot MCP Server

Authentication

Service credential with explicit shopId is the main path, both for writing and reading. Header x-aurora-secret, the same value as AURORA_WEBHOOK_SECRET in the backend; it does not represent any person, it represents the service.

An AI that serves multiple shops has no session, so the shop is a parameter, never implicit context. On the backend side this is the @AllowService() on the stock reading routes: the JwtAuthGuard accepts the secret in place of the JWT, and the ShopContextGuard then requires the shopId — a nonexistent id responds with 404 Oficina não encontrada, not an empty list that could be mistaken for "shop with no stock".

User session (JWT from POST /auth/entrar) still exists for CLI commands, but no longer applies to the tools: POST /mcp/executar is always a service call, and the shop is always an explicit parameter. An AI that serves multiple shops never had a session; keeping both modes only created a path where an omitted shopId meant different things.

Configuration — env is the main path

In production, whoever starts this process is the Aurora runtime, which injects the variables: there is no terminal, and no login command is executed. The server works with a completely empty disk.

Env

Accepted alias

Purpose

FUNILARIA_API_URL

PUBLIC_API_URL

API base URL

FUNILARIA_SERVICE_SECRET

AURORA_WEBHOOK_SECRET

Service secret (x-aurora-secret)

FUNILARIA_SIGNING_SECRET

AURORA_WEBHOOK_SIGNING_SECRET

HMAC signing secret (optional)

FUNILARIA_SHOP_ID

Default shop for consultar_estoque

FUNILARIA_TOKEN

User JWT, if any (optional)

The aliases exist for the classic mistake of copying the backend's .env and the secret "disappearing" because of the different prefix — AURORA_WEBHOOK_SECRET is exactly the same value on both sides.

Secrets are never hardcoded or read from the prompt. The file ~/.config/funilaria-mcp/credentials.json (mode 0600) is a local development convenience; the env always wins and is never written to disk.

On boot, the server writes to stderr (stdout is for the MCP protocol) a line saying what is configured and which env each thing came from — never the value. That is what appears in the Aurora log when someone gets the variable name wrong:

[funilaria-mcp] API: https://api.exemplo.com (FUNILARIA_API_URL) · Credencial de serviço: configurada via AURORA_WEBHOOK_SECRET · ...
[funilaria-mcp] Sem credencial de serviço: as tools de escrita vão recusar toda chamada. Defina FUNILARIA_SERVICE_SECRET no ambiente deste processo.

HMAC signing

When FUNILARIA_SIGNING_SECRET exists, every write also carries:

x-timestamp: <epoch em segundos>
x-signature: sha256=<HMAC-SHA256(`${timestamp}.${corpo}`)>

This is the improvement mapped in ADR-001 (closes replay and log leakage). The backend does not verify it yet — unknown headers are ignored, so you can enable the server side without breaking what is already running.

Installation

In Aurora's AI (production)

Register the server with the variables in the MCP registration itself — no login, no secret in the system prompt:

{
  "command": "npx",
  "args": ["-y", "@expertcustom/funilaria-mcp"],
  "env": {
    "FUNILARIA_API_URL": "https://<api-do-portal>",
    "FUNILARIA_SERVICE_SECRET": "<mesmo valor de AURORA_WEBHOOK_SECRET>"
  }
}

Local, for development

# opção A — env no shell (igual à produção)
FUNILARIA_API_URL=http://localhost:3334 FUNILARIA_SERVICE_SECRET=... npx @expertcustom/funilaria-mcp

# opção B — guardar em ~/.config para não exportar em todo shell
npx @expertcustom/funilaria-mcp login-servico

# sessão de usuário: só é necessária para consultar_estoque sem shopId
npx @expertcustom/funilaria-mcp login

# conferir o que está valendo e de onde veio (nunca imprime segredo)
npx @expertcustom/funilaria-mcp status

# registrar no Claude Code
claude mcp add funilaria --env FUNILARIA_API_URL=http://localhost:3334 -- npx -y @expertcustom/funilaria-mcp

Backend pending items

The four original pending items (unreachable stock webhook, reading without service credential, secret checked after validation, distance as dead code) were fixed in the backend and revalidated against localhost:3334. What remains:

  1. The AI has no way to discover the shopId. It is the only piece of data it needs to know by heart, and today it only arrives via FUNILARIA_SHOP_ID — which ties one server to one shop and breaks the multi-shop case that motivated the service design.

    The cheapest point to solve is lancar_consumo: the backend already identifies the employee and shop by the WhatsApp number, but only returns the confirmation text. If IntakeResult included shopId and memberId, the conversation would flow — "I used 100ml of varnish" → "how much did I spend this month?" would be consultar_balancete with both ids in hand. Without that, the second question has no possible answer.

  2. GET /estoque/movimentos was left out of @AllowService(). The shopId is declared in ListMovementsDto, but the route does not accept service credentials — the parameter has no way to be used. Either mark the route, or remove the field from the DTO so it does not suggest capability that does not exist.

  3. HMAC signing is still not verified. The client already sends x-timestamp and x-signature when there is a signing secret (see above). The server side is missing to close replay and log leakage, as ADR-001 provides.

Development

npm install
npm run build     # tsc estrito, gera dist/
npm start         # sobe o servidor MCP em stdio

Available Tools

1 tool
funilaria_catalogo_indisponivelCatálogo do Funilaria indisponívelA

O backend do Funilaria não respondeu GET /mcp/catalogo, então NENHUMA tool do portal está disponível nesta sessão — não é que elas não existam. Verifique se FUNILARIA_API_URL aponta para o backend certo e se FUNILARIA_SERVICE_SECRET (ou AURORA_WEBHOOK_SECRET) está definido no processo deste MCP; o motivo exato saiu no stderr do servidor. Não tente cumprir a tarefa por outro caminho: não monte requisição HTTP à mão, não invente URL nem segredo. Avise quem pediu que a integração com o portal está fora do ar.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It explains that the tool is not a functional operation but a signal that the catalog is down, that no portal tools are available, and that the exact cause is in the server's stderr. It also discloses the expected fallback behavior (informing the requester) and explicitly forbids bypass attempts. This fully discloses the tool's non-functional nature and the reasoning behind it.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, dense paragraph that front-loads the core message (backend down, no tools available) and then provides corrective actions and prohibitions. Every sentence contributes essential information: the root cause, the implication, troubleshooting steps, and explicit instructions. There is no fluff or redundancy, making it both concise and structurally sound.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given that there is no output schema and the tool is essentially a status indicator, the description is complete. It specifies what happened, why, what to check, what not to do, and what to tell the requester. All necessary operational context is provided, and because the tool has zero parameters and no output expectations, the description fully covers what an agent needs to handle this situation correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema is empty with 0 parameters, so the baseline is 4. The description does not need to add parameter details, but it explains why the tool takes no arguments (it is a placeholder for an unavailable catalog). It also provides environment variable checks (FUNILARIA_API_URL, FUNILARIA_SERVICE_SECRET) as context, which indirectly explains the setup. This exceeds the baseline by giving operational context beyond the mere absence of parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states that the tool indicates the backend did not respond to GET /mcp/catalogo, making the entire portal's tools unavailable in this session. It does not use a classic verb+resource phrasing, but its purpose as an unavailability signal is unambiguous and distinct from any normal tool. Since there are no siblings, the distinguishing aspect is not relevant, but the description still clearly explains what it represents.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly instructs when to use this tool (when it appears, treat it as a session-wide unavailability) and what NOT to do: do not attempt to fulfill the task by other means, do not craft HTTP requests manually, do not invent URLs or secrets. It also tells the agent to inform the requester. This is direct, actionable context with explicit exclusions, covering both usage and alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev0.1.0
    • First observedfunilaria_catalogo_indisponivel

TDQS

A4.1/5.0

Scored across 1 tool

Disambiguation5/5

With only one tool, there is no possibility of tool confusion. The tool's purpose—to report that the catalog is unavailable—is uniquely clear.

Naming Consistency5/5

The single tool name is descriptive and domain-specific, following a clear pattern. With no other tools, there is no inconsistency to penalize.

Tool Count1/5

A single tool that serves only as an error placeholder is far below the expected scope for a catalog server. This is an extreme mismatch with the apparent purpose.

Completeness1/5

The server provides no actual catalog operations; the only tool explicitly states that all functional tools are unavailable. The surface is severely incomplete and unusable.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers