pub struct Client<T: Transport> { /* private fields */ }Expand description
MCP client for connecting to servers.
Implementations§
Source§impl<T: Transport> Client<T>
impl<T: Transport> Client<T>
Sourcepub fn new(transport: T) -> Self
pub fn new(transport: T) -> Self
Create a new client with the given transport.
Uses default client information with the name “pmcp-client” and the current crate version.
§Examples
use pmcp::{Client, StdioTransport};
let transport = StdioTransport::new();
let client = Client::new(transport);Sourcepub fn with_info(transport: T, client_info: Implementation) -> Self
pub fn with_info(transport: T, client_info: Implementation) -> Self
Create a new client with custom info.
Allows specifying custom client name and version information that will be sent to the server during initialization.
§Examples
use pmcp::{Client, StdioTransport, Implementation};
let transport = StdioTransport::new();
let client_info = Implementation::new("my-custom-client", "2.1.0");
let client = Client::with_info(transport, client_info);Sourcepub fn with_options(
transport: T,
client_info: Implementation,
options: ProtocolOptions,
) -> Self
pub fn with_options( transport: T, client_info: Implementation, options: ProtocolOptions, ) -> Self
Create a new client with custom protocol options.
§Examples
use pmcp::{Client, StdioTransport, Implementation};
use pmcp::shared::ProtocolOptions;
// Custom options for high-throughput scenarios
let options = ProtocolOptions {
enforce_strict_capabilities: false,
debounced_notification_methods: vec![
"notifications/progress".to_string(),
"notifications/message".to_string(),
],
};
let transport = StdioTransport::new();
let client_info = Implementation::new("high-throughput-client", "1.0.0");
let client = Client::with_options(transport, client_info, options);Sourcepub fn with_client_options(transport: T, options: ClientOptions) -> Self
pub fn with_client_options(transport: T, options: ClientOptions) -> Self
Construct a client with caller-supplied ClientOptions.
Mirrors Self::new but wires in a ClientOptions value so that
Self::list_all_tools / Self::list_all_prompts / etc. honour a
custom max_iterations cap.
§ClientBuilder parity
ClientBuilder does not currently expose a .client_options() setter.
If you need a custom ClientOptions, construct the client via
Self::with_client_options directly.
§Examples
use pmcp::{Client, ClientOptions};
let opts = ClientOptions::default().with_max_iterations(50);
let _client = Client::with_client_options(transport, opts);Sourcepub async fn initialize(
&mut self,
capabilities: ClientCapabilities,
) -> Result<InitializeResult>
pub async fn initialize( &mut self, capabilities: ClientCapabilities, ) -> Result<InitializeResult>
Initialize the connection with the server.
Performs the MCP initialization handshake, negotiating capabilities and receiving server information. This must be called before using other client methods.
§Host capabilities are registry-derived (sampling / elicitation / roots)
The three host-side capability fields — sampling, elicitation, and
roots — are derived from the handlers registered on
ClientBuilder, not from the value passed here. If no matching host
handler is registered, the corresponding field is forced to None on the
wire even when the caller set it (the anti-capability-lie rule: a client
must not advertise a host capability it cannot service). Register handlers
via ClientBuilder::on_sampling, ClientBuilder::on_elicitation, and
ClientBuilder::on_roots to advertise these capabilities. When a
handler is registered, any caller-configured detail for that field
(e.g. roots.list_changed) is preserved. All other capability fields
(tasks, experimental, …) pass through unchanged.
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities};
let transport = StdioTransport::new();
let mut client = Client::new(transport);
let capabilities = ClientCapabilities::default();
let server_info = client.initialize(capabilities).await?;
println!("Server: {} v{}",
server_info.server_info.name,
server_info.server_info.version);§Severability: this method is NOT gated, and that is measured
SMPL-01 names “initialize/session lifecycle” as v1-only machinery that a
full-v2 build must not carry. Plan 117-14 MEASURED whether
#[cfg(feature = "v1-compat")] could be applied here and took a
documented fallback for two reasons, both recorded in
.planning/phases/117-agents-tester-v1-severability/117-14-SUMMARY.md:
- It is DUAL-era, not v1-only. The
is_v2()branch below is a deliberate Phase-113 compatibility affordance — it sends nothing and exists so v1-shaped application code keeps compiling after opting into v2. Gating this method would delete v2 behaviour, not just v1 behaviour. src/composition/mcp_client.rscalls it, andcompositionis in thefull-v2feature list. Propagating the gate there would mean a composition connection that reports itself initialized without ever having handshaken — a semantic change to a subsystem this plan has no mandate over.
SMPL-01’s “initialize” clause is therefore met on the SERVER side only,
and even there it is the session BOOKKEEPING that is severed, not the
handshake: plan 117-12 moved process_init_session,
update_session_after_init and the rest of the session-lifecycle
functions into v1_session.rs, while a full-v2 server still answers an
initialize POST statelessly (only GET and DELETE are refused 405).
The pure classifiers is_initialize_request /
extract_negotiated_version therefore stay ungated in
streamable_http_server.rs. docs/v1-sunset-policy.md MUST name this as
a known limitation.
§Errors
Returns an error if:
- The client is already initialized
- The server rejects the initialization
- Communication with the server fails
Sourcepub async fn server_discover(&mut self) -> Result<ServerDiscoverResult>
pub async fn server_discover(&mut self) -> Result<ServerDiscoverResult>
Ask a v2 server for its capability projection (server/discover).
v2 has no initialize, so this is how a client learns what the server
supports. It is EXPLICIT: pmcp never calls it implicitly, and never uses
it to CHOOSE an era (Phase-113 D-08 forbids exactly that auto-probe).
Populating capabilities from a call the USER made is a different thing
from probing to decide which protocol to speak — do not “restore” the
latter.
Takes &mut self because it STORES the returned capabilities: after this
call Self::assert_capability enforces on v2 exactly as it does on v1
against initialize-learned ones.
§Errors
Returns an error when the connection did not opt into 2026-07-28
(server/discover does not exist on v1 — a v1 server answers -32601),
when the transport fails, or when the server returns a JSON-RPC error.
Sourcepub fn get_server_capabilities(&self) -> Option<&ServerCapabilities>
pub fn get_server_capabilities(&self) -> Option<&ServerCapabilities>
Get server capabilities after initialization.
Sourcepub fn get_server_version(&self) -> Option<&Implementation>
pub fn get_server_version(&self) -> Option<&Implementation>
Get server version information after initialization.
Sourcepub fn get_instructions(&self) -> Option<&str>
pub fn get_instructions(&self) -> Option<&str>
Get server instructions after initialization.
Sourcepub async fn set_logging_level(&self, level: LoggingLevel) -> Result<()>
pub async fn set_logging_level(&self, level: LoggingLevel) -> Result<()>
Set the logging level on the server.
Sourcepub async fn list_tools(
&self,
cursor: Option<String>,
) -> Result<ListToolsResult>
pub async fn list_tools( &self, cursor: Option<String>, ) -> Result<ListToolsResult>
List available tools.
Retrieves information about all tools available on the server, including their names, descriptions, and input schemas.
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities};
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(ClientCapabilities::default()).await?;
// List all tools
let tools = client.list_tools(None).await?;
for tool in tools.tools {
println!("Tool: {} - {}",
tool.name,
tool.description.unwrap_or_else(|| "No description".to_string()));
}§Arguments
cursor- Optional pagination cursor for retrieving additional results
Sourcepub async fn call_tool(
&self,
name: String,
arguments: Value,
) -> Result<CallToolResult>
pub async fn call_tool( &self, name: String, arguments: Value, ) -> Result<CallToolResult>
Call a tool.
Invokes a server-provided tool with the specified name and arguments. The server must have declared the tool via the tools capability during initialization.
§Arguments
name- The name of the tool to callarguments- JSON value containing the tool’s arguments
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities};
use serde_json::json;
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(ClientCapabilities::default()).await?;
// Call a simple tool with no arguments
let result = client.call_tool(
"list_files".to_string(),
json!({})
).await?;
// Call a tool with specific arguments
let search_result = client.call_tool(
"search".to_string(),
json!({
"query": "rust programming",
"limit": 10
})
).await?;
// Tools can return structured data
if let Some(content) = result.content.first() {
match content {
pmcp::Content::Text { text } => {
println!("Tool result: {}", text);
}
_ => println!("Non-text tool result"),
}
}§Errors
Returns an error if:
- The client is not initialized
- The server doesn’t support tools
- The tool name doesn’t exist
- The arguments are invalid for the tool
- Network or protocol errors occur
§v2 (2026-07-28) behavior
On a connection that opted into 2026-07-28, this method
auto-orchestrates Multi-Round-Trip Elicitation: an input_required
result is answered from the registered host handlers and the request is
resent, up to ClientBuilder::mrtr_round_limit rounds.
Two v2-only error outcomes are therefore possible, both programmatically distinguishable:
Error::is_input_required_unfulfilled— no handler could answer, so the full result is handed back viaError::input_required_result. It is an ERROR here (rather than a value) becauseCallToolResult::contentcarries#[serde(default)]and would otherwise deserialize such a result into a silently EMPTY success. UseSelf::call_tool_mrtrto receive it as a value instead.Error::is_mrtr_round_limit_exceeded— the server kept asking.
On v1 this method is byte-identical to every prior release.
Sourcepub async fn call_tool_with_task(
&self,
name: String,
arguments: Value,
) -> Result<ToolCallResponse>
pub async fn call_tool_with_task( &self, name: String, arguments: Value, ) -> Result<ToolCallResponse>
Call a tool with task augmentation.
Sends a tools/call request with the task field set, signaling to the
server that this client supports async task polling. The server may return
either a CreateTaskResult (async task created) or a CallToolResult
(sync result) depending on the tool’s taskSupport declaration.
Use call_tool instead if you don’t need task support.
§Returns
Ok(ToolCallResponse::Task(task))if the server created an async task. Poll withtasks_getuntil the task reaches a terminal status.Ok(ToolCallResponse::Result(result))if the server returned the result synchronously.
Sourcepub async fn call_tool_with_task_and_meta(
&self,
name: String,
arguments: Value,
meta: RequestMeta,
) -> Result<ToolCallResponse>
pub async fn call_tool_with_task_and_meta( &self, name: String, arguments: Value, meta: RequestMeta, ) -> Result<ToolCallResponse>
Call a tool with task augmentation AND custom request _meta.
Identical to call_tool_with_task except the
request carries the supplied RequestMeta
as _meta, so namespaced/guard state (attached via
RequestMeta::with_meta)
travels alongside the task augmentation in a single tools/call.
Passing an empty RequestMeta behaves like call_tool_with_task.
§Returns
Ok(ToolCallResponse::Task(task))if the server created an async task.Ok(ToolCallResponse::Result(result))if the server returned the result synchronously.
§Errors
Returns an error if the client is not initialized, the server does not support tools, or a network/protocol error occurs.
§Example
let meta = RequestMeta::new()
.with_meta("io.modelcontextprotocol/related-task", serde_json::json!({"taskId": "t-1"}));
let resp = client
.call_tool_with_task_and_meta("member".to_string(), serde_json::json!({}), meta)
.await?;Sourcepub async fn call_tool_with_meta(
&self,
name: String,
arguments: Value,
meta: RequestMeta,
) -> Result<CallToolResult>
pub async fn call_tool_with_meta( &self, name: String, arguments: Value, meta: RequestMeta, ) -> Result<CallToolResult>
Call a tool (non-task) with custom request _meta.
Identical to call_tool except the request carries the
supplied RequestMeta as _meta.
Namespaced/guard state travels via
RequestMeta::with_meta
and is visible to the server tool handler through extra.request_meta.
Passing an empty RequestMeta behaves like call_tool.
§Errors
Returns an error if the client is not initialized, the server does not support tools, the tool name does not exist, or a network/protocol error occurs.
§Example
let meta = RequestMeta::new().with_meta("x-pmcp-team-depth", serde_json::json!(1));
let result = client
.call_tool_with_meta("echo".to_string(), serde_json::json!({}), meta)
.await?;Sourcepub async fn tasks_get(&self, task_id: &str) -> Result<Task>
pub async fn tasks_get(&self, task_id: &str) -> Result<Task>
Get the current status of a task.
Polls the server for the task’s current state. Call this repeatedly
(respecting task.poll_interval) until the task reaches a terminal
status (Completed, Failed, or Cancelled).
§Era awareness (Phase 114, plan 19)
| Era | Wire shape | How it becomes a Task |
|---|---|---|
| v1 | NESTED {"task": {…, "ttl", "pollInterval"}} | unchanged: decode GetTaskResult, return .task |
| v2 | FLAT {taskId, status, createdAt, lastUpdatedAt, ttlMs, …} | decode TaskV2, then TaskV2::to_v1 |
The signature is unchanged on purpose: ttlMs lands on Task::ttl and
pollIntervalMs on Task::poll_interval, so an existing caller’s poll
logic keeps working verbatim against a v2 server.
The v2 arm decodes only the flat BASE task, never the status-discriminated
DetailedTask. That is deliberate: a backend that cannot supply a
terminal task’s result degrades to the bare flat Task, and a strict
decode here would turn “I could not read the detail” into “I could not
read the task at all”. Use Self::tasks_get_detailed when you want the
inlined detail and want a missing one to be an error.
§Errors
Returns an error if:
- The server doesn’t support tasks
- The task ID doesn’t exist or belongs to another owner. On v2 that is
ONE
-32602answer for absent / wrong-owner / EXPIRED alike — the three are deliberately indistinguishable (no existence oracle), so a client must not try to tell them apart.
Sourcepub async fn tasks_get_detailed(&self, task_id: &str) -> Result<DetailedTaskV2>
pub async fn tasks_get_detailed(&self, task_id: &str) -> Result<DetailedTaskV2>
Get a task’s status TOGETHER WITH its status-conditional detail — v2 only (Phase 114, plan 19).
The additive sibling of Self::tasks_get. On 2026-07-28 a
tasks/get result is a flat DetailedTask: one variant per status,
each carrying exactly the key its schema variant marks required —
result on completed, error on failed, inputRequests on
input_required, nothing extra on working / cancelled.
This is what removes the second round trip v1 needed: tasks/result does
not exist on v2 because the terminal payload is already here.
§Strict by design
DetailedTaskV2::from_wire_value
is STATUS-DIRECTED: it reads status first and then REQUIRES that
status’s key. A completed task with no result is an error here rather
than a best-effort decode into a variant that happens to fit — the same
discipline the server-side projection applies when it emits.
§Errors
Error::InvalidStatewhen the connection did not opt into2026-07-28— NO request is sent.- The transport / JSON-RPC errors
Self::tasks_getreturns. Error::ProtocolcarryingErrorCode::PARSE_ERROR(built byError::parse) when the payload’s status and detail disagree.
Sourcepub async fn tasks_result(&self, task_id: &str) -> Result<CallToolResult>
pub async fn tasks_result(&self, task_id: &str) -> Result<CallToolResult>
Get the final result of a completed task — v1 only.
For a task-augmented tools/call, this returns the CallToolResult
that the tool would have returned synchronously. Only valid when
the task status is Completed.
§RETIRED on 2026-07-28 (Phase 114, TASK-03 / D-15)
tasks/result is absent from the tasks extension: the v2 tasks/get
INLINES the terminal payload, so a second round trip has nothing left to
do. A v2 server answers this method -32601. Calling it on a v2
connection therefore fails LOCALLY — no bytes leave the process — with an
Error::retired_on_v2 naming Self::tasks_get_detailed’s method as
the replacement. A clear local error beats a round trip to an opaque
method-not-found.
Sourcepub async fn wait_for_task(
&self,
task_id: &str,
opts: WaitForTaskOptions,
) -> Result<CallToolResult>
pub async fn wait_for_task( &self, task_id: &str, opts: WaitForTaskOptions, ) -> Result<CallToolResult>
Poll a task to terminal status, then return its final result.
Drives tasks/get in a loop until TaskStatus::is_terminal, honoring
the polling interval (caller override, else the task-reported
pollInterval, else a built-in default) and an optional overall timeout.
§Where the terminal result comes from is ERA-SPLIT (Phase 114, plan 19)
| Era | Terminal step |
|---|---|
| v1 | a second round trip: Client::tasks_result, exactly as before |
| v2 | ZERO extra round trips — the result is INLINE in the tasks/get payload the loop already fetched |
tasks/result does not exist on 2026-07-28 (a v2 server answers
-32601), so the v2 arm must not call it — and does not need to, because
a v2 tasks/get on a completed task carries result and on a failed
task carries error. Nothing else in the loop is era-aware: the
classifier, the floor, the budget clamp and the clock are shared.
On v2 a task that reaches failed surfaces its inlined JSON-RPC error
as a typed client error rather than an empty success, and a cancelled
task is an error too — neither has a result to return.
§Wasm safety
The delay between polls uses crate::runtime::sleep (not
tokio::time::sleep directly) and the timeout is measured with
web_time::Instant (not std::time::Instant, which panics on
wasm32), so this compiles and runs in the browser.
§Hot-loop protection
The effective interval is clamped to a small floor (50 ms), so a zero or
absent pollInterval cannot turn the loop into a busy spin.
§Errors
- Propagates
tasks/get/tasks/resulttransport and protocol errors. - Returns
Error::Timeoutwhenopts.max_poll_duration_secselapses before the task reaches a terminal status. Each sleep is clamped to the remaining budget, so a large (possibly server-reported) poll interval cannot overshoot the caller’s budget by more than roughly the 50 ms clamp floor. - Returns
Error::Validationwhen the task entersTaskStatus::InputRequired: that state is NOT terminal and needs client-side action (elicitation) this poller cannot provide, so polling on would hang forever under the default (unbounded) options. Handle the required input, then resume polling — or useClient::wait_for_task_with_inputs, which is exactly this poller with a responder attached and IS the answer to that message.
§Durable and replay consumers
Do not wrap wait_for_task inside a durable / replay workflow step.
It sleeps, loops, and owns the whole polling lifecycle, which is
non-deterministic under replay (each re-execution would re-sleep and
re-poll). A durable consumer should instead call
Task::poll_decision plus
resolve_poll_interval once per tick inside its own memoized step and
persist the decision
between ticks — those are pure, replay-deterministic functions of the
polled task, unlike this blocking poller (D-11 / D-16).
See the pmcp-book “Durable and replay consumers” section
(heading ## Durable and replay consumers in
pmcp-book/src/ch12-7-tasks.md) for the full per-poll pattern:
https://paiml.github.io/rust-mcp-sdk/ch12-7-tasks.html#durable-and-replay-consumers.
(This is a deliberate plain-text/URL reference, not a rustdoc intra-doc
link, so it never fails cargo doc even before that page ships.)
§Example
use pmcp::client::WaitForTaskOptions;
// `result` came from a task-augmented tools/call.
if let Some(meta) = result.related_task() {
let final_result = client
.wait_for_related_task(&meta, WaitForTaskOptions::default())
.await?;
}Sourcepub async fn wait_for_task_with_inputs<F, Fut>(
&self,
task_id: &str,
opts: WaitForTaskOptions,
responder: F,
) -> Result<CallToolResult>
pub async fn wait_for_task_with_inputs<F, Fut>( &self, task_id: &str, opts: WaitForTaskOptions, responder: F, ) -> Result<CallToolResult>
Client::wait_for_task WITH a responder for input_required — v2 only
(Phase 114, TASK-02).
The same poller, with one behaviour added: when the task pauses for
input, responder is handed the task’s inputRequests (read from the
tasks/get payload the loop already fetched — no extra round trip), its
answers are delivered with Client::tasks_update, and polling resumes.
Everything else is Client::wait_for_task VERBATIM — the same
poll_decision() classifier matched with no wildcard arm, the same
MIN_POLL_MS floor, the same remaining-budget clamp, the same
web_time::Instant clock — because it is literally the same function
with a responder passed in.
§v2 only
tasks/update does not exist on 2026-07-28’s predecessor, so a v1 call
fails LOCALLY with no bytes on the wire. Use
Client::wait_for_task plus your own elicitation handling there.
§The input rounds are BOUNDED
A server that keeps re-requesting input cannot spin a client forever: the
number of input_required rounds is capped by the SAME configured bound
the MRTR gather->resend loop uses
(ClientBuilder::mrtr_round_limit, defaulting to
DEFAULT_MRTR_ROUND_LIMIT = 8). It is deliberately the same knob and not
a new constant: both bound “how many times will I answer this server’s
questions before I conclude it is not making progress”, and two
independently-tuned answers to one question is how they drift apart.
Exceeding it returns Error::mrtr_round_limit_exceeded.
§A task is NOT a higher-trust channel
The requests handed to responder are ordinary elicitation / sampling /
roots requests that happen to arrive through a task. Apply the SAME
consent and policy gates you would for a direct server-initiated request;
this poller passes values through and executes nothing server-supplied.
§Errors
Everything Client::wait_for_task returns, plus whatever responder
itself returns (propagated unchanged), plus the round-bound error above.
§Example
use pmcp::client::WaitForTaskOptions;
use pmcp::types::mrtr::InputResponses;
let result = client
.wait_for_task_with_inputs("task-1", WaitForTaskOptions::default(), |requests| async move {
let mut answers = InputResponses::new();
for (key, request) in &requests {
answers.insert(key.clone(), answer_one(request).await?);
}
Ok(answers)
})
.await?;Poll a task referenced by TaskMetadata to terminal, then return its
tasks/result — the zero-glue counterpart of Client::wait_for_task.
Any fields left unset in opts are filled from meta
(WaitForTaskOptions::or_from_metadata) so a caller who holds a
CallToolResult::related_task
result composes without hand-copying poll fields.
§Errors
Same as Client::wait_for_task.
Sourcepub async fn tasks_list(
&self,
cursor: Option<String>,
) -> Result<ListTasksResult>
pub async fn tasks_list( &self, cursor: Option<String>, ) -> Result<ListTasksResult>
List tasks owned by the current client — v1 only.
§RETIRED on 2026-07-28 (Phase 114, TASK-03 / D-15)
tasks/list is absent from the tasks extension, removed as a SECURITY
improvement: with no enumeration primitive a server cannot inadvertently
leak the existence of one caller’s tasks to another. There is no
replacement method — a v2 client keeps the ids it was handed. Calling
this on a v2 connection fails LOCALLY with an Error::retired_on_v2,
with no bytes on the wire.
Sourcepub async fn tasks_cancel(&self, task_id: &str) -> Result<Task>
pub async fn tasks_cancel(&self, task_id: &str) -> Result<Task>
Cancel a running task and report the task as the server now sees it.
§Era awareness (Phase 114, plan 19)
v1 answers a cancel with the NESTED {"task": {…}} envelope and this
method returns that task, exactly as it always has.
v2’s CancelTaskResult is Result — an EMPTY acknowledgement with no
task body at all (inventory row 20), so today’s CancelTaskResult decode
fails outright against it. Because this method’s return type is Task and
cannot change without a MAJOR semver bump, the v2 arm acknowledges the
cancel through Self::tasks_cancel_ack and then performs ONE follow-up
Self::tasks_get. It does NOT synthesise a Task: fabricating
status: cancelled would be inventing status information the server
deliberately did not send.
§Cancellation is cooperative and eventually consistent
That is the SEMANTICS of the empty ack, not a limitation of this client.
The returned task MAY still be working, and MAY later settle on a
terminal status other than cancelled. Callers that only need the
acknowledgement — and do not want the extra round trip — should call
Self::tasks_cancel_ack directly.
Sourcepub async fn tasks_cancel_ack(&self, task_id: &str) -> Result<()>
pub async fn tasks_cancel_ack(&self, task_id: &str) -> Result<()>
Request cancellation and read only the ACKNOWLEDGEMENT (Phase 114).
The zero-invention primitive Self::tasks_cancel is built on: it
accepts ANY successful result — including v2’s bare {} — and returns
(). One round trip, and nothing is claimed about the task’s status.
Works on BOTH eras. On v1 the response body carries a Task which this
method DISCARDS; call Self::tasks_cancel when you want it.
Cancellation is cooperative and eventually consistent: a successful acknowledgement means the request was accepted, NOT that the task has stopped.
Sourcepub async fn tasks_update(
&self,
task_id: &str,
responses: InputResponses,
) -> Result<()>
pub async fn tasks_update( &self, task_id: &str, responses: InputResponses, ) -> Result<()>
Deliver responses to a paused task’s outstanding inputRequests — v2
only (Phase 114, TASK-02).
tasks/update is how an input_required task is un-paused: each key of
responses MUST correspond to a currently-outstanding inputRequests
key from Self::tasks_get_detailed. The acknowledgement is EMPTY, so
this returns ().
§It is sent UNTYPED, on purpose
There is no ClientRequest::TasksUpdate variant and there must not be
one: ClientRequest is public and not #[non_exhaustive], so adding a
variant is a MAJOR semver break. This goes out through the same raw path
Self::server_discover uses.
§A task is NOT a higher-trust channel
The spec is explicit that input requests delivered through a task carry
exactly the trust of the elicitation / sampling they wrap. Whatever
produces responses must apply the SAME consent and policy gates it
would for a direct elicitation/create; this method transmits the values
and executes nothing the server supplied.
§Errors
Error::InvalidStateon a v1 connection —tasks/updatedoes not exist there, and NO bytes are sent.Error::UnsupportedCapability(built byError::capability) when the tasks extension was not negotiated — again with no bytes sent.- The server’s own JSON-RPC error otherwise (e.g. an unknown or already-answered input key).
Sourcepub async fn call_tool_and_poll(
&self,
name: String,
arguments: Value,
max_polls: usize,
) -> Result<CallToolResult>
Available on non-WebAssembly only.
pub async fn call_tool_and_poll( &self, name: String, arguments: Value, max_polls: usize, ) -> Result<CallToolResult>
Call a tool and automatically poll until the task completes.
This is a high-level convenience method that encapsulates the full task lifecycle:
- Calls the tool with task augmentation
- If the server returns a task, polls
tasks/getuntil terminal status - Returns the final
CallToolResult
If the server returns a sync result (no task), returns it immediately.
§Arguments
name- Tool namearguments- Tool argumentsmax_polls- Maximum number of poll attempts before giving up (0 = unlimited)
Sourcepub async fn list_prompts(
&self,
cursor: Option<String>,
) -> Result<ListPromptsResult>
pub async fn list_prompts( &self, cursor: Option<String>, ) -> Result<ListPromptsResult>
List available prompts.
Retrieves information about all prompts available on the server, including their names, descriptions, and required arguments.
§Arguments
cursor- Optional cursor for pagination of large prompt lists
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities};
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(ClientCapabilities::default()).await?;
// List all prompts
let prompts = client.list_prompts(None).await?;
for prompt in prompts.prompts {
println!("Prompt: {} - {}",
prompt.name,
prompt.description.unwrap_or_else(|| "No description".to_string()));
// Show required arguments
if let Some(args) = prompt.arguments {
for arg in args {
println!(" - {}: {} (required: {})",
arg.name,
arg.description.unwrap_or_else(|| "No description".to_string()),
arg.required);
}
}
}§Errors
Returns an error if:
- The client is not initialized
- The server doesn’t support prompts
- Network or protocol errors occur
Sourcepub async fn get_prompt(
&self,
name: String,
arguments: HashMap<String, String>,
) -> Result<GetPromptResult>
pub async fn get_prompt( &self, name: String, arguments: HashMap<String, String>, ) -> Result<GetPromptResult>
Get a prompt.
Retrieves a specific prompt from the server with the provided arguments. The prompt is processed by the server and returned with filled-in content.
§Arguments
name- The name of the prompt to retrievearguments- Key-value pairs for prompt arguments
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities};
use std::collections::HashMap;
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(ClientCapabilities::default()).await?;
// Get a prompt with arguments
let mut args = HashMap::new();
args.insert("language".to_string(), "Rust".to_string());
args.insert("topic".to_string(), "async programming".to_string());
let prompt_result = client.get_prompt(
"code_review".to_string(),
args
).await?;
println!("Prompt description: {}",
prompt_result.description.unwrap_or_else(|| "No description".to_string()));
// Process the prompt messages
for message in prompt_result.messages {
println!("Role: {}", message.role);
match &message.content {
pmcp::Content::Text { text } => {
println!("Content: {}", text);
}
_ => println!("Non-text content"),
}
}§Errors
Returns an error if:
- The client is not initialized
- The server doesn’t support prompts
- The prompt name doesn’t exist
- Required arguments are missing
- Network or protocol errors occur
§v2 (2026-07-28) behavior
Auto-orchestrates MRTR exactly as Self::call_tool documents,
including the Error::is_input_required_unfulfilled and
Error::is_mrtr_round_limit_exceeded outcomes. See
Self::get_prompt_mrtr to receive an unfulfilled input_required as
a value. v1 is byte-identical to every prior release.
Sourcepub async fn call_tool_typed<A: Serialize + ?Sized + Sync>(
&self,
name: impl Into<String> + Send,
args: &A,
) -> Result<CallToolResult>
pub async fn call_tool_typed<A: Serialize + ?Sized + Sync>( &self, name: impl Into<String> + Send, args: &A, ) -> Result<CallToolResult>
Call a tool with typed, serializable arguments.
Serializes args via serde_json::to_value and delegates to
Self::call_tool. Serialization failures are mapped to
Error::validation with the underlying serde error message.
§Examples
use serde::Serialize;
#[derive(Serialize)]
struct Search { query: String, limit: u32 }
let _ = client.call_tool_typed(
"search",
&Search { query: "rust mcp".into(), limit: 10 },
).await?;Sourcepub async fn call_tool_typed_with_task<A: Serialize + ?Sized + Sync>(
&self,
name: impl Into<String> + Send,
args: &A,
) -> Result<ToolCallResponse>
pub async fn call_tool_typed_with_task<A: Serialize + ?Sized + Sync>( &self, name: impl Into<String> + Send, args: &A, ) -> Result<ToolCallResponse>
Typed sibling of Self::call_tool_with_task.
Delegates to the two-argument Self::call_tool_with_task; there is no
TaskMetadata parameter on the live client API, so none is exposed here.
§Examples
use serde::Serialize;
#[derive(Serialize)]
struct Args { file: String }
let _ = client.call_tool_typed_with_task("scan", &Args { file: "a.rs".into() }).await?;Sourcepub async fn call_tool_typed_and_poll<A: Serialize + ?Sized + Sync>(
&self,
name: impl Into<String> + Send,
args: &A,
max_polls: usize,
) -> Result<CallToolResult>
Available on non-WebAssembly only.
pub async fn call_tool_typed_and_poll<A: Serialize + ?Sized + Sync>( &self, name: impl Into<String> + Send, args: &A, max_polls: usize, ) -> Result<CallToolResult>
Typed sibling of Self::call_tool_and_poll.
Delegates to the three-argument Self::call_tool_and_poll
(name, arguments, max_polls: usize). There is no poll_interval or
TaskMetadata parameter on the live client API — the server-supplied
poll_interval is honoured internally by call_tool_and_poll.
max_polls = 0 means unlimited polls, matching the sibling’s semantics.
§Examples
use serde::Serialize;
#[derive(Serialize)]
struct Args { job: String }
let _ = client.call_tool_typed_and_poll(
"build",
&Args { job: "nightly".into() },
30, // max_polls
).await?;Sourcepub async fn get_prompt_typed<A: Serialize + ?Sized + Sync>(
&self,
name: impl Into<String> + Send,
args: &A,
) -> Result<GetPromptResult>
pub async fn get_prompt_typed<A: Serialize + ?Sized + Sync>( &self, name: impl Into<String> + Send, args: &A, ) -> Result<GetPromptResult>
Get a prompt with typed, serializable arguments.
Serializes args to a JSON object, then coerces each leaf to a String
for the wire-level HashMap<String, String> arguments:
nullentries are omittedstringentries pass through unchanged (no JSON-quoting)numberandboolentries useDisplay(e.g.42,true)arrayandobjectentries are re-serialized viaserde_json::to_string
Non-object top-level serializations are rejected with
Error::validation.
§Examples
use serde::Serialize;
#[derive(Serialize)]
struct SummaryArgs { topic: String, length: u32 }
let _ = client.get_prompt_typed(
"summarize",
&SummaryArgs { topic: "rust async".into(), length: 200 },
).await?;Sourcepub async fn list_resources(
&self,
cursor: Option<String>,
) -> Result<ListResourcesResult>
pub async fn list_resources( &self, cursor: Option<String>, ) -> Result<ListResourcesResult>
List available resources.
Retrieves information about all resources available on the server, including their names, descriptions, URIs, and MIME types.
§Arguments
cursor- Optional cursor for pagination of large resource lists
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities};
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(ClientCapabilities::default()).await?;
// List all resources
let resources = client.list_resources(None).await?;
for resource in resources.resources {
println!("Resource: {} ({})", resource.name, resource.uri);
if let Some(description) = resource.description {
println!(" Description: {}", description);
}
if let Some(mime_type) = resource.mime_type {
println!(" MIME Type: {}", mime_type);
}
}§Errors
Returns an error if:
- The client is not initialized
- The server doesn’t support resources
- Network or protocol errors occur
Sourcepub async fn list_resource_templates(
&self,
cursor: Option<String>,
) -> Result<ListResourceTemplatesResult>
pub async fn list_resource_templates( &self, cursor: Option<String>, ) -> Result<ListResourceTemplatesResult>
List resource templates.
Retrieves information about all resource templates available on the server. Resource templates define patterns for dynamically generated resources.
§Arguments
cursor- Optional cursor for pagination of large template lists
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities};
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(ClientCapabilities::default()).await?;
// List all resource templates
let templates = client.list_resource_templates(None).await?;
for template in templates.resource_templates {
println!("Template: {} ({})", template.name, template.uri_template);
if let Some(description) = template.description {
println!(" Description: {}", description);
}
}§Errors
Returns an error if:
- The client is not initialized
- The server doesn’t support resource templates
- Network or protocol errors occur
Sourcepub async fn list_all_tools(&self) -> Result<Vec<ToolInfo>>
pub async fn list_all_tools(&self) -> Result<Vec<ToolInfo>>
List all tools across all pages, auto-paginating on next_cursor.
Loops calling Self::list_tools, terminating when the server returns
next_cursor: None. Safety cap: if the loop runs more than
self.options.max_iterations iterations (default 100), returns
Error::Validation instead of continuing or silently truncating.
Empty-string cursors (Some("")) do NOT terminate the loop — only
None does. This matches the MCP spec, which treats the cursor as an
opaque server token and does not ascribe meaning to the empty string.
§Memory
This helper accumulates all pages in memory before returning. For
very large servers, prefer the paginated single-page
Self::list_tools and stream the output yourself — this helper is a
convenience API and will amplify memory usage proportional to the
total tool count.
§Errors
- Any error surfaced by
Self::list_toolspropagates unchanged. - Cap exceeded →
Error::Validation("list_all_tools exceeded max_iterations cap of N pages").
§Examples
let tools = client.list_all_tools().await?;
println!("discovered {} tools", tools.len());Sourcepub async fn list_all_prompts(&self) -> Result<Vec<PromptInfo>>
pub async fn list_all_prompts(&self) -> Result<Vec<PromptInfo>>
List all prompts across all pages, auto-paginating on next_cursor.
Semantics identical to Self::list_all_tools: bounded by
self.options.max_iterations, terminates only on next_cursor: None,
returns Error::Validation on cap exceeded.
§Memory
Accumulates all pages in memory; prefer Self::list_prompts for
very large servers.
§Errors
- Any error surfaced by
Self::list_promptspropagates unchanged. - Cap exceeded →
Error::Validation("list_all_prompts exceeded max_iterations cap of N pages").
§Examples
let prompts = client.list_all_prompts().await?;
println!("discovered {} prompts", prompts.len());Sourcepub async fn list_all_resources(&self) -> Result<Vec<ResourceInfo>>
pub async fn list_all_resources(&self) -> Result<Vec<ResourceInfo>>
List all resources across all pages, auto-paginating on next_cursor.
Semantics identical to Self::list_all_tools: bounded by
self.options.max_iterations, terminates only on next_cursor: None,
returns Error::Validation on cap exceeded.
§Memory
Accumulates all pages in memory; prefer Self::list_resources for
very large servers.
§Errors
- Any error surfaced by
Self::list_resourcespropagates unchanged. - Cap exceeded →
Error::Validation("list_all_resources exceeded max_iterations cap of N pages").
§Examples
let resources = client.list_all_resources().await?;
println!("discovered {} resources", resources.len());Sourcepub async fn list_all_resource_templates(&self) -> Result<Vec<ResourceTemplate>>
pub async fn list_all_resource_templates(&self) -> Result<Vec<ResourceTemplate>>
List all resource templates across all pages, auto-paginating on
next_cursor.
Uses the distinct resources/templates/list capability path (all
other list_all_* helpers hit their own methods). Semantics otherwise
identical to Self::list_all_tools: bounded by
self.options.max_iterations, terminates only on next_cursor: None,
returns Error::Validation on cap exceeded.
§Memory
Accumulates all pages in memory; prefer
Self::list_resource_templates for very large servers.
§Errors
- Any error surfaced by
Self::list_resource_templatespropagates unchanged. - Cap exceeded →
Error::Validation("list_all_resource_templates exceeded max_iterations cap of N pages").
§Examples
let templates = client.list_all_resource_templates().await?;
println!("discovered {} templates", templates.len());Sourcepub async fn read_resource(&self, uri: String) -> Result<ReadResourceResult>
pub async fn read_resource(&self, uri: String) -> Result<ReadResourceResult>
Read a resource.
Retrieves the content of a specific resource from the server by its URI. Resources can contain text, binary data, or structured content.
§Arguments
uri- The URI of the resource to read
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities};
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(ClientCapabilities::default()).await?;
// Read a text resource
let resource = client.read_resource("file://readme.txt".to_string()).await?;
for content in resource.contents {
match content {
pmcp::Content::Text { text } => {
println!("Text content: {}", text);
}
pmcp::Content::Resource { uri, .. } => {
println!("Resource reference: {}", uri);
}
_ => println!("Other content type"),
}
}§Errors
Returns an error if:
- The client is not initialized
- The server doesn’t support resources
- The resource URI doesn’t exist
- Access to the resource is denied
- Network or protocol errors occur
§v2 (2026-07-28) behavior
Auto-orchestrates MRTR exactly as Self::call_tool documents. This
method is where the missing return type BIT the hardest:
ReadResourceResult.contents has no serde default, so an
input_required result cannot be deserialized into it at all and would
surface as an opaque parse error. It now surfaces as an
Error::is_input_required_unfulfilled carrying the full result, or —
via Self::read_resource_mrtr — as a value. v1 is byte-identical to
every prior release.
Sourcepub async fn subscribe_resource(&self, uri: String) -> Result<()>
pub async fn subscribe_resource(&self, uri: String) -> Result<()>
Subscribe to resource updates.
Subscribes to receive notifications when a resource changes. The server will send notifications when the subscribed resource is modified.
§Arguments
uri- The URI of the resource to subscribe to
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities};
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(ClientCapabilities::default()).await?;
// Subscribe to a configuration file
client.subscribe_resource("file://config/settings.json".to_string()).await?;
// Now the client will receive notifications when settings.json changes
// Handle notifications in your event loop§v2 behavior (2026-07-28)
resources/subscribe was REMOVED from the 2026-07-28 schema and replaced
by the subscriptions/listen stream. On a connection that opted into
that version this method sends NOTHING and returns
Error::retired_on_v2 immediately — a
v2 server answers the retired RPC with 404 + -32601, so the round
trip can only fail. Use
Client::subscriptions_listen with
SubscriptionFilter::resource_subscriptions
instead. The v1 path below is unchanged.
§Errors
Returns an error if:
- The connection speaks 2026-07-28 (see v2 behavior above)
- The client is not initialized
- The server doesn’t support resource subscriptions
- The resource URI doesn’t exist
- Network or protocol errors occur
Sourcepub async fn unsubscribe_resource(&self, uri: String) -> Result<()>
pub async fn unsubscribe_resource(&self, uri: String) -> Result<()>
Unsubscribe from resource updates.
Unsubscribes from notifications for a previously subscribed resource. After unsubscribing, the client will no longer receive change notifications.
§Arguments
uri- The URI of the resource to unsubscribe from
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities};
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(ClientCapabilities::default()).await?;
// Subscribe to a resource
client.subscribe_resource("file://config/settings.json".to_string()).await?;
// Later, unsubscribe when no longer needed
client.unsubscribe_resource("file://config/settings.json".to_string()).await?;§v2 behavior (2026-07-28)
resources/unsubscribe was REMOVED from the 2026-07-28 schema along with
resources/subscribe. On a connection that opted into that version this
method sends NOTHING and returns
Error::retired_on_v2 immediately.
Unsubscribing on v2 means DROPPING the
SubscriptionStream
returned by Client::subscriptions_listen,
which closes the connection and releases the server’s registry slot. The
v1 path below is unchanged.
§Errors
Returns an error if:
- The connection speaks 2026-07-28 (see v2 behavior above)
- The client is not initialized
- The server doesn’t support resource subscriptions
- The resource URI was not previously subscribed to
- Network or protocol errors occur
Sourcepub async fn complete(&self, params: CompleteRequest) -> Result<CompleteResult>
pub async fn complete(&self, params: CompleteRequest) -> Result<CompleteResult>
Request completion from the server.
Requests auto-completion suggestions from the server for a given context. This is useful for implementing IDE-like features with contextual suggestions.
§Arguments
params- The completion request parameters
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities, CompleteRequest};
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(ClientCapabilities::default()).await?;
// Request completion for partial text
let completion_request = CompleteRequest {
r#ref: pmcp::CompletionReference::Resource {
uri: "file://code.rs".to_string(),
},
argument: pmcp::CompletionArgument {
name: "function_name".to_string(),
value: "calc_".to_string(),
},
};
let completions = client.complete(completion_request).await?;
for completion in completions.completion.values {
println!("Suggestion: {}", completion);
}§Errors
Returns an error if:
- The client is not initialized
- The server doesn’t support completions
- The completion context is invalid
- Network or protocol errors occur
Sourcepub async fn create_message(
&self,
params: CreateMessageParams,
) -> Result<CreateMessageResult>
pub async fn create_message( &self, params: CreateMessageParams, ) -> Result<CreateMessageResult>
Create a message using sampling (for LLM providers).
Requests the server to generate a message using its language model capabilities. This is typically used by servers that provide LLM functionality.
§The “LLM-server pattern” (INVERSE of spec host sampling)
This method is the LLM-server pattern: the client asks a server
whose pmcp::SamplingHandler runs the LLM. It
is the inverse of MCP spec host sampling, where a server requests
sampling and the client answers via a
pmcp::client::host::HostSamplingHandler.
Both directions are supported and neither is deprecated — pick the one
that matches who owns the model. This path is unchanged by the client
host surface.
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities, CreateMessageParams, SamplingMessage};
let mut capabilities = ClientCapabilities::default();
capabilities.sampling = Some(Default::default());
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(capabilities).await?;
// Create a message with the LLM
let msg = SamplingMessage::new(
pmcp::types::Role::User,
pmcp::types::SamplingMessageContent::Text {
text: "Explain how to implement a binary search tree".to_string(),
meta: None,
},
);
let prefs = pmcp::types::ModelPreferences::new()
.with_hints(vec![pmcp::types::ModelHint::new("gpt-4")])
.with_cost_priority(0.5)
.with_speed_priority(0.3)
.with_intelligence_priority(0.2);
let mut request = CreateMessageParams::new(vec![msg])
.with_model_preferences(prefs)
.with_system_prompt("You are a helpful programming assistant")
.with_temperature(0.7)
.with_max_tokens(1000);
request.include_context = pmcp::types::IncludeContext::ThisServer;
let result = client.create_message(request).await?;
println!("Model: {}", result.model);
println!("Response: {:?}", result.content);§Errors
Returns an error if:
- The client is not initialized
- The server doesn’t support sampling
- The request parameters are invalid
- Network or protocol errors occur
Sourcepub async fn send_roots_list_changed(&self) -> Result<()>
pub async fn send_roots_list_changed(&self) -> Result<()>
Send roots list changed notification.
Notifies the server that the client’s root list has changed. This is typically sent when the workspace or project roots are modified.
§Examples
use pmcp::{ClientBuilder, StdioTransport, ClientCapabilities};
use pmcp::types::roots::{ListRootsResult, Root};
// Roots advertisement is registry-derived (HOST-05): the client must
// register a roots provider for the `roots` capability to reach the
// wire. Build via `ClientBuilder` and register one with `on_roots`.
let transport = StdioTransport::new();
let mut client = ClientBuilder::new(transport)
.on_roots(|| async {
Ok(ListRootsResult {
roots: vec![Root {
uri: "file:///workspace".to_string(),
name: Some("workspace".to_string()),
}],
})
})
.build();
// With a provider registered, a caller-set `list_changed` is preserved,
// so the client advertises that it emits roots-list-changed notices.
let mut capabilities = ClientCapabilities::default();
capabilities.roots = Some(pmcp::RootsCapabilities { list_changed: true });
client.initialize(capabilities).await?;
// Notify server when project roots change
client.send_roots_list_changed().await?;§Errors
Returns an error if:
- The client is not initialized
- The client doesn’t support roots list changed notifications
- Network or protocol errors occur
Sourcepub fn authenticate(&self, auth_info: &AuthInfo) -> Result<()>
pub fn authenticate(&self, auth_info: &AuthInfo) -> Result<()>
Authenticate with the server.
Performs authentication using the provided authentication information. This should be called after initialization if the server requires authentication.
§Examples
use pmcp::{Client, StdioTransport, AuthInfo, AuthScheme};
let transport = StdioTransport::new();
let mut client = Client::new(transport);
// Initialize first
client.initialize(pmcp::ClientCapabilities::default()).await?;
// Authenticate with bearer token
let auth = AuthInfo {
scheme: AuthScheme::Bearer,
token: Some("your-api-token".to_string()),
oauth: None,
params: Default::default(),
};
client.authenticate(&auth)?;§Errors
Returns an error if:
- The client is not initialized
- Authentication fails
- The server doesn’t support authentication
Sourcepub async fn cancel_request(&self, request_id: &RequestId) -> Result<()>
pub async fn cancel_request(&self, request_id: &RequestId) -> Result<()>
Cancel a request.
Sends a cancellation notification for an active request. This allows graceful termination of long-running operations.
§Arguments
request_id- The ID of the request to cancel
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities, RequestId};
use serde_json::json;
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(ClientCapabilities::default()).await?;
// Start a long-running operation
let request_id = RequestId::String("long-operation-123".to_string());
// Later, cancel the request if needed
client.cancel_request(&request_id).await?;§Errors
Returns an error if:
- Network or protocol errors occur while sending the cancellation
Sourcepub async fn send_progress(&self, progress: ProgressNotification) -> Result<()>
pub async fn send_progress(&self, progress: ProgressNotification) -> Result<()>
Send a progress notification.
Sends a progress update for a long-running operation. This allows the server or client to track operation progress.
§Arguments
progress- The progress notification to send
§Examples
use pmcp::{Client, StdioTransport, ClientCapabilities, ProgressNotification, RequestId};
let transport = StdioTransport::new();
let mut client = Client::new(transport);
client.initialize(ClientCapabilities::default()).await?;
// Send progress update for a file processing operation
let progress = ProgressNotification::new(
pmcp::ProgressToken::String("file-processing".to_string()),
75.0,
Some("Processing files...".to_string()),
);
client.send_progress(progress).await?;§Errors
Returns an error if:
- Network or protocol errors occur while sending the notification
Sourcepub async fn call_tool_mrtr(
&self,
name: String,
arguments: Value,
) -> Result<MrtrOutcome<CallToolResult>>
pub async fn call_tool_mrtr( &self, name: String, arguments: Value, ) -> Result<MrtrOutcome<CallToolResult>>
Call a tool, auto-orchestrating MRTR, and observe an unfulfilled
input_required result instead of losing it (Phase 113, CLNT-02).
The additive sibling of Self::call_tool. Use it whenever a
MrtrOutcome::InputRequired is a normal outcome for your application
rather than an error — for example when your client wants to surface the
server’s inputRequests in its own UI instead of registering a
ClientBuilder::on_elicitation handler.
On a v1 connection there is no MRTR, so this simply delegates to
Self::call_tool and always returns MrtrOutcome::Complete.
§Errors
As Self::call_tool, plus Error::mrtr_round_limit_exceeded when
the server keeps asking for input past
ClientBuilder::mrtr_round_limit.
Sourcepub async fn get_prompt_mrtr(
&self,
name: String,
arguments: HashMap<String, String>,
) -> Result<MrtrOutcome<GetPromptResult>>
pub async fn get_prompt_mrtr( &self, name: String, arguments: HashMap<String, String>, ) -> Result<MrtrOutcome<GetPromptResult>>
Get a prompt, auto-orchestrating MRTR. See Self::call_tool_mrtr.
§Errors
Sourcepub async fn read_resource_mrtr(
&self,
uri: String,
) -> Result<MrtrOutcome<ReadResourceResult>>
pub async fn read_resource_mrtr( &self, uri: String, ) -> Result<MrtrOutcome<ReadResourceResult>>
Read a resource, auto-orchestrating MRTR. See Self::call_tool_mrtr.
This one matters even more than the others: ReadResourceResult.contents
has no serde default, so an input_required result cannot be
deserialized into it at all — without this method (or
Error::input_required_unfulfilled) the outcome would surface as an
opaque parse error.
§Errors
As Self::read_resource, plus Error::mrtr_round_limit_exceeded.
Source§impl<T> Client<T>where
T: Transport + EventStreamTransport,
impl<T> Client<T>where
T: Transport + EventStreamTransport,
Sourcepub async fn subscriptions_listen(
&self,
notifications: SubscriptionFilter,
) -> Result<SubscriptionStream>
Available on crate feature streamable-http and non-WebAssembly only.
pub async fn subscriptions_listen( &self, notifications: SubscriptionFilter, ) -> Result<SubscriptionStream>
streamable-http and non-WebAssembly only.Open a v2 subscriptions/listen stream and receive change notifications
(HTTP-04).
The 2026-07-28 schema REMOVED resources/subscribe and
resources/unsubscribe and replaced both with this single long-lived
stream. The returned SubscriptionStream
has already consumed the server’s mandatory acknowledgement — read the
AGREED filter from
acknowledged()
before polling — and then yields one item per delivered notification.
Dropping the returned stream closes the underlying HTTP response, which
is what releases the server’s registry slot; there is no close() to
forget.
§Every call mints a FRESH subscription id
The subscription id IS the JSON-RPC request id of this call, and this
method mints a fresh Uuid::new_v4() for it every time. It is never
derived from the transport, from a counter, or from a previous stream.
That is a CONTRACT, not an implementation detail, and it is what makes a
pmcp client structurally immune to the reconnect collision: the server
refuses a second LIVE registration under a (principal, subscriptionId)
pair it already holds, and it CANNOT tell an ungracefully disconnected
peer from a live one (the receiver and the registry guard live in one
stream-state tuple, so the entry survives until Hyper drops the response
body — at which moment RAII reclaims it anyway). A client that reused its
id when reconnecting would therefore be refused for the remainder of the
server’s keep-alive window. Because every call here mints a fresh id, a
reconnect after ANY disconnect — graceful or not — can never collide with
the incumbent the server may still consider live.
The guard against a future refactor making ids sticky is the live
tripwire successive_listen_calls_mint_distinct_subscription_ids in
tests/v2_subscriptions_client.rs, which opens two streams from ONE
client and asserts their acknowledged ids DIFFER.
A third-party client that does reuse an id is refused with the RETRYABLE
RATE_LIMITED (-32005, delivered at HTTP 200), so backing off and
retrying is the correct response — but minting a fresh id, as this method
does, is strictly better.
§D-11: polling remains the RECOMMENDED enterprise mechanism
Polling over the Tasks mechanism stays pmcp’s recommended mechanism for enterprise remote deployments. This stream is the spec-conformant OPT-IN: its server side is documented single-instance / sticky-routed only, because the server’s subscription registry is instance-local. Behind a non-sticky load balancer a subscriber silently under-receives.
§Examples
use futures::StreamExt;
use pmcp::shared::streamable_http::StreamableHttpTransportConfigBuilder;
use pmcp::shared::StreamableHttpTransport;
use pmcp::types::protocol::{ProtocolVersion, PROTOCOL_VERSION_2026_07_28};
use pmcp::types::subscriptions::SubscriptionFilter;
use pmcp::ClientBuilder;
let url = url::Url::parse("https://example.invalid/mcp").unwrap();
let transport =
StreamableHttpTransport::new(StreamableHttpTransportConfigBuilder::new(url).build());
let client = ClientBuilder::new(transport)
.with_protocol_version(ProtocolVersion(PROTOCOL_VERSION_2026_07_28.to_string()))?
.build();
let filter = SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
};
let mut stream = client.subscriptions_listen(filter).await?;
println!("agreed: {:?}", stream.acknowledged().notifications);
while let Some(notification) = stream.next().await {
println!("{:?}", notification?);
}§Errors
Returns an error when:
- the connection did not opt into
2026-07-28— NO request is sent, and the message namesClientBuilder::with_protocol_version; - the server rejected the request, in which case its own JSON-RPC error
is returned UNCHANGED (a server advertising no subscription-delivered
capability answers
-32601, which is how “this server does not do subscriptions” is distinguished from a transport fault); - the first frame on the stream is not the mandatory acknowledgement, or
is tagged with a different
subscriptionId.