WebMCP in production|INEMA.CLUB-PRO

In this lesson you will

  1. 1What a log line needs to have
  2. 2The observado() wrapper on the three doors
  3. 3Error, refusal, cancellation, and the MRTR request
  4. 4Why the log keeps the field name and not the value

Goal: log every call across the three surfaces with result and time, with no personal data, and read the possible outcomes in the log.

01From the request to the line in the log

The log sits at the boundary, not inside the rule. Click each step.

Click each block in the diagram to see what it does.

02The outcomes that matter

Each one calls for a different action from whoever runs the site.

1. erro

Input that the rule does not accept: invalid topic, workshop with no spots left.

Many errors on the same tool = a confusing description or schema for the agent.

Kitmotivo: tema inválido: cobol

03The wrapper

The same file serves the page and the server. On the page, in production, the line would go to your backend (sendBeacon, for example); in the kit it stays in memory.

File · kit/pagina/registro.mjs
// registro.mjs — observabilidade nas fronteiras: uma linha por chamada de ferramenta, em qualquer superfície.
// Roda no navegador (página) e no Node (servidor MCP). Não guarda dado pessoal: só o NOME dos campos de entrada.
// Resultado: ok | erro (regra recusou) | recusado (a pessoa disse não) | cancelado (AbortSignal) | excecao
//            | pediu-pessoa (MCP: a 1ª volta do MRTR devolve input_required e o cliente pergunta ao usuário).

const linhas = [];
let destino = null;   // função que recebe cada linha (no servidor: grava JSONL; na página: sendBeacon em produção)

export function aoRegistrar(fn) { destino = fn; }
export function linhasRegistradas() { return linhas.map(l => ({ ...l })); }

export function classificar(r) {
  if (r && typeof r === 'object') {
    if (r.resultType === 'input_required') return 'pediu-pessoa';
    if ('erro' in r) return 'erro';
    if (r.reservado === false) return 'recusado';
  }
  return 'ok';
}

export function anotar(linha) {
  const l = { t: new Date().toISOString(), ...linha };
  linhas.push(l);
  globalThis.__registro = linhas;    // os testes leem daqui
  destino?.(l);
  return l;
}

// Embrulha o execute de uma ferramenta: mede o tempo e anota o resultado, sem mudar o que ela devolve.
export function observado(superficie, ferramenta, fn) {
  return async (input = {}, opcoes) => {
    const inicio = performance.now();
    const base = { superficie, ferramenta, campos: Object.keys(input).sort() };
    try {
      const r = await fn(input, opcoes);
      anotar({ ...base, resultado: classificar(r), ms: Math.round(performance.now() - inicio), ...(r?.erro ? { motivo: r.erro } : {}) });
      return r;
    } catch (e) {
      anotar({ ...base, resultado: e.name === 'AbortError' ? 'cancelado' : 'excecao', ms: Math.round(performance.now() - inicio), motivo: e.name });
      throw e;
    }
  };
}

04Trigger the outcomes and read the log

The script searches, gets the topic wrong, refuses, cancels, and books through MCP with a refusal. It writes logs/chamadas.jsonl.

Terminal · call log
cd ~/projetos/formacao-webmcp-expert-v72/kit
node scripts/observar.mjs nativo; echo "saída: $?"
tail -1 logs/chamadas.jsonl
Example from the repository
== Chromium 156.0.8078.4 (Playwright) · modo: nativo
html   buscar_oficinas  ok              0 ms
webmcp buscar_oficinas  ok              0 ms
webmcp buscar_oficinas  erro            3 ms · tema inválido: cobol
webmcp reservar_vaga    recusado      184 ms
webmcp reservar_vaga    cancelado     319 ms · AbortError
mcp    buscar_oficinas  ok              0 ms
mcp    reservar_vaga    pediu-pessoa   26 ms
mcp    reservar_vaga    recusado        2 ms
REGISTRO: 8 chamadas · ok 3 · erro 1 · recusado 2 · cancelado 1 · pediu-pessoa 1 · dado pessoal no registro: não
gravado em logs/chamadas.jsonl
saída: 0
{"t":"2026-10-19T20:03:13.159Z","superficie":"mcp","ferramenta":"reservar_vaga","campos":["id","nome"],"resultado":"recusado","ms":2}

The last line is a real line from the file: the fields id and nome show up by name, never by value. The milliseconds vary on each run.

05The leak rehearsal

On a throwaway copy of the kit.

Terminal · log with personal data
cd ~/projetos/formacao-webmcp-expert-v72/kit
E=$(bash scripts/copia.sh a3)
# o erro comum: registrar a entrada inteira "para depurar"
sed -i 's/campos: Object.keys(input).sort()/entrada: input/' $E/pagina/registro.mjs
cd $E && node scripts/observar.mjs nativo | grep "REGISTRO"; echo "saída: ${PIPESTATUS[0]}"

06Practice

Read the log, watch the leak get caught, and decide what your site logs. Each exercise ends in a done when.

E1 · The outcomes in the log

5 min

Run the logging block and count the lines by outcome.

Done when: node scripts/observar.mjs nativo; echo "saída: $?" → REGISTRO: 8 chamadas · ok 3 · erro 1 · recusado 2 · cancelado 1 · pediu-pessoa 1 · dado pessoal no registro: não and saída: 0.

E2 · Catch the leak

3 min

Run the log rehearsal with the whole input.

Done when: The block prints dado pessoal no registro: SIM and saída: 1.

E3 · Your site's log

15 min

For your site's tools, write which fields go into the log (name only), where the line goes in production, and who looks at the summary each week.

Done when: The file registro-[your site].md has one line per tool, the log destination, and the owner, and no field with an input value.

07Lesson quiz

The explanation appears after the answer.

Question 1 of 4

The MCP booking with confirmation left two lines in the log. Why?

08In three sentences

LineSurface, tool, field names, outcome, and time: one per call.
OutcomeError, refusal, cancellation, and an MRTR request tell different stories.
PrivacyThe input value stays out of the log; the script catches whoever forgets.

Going further

Previous lessonNext lesson