Skip to content

Micrometer MCP

View Markdown

Status: Stable Package: dev.vertique.mcp.micrometer Artifact: vertique-micrometer-mcp Depends on: vertique-micrometer-core, vertique-mcp-core, vertique-core, vertique-config-core API reference: javadoc.io

Observe-only Micrometer metrics adapter for the MCP server. When installed alongside McpServerModule and MicrometerModule, it records a per-request timer (vertique.mcp.server.requests), an in-flight-requests gauge (vertique.mcp.server.active), a validation-failures counter (vertique.mcp.validation.failures), and a per-tool-call timer (vertique.mcp.tool.calls) — all with bounded, low-cardinality tags and no payload, identity, or trace-id label.

The module compiles against vertique-micrometer-core for the MetricsConfig type and vertique-mcp-core for the neutral lifecycle SPI. It also requires a MeterRegistry binding on the Dagger graph — normally supplied by MicrometerModule; without it (or another MeterRegistry provider) the component does not compile. Only the metrics.enabled gate is optional: the module declares @BindsOptionalOf MetricsConfig independently, so the gate defaults to enabled when MicrometerModule is absent. It never modifies MCP request or response processing, and it never submits audit records.


Install vertique-micrometer-mcp whenever an application exposes the MCP server via McpServerModule and metrics are enabled. Pair it with vertique-micrometer-core and at least one backend module (e.g., vertique-micrometer-registry-prometheus) to make the emitted meters visible. Without a backend the injected MeterRegistry is an empty composite whose recording is a no-op. Omitting this module entirely is also supported: open-core MCP has no dependency on it, and MCP protocol behavior is identical either way.


Observe-only. The module contributes exactly one McpRequestLifecycleObserver into the multibinding set MCP core’s McpCompletionCoordinator drives. The observer’s callback bodies are wrapped in try/catch that logs at WARN and swallows, so a misbehaving registry or a throwing callback never affects MCP request processing.

Active-gauge design. open(...) increments the gauge; the returned session’s onCompleted decrements it. Every opened session receives exactly one completion callback on every settlement path — including rejection, failure, and cancellation — so the gauge always returns to zero, matching the contract’s “the lifecycle start callback increments the untagged active gauge; post-wire completion decrements it and records the other instruments.” A decrement below zero is clamped at 0 with a one-time WARN, mirroring vertique-micrometer-core’s SecurityMetricsObserver.

All recording happens at completion, not at logical settlement. The observer does not override onTerminal; every instrument reflects the request’s actual transport settlement (McpRequestCompletedEvent), not merely its logical outcome.

Bounded dimensions only (FR-MCP-201, FR-MCP-203). Every tag value is drawn from a fixed, low-cardinality domain:

  • method — an McpMethod name, with McpMethod.OTHER remapped to the underscore-prefixed literal _OTHER so an unrecognized client-provided method never looks like a raw client string.
  • tool — McpRequestTerminalEvent#toolName(), which the terminal event’s own compact constructor already bounds to either the literal UNKNOWN or a generated, registered tool name matching [A-Za-z0-9_.-]{1,128}. An unresolved client-requested tool name never reaches this observer as a distinct value — every producer upstream of the lifecycle SPI collapses it to UNKNOWN first (see McpRequestTerminalEvent’s javadoc on its placeholder-descriptor decision point).
  • outcome, error.type, result.type, transport.outcome — the corresponding enum names, each drawn from a small fixed set.

No request ID, correlation ID, trace ID, principal, argument, result, or exception text is ever read by this class, so none can become a tag (FR-MCP-202). The observer performs no meter-filter registration of its own; each tag value’s domain is bounded structurally by the type it is read from, independent of any registry-level cardinality guard.

Defense in depth. vertique-micrometer-core’s CardinalityGuard.GUARDED_TAG_KEYS now also lists tool, result.type, and transport.outcome alongside method, outcome, and error.type, so a future producer that ever bypassed the structural bound above would still be capped and warned by the registry-level guard, not silently unbounded.

