Skip to content

feat(sync): write a backend's own log lines to the worker log - #44

Merged
mgd43b merged 3 commits into
mainfrom
claude/mcp-log-notifications
Sep 24, 2026
Merged

mgd43b merged 3 commits into
mainfrom
claude/mcp-log-notifications

Conversation

@mgd43b

@mgd43b mgd43b commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

What was wrong

A backend that declares the MCP logging capability sends its log as notifications/message on stdout. McpSession's read loop only handled messages with a numeric id (responses) and dropped everything else, so those lines were read and thrown away. They include the lines that explain a failed sync: a storage error's reason, a warning about a file, how far indexing got. SocratiCode started sending every log line this way in 1.15.0, and it sends some of them before it answers initialize.

Two more problems in the same read loop:

  • Any message with a numeric id was taken as a response, even one with a method. A notification or server request carrying the id of a pending call resolved that call with an empty result. The tool then looked like it had succeeded with no text.
  • A bare JSON value on stdout, such as null, threw out of the data handler. On current main this is an uncaught exception (Cannot read properties of null (reading 'id')), checked below.

What this does

  • mcp.ts: adds a new onLog option to McpClientOptions. Each notifications/message goes through parseLogMessage into { level, logger?, text }:
    • Levels. MCP's eight levels map to four: debug; info (and notice); warn (warning); error (and critical, alert, emergency). A level it does not recognise is logged as info. Dropping the line would lose it, and guessing a higher level would raise false alarms. Levels are matched case-insensitively.
    • data. A string is kept as sent. Anything else is written with JSON.stringify. The result is flattened to one line, with line breaks becoming |, the same way the worker already flattens a parked error. It is capped at 2,000 characters with … [N more chars] at the end, and never cuts a surrogate pair in half.
    • Robustness. A notification with missing or odd params is ignored. A sink that throws loses only that line. A message with a method is never taken as a response. A bare JSON value is skipped. Other notifications and server requests are ignored, as before.
  • provider.ts: IndexRequest gets an optional log?: (line: BackendLogLine) => void, with BackendLogLevel = "debug" | "info" | "warn" | "error". Both types are exported. The field is optional, so existing custom providers still compile and behave as before.
  • McpIndexProvider.index passes req.log through to the session. The backend's logger name goes in front of the message only when it differs from the provider's name, since the label already shows the provider.
  • worker.ts writes each line with the existing Logger.tag, as [<provider>:<level>] <message>. It therefore goes wherever the worker log already goes: the rotated log file always, and also stdout for sync, drain and once, which already echo the log. That covers both a script that captures sync's output and the log a hook-driven drain leaves behind.

Example, from the end-to-end run below (the path is shortened):

2026-09-24T13:59:48Z [socraticode:warn] EXTENSION_LANGUAGE_MAP: ignored invalid entries. Each must be `<ext>:<language>` with a language SocratiCode has an AST grammar for (e.g. .inc:php). {"ignored":[".foo:nolanguage"]}

Default verbosity: forward everything the backend sends

codeindex-sync has no verbosity setting. Logger has no levels, and the CLI has no --verbose or --quiet. So nothing is filtered here, and no logging/setLevel is sent. The backend already filters by its own configured level (SocratiCode: SOCRATICODE_LOG_LEVEL, default info), and that setting already lives in the provider's env block. A second filter here would mean two settings that have to agree, and a debug level set on the backend would be dropped here without any sign. The docs now say how to quiet it (SOCRATICODE_LOG_LEVEL=warn) and point out that the log rotates at 2 MB, keeping one previous file.

Scope

Only index runs, which are the worker path, carry log. list, doctor, status, cleanup and claim have no worker log and still drop backend log lines. Routing those to the terminal would be a separate decision about interactive output.

Docs

  • docs/extending.md: what happens to a backend's log (mapping, flattening, cap, no filtering), and req.log for custom providers.
  • docs/socraticode.md: a troubleshooting entry for reading the [socraticode:…] lines and quieting them.
  • README.md: one sentence in the "log is the diagnostic" bullet.
  • CHANGELOG.md is not touched, because release-please writes it.

