chore: add #[non_exhaustive] and mutation methods to improve compatibility (#715)

* chore: add #[non_exhaustive] to reduce backwards-incompatible changes going forward

* fix: remove ProtocolVersion import

* fix: add a few more with_ mutator methods

---------

Co-authored-by: Alex Hancock <alexhancock@block.xyz>
This commit is contained in:
Jack Amadeo 2026-03-03 10:38:01 -05:00 • committed by GitHub
commit f63718d202
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1568 additions and 1110 deletions

View file

@ -11,6 +11,7 @@ use super::{
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct Annotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<Role>>,

View file

@ -243,6 +243,7 @@ pub struct SamplingCapability {
/// ```
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct ClientCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental: Option<ExperimentalCapabilities>,
@ -280,6 +281,7 @@ pub struct ClientCapabilities {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental: Option<ExperimentalCapabilities>,

View file

@ -38,6 +38,17 @@ pub struct RawEmbeddedResource {
pub meta: Option<super::Meta>,
pub resource: ResourceContents,
}
impl RawEmbeddedResource {
/// Create a new RawEmbeddedResource.
pub fn new(resource: ResourceContents) -> Self {
Self {
meta: None,
resource,
}
}
}
pub type EmbeddedResource = Annotated<RawEmbeddedResource>;
impl EmbeddedResource {
@ -63,6 +74,7 @@ pub type AudioContent = Annotated<RawAudioContent>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct ToolUseContent {
/// Unique identifier for this tool call
pub id: String,
@ -79,6 +91,7 @@ pub struct ToolUseContent {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct ToolResultContent {
/// Optional metadata
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]

View file

@ -89,6 +89,7 @@ pub enum StringFormat {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct StringSchema {
/// Type discriminator
#[serde(rename = "type")]
@ -237,6 +238,7 @@ impl StringSchema {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct NumberSchema {
/// Type discriminator
#[serde(rename = "type")]
@ -444,6 +446,7 @@ impl IntegerSchema {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct BooleanSchema {
/// Type discriminator
#[serde(rename = "type")]
@ -513,6 +516,16 @@ pub struct ConstTitle {
pub title: String,
}
impl ConstTitle {
/// Create a new ConstTitle.
pub fn new(const_: impl Into<String>, title: impl Into<String>) -> Self {
Self {
const_: const_.into(),
title: title.into(),
}
}
}
/// Legacy enum schema, keep for backward compatibility
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -533,6 +546,7 @@ pub struct LegacyEnumSchema {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct UntitledSingleSelectEnumSchema {
#[serde(rename = "type")]
pub type_: StringTypeConst,
@ -550,6 +564,7 @@ pub struct UntitledSingleSelectEnumSchema {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct TitledSingleSelectEnumSchema {
#[serde(rename = "type")]
pub type_: StringTypeConst,
@ -563,6 +578,19 @@ pub struct TitledSingleSelectEnumSchema {
pub default: Option<String>,
}
impl TitledSingleSelectEnumSchema {
/// Create a new TitledSingleSelectEnumSchema.
pub fn new(one_of: Vec<ConstTitle>) -> Self {
Self {
type_: StringTypeConst,
title: None,
description: None,
one_of,
default: None,
}
}
}
/// Combined single-select
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@ -592,10 +620,18 @@ pub struct TitledItems {
pub any_of: Vec<ConstTitle>,
}
impl TitledItems {
/// Create a new TitledItems.
pub fn new(any_of: Vec<ConstTitle>) -> Self {
Self { any_of }
}
}
/// Multi-select untitled options
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UntitledMultiSelectEnumSchema {
#[serde(rename = "type")]
pub type_: ArrayTypeConst,
@ -616,6 +652,7 @@ pub struct UntitledMultiSelectEnumSchema {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct TitledMultiSelectEnumSchema {
#[serde(rename = "type")]
pub type_: ArrayTypeConst,
@ -632,6 +669,51 @@ pub struct TitledMultiSelectEnumSchema {
pub default: Option<Vec<String>>,
}
impl TitledMultiSelectEnumSchema {
/// Create a new TitledMultiSelectEnumSchema.
pub fn new(items: TitledItems) -> Self {
Self {
type_: ArrayTypeConst,
title: None,
description: None,
min_items: None,
max_items: None,
items,
default: None,
}
}
/// Set the title.
pub fn with_title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
self.title = Some(title.into());
self
}
/// Set the description.
pub fn with_description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
self.description = Some(description.into());
self
}
/// Set the minimum number of items.
pub fn with_min_items(mut self, min_items: u64) -> Self {
self.min_items = Some(min_items);
self
}
/// Set the maximum number of items.
pub fn with_max_items(mut self, max_items: u64) -> Self {
self.max_items = Some(max_items);
self
}
/// Set the default values.
pub fn with_default(mut self, default: Vec<String>) -> Self {
self.default = Some(default);
self
}
}
/// Multi-select enum options
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]

View file

@ -7,9 +7,10 @@ use super::{
};
/// A prompt that can be used to generate text from a model
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct Prompt {
/// The name of the prompt
pub name: String,
@ -49,11 +50,46 @@ impl Prompt {
meta: None,
}
}
/// Create a new prompt from raw fields (used by the macro)
pub fn from_raw(
name: impl Into<String>,
description: Option<impl Into<String>>,
arguments: Option<Vec<PromptArgument>>,
) -> Self {
Prompt {
name: name.into(),
title: None,
description: description.map(Into::into),
arguments,
icons: None,
meta: None,
}
}
/// Set the human-readable title
pub fn with_title(mut self, title: Option<String>) -> Self {
self.title = title;
self
}
/// Set the icons
pub fn with_icons(mut self, icons: Option<Vec<Icon>>) -> Self {
self.icons = icons;
self
}
/// Set the metadata
pub fn with_meta(mut self, meta: Option<Meta>) -> Self {
self.meta = meta;
self
}
}
/// Represents a prompt argument that can be passed to customize the prompt
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct PromptArgument {
/// The name of the argument
pub name: String,
@ -68,6 +104,36 @@ pub struct PromptArgument {
pub required: Option<bool>,
}
impl PromptArgument {
/// Create a new prompt argument
pub fn new<N: Into<String>>(name: N) -> Self {
PromptArgument {
name: name.into(),
title: None,
description: None,
required: None,
}
}
/// Set the title
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
self.title = Some(title.into());
self
}
/// Set the description
pub fn with_description<D: Into<String>>(mut self, description: D) -> Self {
self.description = Some(description.into());
self
}
/// Set the required flag
pub fn with_required(mut self, required: bool) -> Self {
self.required = Some(required);
self
}
}
/// Represents the role of a message sender in a prompt conversation
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -112,6 +178,7 @@ impl PromptMessageContent {
/// A message in a prompt conversation
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct PromptMessage {
/// The role of the message sender
pub role: PromptMessageRole,
@ -120,6 +187,11 @@ pub struct PromptMessage {
}
impl PromptMessage {
/// Create a new prompt message with the given role and content
pub fn new(role: PromptMessageRole, content: PromptMessageContent) -> Self {
Self { role, content }
}
/// Create a new text message with the given role and text content
pub fn new_text<S: Into<String>>(role: PromptMessageRole, text: S) -> Self {
Self {

View file

@ -80,6 +80,7 @@ pub enum ResourceContents {
}
impl ResourceContents {
/// Create text resource contents.
pub fn text(text: impl Into<String>, uri: impl Into<String>) -> Self {
Self::TextResourceContents {
uri: uri.into(),
@ -88,6 +89,34 @@ impl ResourceContents {
meta: None,
}
}
/// Create blob resource contents.
pub fn blob(blob: impl Into<String>, uri: impl Into<String>) -> Self {
Self::BlobResourceContents {
uri: uri.into(),
mime_type: None,
blob: blob.into(),
meta: None,
}
}
/// Set the MIME type on this resource contents.
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
match &mut self {
Self::TextResourceContents { mime_type: mt, .. } => *mt = Some(mime_type.into()),
Self::BlobResourceContents { mime_type: mt, .. } => *mt = Some(mime_type.into()),
}
self
}
/// Set the metadata on this resource contents.
pub fn with_meta(mut self, meta: Meta) -> Self {
match &mut self {
Self::TextResourceContents { meta: m, .. } => *m = Some(meta),
Self::BlobResourceContents { meta: m, .. } => *m = Some(meta),
}
self
}
}
impl RawResource {
@ -104,6 +133,80 @@ impl RawResource {
meta: None,
}
}
/// Set the human-readable title.
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
/// Set the description.
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
/// Set the MIME type.
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
/// Set the size in bytes.
pub fn with_size(mut self, size: u32) -> Self {
self.size = Some(size);
self
}
/// Set the icons.
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
self.icons = Some(icons);
self
}
/// Set the metadata.
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = Some(meta);
self
}
}
impl RawResourceTemplate {
/// Creates a new RawResourceTemplate with a URI template and name.
pub fn new(uri_template: impl Into<String>, name: impl Into<String>) -> Self {
Self {
uri_template: uri_template.into(),
name: name.into(),
title: None,
description: None,
mime_type: None,
icons: None,
}
}
/// Set the human-readable title.
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
/// Set the description.
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
/// Set the MIME type.
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
/// Set the icons.
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
self.icons = Some(icons);
self
}
}
#[cfg(test)]

View file

@ -28,6 +28,7 @@ pub enum TaskStatus {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct Task {
/// Unique task identifier generated by the receiver.
pub task_id: String,
@ -48,14 +49,60 @@ pub struct Task {
pub poll_interval: Option<u64>,
}
impl Task {
/// Create a new Task with required fields.
pub fn new(
task_id: String,
status: TaskStatus,
created_at: String,
last_updated_at: String,
) -> Self {
Self {
task_id,
status,
status_message: None,
created_at,
last_updated_at,
ttl: None,
poll_interval: None,
}
}
/// Set the status message.
pub fn with_status_message(mut self, status_message: impl Into<String>) -> Self {
self.status_message = Some(status_message.into());
self
}
/// Set the TTL in milliseconds. `None` means unlimited retention.
pub fn with_ttl(mut self, ttl: u64) -> Self {
self.ttl = Some(ttl);
self
}
/// Set the poll interval in milliseconds.
pub fn with_poll_interval(mut self, poll_interval: u64) -> Self {
self.poll_interval = Some(poll_interval);
self
}
}
/// Wrapper returned by task-augmented requests (CreateTaskResult in SEP-1686).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct CreateTaskResult {
pub task: Task,
}
impl CreateTaskResult {
/// Create a new CreateTaskResult.
pub fn new(task: Task) -> Self {
Self { task }
}
}
/// Response to a `tasks/get` request.
///
/// Per spec, `GetTaskResult = allOf[Result, Task]` — the Task fields are
@ -78,8 +125,16 @@ pub struct GetTaskResult {
/// serialized as a JSON value.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct GetTaskPayloadResult(pub Value);
impl GetTaskPayloadResult {
/// Create a new GetTaskPayloadResult with the given value.
pub fn new(value: Value) -> Self {
Self(value)
}
}
/// Response to a `tasks/cancel` request.
///
/// Per spec, `CancelTaskResult = allOf[Result, Task]` — same shape as `GetTaskResult`.
@ -104,3 +159,14 @@ pub struct TaskList {
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<u64>,
}
impl TaskList {
/// Create a new TaskList.
pub fn new(tasks: Vec<Task>) -> Self {
Self {
tasks,
next_cursor: None,
total: None,
}
}
}

View file

@ -10,9 +10,10 @@ use serde_json::Value;
use super::{Icon, JsonObject, Meta};
/// A tool that can be used by a model.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct Tool {
/// The name of the tool
pub name: Cow<'static, str>,
@ -67,6 +68,7 @@ pub enum TaskSupport {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct ToolExecution {
/// Indicates whether this tool supports task-based invocation.
///
@ -83,6 +85,11 @@ impl ToolExecution {
Self::default()
}
/// Create a ToolExecution from raw optional fields.
pub fn from_raw(task_support: Option<TaskSupport>) -> Self {
Self { task_support }
}
/// Set the task support mode.
pub fn with_task_support(mut self, task_support: TaskSupport) -> Self {
self.task_support = Some(task_support);
@ -101,6 +108,7 @@ impl ToolExecution {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct ToolAnnotations {
/// A human-readable title for the tool.
#[serde(skip_serializing_if = "Option::is_none")]
@ -145,6 +153,24 @@ impl ToolAnnotations {
pub fn new() -> Self {
Self::default()
}
/// Create a new ToolAnnotations with all fields specified
pub fn from_raw(
title: Option<String>,
read_only_hint: Option<bool>,
destructive_hint: Option<bool>,
idempotent_hint: Option<bool>,
open_world_hint: Option<bool>,
) -> Self {
ToolAnnotations {
title,
read_only_hint,
destructive_hint,
idempotent_hint,
open_world_hint,
}
}
pub fn with_title<T>(title: T) -> Self
where
T: Into<String>,
@ -211,6 +237,59 @@ impl Tool {
}
}
/// Create a new tool with just a name and input schema (no description)
pub fn new_with_raw<N, S>(
name: N,
description: Option<Cow<'static, str>>,
input_schema: S,
) -> Self
where
N: Into<Cow<'static, str>>,
S: Into<Arc<JsonObject>>,
{
Tool {
name: name.into(),
title: None,
description,
input_schema: input_schema.into(),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
}
}
/// Set the human-readable title
pub fn with_title(mut self, title: Option<String>) -> Self {
self.title = title;
self
}
/// Set the output schema from a raw value
pub fn with_raw_output_schema(mut self, output_schema: Option<Arc<JsonObject>>) -> Self {
self.output_schema = output_schema;
self
}
/// Set the annotations
pub fn with_annotations(mut self, annotations: Option<ToolAnnotations>) -> Self {
self.annotations = annotations;
self
}
/// Set the icons
pub fn with_icons(mut self, icons: Option<Vec<Icon>>) -> Self {
self.icons = icons;
self
}
/// Set the metadata
pub fn with_meta(mut self, meta: Option<Meta>) -> Self {
self.meta = meta;
self
}
pub fn annotate(self, annotations: ToolAnnotations) -> Self {
Tool {
annotations: Some(annotations),
@ -219,11 +298,9 @@ impl Tool {
}
/// Set the execution configuration for this tool.
pub fn with_execution(self, execution: ToolExecution) -> Self {
Tool {
execution: Some(execution),
..self
}
pub fn with_execution(mut self, execution: Option<ToolExecution>) -> Self {
self.execution = execution;
self
}
/// Returns the task support mode for this tool.