MCP Forge

How to monitor and log an MCP server

A practical guide. Updated 2026.

An MCP server is driven by a language model, so when something goes wrong you usually cannot reproduce it. The model picked the arguments, looped on a confusing result, or called a tool you did not expect. Good logs are how you find out what actually happened after the fact. The hard part is logging enough to debug an incident without writing secrets, prompts, or user data into a file that later leaks. This guide covers what to record, what to leave out, how to structure it, and the few alerts worth setting up first.

What to log on every tool call

Treat each tool call as one event and log a single line for it when it completes. The goal is to answer "who called what, did it work, and how long did it take" without reading the code. Capture these fields:

Argument shape is useful, argument content usually is not. Log the argument keys and value types, or a count, rather than the values themselves. That tells you the model called send_email with a to and body field without putting the email body in your logs.

What never goes in a log

Logs get shipped to third-party platforms, copied into tickets, and read by people who are not on the project. Assume anything you write will be seen by someone who should not see the underlying data. Keep these out:

The reliable way to enforce this is a redaction step at the boundary, not careful discipline at every log call. Maintain a deny-list of sensitive keys and strip them before anything is serialized.

const SENSITIVE = new Set([
  "password", "token", "secret", "apikey", "api_key",
  "authorization", "cookie", "set-cookie", "connectionstring",
]);

function redact(obj) {
  if (Array.isArray(obj)) return obj.map(redact);
  if (obj && typeof obj === "object") {
    const out = {};
    for (const [k, v] of Object.entries(obj)) {
      out[k] = SENSITIVE.has(k.toLowerCase()) ? "[redacted]" : redact(v);
    }
    return out;
  }
  if (typeof obj === "string" && obj.length > 200) {
    return `[string len=${obj.length}]`; // never log long blobs verbatim
  }
  return obj;
}

Make the logs structured

Write JSON, one object per line (JSON Lines). A human can still read it, and any log platform can index it without a fragile regex. Plain-text logs like tool send_email failed in 240ms are unparseable at scale; the same event as a JSON object is queryable by field. Here is a small wrapper that logs one structured event per call and times it:

function logEvent(fields) {
  process.stdout.write(JSON.stringify({
    ts: new Date().toISOString(),
    level: fields.error ? "error" : "info",
    ...fields,
  }) + "\n");
}

async function withLogging(req, ctx, run) {
  const start = performance.now();
  const base = {
    request_id: ctx.requestId,
    client: ctx.clientId ?? ctx.sessionId ?? "anon",
    tool: req.tool,
    method: "tools/call",
    arg_keys: Object.keys(req.args ?? {}),   // shape, not values
  };
  try {
    const result = await run();
    logEvent({ ...base, ok: true, ms: Math.round(performance.now() - start) });
    return result;
  } catch (err) {
    logEvent({
      ...base, ok: false, ms: Math.round(performance.now() - start),
      error: err.code ?? "internal_error", message: err.message,
    });
    throw err;
  }
}

Log to stdout and let the platform handle the rest. Do not manage log files inside the process. In containers, stdout is collected by the runtime; on a host, your service manager (systemd, for example) captures it. This keeps the server simple and means rotation and shipping are someone else's job.

Track latency and error rate, not just counts

A single number like "1,200 calls today" hides every problem worth knowing about. Three signals catch most incidents:

If you already emit the structured event above, every one of these is a query over tool, ok, ms, and client. No extra instrumentation needed.

Basic alerting worth setting up first

You do not need a full observability stack on day one. Start with a handful of alerts that map to "a human should look now," and tune the thresholds to your traffic so they do not cry wolf:

Route these to wherever you already get paged. The point is not coverage, it is catching the obvious failures before a user reports them.

Check what you are leaking before you ship

Redaction is easy to get subtly wrong: a nested field, a header logged by a library, an error object that serializes its full request context. Before shipping, scan for the patterns that put sensitive data on the wire in the first place. The free mcp-audit tool flags remote MCP servers running without authentication or over cleartext, the cases where unredacted logs and verbose errors do the most damage. It runs locally with no dependencies, so it is a quick check before a release. Pair it with a manual read of a few real log lines from staging: that is the fastest way to spot a secret or a payload that slipped through.

Structured logging without rolling your own

MCP Forge Kit ships a redacting JSON logger with per-tool timing, request IDs, and a secret deny-list wired into a hardened server template, so you log enough to debug without leaking.

Get MCP Forge Kit, €39

Related: Secrets management · Rate limiting · Security checklist