What I ran

  • npm run lint, npm run typecheck, npm run build, npm test: 444 passed, 4 skipped (the skipped ones are the Qdrant integration suite, which needs a live store). Baseline on main was 428 passed, 4 skipped.

  • New tests:

    • test/mcp.test.ts, run against the existing stub server:
      • a line before the initialize reply is delivered, and the handshake still completes;
      • all eight levels, plus unknown, upper-case and non-string levels;
      • non-string data (object, number, null, false, array) and multi-line strings;
      • the length cap;
      • a malformed stream: null, 42, a bare string and an array on stdout, notifications/message with no params, string params, array params, no data, or blank data, a notification carrying the pending call's id, a server request with that id, and notifications/progress. The call still gets its real reply.
      • a sink that throws;
      • no onLog;
      • parseLogMessage unit tests.
    • test/provider-backend-log.test.ts: lines from start-up, the call and shutdown reach req.log; the logger-name prefix rule; a failed run still carries the logged reason.
    • test/worker.test.ts: lines land in the log file as [backend:info] and [backend:error], between [start] and [give-up].
    • test/cli.test.ts: sync against a stub backend prints [stub:warn] and [stub:info] lines, including one sent before initialize, to stdout and writes them to sync.log.
  • Each guard disabled in turn, full suite run, then restored. What went red:

    Guard disabled Failed
    route notifications/message to onLog 10 tests: every log-delivery test in mcp, provider and cli
    a message with a method is never a response carries on answering calls… (expected '' to be 'ok')
    skip a bare JSON value carries on answering calls… (TypeError: Cannot read properties of null (reading 'method'), call hung to timeout)
    try/catch around the sink survives a log sink that throws (call hung to timeout)
    flatten line breaks writes data that is not a string as JSON, on one line, keeps the logger's name, on one bounded line
    length cap caps an absurdly long line…, keeps the logger's name…, never cuts a character in half
    surrogate back-off never cuts a character in half
    critical → error maps MCP's eight levels onto debug, info, warn and error
    case-insensitive level logs a line whose level it does not recognise as info…
    empty text → no line carries on answering calls…, is null for params that carry nothing to log
    missing data → no line is null for params that carry nothing to log
    worker passes log worker test and cli test
    provider passes onLog both provider tests and the cli test
    logger-name prefix rule both provider tests
  • The null crash on current main: I drove origin/main's McpSession against a stub that writes null before its reply. Result: UNCAUGHT: Cannot read properties of null (reading 'id').

  • End to end with real SocratiCode 1.15.0. I ran codeindex-sync sync from a scratch HOME against the real socraticode binary, with QDRANT_MODE=external and Qdrant and Ollama pointed at a closed loopback port, so nothing was written anywhere. I set EXTENSION_LANGUAGE_MAP to an invalid entry to get a real warning.

    • Before (main), stdout and sync.log hold only [start] and [give-up] … Cannot reach external Qdrant.
    • After, both also carry [socraticode:warn] EXTENSION_LANGUAGE_MAP: … and [socraticode:info] Auto-resume: disabled …, both sent before the initialize reply. They also carry Checking external Qdrant …, Waiting for Qdrant to be ready …, and the two graceful-shutdown lines.
    • once and drain printed the same 7 lines. doctor printed none.
    • SOCRATICODE_LOG_LEVEL=warn cut the stream down to the one warning, which the docs' advice relies on.

What I could not verify

  • A real successful index against a live store. I did not run one, so I have not measured how many info lines a large reindex produces. From SocratiCode's source, it logs per changed file and per batch checkpoint at info, so a big reindex could use a noticeable share of the 2 MB log. The docs point to SOCRATICODE_LOG_LEVEL=warn for that.
  • Windows. Not run. Nothing here is platform-specific, but CI covers only Ubuntu and macOS.

Summary by CodeRabbit

  • New Features
    • Backend log messages are now included in worker logs and shown during sync, drain, and once, with provider and severity labels.
    • Forwarded logs support severity levels, optional logger names, and long-message handling.
  • Documentation
    • Updated extension and troubleshooting guidance with log-forwarding behavior, output examples, filtering details, and log rotation information.

An MCP server that declares the `logging` capability sends its log as
`notifications/message` on stdout. The stdio client read those lines and
dropped them, because it only handled responses, so a backend's own account
of a failure never reached the log.

McpSession now hands each one to an `onLog` option. The level is mapped
from MCP's eight onto debug/info/warn/error, `data` that is not a string is
written as JSON, and the line is flattened to one line and capped at 2000
characters. A notification can arrive before the initialize reply. The
provider passes the lines to a new optional `IndexRequest.log`, and the worker
writes them as `[<provider>:<level>] message`, to the log file and to the
terminal echo that `sync`, `drain` and `once` already print.

Two related fixes to the read loop: a message that has a `method` is never
taken as a response, whatever id it carries, and a bare JSON value such as
`null` on stdout no longer throws out of the data handler.
Copilot AI lite review requested due to automatic review settings September 24, 2026 14:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

  • Ask an admin to enable usage-based reviews

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Next included review available in 32 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 83 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: aeb4b0bd-c05a-4805-a9a4-d2e5336b66be

📥 Commits

Reviewing files that changed from the base of the PR and between 4361510 and 9ff5b97.

📒 Files selected for processing (1)
  • test/mcp.test.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: c79db3d4-510c-445f-acdd-26482b8246a3

📥 Commits

Reviewing files that changed from the base of the PR and between da0308a and 4361510.

📒 Files selected for processing (12)
  • README.md
  • docs/extending.md
  • docs/socraticode.md
  • src/index.ts
  • src/mcp.ts
  • src/provider.ts
  • src/providers/mcp-provider.ts
  • src/worker.ts
  • test/cli.test.ts
  • test/mcp.test.ts
  • test/provider-backend-log.test.ts
  • test/worker.test.ts

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.


📝 Walkthrough

Walkthrough

MCP notifications/message entries are parsed into backend log lines and forwarded during indexing through the provider and worker. The worker tags these lines with the provider name and level. Documentation and tests cover parsing, formatting, delivery, and command output.

Changes

Backend Log Forwarding

Layer / File(s) Summary
Parse and route MCP log notifications
src/mcp.ts, test/mcp.test.ts
MCP log notifications are parsed, normalized, and sent to an optional callback. Tests cover level mapping, formatting, truncation, malformed input, and callback behavior.
Forward backend logs through indexing
src/provider.ts, src/index.ts, src/providers/mcp-provider.ts, src/worker.ts, test/provider-backend-log.test.ts, test/worker.test.ts, test/cli.test.ts, README.md, docs/extending.md, docs/socraticode.md
The MCP provider passes backend log lines through the index request. The worker adds provider and level tags. Tests and documentation cover the forwarding path and output.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant MCPBackend
  participant McpSession
  participant McpIndexProvider
  participant Worker
  participant CommandOutput
  MCPBackend->>McpSession: notifications/message
  McpSession->>McpIndexProvider: normalized log message
  McpIndexProvider->>Worker: log line through IndexRequest.log
  Worker->>CommandOutput: provider-and-level tagged entry
Loading

Merge Risk: ⚪ Minimal · up to 43615

Backend logs are forwarded during indexing, including messages sent before initialization. No actionable issue remains on the supplied evidence; the change is ready for normal merge checks.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: forwarding a backend's log lines to the worker log.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 9 files. (3 skipped: 3 u…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

The shell dies before its server, so the server is re-parented and reaped
by init on init's schedule. Until then its pid still answers `kill(pid, 0)`,
and the test's last assertion failed on Linux runners now and then, on
main as well as on branches. Poll for up to a second first, as the
forwarding-launcher test beside it already does.
@mgd43b
mgd43b merged commit 4d0f18a into main Sep 24, 2026
5 checks passed
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