Skip to content

vmcp: accept injected *slog.Logger via Config (avoid sharing slog.Default with the embedder) #5381

@JAORMX

Description

@JAORMX

Problem

pkg/vmcp/server library code uses package-level slog calls (slog.Info, slog.Warn, slog.Error, etc.) throughout. These read slog.Default() at call time. There is no public field on vmcpserver.Config for an embedder to inject a *slog.Logger.

For library consumers that embed pkg/vmcp/server inside a multi-component Go process with its own logger context, this means vmcp's logs share the embedder's process-wide slog.Default(). Useful when that's what the embedder wants; friction when the embedder wants per-component tagging, separate sinks, or different log levels per component.

The workarounds today:

  • Mutate slog.SetDefault() globally before vmcp lifecycle calls, then restore after. Fragile (any future internal vmcp callsite that reads the default at a different point breaks the save/restore window), and clobbers the embedder's own logger config for the duration.
  • Accept that vmcp logs land in the embedder's default sink with no per-component metadata.

The standalone cmd/vmcp binary doesn't run into this because it owns its process and sets slog.SetDefault() once at startup.

Requested fix

Add Logger *slog.Logger to vmcpserver.Config. Behavior:

  • If Config.Logger == nil: current behavior preserved. vmcp's package-level slog calls fall back to slog.Default().
  • If Config.Logger != nil: vmcp uses the injected logger for all internal logging. Internally this likely means routing the package-level helpers through a private function that prefers Config.Logger when set.

This is backward-compatible (additive Config field) and gives library consumers a clean opt-in injection point.

Suggested API

type Config struct {
    // ... existing fields ...

    // Logger, when non-nil, is used for all vmcp internal logging.
    // When nil, vmcp's package-level slog calls fall back to
    // slog.Default() (current behavior).
    Logger *slog.Logger
}

The zap-side equivalent can land as a second additive field (ZapLogger *zap.Logger) or, preferably, vmcp uses an internal slog→zap adapter so a single Config.Logger covers both. Either is fine from a consumer perspective.

Acceptance

  • Existing tests pass; standalone cmd/vmcp binary behavior unchanged when Config.Logger is left nil.
  • A new test confirms that, with Config.Logger set to a fresh *slog.Logger, vmcp's emissions go to the injected handler rather than the package default.
  • A note in pkg/vmcp/doc.go (or the server README) documents the library-consumer pattern.

Why this matters

The change is backward-compatible and unblocks a clean library-embedding story without forcing every consumer to wrap vmcp lifecycle calls in save/restore boilerplate or to accept vmcp's logs sharing the embedder's default sink unmodified.

Happy to send a PR if the API shape above is roughly the direction you'd accept; flag any deviations before I start.


Edited 2026-05-26: the original framing claimed vmcp itself mutates slog.SetDefault / zap.ReplaceGlobals at startup. On closer inspection that mutation is consumer-side (in a library-embedding consumer's own bootstrap), not in pkg/vmcp/*. The underlying request stands: an injectable Config.Logger is the clean fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestgoPull requests that update go codevmcpVirtual MCP Server related issues

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions