Skip to content

feat(server): WithLogger consolidates structured logging onto *slog.Logger#892

Open
QuentinBisson wants to merge 1 commit into
mark3labs:mainfrom
QuentinBisson:feat/server-with-logger
Open

feat(server): WithLogger consolidates structured logging onto *slog.Logger#892
QuentinBisson wants to merge 1 commit into
mark3labs:mainfrom
QuentinBisson:feat/server-with-logger

Conversation

@QuentinBisson
Copy link
Copy Markdown
Contributor

@QuentinBisson QuentinBisson commented May 18, 2026

Why

The server has `WithTracer` for OTEL spans on every dispatched JSON-RPC method (#856) but no symmetric primitive for structured logging. The existing logger surface is fragmented:

  • `util.Logger` is a tiny `Infof/Errorf` printf interface used by every transport's lifecycle logging (panic recovery, SSE write failures, session expiry, …).
  • The transport-server `server.WithLogger(util.Logger)` configures only one transport (StreamableHTTP).
  • There's no entry point for per-request protocol-level logging (`initialize`, `tools/list`, `tools/call`, etc.).

Users who want structured logs today either wrap individual tool handlers in their own `ToolHandlerMiddleware` (only sees `tools/call`) or implement `util.Logger` against their slog backend twice — once per transport.

What

Single `*slog.Logger`-based logging concept across the server and the client transports.

  1. `util.Logger` removed. The `util/` package is gone. Every `util.Logger` field and every `With*Logger` option now takes `*slog.Logger`. A nil logger falls back to `slog.Default()`. Every `Infof`/`Errorf` call site is rewritten to slog `Info`/`Error` with structured key-value pairs (`err`, `panic`, `method`, `session`, …).

  2. *`server.WithLogger(slog.Logger) ServerOption` (new) — server-wide structured logger. The server emits:

    • One `mcp.request` line per dispatched JSON-RPC method, INFO level, with attributes `mcp.method`, `mcp.session.id` (when set), `mcp.protocol.version` (from `Mcp-Protocol-Version`), `duration_s`, `outcome` (`ok`/`error`), `error` (when set).
    • One `mcp.tool` line per tool handler invocation, INFO level, with attributes `mcp.tool.name`, `duration_s`, `outcome` (`ok`/`error`/`error_result`), `error` (when set).

    Lines inherit the active span's TraceID/SpanID via slog's context-aware handlers when a Tracer is installed via `WithTracer`, so log records can be joined with spans in observability backends.

  3. Renames to free the `WithLogger` name on `MCPServer`:

    Before After
    `server.WithLogger(util.Logger) StreamableHTTPOption` `server.WithStreamableHTTPLogger(*slog.Logger) StreamableHTTPOption`
    `transport.WithCommandLogger(util.Logger)` `transport.WithCommandLogger(*slog.Logger)`
    `transport.WithSSELogger(util.Logger)` `transport.WithSSELogger(*slog.Logger)`
    `transport.WithHTTPLogger(util.Logger)` `transport.WithHTTPLogger(*slog.Logger)`
    `transport.WithLogger(util.Logger)` (deprecated alias) `transport.WithLogger(*slog.Logger)` (still deprecated, delegates to WithHTTPLogger)

Design parity with WithTracer

Concern `WithTracer` (existing) `WithLogger` (this PR)
Server option `WithTracer(tracing.Tracer)` `WithLogger(*slog.Logger)`
Per-method hook `startMessageSpan` in `request_handler.go` `startMessageLog` in `request_handler.go`
Tool-level `toolTracingMiddleware` auto-registered `toolLoggingMiddleware` auto-registered
Nil handling nil tracer → no-op nil logger → no nil-deref, transport defaults to slog.Default()
Attribute names `mcp.method`, `mcp.tool.name`, `mcp.session.id`, `mcp.protocol.version` identical

Breaking changes

  • `util.Logger` consumers (in-tree or external) must switch to `*slog.Logger`. The `util/` package is removed.
  • `server.WithLogger(util.Logger)` callers must rename to `server.WithStreamableHTTPLogger(*slog.Logger)`.
  • Transport `With*Logger` callers passing custom `util.Logger` implementations must wrap them in a `*slog.Logger` (e.g. `slog.New(yourHandler)`).

Pre-1.0 library, no soft-deprecation period.

Validation

`go test ./...` clean — 14 packages, every transport's test suite included. Five new tests on `WithLogger` (server) cover nil-noop, request-line OK, JSON-RPC error outcome, tool middleware OK, tool middleware error, tool middleware IsError. Existing transport tests' bespoke `testLogger` / `panicTestLogger` types were rewritten to implement `slog.Handler` instead of `util.Logger`; they continue to capture messages for assertions.

Summary by CodeRabbit

  • New Features

    • Server now emits structured request and tool logs with timing, outcome, and tracing-friendly fields; optional OTEL wiring forwards these logs to observability pipelines.
  • Refactor

    • Unified client and server logging on Go's structured slog, removing the legacy logger abstraction and standardizing structured error/diagnostic messages across transports.
  • Tests

    • Added and updated tests to validate structured logging, middleware log emission, and OTEL log routing.

Review Change Stack

@mark-iii-labs-huly
Copy link
Copy Markdown

Connected to Huly®: MCP_G-457

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Migrate logging from a custom util.Logger to Go's log/slog across client transports and server; add structured per-request and per-tool server logs; introduce an OTEL slog bridge and tests; update tests to capture slog records and remove the legacy util logger.

Changes

Structured logger migration

Layer / File(s) Summary
Client panic-recovery test helper
client/transport/panic_recovery_test.go
Replace ad-hoc panic test logger with an slog.Handler capturing Record.Message and add slogger() constructor used by tests.
Client SSE transport
client/transport/sse.go, client/transport/sse_test.go
Switch SSE to accept/store *slog.Logger (nil → slog.Default()), convert formatted logs to structured slog calls (err/origin/message), and update tests to use newTestLogger.
Client Stdio transport
client/transport/stdio.go, client/transport/stdio_test.go
Switch Stdio to accept/store *slog.Logger (nil → slog.Default()), convert read/write/marshal error logs to structured slog calls, and update tests to use newTestLogger.
Client StreamableHTTP transport and tests
client/transport/streamable_http.go, client/transport/streamable_http_test.go
Switch StreamableHTTP to *slog.Logger (nil → slog.Default()), add WithHTTPLogger and update deprecated WithLogger, convert many Infof/Errorf sites to structured slog with keys (panic/err/method/session/request), and replace test logger stubs with an slog.Handler channel capture.
OTEL slog bridge and tests
otel/go.mod, otel/logger.go, otel/logger_test.go
Add OTEL slog bridge NewSlogLogger(provider, scope) *slog.Logger and WithServerLogging helper; pin OTEL logging modules in otel/go.mod; add tests that assert slog records are exported to an SDK provider.
Server request and tool logging infra
server/logger.go, server/logger_test.go
Add WithLogger(*slog.Logger) server option; implement startMessageLog finalizer emitting mcp.request INFO (method/session/duration/outcome) and toolLoggingMiddleware emitting mcp.tool INFO per invocation with outcome and duration; add table-driven tests validating emitted JSON fields/outcomes and middleware registration.
Server wiring and handler
server/server.go, server/request_handler.go
Add requestLogger *slog.Logger to MCPServer, import log/slog, and update HandleMessage to start and defer per-message logging so responses are included.
Server StreamableHTTP transport
server/streamable_http.go
Replace server transport logger with *slog.Logger (add WithStreamableHTTPLogger), default to slog.Default(), and convert numerous transport log sites (POST, SSE, panic recoveries, sweeper, cleanup) to structured slog with keys like err, panic, session, and request.
Remove legacy util logger
util/logger.go
Remove the util.Logger interface and DefaultLogger() implementation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • mark3labs/mcp-go#525: Overlaps client transport logger wiring for SSE/STDIO; earlier PR added custom logger injection that this PR migrates to slog.
  • mark3labs/mcp-go#273: Related to prior StreamableHTTP implementation and its WithLogger(util.Logger) option which this PR replaces with slog wiring.
  • mark3labs/mcp-go#317: Related changes to client StreamableHTTP logger wiring and call-site logging.

Suggested labels

type: enhancement

Suggested reviewers

  • ezynda3
  • rwjblue-glean
  • pottekkat
  • cryo-zd
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: consolidating structured logging onto *slog.Logger at the server level with a new WithLogger function.
Description check ✅ Passed The description is comprehensive and complete, covering Why, What, Design parity, Breaking changes, and Validation sections with detailed explanations and tables.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@QuentinBisson QuentinBisson force-pushed the feat/server-with-logger branch 2 times, most recently from 86b4abe to 6b9a6da Compare May 18, 2026 13:36
@QuentinBisson QuentinBisson changed the title feat(server): add WithRequestLogger for structured JSON-RPC logging feat(server): WithLogger consolidates structured logging onto *slog.Logger May 18, 2026
@QuentinBisson QuentinBisson force-pushed the feat/server-with-logger branch from 6b9a6da to 39e19a1 Compare May 18, 2026 13:44
@QuentinBisson
Copy link
Copy Markdown
Contributor Author

@ezynda3 — would love your read on this PR's direction before going further. Two concrete questions:

  1. Scope of the breaking change. Right now this removes `util.Logger` and the `util/` package outright, and rewrites every `Infof`/`Errorf` call site to slog `Info`/`Error` with structured key-value pairs. Pre-1.0, but it touches every transport. Alternative if you'd rather a softer migration: keep `util.Logger` as a thin adapter over `*slog.Logger`, mark it deprecated, and convert call sites lazily. The all-at-once version is in the current diff; happy to reshape if you prefer the soft path.

  2. Naming. `server.WithLogger(util.Logger) StreamableHTTPOption` was the public name and I renamed it to `server.WithStreamableHTTPLogger` so `server.WithLogger(*slog.Logger) ServerOption` (this PR's new option) can carry the bare name. Open to other naming if you prefer — e.g. keep the existing one as `WithLogger` and call mine `WithRequestLogger` instead. I leaned toward "the canonical logger goes on the server, transport-specific ones get longer names" but it's a judgement call.

Either way, the protocol-level lines this option emits (`mcp.request`, `mcp.tool`, with matching attributes to `WithTracer`) are the part that actually matters; happy to break the PR up if the consolidation is too much in one go.

@QuentinBisson QuentinBisson marked this pull request as ready for review May 18, 2026 14:00
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
server/logger_test.go (1)

46-163: ⚡ Quick win

Align this suite with repo test conventions (testify + table-driven format).

These new tests currently rely on t.Fatalf and one-off test bodies; please migrate to table-driven tests using assert/require for consistency and maintainability.

As per coding guidelines **/*_test.go: "Use testify/assert and testify/require for testing; implement table-driven tests with tests := []struct{ name, ... }."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/logger_test.go` around lines 46 - 163, The tests in TestWithLogger_*
use t.Fatalf and one-off bodies instead of the repo convention of table-driven
tests with testify; refactor each TestWithLogger_* (e.g.,
TestWithLogger_NilIsNoOp, TestWithLogger_EmitsRequestLineOnHandled,
TestWithLogger_EmitsErrorOnJSONRPCError, TestWithLogger_RegistersToolMiddleware,
TestWithLogger_ToolMiddleware_HandlerError,
TestWithLogger_ToolMiddleware_IsErrorResult) into table-driven subtests using a
tests := []struct{name string; ...} slice and t.Run for each case, replace
t.Fatalf checks with testify/require or assert calls (require for fatal
preconditions, assert for expectations), and keep existing helpers
(newLoggerCapturingTo, s.startMessageLog, toolLoggingMiddleware, decodeLines,
findLine) while consolidating repeated setup into the table entries to reduce
duplication and improve readability.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@server/logger_test.go`:
- Around line 46-163: The tests in TestWithLogger_* use t.Fatalf and one-off
bodies instead of the repo convention of table-driven tests with testify;
refactor each TestWithLogger_* (e.g., TestWithLogger_NilIsNoOp,
TestWithLogger_EmitsRequestLineOnHandled,
TestWithLogger_EmitsErrorOnJSONRPCError, TestWithLogger_RegistersToolMiddleware,
TestWithLogger_ToolMiddleware_HandlerError,
TestWithLogger_ToolMiddleware_IsErrorResult) into table-driven subtests using a
tests := []struct{name string; ...} slice and t.Run for each case, replace
t.Fatalf checks with testify/require or assert calls (require for fatal
preconditions, assert for expectations), and keep existing helpers
(newLoggerCapturingTo, s.startMessageLog, toolLoggingMiddleware, decodeLines,
findLine) while consolidating repeated setup into the table entries to reduce
duplication and improve readability.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2a7eeacc-d058-45b7-a0f1-7f2a060561a8

📥 Commits

Reviewing files that changed from the base of the PR and between ca26738 and 39e19a1.

⛔ Files ignored due to path filters (1)
  • server/internal/gen/request_handler.go.tmpl is excluded by !**/gen/**
📒 Files selected for processing (13)
  • client/transport/panic_recovery_test.go
  • client/transport/sse.go
  • client/transport/sse_test.go
  • client/transport/stdio.go
  • client/transport/stdio_test.go
  • client/transport/streamable_http.go
  • client/transport/streamable_http_test.go
  • server/logger.go
  • server/logger_test.go
  • server/request_handler.go
  • server/server.go
  • server/streamable_http.go
  • util/logger.go
💤 Files with no reviewable changes (1)
  • util/logger.go

@QuentinBisson QuentinBisson force-pushed the feat/server-with-logger branch from 39e19a1 to 9b20cf3 Compare May 18, 2026 14:07
@QuentinBisson
Copy link
Copy Markdown
Contributor Author

Pushed an OTEL convenience pair on the `otel/` submodule so users wiring all three observability signals get parallel call sites — `otel.WithServerLogging(provider, scope)` alongside the existing `otel.WithServerTracing` / `otel.WithServerMetrics`. Implementation is a one-liner over `go.opentelemetry.io/contrib/bridges/otelslog`:

```go
func WithServerLogging(provider log.LoggerProvider, scope string) server.ServerOption {
return server.WithLogger(NewSlogLogger(provider, scope))
}
```

Core `server.WithLogger` still takes `*slog.Logger` directly — non-OTEL users (Loki/stdlib JSON/plain stderr) keep that path. Reasoning: `tracing` and `metrics` abstract because Go has no stdlib equivalent; `slog` is stdlib, so taking it directly is the more idiomatic choice and lets handlers compose.

The `server.WithLogger` docstring now points at `otel.WithServerLogging` for callers who want OTEL log correlation, so the discoverability path is clear.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@otel/logger_test.go`:
- Around line 68-73: The test incorrectly calls kv.Value.AsString() for all
attributes in records[0].WalkAttributes, which fails for Float64 attributes like
"duration_s"; update the attribute handling in the WalkAttributes callback to
switch on kv.Value.Type() (or use the provided type methods) and call AsString()
for string values and AsFloat64() for float values (or convert AsFloat64() to a
string if you must keep the attrs map as map[string]string), then assert
"duration_s" using the numeric value or its formatted string; reference the
WalkAttributes callback, log.KeyValue, kv.Value.AsString(),
kv.Value.AsFloat64(), and the "duration_s" key when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 05fc97b6-88c5-49e8-bca1-253b810a74c3

📥 Commits

Reviewing files that changed from the base of the PR and between 39e19a1 and 9b20cf3.

⛔ Files ignored due to path filters (2)
  • otel/go.sum is excluded by !**/*.sum
  • server/internal/gen/request_handler.go.tmpl is excluded by !**/gen/**
📒 Files selected for processing (16)
  • client/transport/panic_recovery_test.go
  • client/transport/sse.go
  • client/transport/sse_test.go
  • client/transport/stdio.go
  • client/transport/stdio_test.go
  • client/transport/streamable_http.go
  • client/transport/streamable_http_test.go
  • otel/go.mod
  • otel/logger.go
  • otel/logger_test.go
  • server/logger.go
  • server/logger_test.go
  • server/request_handler.go
  • server/server.go
  • server/streamable_http.go
  • util/logger.go
💤 Files with no reviewable changes (1)
  • util/logger.go
✅ Files skipped from review due to trivial changes (1)
  • client/transport/stdio_test.go
🚧 Files skipped from review as they are similar to previous changes (11)
  • client/transport/sse_test.go
  • server/request_handler.go
  • server/server.go
  • client/transport/panic_recovery_test.go
  • server/logger.go
  • client/transport/streamable_http_test.go
  • client/transport/stdio.go
  • client/transport/sse.go
  • server/logger_test.go
  • server/streamable_http.go
  • client/transport/streamable_http.go

Comment thread otel/logger_test.go Outdated
@QuentinBisson QuentinBisson force-pushed the feat/server-with-logger branch from 9b20cf3 to f763122 Compare May 18, 2026 14:14
@QuentinBisson
Copy link
Copy Markdown
Contributor Author

@coderabbitai — addressed in the latest push. The five `TestWithLogger_*` tests are consolidated into three table-driven test functions using `testify/assert` + `testify/require`:

  • `TestWithLogger_NilIsNoOp` — single behaviour, kept as-is but switched to `require.Nil`.
  • `TestWithLogger_RequestLine` — table covers the two `startMessageLog` outcomes (`ok` on nil response, `error` on JSONRPCError).
  • `TestWithLogger_ToolMiddleware` — table covers the three `toolLoggingMiddleware` outcomes (handler ok, handler error, IsError result).
  • `TestWithLogger_RegistersToolMiddleware` — kept separate because it exercises the middleware-registration plumbing rather than the middleware function directly.

All assertions migrated to `require` (fatal preconditions) / `assert` (expectations). 6 subtests in total, all green.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Tip

For best results, initiate chat on the files or code changes.

@QuentinBisson Thanks for the detailed breakdown — the restructuring looks well-considered. Keeping TestWithLogger_RegistersToolMiddleware separate is the right call; it tests the middleware-registration wiring rather than the middleware's runtime behavior, so forcing it into the same table would muddy the intent. The require / assert split (fatal preconditions vs. expectations) is exactly the idiomatic testify pattern.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/transport/streamable_http.go`:
- Around line 76-82: The option closure WithHTTPLogger currently mutates the
captured parameter `logger` (e.g., `logger = slog.Default()`), which can cause
races when the same option is reused; change the closure to avoid reassigning
the captured variable by setting `sc.logger` directly: inside the returned
function check `if logger == nil` and assign `sc.logger = slog.Default()` else
`sc.logger = logger`, leaving the captured `logger` unchanged—this fixes the
race while keeping the behavior of `WithHTTPLogger` on `StreamableHTTP` via the
`StreamableHTTPCOption` type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a9518314-32c8-4c55-b409-0873fcd1b19d

📥 Commits

Reviewing files that changed from the base of the PR and between 9b20cf3 and f763122.

⛔ Files ignored due to path filters (2)
  • otel/go.sum is excluded by !**/*.sum
  • server/internal/gen/request_handler.go.tmpl is excluded by !**/gen/**
📒 Files selected for processing (16)
  • client/transport/panic_recovery_test.go
  • client/transport/sse.go
  • client/transport/sse_test.go
  • client/transport/stdio.go
  • client/transport/stdio_test.go
  • client/transport/streamable_http.go
  • client/transport/streamable_http_test.go
  • otel/go.mod
  • otel/logger.go
  • otel/logger_test.go
  • server/logger.go
  • server/logger_test.go
  • server/request_handler.go
  • server/server.go
  • server/streamable_http.go
  • util/logger.go
💤 Files with no reviewable changes (1)
  • util/logger.go
✅ Files skipped from review due to trivial changes (2)
  • client/transport/sse_test.go
  • otel/logger.go
🚧 Files skipped from review as they are similar to previous changes (11)
  • client/transport/stdio_test.go
  • server/request_handler.go
  • otel/logger_test.go
  • client/transport/streamable_http_test.go
  • otel/go.mod
  • server/logger.go
  • client/transport/sse.go
  • server/server.go
  • server/streamable_http.go
  • client/transport/panic_recovery_test.go
  • client/transport/stdio.go

Comment thread client/transport/streamable_http.go
Comment thread client/transport/streamable_http.go Outdated
@QuentinBisson QuentinBisson force-pushed the feat/server-with-logger branch from f763122 to c3e2ff3 Compare May 18, 2026 14:51
@QuentinBisson
Copy link
Copy Markdown
Contributor Author

@coderabbitai — addressed the otel/logger_test.go bug. `kv.Value.AsString()` was being called on every attribute including the `duration_s` Float64, which would silently return garbage. Switched to a type-aware walker that uses `AsString` for the method key and `AsFloat64` for the duration, with separate assertions:

```go
var gotMethod string
var gotDuration float64
records[0].WalkAttributes(func(kv log.KeyValue) bool {
switch kv.Key {
case "mcp.method":
gotMethod = kv.Value.AsString()
case "duration_s":
gotDuration = kv.Value.AsFloat64()
}
return true
})
require.Equal(t, "tools/list", gotMethod)
require.InDelta(t, 0.001, gotDuration, 0.0)
```

Force-pushed.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat otel/logger_test.go 2>/dev/null || fd -e go logger_test.go otel --exec cat {}

Length of output: 2244


@QuentinBisson The type-aware walker is the right fix — calling AsString() on a Float64 value would indeed return an empty string silently, so asserting on it would have been a vacuous pass. The corrected code is solid.

One minor nit worth a second look: require.InDelta(t, 0.001, gotDuration, 0.0) with a delta of exactly 0.0 is effectively a strict == check. That's fine here because duration_s is a hard-coded constant (slog.Float64("duration_s", 0.001)) rather than a real elapsed time, so both sides carry the same IEEE 754 bit pattern and won't diverge. That said, InDelta normally signals tolerance to the reader — using it with 0.0 is a bit of a semantic mismatch. Consider either:

require.Equal(t, 0.001, gotDuration)             // explicit exact equality
// or
require.InDelta(t, 0.001, gotDuration, 1e-9)     // still deterministic, but reads as "float comparison with tolerance"

Neither is broken; it's purely a readability call.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
client/transport/sse_test.go (1)

786-793: 💤 Low value

Consider using testify assertions for consistency.

The test uses require for some assertions (lines 775, 782) but reverts to manual t.Errorf and t.Fatal for the log message checks. For consistency with the coding guidelines and the rest of the test, consider using testify assertions.

♻️ Refactor to use testify consistently
 	// Wait for the error log message about unmarshaling
 	select {
 	case logMsg := <-logChan:
-		if !strings.Contains(logMsg, "Error unmarshaling message") {
-			t.Errorf("Expected error log about unmarshaling message, got: %s", logMsg)
-		}
+		require.Contains(t, logMsg, "Error unmarshaling message", "Expected error log about unmarshaling message")
 	case <-time.After(3 * time.Second):
-		t.Fatal("Timeout waiting for error log message")
+		require.Fail(t, "Timeout waiting for error log message")
 	}

As per coding guidelines, use testify/assert and testify/require for testing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/transport/sse_test.go` around lines 786 - 793, The test mixes manual
t.Errorf/t.Fatal with testify; replace the select block that reads from logChan
and uses strings.Contains and t.Errorf/t.Fatal with testify assertions for
consistency: use require.Eventually (or require.Eventuallyf) to wait up to 3s
for a message on logChan and then use assert.Contains to assert the message
contains "Error unmarshaling message"; update the code referencing logChan and
strings.Contains and remove the t.Errorf/t.Fatal calls so the test uses
testify/assert and testify/require consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@client/transport/sse_test.go`:
- Around line 786-793: The test mixes manual t.Errorf/t.Fatal with testify;
replace the select block that reads from logChan and uses strings.Contains and
t.Errorf/t.Fatal with testify assertions for consistency: use require.Eventually
(or require.Eventuallyf) to wait up to 3s for a message on logChan and then use
assert.Contains to assert the message contains "Error unmarshaling message";
update the code referencing logChan and strings.Contains and remove the
t.Errorf/t.Fatal calls so the test uses testify/assert and testify/require
consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4cf604e6-4af7-4fe6-a5e7-862a3fa5224b

📥 Commits

Reviewing files that changed from the base of the PR and between f763122 and c3e2ff3.

⛔ Files ignored due to path filters (2)
  • otel/go.sum is excluded by !**/*.sum
  • server/internal/gen/request_handler.go.tmpl is excluded by !**/gen/**
📒 Files selected for processing (16)
  • client/transport/panic_recovery_test.go
  • client/transport/sse.go
  • client/transport/sse_test.go
  • client/transport/stdio.go
  • client/transport/stdio_test.go
  • client/transport/streamable_http.go
  • client/transport/streamable_http_test.go
  • otel/go.mod
  • otel/logger.go
  • otel/logger_test.go
  • server/logger.go
  • server/logger_test.go
  • server/request_handler.go
  • server/server.go
  • server/streamable_http.go
  • util/logger.go
💤 Files with no reviewable changes (1)
  • util/logger.go
✅ Files skipped from review due to trivial changes (1)
  • client/transport/stdio_test.go
🚧 Files skipped from review as they are similar to previous changes (12)
  • client/transport/panic_recovery_test.go
  • otel/logger.go
  • server/request_handler.go
  • server/server.go
  • client/transport/streamable_http_test.go
  • server/logger.go
  • otel/go.mod
  • server/streamable_http.go
  • client/transport/sse.go
  • server/logger_test.go
  • client/transport/stdio.go
  • client/transport/streamable_http.go

@QuentinBisson QuentinBisson force-pushed the feat/server-with-logger branch from c3e2ff3 to 4b9cc49 Compare May 18, 2026 15:17
@QuentinBisson
Copy link
Copy Markdown
Contributor Author

@coderabbitai — both addressed in the latest push.

Closure variable mutation (race): the `With*Logger` options previously did `if logger == nil { logger = slog.Default() }` which reassigned the captured parameter; reusing the same option across multiple servers/transports would race. Fixed across all four sites (`WithCommandLogger`, `WithSSELogger`, `WithHTTPLogger`, `WithStreamableHTTPLogger`) by assigning the default directly to the field and early-returning:

```go
return func(s *Stdio) {
if logger == nil {
s.logger = slog.Default()
return
}
s.logger = logger
}
```

`server.WithLogger` (the protocol-level option) didn't have this pattern — it never reassigned `logger` — but checked it anyway.

Test assertion consistency in sse_test.go: the unmarshaling-error select block mixed `t.Errorf` / `t.Fatal` with the surrounding `require` style. Switched to `require.Contains` / `require.Fail`:

```go
select {
case logMsg := <-logChan:
require.Contains(t, logMsg, "Error unmarshaling message")
case <-time.After(3 * time.Second):
require.Fail(t, "Timeout waiting for error log message")
}
```

Force-pushed; `go test ./...` clean.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the closure fix in With*Logger options
echo "=== WithCommandLogger ==="
rg -n -A10 "func WithCommandLogger" client/transport/stdio.go

echo "=== WithSSELogger ==="
rg -n -A10 "func WithSSELogger" client/transport/sse.go

echo "=== WithHTTPLogger ==="
rg -n -A10 "func WithHTTPLogger" client/transport/streamable_http.go

echo "=== WithStreamableHTTPLogger ==="
rg -n -A10 "func WithStreamableHTTPLogger" server/streamable_http.go

echo "=== sse_test.go select block ==="
rg -n -A6 "Error unmarshaling" client/transport/sse_test.go

Length of output: 1515


@QuentinBisson Both fixes are confirmed in the code:

Closure variable mutation: All four sites follow the safe pattern — the captured logger parameter is never mutated, just read:

if logger == nil {
    s.logger = slog.Default()
    return
}
s.logger = logger

This correctly eliminates the race when an option closure is reused across multiple server/transport instances.

sse_test.go assertion consistency: The select block now uniformly uses require.Contains / require.Fail, consistent with the rest of the test suite.

Both look great. 🐇✨

…ogger

Replace the bespoke util.Logger printf interface with stdlib *slog.Logger
across the server and client transports. Promote the new
WithRequestLogger option to WithLogger as the single canonical entry
point for server-side structured logging; rename the existing
transport-server WithLogger to WithStreamableHTTPLogger so the two no
longer compete for the same name in package server.

Concrete surface:

  - util.Logger and util.DefaultLogger are removed; the util/ package
    is gone.
  - All transport logger fields and With*Logger options now take
    *slog.Logger; nil falls back to slog.Default(). Affected:
    server.WithStreamableHTTPLogger (renamed from WithLogger),
    transport.WithCommandLogger, transport.WithSSELogger,
    transport.WithHTTPLogger, transport.WithLogger (deprecated alias).
  - server.WithLogger installs a server-wide *slog.Logger that emits
    one mcp.request line per JSON-RPC dispatch (attributes mcp.method,
    mcp.session.id, mcp.protocol.version, duration_s, outcome, error)
    and one mcp.tool line per tool handler invocation (mcp.tool.name,
    duration_s, outcome, error). Lines inherit the active span's
    TraceID/SpanID when WithTracer is installed.
  - Every existing Infof/Errorf call site is rewritten to slog Info/Error
    with structured key-value pairs (err, panic, method, session, …).

Behavioural change for callers: WithLogger no longer takes
util.Logger. Pre-1.0 library; callers who construct their own
util.Logger implementation switch to *slog.Logger.
@QuentinBisson QuentinBisson force-pushed the feat/server-with-logger branch from 4b9cc49 to 5deabdc Compare May 18, 2026 15:20
@QuentinBisson
Copy link
Copy Markdown
Contributor Author

@coderabbitai — addressed the payload-logging concern at both sites in `client/transport/streamable_http.go`. The original code at those sites already logged the raw payloads (`Errorf("...:%s", data)` / `Errorf("...:%s", body)`) so my slog rewrite carried the regression forward; this PR is the right moment to tighten:

```go
// line 728
c.logger.Info("failed to unmarshal message (non-fatal)", "err", err, "message_len", len(data))

// line 1132
c.logger.Error("server rejected response", "status", resp.StatusCode, "body_len", len(body))
```

Operators who need the raw payload for debugging can still capture it via a transport-level interceptor or by bumping log verbosity in a wrapper; the default no longer leaks request/response bodies into log pipelines. Force-pushed.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Around line 728 (unmarshal message) ==="
rg -n -B2 -A2 "failed to unmarshal message" client/transport/streamable_http.go

echo "=== Around line 1132 (server rejected response) ==="
rg -n -B2 -A2 "server rejected response" client/transport/streamable_http.go

Length of output: 569


@QuentinBisson Both sites are confirmed in the code:

  • Line 728"message_len", len(data): the unmarshal-failure path now logs the data length instead of the raw SSE frame payload.
  • Line 1132"body_len", len(body): the rejected-response path logs the body length instead of the raw HTTP response body.

Both fixes are clean. Logging the byte length still gives operators a useful signal (e.g. unexpectedly large or empty payloads) without risking credential or PII leakage into log pipelines.

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.

1 participant