Zero-overhead when unconfigured. Before VertiqueApplication bootstrap the injected MeterRegistry is an empty composite whose recording is a no-op (NFR-TEL-003). When the optional MetricsConfig binding is absent (i.e., MicrometerModule is not installed), the observer defaults to enabled — it records against whatever registry is injected.


Dagger @Module. The only public type in this artifact. Install it explicitly to contribute one McpRequestLifecycleObserver into the MCP lifecycle multibinding set and to declare @BindsOptionalOf MetricsConfig so the metrics.enabled gate works with or without MicrometerModule. When that optional is empty, recording defaults to enabled. The application must still provide a MeterRegistry binding (normally from MicrometerModule).

@Component(modules = {
VertxModule.class,
McpServerModule.class,
MicrometerModule.class,
McpMicrometerModule.class,
// ...
})
interface AppComponent { /* ... */ }

Per-request timer. One sample is recorded per McpRequestCompletedEvent.

Tag Values Notes
method SERVER_DISCOVER, TOOLS_LIST, TOOLS_CALL, _OTHER McpMethod.OTHER is remapped to _OTHER
outcome SUCCESS, TOOL_ERROR, REJECTED, FAILED, CANCELLED McpOutcome enum name
error.type NONE, HTTP, PROTOCOL, AUTHENTICATION, AUTHORIZATION, INPUT_VALIDATION, INPUT_PROCESSING, INTERCEPTOR, HANDLER, OUTPUT_VALIDATION, SERIALIZATION, TIMEOUT, TRANSPORT, RATE_LIMIT, INTERNAL McpErrorType enum name
result.type NONE, COMPLETE McpResultType enum name
transport.outcome WRITTEN, DISCONNECTED, RESET, WRITE_FAILED McpTransportOutcome enum name

Prometheus rendering: vertique_mcp_server_requests_seconds_count / vertique_mcp_server_requests_seconds_sum / vertique_mcp_server_requests_seconds_bucket.

Instantaneous count of in-flight MCP requests. Untagged.

Prometheus rendering: vertique_mcp_server_active.

vertique.mcp.validation.failures — Counter

Section titled “vertique.mcp.validation.failures — Counter”

Incremented once per completed request whose terminal error.type is INPUT_VALIDATION or OUTPUT_VALIDATION.

Tag Values Notes
method see above
tool generated tool name, or UNKNOWN UNKNOWN for a non-TOOLS_CALL request or an unresolved tool
error.type INPUT_VALIDATION or OUTPUT_VALIDATION the only two values this counter ever carries

Prometheus rendering: vertique_mcp_validation_failures_total.

Recorded only for TOOLS_CALL requests, once per completed request.

Tag Values Notes
tool generated tool name, or UNKNOWN see the tool dimension note above
outcome see above
error.type see above

Prometheus rendering: vertique_mcp_tool_calls_seconds_count / vertique_mcp_tool_calls_seconds_sum / vertique_mcp_tool_calls_seconds_bucket.


Type Qualifier Description
MetricsConfig optional (@BindsOptionalOf) Declared for the metrics.enabled gate
McpRequestLifecycleObserver @IntoSet Per-request MCP meters contribution

Owns no module-local configuration keys. Observe-only: swallowed observer failures never alter MCP request processing.


  • io.micrometer:micrometer-core — MeterRegistry, Timer, Tags; no Vert.x Micrometer integration types. This is the only third-party Micrometer artifact on the compile classpath.
  • dev.vertique:vertique-micrometer-core — MetricsConfig, the metrics.enabled gate the observer injects as Optional<MetricsConfig>.
  • dev.vertique:vertique-mcp-core — McpRequestLifecycleObserver, McpRequestObservation, McpRequestCompletedEvent, McpRequestTerminalEvent, and the bounded lifecycle enums.
  • com.google.dagger:dagger, jakarta.inject:jakarta.inject-api
  • org.slf4j:slf4j-api, org.projectlombok:lombok (provided)