#[non_exhaustive]pub struct RequestHandlerExtra {Show 14 fields
pub cancellation_token: CancellationToken,
pub request_id: String,
pub session_id: Option<String>,
pub auth_info: Option<AuthInfo>,
pub auth_context: Option<AuthContext>,
pub metadata: HashMap<String, String>,
pub progress_reporter: Option<Arc<dyn ProgressReporter>>,
pub log_sink: Option<Arc<dyn Fn(Notification) + Send + Sync>>,
pub log_level: Option<LoggingLevel>,
pub task_request: Option<Value>,
pub request_meta: Option<Value>,
pub protocol_context: Option<ProtocolContext>,
pub extensions: Extensions,
pub peer: Option<Arc<dyn PeerHandle>>,
/* private fields */
}Expand description
Extra context passed to request handlers.
Fields (Non-exhaustive)§
This struct is marked as non-exhaustive
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.cancellation_token: CancellationTokenCancellation token for the request
request_id: StringRequest ID
session_id: Option<String>Session ID
auth_info: Option<AuthInfo>Authentication info
auth_context: Option<AuthContext>Validated authentication context (if auth is enabled)
metadata: HashMap<String, String>Custom metadata for middleware (e.g., OAuth tokens, session data)
Security Note: Metadata may contain sensitive values like OAuth tokens. The Debug implementation redacts these values to prevent accidental logging.
progress_reporter: Option<Arc<dyn ProgressReporter>>Optional progress reporter for this request
log_sink: Option<Arc<dyn Fn(Notification) + Send + Sync>>Request-scoped notification sink for notifications/message records
emitted by RequestHandlerExtra::log / RequestHandlerExtra::log_with_data.
UNGATED by any progress token — a log record is not progress, and a
client that never sent a progressToken still gets its logs. None
when the enclosing dispatch path attached no sink (unit-test fixtures,
RequestHandlerExtra::default(), transports without a back-channel);
emission is then a silent no-op returning Ok(()). See
RequestHandlerExtra::log for the accepted cost of that silence.
log_level: Option<LoggingLevel>The log level resolved for this request, if anything set one.
None means nothing set one, and the effective level falls back to
DEFAULT_LOG_LEVEL. Records strictly below the effective level are
dropped by RequestHandlerExtra::log before anything is constructed.
task_request: Option<Value>Task augmentation request from the client (MCP Tasks).
When Some, the client supports async task polling and requested
task-augmented behavior for this call. The tool handler can check
this to decide between a sync path (await and return results) or
an async path (create task, return immediately).
When None, the client does not support tasks or did not request
task mode — the tool should return results synchronously.
request_meta: Option<Value>The request’s _meta object as raw JSON (MCP _meta).
Populated by the dispatcher from the incoming tools/call request so
handlers can read arbitrary namespaced _meta keys (beyond the typed
progress_token/_task_id) without a typed dependency on RequestMeta.
None when the request carried no _meta.
protocol_context: Option<ProtocolContext>The per-request protocol context resolved once at ingress (MCP v2.5 version plumbing).
When Some, the dispatcher resolved the negotiated protocol version into
an Era plus optional self-reported client
identity and surfaced it here so handlers can era-gate behavior via the
typed era /
protocol_version /
client_info /
client_capabilities
accessors instead of reading ambient session state. None when no
context was resolved (unit-test fixtures, pre-v2.5 dispatch paths).
Security: the client identity carried here is SELF-REPORTED and informational only — see the accessor rustdoc. It MUST NOT be used as an authorization anchor; real identity binds to the OAuth token.
extensions: ExtensionsTyped request-scoped state for middleware→handler transfer.
Inserting values requires T: Clone + Send + Sync + 'static. Debug prints type names only,
not values, making this safe for logging. Cloning RequestHandlerExtra clones the entire
extensions map — prefer Arc<T> for large values.
peer: Option<Arc<dyn PeerHandle>>Server-to-client back-channel for sample / list_roots / progress_notify.
None when the request originated from a code path (wasm dispatch, unit-test
fixture, any ServerCore::new(...) without
crate::server::core::ServerCore::with_server_request_dispatcher) that did
not configure a ServerRequestDispatcher. Tool/prompt/resource handlers
running under a fully-configured ServerCore receive a populated peer handle
that routes sample / list_roots / progress_notify back to the originating
client.
§Session isolation
Each Server instance owns its own dispatcher, so peer handles are scoped to
the enclosing Server — cross-session confusion requires cross-process access.
Implementations§
Source§impl RequestHandlerExtra
impl RequestHandlerExtra
Sourcepub fn new(request_id: String, cancellation_token: CancellationToken) -> Self
pub fn new(request_id: String, cancellation_token: CancellationToken) -> Self
Create new handler extra context.
Sourcepub fn with_session_id(self, session_id: Option<String>) -> Self
pub fn with_session_id(self, session_id: Option<String>) -> Self
Set the session ID.
Sourcepub fn with_auth_info(self, auth_info: Option<AuthInfo>) -> Self
pub fn with_auth_info(self, auth_info: Option<AuthInfo>) -> Self
Set the auth info.
Sourcepub fn with_auth_context(self, auth_context: Option<AuthContext>) -> Self
pub fn with_auth_context(self, auth_context: Option<AuthContext>) -> Self
Set the auth context.
Sourcepub fn with_progress_reporter(
self,
progress_reporter: Option<Arc<dyn ProgressReporter>>,
) -> Self
pub fn with_progress_reporter( self, progress_reporter: Option<Arc<dyn ProgressReporter>>, ) -> Self
Attach a progress reporter.
Sourcepub fn with_log_sink(
self,
sink: Arc<dyn Fn(Notification) + Send + Sync>,
) -> Self
pub fn with_log_sink( self, sink: Arc<dyn Fn(Notification) + Send + Sync>, ) -> Self
Attach the request-scoped notification sink for log records.
The construction seam the dispatch layer uses to wire
log / log_with_data to whatever
back-channel this request has. Without it both emitters are silent
no-ops that still return Ok(()).
Sourcepub fn with_log_level(self, level: LoggingLevel) -> Self
pub fn with_log_level(self, level: LoggingLevel) -> Self
Set the log level for this request.
Records strictly below level are dropped by the emitters. When this is
never called the effective level is DEFAULT_LOG_LEVEL.
Sourcepub fn with_task_request(self, task_request: Option<Value>) -> Self
pub fn with_task_request(self, task_request: Option<Value>) -> Self
Set the task request from the client’s tools/call params.
When present, the tool handler knows the client supports task-augmented responses and can choose an async path (create task, return immediately) instead of awaiting the full result.
Sourcepub fn with_request_meta(self, meta: Option<Value>) -> Self
pub fn with_request_meta(self, meta: Option<Value>) -> Self
Attach the request’s _meta object (raw JSON) for handler inspection.
Handlers can then read arbitrary namespaced _meta keys (e.g. team-guard
depth/ancestor state) via extra.request_meta. Pass None when the
request carried no _meta.
Sourcepub fn with_protocol_context(self, ctx: Option<ProtocolContext>) -> Self
pub fn with_protocol_context(self, ctx: Option<ProtocolContext>) -> Self
Attach the per-request ProtocolContext
resolved at ingress (MCP v2.5 version plumbing).
Populated by the dispatch layer (Plan 04) after negotiating the protocol
version; handlers then read the era and client identity via the typed
era / protocol_version /
client_info /
client_capabilities accessors. Pass None
when no context was resolved.
Sourcepub fn era(&self) -> Option<Era>
pub fn era(&self) -> Option<Era>
Returns the resolved protocol Era for
this request, or None when no ProtocolContext
was attached.
Security: the era is derived from the negotiated protocol version — it is a behavioral switch, NOT an identity or authorization signal.
Sourcepub fn protocol_version(&self) -> Option<&ProtocolVersion>
pub fn protocol_version(&self) -> Option<&ProtocolVersion>
Returns the exact negotiated
ProtocolVersion for this request, or
None when no ProtocolContext
was attached.
Sourcepub fn client_info(&self) -> Option<&Implementation>
pub fn client_info(&self) -> Option<&Implementation>
Returns the client’s SELF-REPORTED implementation info, or None when
absent.
Security — self-reported, not for authorization: this value is the
client-supplied clientInfo surfaced verbatim from initialization. It is
informational ONLY (telemetry, feature-hints, logging) and MUST NOT be
used as an authorization anchor or trusted identity. Real identity binds
to the OAuth token, enforced in Phase 114 (TASK-05). No authorization
decision is made from this accessor in this phase.
Sourcepub fn client_capabilities(&self) -> Option<&ClientCapabilities>
pub fn client_capabilities(&self) -> Option<&ClientCapabilities>
Returns the client’s SELF-REPORTED advertised capabilities, or None
when absent.
§Both eras reach this accessor
On MCP 2026-07-28 the value comes from the per-request
_meta["io.modelcontextprotocol/clientCapabilities"], resolved once at
ingress. On MCP 2025-11-25 it comes from the initialize handshake,
folded into the same ProtocolContext at the dispatch root by
core::fold_v1_handshake_capabilities (Phase 118.1-08, G-9). This method
reads ONLY protocol_context.client_capabilities in either case — it
deliberately does NOT reach for the server-level client_capabilities
lock, because it is a sync method on a value already moved into the
handler while the dispatch path holds that lock, and that shape
deadlocks.
Absent both signals the answer is None, never a fabricated default.
Security — self-reported, not for authorization: like
client_info, these capabilities are client-supplied
and informational ONLY. They MUST NOT be used as an authorization anchor;
real identity binds to the OAuth token (Phase 114 / TASK-05).
Sourcepub fn input_responses(&self) -> Option<&InputResponses>
pub fn input_responses(&self) -> Option<&InputResponses>
The client’s answers to a PREVIOUS round’s inputRequests
(MCP 2026-07-28 multi-round-trip elicitation, HTTP-03).
Returns None when the request carried no inputResponses, on v1, and
on the re-elicitation path (an unknown-key or expired requestState is
re-run as a pristine FIRST call, so nothing MRTR-shaped is observable).
Security — CLIENT-SUPPLIED and UNTRUSTED. Every value here came off
the wire. It is bounded in count, per-entry size, total size and nesting
depth at transport ingress, and each entry is proven to be one of the
three spec-permitted result shapes — but nothing validates it against the
schema the handler asked for. A handler MUST schema-validate an entry
before acting on it, exactly as it would validate tool arguments. Compare
mrtr_continuation, which is server-minted.
Sourcepub fn mrtr_continuation(&self) -> Option<&Value>
pub fn mrtr_continuation(&self) -> Option<&Value>
The DECRYPTED continuation state from a VERIFIED requestState token
(MCP 2026-07-28 multi-round-trip elicitation, HTTP-03).
Security — SERVER-MINTED and TRUSTED. This value is whatever the
handler itself sealed on a previous round. It reaches here only after the
server-owned AEAD codec authenticated the token against the authenticated
principal, the live method and a digest of the request’s salient
parameters, so a tampered, cross-principal or cross-request token never
produces a Some — it produces a JSON-RPC error instead.
None on a first call, on v1, and inside a re-run handler.
Sourcepub fn mrtr_round(&self) -> Option<u8>
pub fn mrtr_round(&self) -> Option<u8>
The multi-round-trip round counter carried by a verified requestState.
A handler uses this to decide it has asked enough times and should fail
or degrade rather than elicit again (D-09). Counts from 0 for the round
the FIRST continuation was minted in; None on a first call, on v1, and
inside a re-run handler.
Sourcepub fn trace_context(&self) -> Option<TraceContext>
pub fn trace_context(&self) -> Option<TraceContext>
Extracts the W3C trace-context self-reported in the request _meta
(MCP v2.5, VERS-09), reading the existing
request_meta — no dedicated field is stored.
Returns Some only when request_meta carries an in-bounds
traceparent string; None when request_meta is None or lacks a
valid traceparent. Delegates to
TraceContext::from_meta.
Security — raw, bounded, untrusted: the returned
traceparent/tracestate/baggage values are RAW and UNVALIDATED
self-reported client data, only length-bounded at ingress
(MAX_TRACE_VALUE_LEN) per the Plan-01 contract — see
TraceContext. They MUST NOT be
treated as trusted or safe to interpolate into logs/queries without
independent sanitization.
Sourcepub fn extensions(&self) -> &Extensions
pub fn extensions(&self) -> &Extensions
Returns a reference to the typed extensions map.
§Example
use pmcp::RequestHandlerExtra;
let extra = RequestHandlerExtra::default();
let _: Option<&String> = extra.extensions().get::<String>();Sourcepub fn extensions_mut(&mut self) -> &mut Extensions
pub fn extensions_mut(&mut self) -> &mut Extensions
Returns a mutable reference to the typed extensions map.
§Example
use pmcp::RequestHandlerExtra;
let mut extra = RequestHandlerExtra::default();
extra.extensions_mut().insert(42u64);Sourcepub fn with_peer(self, peer: Arc<dyn PeerHandle>) -> Self
pub fn with_peer(self, peer: Arc<dyn PeerHandle>) -> Self
Attach a peer handle for server-to-client RPCs.
When set, tool/prompt/resource handlers can invoke extra.peer().unwrap().sample(...)
(or list_roots / progress_notify) to make outbound requests back to the
originating client. The peer handle is populated by the enclosing
ServerCore at each dispatch site when a
[crate::server::server_request_dispatcher::ServerRequestDispatcher]
has been attached — tests or ad-hoc constructions that skip the dispatcher
leave this as None, and handlers should treat None as “no client
available for back-channel”.
§Example
use pmcp::RequestHandlerExtra;
use std::sync::Arc;
let extra = RequestHandlerExtra::default().with_peer(build_peer());Sourcepub fn peer(&self) -> Option<&Arc<dyn PeerHandle>>
pub fn peer(&self) -> Option<&Arc<dyn PeerHandle>>
Returns the peer handle, if one was attached.
Handlers should treat None as “no client back-channel available” and
skip any sample/list_roots-dependent code paths gracefully.
Sourcepub fn set_result_meta(&self, meta: Map<String, Value>)
pub fn set_result_meta(&self, meta: Map<String, Value>)
Merge handler-authored keys into the result _meta for this call.
This is the lowest-friction way for an existing hand-written handler (one
that returns a plain Value and does NOT override
ToolHandler::handle_output)
to attach task augmentation or custom _meta without owning the full
envelope. The keys are accumulated in an interior-mutable slot and merged
onto the outgoing CallToolResult’s _meta after the handler returns.
Takes &self: the slot is interior-mutable, so this works even though the
handler receives RequestHandlerExtra by value (the slot Arc is shared
with the clone the dispatcher retained before the by-value move).
§Merge precedence
Keys MERGE — never a whole-map replace:
- a handler-set key overwrites a same-name key (whether set by an
earlier
set_result_metacall or emitted by widget/native enrichment); - all unrelated existing
_metakeys (widget/native emission) are preserved; - repeated calls accumulate into the same slot — a later call’s colliding keys win, non-colliding keys from both calls survive.
§Path scope
This applies on both tool-output paths, on both dispatchers
(Server and ServerCore):
- Payload path — the keys merge onto the envelope the dispatcher
builds (text-wrap / widget enrichment /
outputSchemabridge). ToolOutput::Result(verbatim) path — since D-06 (Phase 118.1) the keys merge onto the envelope the HANDLER authored, immediately before it is emitted. That arm still bypasses response middleware, the task create-path gate and the text-wrap tail (D-04 / D-04a); the bypass covers the response pipeline, not the handler’s own_meta, which is authored by the same handler at the same trust level as the envelope itself. Before D-06 the keys were silently DROPPED here.
A verbatim handler may of course still set _meta on the value directly,
e.g. via
CallToolResult::with_related_task;
when it does both, the merge precedence above decides — the
set_result_meta key wins the collision, and the envelope’s unrelated
keys survive.
Sourcepub fn is_task_request(&self) -> bool
pub fn is_task_request(&self) -> bool
Returns true if the client requested task-augmented behavior.
Sourcepub fn auth_context(&self) -> Option<&AuthContext>
pub fn auth_context(&self) -> Option<&AuthContext>
Get the auth context if available.
Sourcepub fn get_metadata(&self, key: &str) -> Option<&String>
pub fn get_metadata(&self, key: &str) -> Option<&String>
Get metadata value by key.
Metadata is typically set by middleware (e.g., OAuth token injection).
Sourcepub fn set_metadata(&mut self, key: String, value: String)
pub fn set_metadata(&mut self, key: String, value: String)
Set metadata value.
This is typically used by middleware to inject data for tools to consume.
Sourcepub fn is_cancelled(&self) -> bool
pub fn is_cancelled(&self) -> bool
Check if the request has been cancelled.
Sourcepub async fn report_progress(
&self,
progress: f64,
total: Option<f64>,
message: Option<String>,
) -> Result<()>
pub async fn report_progress( &self, progress: f64, total: Option<f64>, message: Option<String>, ) -> Result<()>
Report progress if a reporter is available.
Sourcepub async fn report_percent(
&self,
percent: f64,
message: Option<String>,
) -> Result<()>
pub async fn report_percent( &self, percent: f64, message: Option<String>, ) -> Result<()>
Report percentage progress (0-100) if available.
Sourcepub fn log(&self, level: LoggingLevel, message: impl Into<String>) -> Result<()>
pub fn log(&self, level: LoggingLevel, message: impl Into<String>) -> Result<()>
Emit an MCP notifications/message log record to this request’s client.
The first PRODUCTION constructor of
ServerNotification::LogMessage.
Synchronous on purpose: the sink is a synchronous
Arc<dyn Fn(Notification) + Send + Sync>, no trait is involved, and
making this async would force every logging call site into an .await
to buy nothing.
§Ok(()) is NOT delivery acknowledgement
Ok(()) means the record was handed to whatever sink this request has,
or that there was none — it does NOT mean the client received it. The
sink’s type is Fn(Notification) -> (): it returns unit and therefore
cannot report failure, and the fallback notification_tx path in
Server likewise ignores its own try_send result. Do not build retry
logic on this Result. It exists so that a future “sink refused” signal
is an additive change rather than a breaking one.
§No sink means silence, not an error
With no sink attached the call is a no-op returning Ok(()), exactly as
report_progress is. That keeps a handler
callable outside a server —
RequestHandlerExtra::default() is documented for
testing and simple tool invocations, and a handler that logs must not
become un-unit-testable. The accepted cost, stated plainly: a MISPLUMBED
transport looks identical to a quiet handler. The conformance fence — a
test asserting records actually arrive over the wire — is what catches
that, so this is a production-diagnostics hole rather than a false green
in the gate.
§Level filtering
A record strictly below the effective level (this request’s
with_log_level, else DEFAULT_LOG_LEVEL) is
dropped before anything is constructed. Comparison is on the typed
LoggingLevel, whose declaration order is
syslog severity order — never on the serialized strings, where
"critical" < "debug" inverts the filter.
§No rate limiting, and what that does and does not promise
Unlike progress reporting, this emitter applies NO rate limit. Progress values are idempotent — dropping an intermediate one loses nothing because the next supersedes it — whereas each log record is the only copy of its information, so a limiter would silently delete evidence.
That is a statement about the EMITTER, not a promise that no record can
ever be dropped downstream. On MCP 2026-07-28 the vehicle a record rides
is the request’s bounded progress queue: an mpsc::channel(64) with an
explicit DROP-NEWEST try_send policy, because the sink closure is
synchronous and must not block. A handler emitting more than that
capacity of notifications in one call LOSES THE EXCESS, and every one of
those calls still returns Ok(()). A handler that must not lose records
should keep a single call under the queue capacity.
§When records go nowhere on 2026-07-28
That vehicle is attached only when the request is multi-frame eligible —
era is 2026-07-28, the server is not configured for JSON responses, and
the request’s Accept header includes text/event-stream. A 2026-07-28
client that asked for JSON only therefore receives no log records at all.
That is correct behaviour for a client that declined the streaming
channel, not a bug.
§Capabilities
Emission is deliberately NOT gated on
ServerCapabilities::logging. The
sink is per-request and the capability is advisory; gating here would
make a correctly-plumbed server silently mute. Servers SHOULD still
declare the capability so clients know to expect records.
§Example
use pmcp::types::LoggingLevel;
use pmcp::RequestHandlerExtra;
// No server, so no sink — the call succeeds and emits nothing.
let extra = RequestHandlerExtra::default();
assert!(extra.log(LoggingLevel::Info, "starting work").is_ok());Sourcepub fn log_with_data(
&self,
level: LoggingLevel,
message: impl Into<String>,
data: Value,
) -> Result<()>
pub fn log_with_data( &self, level: LoggingLevel, message: impl Into<String>, data: Value, ) -> Result<()>
Emit a notifications/message log record carrying structured data.
Identical to log in every respect — same synchronous
emission, same level filtering, same no-sink silence, and the same
Ok(())-is-not-acknowledgement caveat — except that data is attached
to the record’s data member.
§Example
use pmcp::types::LoggingLevel;
use pmcp::RequestHandlerExtra;
use serde_json::json;
let extra = RequestHandlerExtra::default();
assert!(extra
.log_with_data(LoggingLevel::Warning, "retrying", json!({ "attempt": 2 }))
.is_ok());