Skip to content

feat(mcp): add request log writer (PoC for #1351)#1487

Closed
nhangen wants to merge 3 commits into
abhigyanpatwari:mainfrom
nhangen:feat/mcp-request-log
Closed

feat(mcp): add request log writer (PoC for #1351)#1487
nhangen wants to merge 3 commits into
abhigyanpatwari:mainfrom
nhangen:feat/mcp-request-log

Conversation

@nhangen

@nhangen nhangen commented May 10, 2026

Copy link
Copy Markdown

Refs #1351.

Summary

Wraps the CallToolRequestSchema handler with an instrumented() helper that appends one JSONL line per tools/call to a configurable log file:

{"ts":"2026-05-10T18:00:00Z","tool":"impact","durationMs":214,"resultBytes":1834,"error":null}

The same wrapper can host an OTLP span emitter alongside or instead of the JSONL writer — the single seam is src/mcp/server.ts:166.

Changes

  • gitnexus/src/mcp/request-log.tsresolveLogPath(), appendRequestLog(), instrumented(toolName, fn, resultBytesOf?, errorOf?). Async append, mkdir -p on the parent, errors swallowed.
  • gitnexus/src/mcp/server.ts — wraps the existing handler. Behaviour unchanged when logging is disabled. errorOf reads the { isError: true, ... } envelope so tool errors land in the log with their message (the handler converts thrown errors into a returned envelope, so the wrapper's catch branch never sees them).
  • gitnexus/test/unit/mcp/request-log.test.ts — 14 unit tests.

Configuration

Opt-in default:

GITNEXUS_MCP_REQUEST_LOG Result
unset / empty disabled
off / false / 0 disabled
on / true / 1 ~/.gitnexus/mcp-requests.log
/abs/path/log that path

Privacy

Tool inputs (params/rawArgs) are never logged. The error field captures error messages verbatim; tool errors routinely echo input (cypher parse errors include the offending clause, etc.). Treat the log as input-adjacent.

Verify

cd gitnexus
npm install
npx vitest run test/unit/mcp/request-log.test.ts   # 14 pass

End-to-end after npm run build:

GITNEXUS_MCP_REQUEST_LOG=on gitnexus mcp
# drive via any MCP client, then:
cat ~/.gitnexus/mcp-requests.log

Risk

  • One JSON.stringify + one fs.appendFile per tool call. Fire-and-forget; the request handler returns without awaiting.
  • In-flight writes can be lost on shutdown() — no drain in this PoC. See decision table below.
  • Unbounded log size — no rotation in this PoC.
  • Rollback: revert the commit. No DB, schema, or index changes; no new runtime deps.

Recommended decisions for v1

# Decision Recommendation
2 JSONL only vs JSONL + OTLP Both. JSONL stays as the local fallback; OTLP exporter behind OTEL_EXPORTER_OTLP_ENDPOINT. Same wrapper hosts both.
3 Log tool input vs size-only Size-only. error already leaks input under failure; broadening would move privacy backwards.
4 Log rotation Daily rotation, 30-day retention, opt-out via env. Free with pino transports (see #6).
5 Drain on shutdown() Yes — await Promise.allSettled(pending) before server.close(). Cheap and bounded.
6 Hand-rolled appendFile vs core/logger.ts (pino) Refactor to pino. createLogger('mcp-requests', { ... }) gives NDJSON, log-injection hardening, and the existing test seam.

One open question

Opt-in or opt-out default? Opt-in maximises privacy posture; opt-out gives server-side ground truth without setup. Both are one line in resolveLogPath(). Maintainer's call.

Draft until that decision lands and we scope whether #2/#4/#5/#6 ship in this PR or as follow-ups.


Local CI:

  • npx vitest run test/unit/mcp/request-log.test.ts — 14 pass.
  • npx tsc --noEmit — clean on the changed files (baseline has unrelated tree-sitter errors from --ignore-scripts install).
  • .husky/pre-commit not run locally; please verify in CI.

Wraps the CallToolRequestSchema handler with a JSONL writer that appends one entry per call to ~/.gitnexus/mcp-requests.log with ts, tool, durationMs, resultBytes, error fields. A simpler counterpart to the OpenTelemetry/Prometheus direction discussed in abhigyanpatwari#1351 — kept small so the design conversation can happen on top of working code. Either approach plugs into the same instrumented() wrapper. Configuration via GITNEXUS_MCP_REQUEST_LOG env var (off / on / explicit path). Failures to write are swallowed; server availability matters more than logging fidelity. Tests: 11 new in test/unit/mcp/request-log.test.ts (pass via npx vitest run; typecheck clean on the changed files).
@vercel

vercel Bot commented May 10, 2026

Copy link
Copy Markdown

@nhangen is attempting to deploy a commit to the NexusCore Team on Vercel.

A member of the Team first needs to authorize it.

nhangen added 2 commits May 10, 2026 16:32
Pre-review the resolveLogPath was opt-out (unset → default path) which contradicted the docstring and PR body claim of opt-in. Both pr-review-toolkit and a meta-auditor flagged it; meta-auditor also caught that 'GITNEXUS_MCP_REQUEST_LOG=on' was treated as a literal file path named 'on' in cwd because the on/true/1 branch never existed. Fix: unset/empty/off/false/0 → null (disabled); on/true/1 → default ~/.gitnexus/mcp-requests.log; explicit path → that path. Tests grow from 11 → 12 covering the new affirmative-boolean branch and pinning the opt-in default.
Three independent expert reviewers (security/privacy, async-correctness, project-fit) reviewed the PoC. Two real code issues:

1. Async-correctness reviewer found that the MCP CallToolRequestSchema handler converts thrown tool errors into a returned { isError: true } envelope BEFORE the instrumented wrapper sees them — so the wrapper's catch branch never fires for tool errors and the log was recording error: null for every failure. Fix: instrumented() now accepts an optional errorOf(result) hook; the wrap site at server.ts reads result.isError and passes the error text through.

2. Security/privacy reviewer noted that while tool inputs (params/rawArgs) are not logged, the error field captures error messages verbatim, and tool errors routinely echo user input (cypher parse errors include the offending clause, etc.). Documented in the module header so operators treat the log as input-adjacent.

Project-fit reviewer predicted the upstream maintainer will ask why we don't use the project's existing pino logger (core/logger.ts) — recorded in the open design questions on the PR body rather than rewriting the PoC.

Tests: 12 → 14 (one for envelope-error logging via errorOf, one pinning the no-errorOf back-compat default of error: null on success).
@nhangen

nhangen commented May 10, 2026

Copy link
Copy Markdown
Author

Apologies for the noise here. I misread the assignment on the first pass — @magyargergo asked for OpenTelemetry/Prometheus with quantized, anonymous metrics, and I shipped a JSONL request log instead, with OTel framed as a follow-up. That inverts the ask. Closing this and starting fresh with an OTel meter + Prometheus exporter as the primary path. Will open a new PR shortly.

@nhangen nhangen closed this May 10, 2026
@nhangen nhangen deleted the feat/mcp-request-log branch May 10, 2026 22:28
@magyargergo

Copy link
Copy Markdown
Collaborator

@nhangen this could be interesting 🧐 I'm open for something new if they are well tested in practice and there are working solutions. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants