From b2d95493870e6e81b79c375917dc2915251d2ac5 Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Wed, 30 Jul 2025 10:33:58 -0400 Subject: [PATCH 1/5] Upgrade rmcp from 0.2.1 to 0.3.1 - Updated workspace dependency in Cargo.toml - Fixed breaking changes from schemars 0.8.x to 1.0.4 upgrade - Rewrote OpenAPI schema conversion to use public schemars API - All tests pass and code compiles successfully The schemars upgrade made the schema module private, requiring a complete rewrite of the schema conversion functions to work with JSON values directly instead of internal schema types. --- Cargo.lock | 62 ++++-- Cargo.toml | 2 +- crates/goose-server/src/openapi.rs | 321 ++++++++++++++++------------- 3 files changed, 226 insertions(+), 159 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19ccc01576a4..b434baefb901 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1981,8 +1981,18 @@ version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.10", + "darling_macro 0.20.10", +] + +[[package]] +name = "darling" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a79c4acb1fd5fa3d9304be4c76e031c54d2e92d172a393e24b19a14fe8532fe9" +dependencies = [ + "darling_core 0.21.0", + "darling_macro 0.21.0", ] [[package]] @@ -1999,13 +2009,38 @@ dependencies = [ "syn 2.0.99", ] +[[package]] +name = "darling_core" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74875de90daf30eb59609910b84d4d368103aaec4c924824c6799b28f77d6a1d" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.99", +] + [[package]] name = "darling_macro" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ - "darling_core", + "darling_core 0.20.10", + "quote", + "syn 2.0.99", +] + +[[package]] +name = "darling_macro" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79f8e61677d5df9167cd85265f8e5f64b215cdea3fb55eebc3e622e44c7a146" +dependencies = [ + "darling_core 0.21.0", "quote", "syn 2.0.99", ] @@ -6761,9 +6796,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.2.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37f2048a81a7ff7e8ef6bc5abced70c3d9114c8f03d85d7aaaafd9fd04f12e9e" +checksum = "824daba0a34f8c5c5392295d381e0800f88fd986ba291699f8785f05fa344c1e" dependencies = [ "base64 0.22.1", "chrono", @@ -6782,11 +6817,11 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "0.2.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72398e694b9f6dbb5de960cf158c8699e6a1854cb5bbaac7de0646b2005763c4" +checksum = "ad6543c0572a4dbc125c23e6f54963ea9ba002294fd81dd4012c204219b0dcaa" dependencies = [ - "darling", + "darling 0.21.0", "proc-macro2", "quote", "serde_json", @@ -7072,12 +7107,13 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.22" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" dependencies = [ "chrono", "dyn-clone", + "ref-cast", "schemars_derive", "serde", "serde_json", @@ -7085,9 +7121,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.22" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" dependencies = [ "proc-macro2", "quote", @@ -7278,7 +7314,7 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ - "darling", + "darling 0.20.10", "proc-macro2", "quote", "syn 2.0.99", diff --git a/Cargo.toml b/Cargo.toml index cad9d62a672a..bbbe31185272 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ description = "An AI agent" uninlined_format_args = "allow" [workspace.dependencies] -rmcp = { version = "0.2.1", features = ["schemars"] } +rmcp = { version = "0.3.1", features = ["schemars"] } # Patch for Windows cross-compilation issue with crunchy [patch.crates-io] diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index a4f0495f672d..37f1520a8cdd 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -17,7 +17,6 @@ use rmcp::model::{ }; use utoipa::{OpenApi, ToSchema}; -use rmcp::schemars::schema::{InstanceType, SchemaObject, SingleOrVec}; use utoipa::openapi::schema::{ AdditionalProperties, AnyOfBuilder, ArrayBuilder, ObjectBuilder, OneOfBuilder, Schema, SchemaFormat, SchemaType, @@ -30,7 +29,7 @@ macro_rules! derive_utoipa { impl<'__s> ToSchema<'__s> for $schema_name { fn schema() -> (&'__s str, utoipa::openapi::RefOr) { - let settings = rmcp::schemars::gen::SchemaSettings::openapi3(); + let settings = rmcp::schemars::generate::SchemaSettings::openapi3(); let generator = settings.into_generator(); let schema = generator.into_root_schema_for::<$inner_type>(); let schema = convert_schemars_to_utoipa(schema); @@ -44,119 +43,128 @@ macro_rules! derive_utoipa { }; } -fn convert_schemars_to_utoipa(schema: rmcp::schemars::schema::RootSchema) -> RefOr { - convert_schema_object(&rmcp::schemars::schema::Schema::Object( - schema.schema.clone(), - )) -} +fn convert_schemars_to_utoipa(schema: rmcp::schemars::Schema) -> RefOr { + // For schemars 1.0+, we need to work with the public API + // The schema is now a wrapper around a JSON Value that can be either an object or bool + if let Some(true) = schema.as_bool() { + return RefOr::T(Schema::Object(ObjectBuilder::new().build())); + } -fn convert_schema_object(schema: &rmcp::schemars::schema::Schema) -> RefOr { - match schema { - rmcp::schemars::schema::Schema::Object(schema_object) => { - convert_schema_object_inner(schema_object) - } - rmcp::schemars::schema::Schema::Bool(true) => { - RefOr::T(Schema::Object(ObjectBuilder::new().build())) - } - rmcp::schemars::schema::Schema::Bool(false) => { - RefOr::T(Schema::Object(ObjectBuilder::new().build())) - } + if let Some(false) = schema.as_bool() { + return RefOr::T(Schema::Object(ObjectBuilder::new().build())); } + + // For object schemas, we'll need to work with the JSON Value directly + if let Some(obj) = schema.as_object() { + return convert_json_object_to_utoipa(obj); + } + + // Fallback + RefOr::T(Schema::Object(ObjectBuilder::new().build())) } -fn convert_schema_object_inner(schema: &SchemaObject) -> RefOr { - // Handle references first - if let Some(reference) = &schema.reference { +fn convert_json_object_to_utoipa( + obj: &serde_json::Map, +) -> RefOr { + use serde_json::Value; + + // Handle $ref + if let Some(Value::String(reference)) = obj.get("$ref") { return RefOr::Ref(Ref::new(reference.clone())); } - // Handle subschemas (oneOf, allOf, anyOf) - if let Some(subschemas) = &schema.subschemas { - if let Some(one_of) = &subschemas.one_of { - let schemas: Vec> = one_of.iter().map(convert_schema_object).collect(); - let mut builder = OneOfBuilder::new(); - for schema in schemas { - builder = builder.item(schema); + // Handle oneOf, allOf, anyOf + if let Some(Value::Array(one_of)) = obj.get("oneOf") { + let mut builder = OneOfBuilder::new(); + for item in one_of { + if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) { + builder = builder.item(convert_schemars_to_utoipa(schema)); } - return RefOr::T(Schema::OneOf(builder.build())); } - if let Some(all_of) = &subschemas.all_of { - let schemas: Vec> = all_of.iter().map(convert_schema_object).collect(); - let mut all_of = AllOfBuilder::new(); - for schema in schemas { - all_of = all_of.item(schema); + return RefOr::T(Schema::OneOf(builder.build())); + } + + if let Some(Value::Array(all_of)) = obj.get("allOf") { + let mut builder = AllOfBuilder::new(); + for item in all_of { + if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) { + builder = builder.item(convert_schemars_to_utoipa(schema)); } - return RefOr::T(Schema::AllOf(all_of.build())); } - if let Some(any_of) = &subschemas.any_of { - let schemas: Vec> = any_of.iter().map(convert_schema_object).collect(); - let mut any_of = AnyOfBuilder::new(); - for schema in schemas { - any_of = any_of.item(schema); + return RefOr::T(Schema::AllOf(builder.build())); + } + + if let Some(Value::Array(any_of)) = obj.get("anyOf") { + let mut builder = AnyOfBuilder::new(); + for item in any_of { + if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) { + builder = builder.item(convert_schemars_to_utoipa(schema)); } - return RefOr::T(Schema::AnyOf(any_of.build())); } + return RefOr::T(Schema::AnyOf(builder.build())); } - // Handle based on instance type - match &schema.instance_type { - Some(SingleOrVec::Single(instance_type)) => { - convert_single_instance_type(instance_type, schema) - } - Some(SingleOrVec::Vec(instance_types)) => { + // Handle type-based schemas + match obj.get("type") { + Some(Value::String(type_str)) => convert_typed_schema(type_str, obj), + Some(Value::Array(types)) => { // Multiple types - use AnyOf - let schemas: Vec> = instance_types - .iter() - .map(|instance_type| convert_single_instance_type(instance_type, schema)) - .collect(); - let mut any_of = AnyOfBuilder::new(); - for schema in schemas { - any_of = any_of.item(schema); + let mut builder = AnyOfBuilder::new(); + for type_val in types { + if let Value::String(type_str) = type_val { + builder = builder.item(convert_typed_schema(type_str, obj)); + } } - RefOr::T(Schema::AnyOf(any_of.build())) - } - None => { - // No type specified - create a generic schema - RefOr::T(Schema::Object(ObjectBuilder::new().build())) + RefOr::T(Schema::AnyOf(builder.build())) } + None => RefOr::T(Schema::Object(ObjectBuilder::new().build())), + _ => RefOr::T(Schema::Object(ObjectBuilder::new().build())), // Handle other value types } } -fn convert_single_instance_type( - instance_type: &InstanceType, - schema: &SchemaObject, +fn convert_typed_schema( + type_str: &str, + obj: &serde_json::Map, ) -> RefOr { - match instance_type { - InstanceType::Object => { + use serde_json::Value; + + match type_str { + "object" => { let mut object_builder = ObjectBuilder::new(); - if let Some(object_validation) = &schema.object { - // Add properties - for (name, prop_schema) in &object_validation.properties { - let prop = convert_schema_object(prop_schema); - object_builder = object_builder.property(name, prop); + // Add properties + if let Some(Value::Object(properties)) = obj.get("properties") { + for (name, prop_value) in properties { + if let Ok(prop_schema) = rmcp::schemars::Schema::try_from(prop_value.clone()) { + let prop = convert_schemars_to_utoipa(prop_schema); + object_builder = object_builder.property(name, prop); + } } + } - // Add required fields - for required_field in &object_validation.required { - object_builder = object_builder.required(required_field); + // Add required fields + if let Some(Value::Array(required)) = obj.get("required") { + for req in required { + if let Value::String(field_name) = req { + object_builder = object_builder.required(field_name); + } } + } - // Handle additional properties - if let Some(additional) = &object_validation.additional_properties { - match &**additional { - rmcp::schemars::schema::Schema::Bool(false) => { - object_builder = object_builder - .additional_properties(Some(AdditionalProperties::FreeForm(false))); - } - rmcp::schemars::schema::Schema::Bool(true) => { - object_builder = object_builder - .additional_properties(Some(AdditionalProperties::FreeForm(true))); - } - rmcp::schemars::schema::Schema::Object(obj) => { - let schema = convert_schema_object( - &rmcp::schemars::schema::Schema::Object(obj.clone()), - ); + // Handle additional properties + if let Some(additional) = obj.get("additionalProperties") { + match additional { + Value::Bool(false) => { + object_builder = object_builder + .additional_properties(Some(AdditionalProperties::FreeForm(false))); + } + Value::Bool(true) => { + object_builder = object_builder + .additional_properties(Some(AdditionalProperties::FreeForm(true))); + } + _ => { + if let Ok(schema) = rmcp::schemars::Schema::try_from(additional.clone()) { + let schema = convert_schemars_to_utoipa(schema); object_builder = object_builder .additional_properties(Some(AdditionalProperties::RefOr(schema))); } @@ -166,117 +174,140 @@ fn convert_single_instance_type( RefOr::T(Schema::Object(object_builder.build())) } - InstanceType::Array => { + "array" => { let mut array_builder = ArrayBuilder::new(); - if let Some(array_validation) = &schema.array { - // Add items schema - if let Some(items) = &array_validation.items { - match items { - rmcp::schemars::schema::SingleOrVec::Single(item_schema) => { - let item_schema = convert_schema_object(item_schema); + // Add items schema + if let Some(items) = obj.get("items") { + match items { + Value::Object(_) | Value::Bool(_) => { + if let Ok(item_schema) = rmcp::schemars::Schema::try_from(items.clone()) { + let item_schema = convert_schemars_to_utoipa(item_schema); array_builder = array_builder.items(item_schema); } - rmcp::schemars::schema::SingleOrVec::Vec(item_schemas) => { - // Multiple item types - use AnyOf - let schemas: Vec> = - item_schemas.iter().map(convert_schema_object).collect(); - let mut any_of = AnyOfBuilder::new(); - for schema in schemas { - any_of = any_of.item(schema); + } + Value::Array(item_schemas) => { + // Multiple item types - use AnyOf + let mut any_of = AnyOfBuilder::new(); + for item in item_schemas { + if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) { + any_of = any_of.item(convert_schemars_to_utoipa(schema)); } - let any_of_schema = RefOr::T(Schema::AnyOf(any_of.build())); - array_builder = array_builder.items(any_of_schema); } + let any_of_schema = RefOr::T(Schema::AnyOf(any_of.build())); + array_builder = array_builder.items(any_of_schema); } + _ => {} } + } - // Add constraints - if let Some(min_items) = array_validation.min_items { - array_builder = array_builder.min_items(Some(min_items as usize)); + // Add constraints + if let Some(Value::Number(min_items)) = obj.get("minItems") { + if let Some(min) = min_items.as_u64() { + array_builder = array_builder.min_items(Some(min as usize)); } - if let Some(max_items) = array_validation.max_items { - array_builder = array_builder.max_items(Some(max_items as usize)); + } + if let Some(Value::Number(max_items)) = obj.get("maxItems") { + if let Some(max) = max_items.as_u64() { + array_builder = array_builder.max_items(Some(max as usize)); } } RefOr::T(Schema::Array(array_builder.build())) } - InstanceType::String => { + "string" => { let mut object_builder = ObjectBuilder::new().schema_type(SchemaType::String); - if let Some(string_validation) = &schema.string { - if let Some(min_length) = string_validation.min_length { - object_builder = object_builder.min_length(Some(min_length as usize)); - } - if let Some(max_length) = string_validation.max_length { - object_builder = object_builder.max_length(Some(max_length as usize)); + if let Some(Value::Number(min_length)) = obj.get("minLength") { + if let Some(min) = min_length.as_u64() { + object_builder = object_builder.min_length(Some(min as usize)); } - if let Some(pattern) = &string_validation.pattern { - object_builder = object_builder.pattern(Some(pattern.clone())); + } + if let Some(Value::Number(max_length)) = obj.get("maxLength") { + if let Some(max) = max_length.as_u64() { + object_builder = object_builder.max_length(Some(max as usize)); } } - - if let Some(format) = &schema.format { + if let Some(Value::String(pattern)) = obj.get("pattern") { + object_builder = object_builder.pattern(Some(pattern.clone())); + } + if let Some(Value::String(format)) = obj.get("format") { object_builder = object_builder.format(Some(SchemaFormat::Custom(format.clone()))); } RefOr::T(Schema::Object(object_builder.build())) } - InstanceType::Number => { + "number" => { let mut object_builder = ObjectBuilder::new().schema_type(SchemaType::Number); - if let Some(number_validation) = &schema.number { - if let Some(minimum) = number_validation.minimum { - object_builder = object_builder.minimum(Some(minimum)); + if let Some(Value::Number(minimum)) = obj.get("minimum") { + if let Some(min) = minimum.as_f64() { + object_builder = object_builder.minimum(Some(min)); } - if let Some(maximum) = number_validation.maximum { - object_builder = object_builder.maximum(Some(maximum)); + } + if let Some(Value::Number(maximum)) = obj.get("maximum") { + if let Some(max) = maximum.as_f64() { + object_builder = object_builder.maximum(Some(max)); } - if let Some(exclusive_minimum) = number_validation.exclusive_minimum { - object_builder = object_builder.exclusive_minimum(Some(exclusive_minimum)); + } + if let Some(Value::Number(exclusive_minimum)) = obj.get("exclusiveMinimum") { + if let Some(min) = exclusive_minimum.as_f64() { + object_builder = object_builder.exclusive_minimum(Some(min)); } - if let Some(exclusive_maximum) = number_validation.exclusive_maximum { - object_builder = object_builder.exclusive_maximum(Some(exclusive_maximum)); + } + if let Some(Value::Number(exclusive_maximum)) = obj.get("exclusiveMaximum") { + if let Some(max) = exclusive_maximum.as_f64() { + object_builder = object_builder.exclusive_maximum(Some(max)); } - if let Some(multiple_of) = number_validation.multiple_of { - object_builder = object_builder.multiple_of(Some(multiple_of)); + } + if let Some(Value::Number(multiple_of)) = obj.get("multipleOf") { + if let Some(mult) = multiple_of.as_f64() { + object_builder = object_builder.multiple_of(Some(mult)); } } RefOr::T(Schema::Object(object_builder.build())) } - InstanceType::Integer => { + "integer" => { let mut object_builder = ObjectBuilder::new().schema_type(SchemaType::Integer); - if let Some(number_validation) = &schema.number { - if let Some(minimum) = number_validation.minimum { - object_builder = object_builder.minimum(Some(minimum)); + if let Some(Value::Number(minimum)) = obj.get("minimum") { + if let Some(min) = minimum.as_f64() { + object_builder = object_builder.minimum(Some(min)); } - if let Some(maximum) = number_validation.maximum { - object_builder = object_builder.maximum(Some(maximum)); + } + if let Some(Value::Number(maximum)) = obj.get("maximum") { + if let Some(max) = maximum.as_f64() { + object_builder = object_builder.maximum(Some(max)); } - if let Some(exclusive_minimum) = number_validation.exclusive_minimum { - object_builder = object_builder.exclusive_minimum(Some(exclusive_minimum)); + } + if let Some(Value::Number(exclusive_minimum)) = obj.get("exclusiveMinimum") { + if let Some(min) = exclusive_minimum.as_f64() { + object_builder = object_builder.exclusive_minimum(Some(min)); } - if let Some(exclusive_maximum) = number_validation.exclusive_maximum { - object_builder = object_builder.exclusive_maximum(Some(exclusive_maximum)); + } + if let Some(Value::Number(exclusive_maximum)) = obj.get("exclusiveMaximum") { + if let Some(max) = exclusive_maximum.as_f64() { + object_builder = object_builder.exclusive_maximum(Some(max)); } - if let Some(multiple_of) = number_validation.multiple_of { - object_builder = object_builder.multiple_of(Some(multiple_of)); + } + if let Some(Value::Number(multiple_of)) = obj.get("multipleOf") { + if let Some(mult) = multiple_of.as_f64() { + object_builder = object_builder.multiple_of(Some(mult)); } } RefOr::T(Schema::Object(object_builder.build())) } - InstanceType::Boolean => RefOr::T(Schema::Object( + "boolean" => RefOr::T(Schema::Object( ObjectBuilder::new() .schema_type(SchemaType::Boolean) .build(), )), - InstanceType::Null => RefOr::T(Schema::Object( + "null" => RefOr::T(Schema::Object( ObjectBuilder::new().schema_type(SchemaType::String).build(), )), + _ => RefOr::T(Schema::Object(ObjectBuilder::new().build())), } } From 2c301b062ba170305bcb04ff559e11d239fb8cfb Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Wed, 30 Jul 2025 14:05:51 -0400 Subject: [PATCH 2/5] Fix OpenAPI schema generation for rmcp 0.3.1 upgrade - Added missing schema definitions for RawTextContent, RawImageContent, and RawEmbeddedResource - Added Annotated schema definition to resolve missing pointer - Updated imports to include all necessary rmcp model types - Fixed schema conversion to handle the new rmcp content type structure The rmcp upgrade from 0.2.1 to 0.3.1 restructured content types, requiring additional schema definitions to be generated for the raw content types that are referenced by the Content enum but weren't being included in the OpenAPI schema generation. --- crates/goose-server/src/openapi.rs | 26 +++- ui/desktop/openapi.json | 184 +++++++++++++++++------------ ui/desktop/src/api/types.gen.ts | 52 ++++---- 3 files changed, 164 insertions(+), 98 deletions(-) diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 37f1520a8cdd..fd084d9679fa 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -13,7 +13,7 @@ use goose::session::info::SessionInfo; use goose::session::SessionMetadata; use rmcp::model::{ Annotations, Content, EmbeddedResource, ImageContent, ResourceContents, Role, TextContent, - Tool, ToolAnnotations, + Tool, ToolAnnotations, RawTextContent, RawImageContent, RawEmbeddedResource, Annotated, }; use utoipa::{OpenApi, ToSchema}; @@ -316,11 +316,31 @@ derive_utoipa!(Content as ContentSchema); derive_utoipa!(EmbeddedResource as EmbeddedResourceSchema); derive_utoipa!(ImageContent as ImageContentSchema); derive_utoipa!(TextContent as TextContentSchema); +derive_utoipa!(RawTextContent as RawTextContentSchema); +derive_utoipa!(RawImageContent as RawImageContentSchema); +derive_utoipa!(RawEmbeddedResource as RawEmbeddedResourceSchema); derive_utoipa!(Tool as ToolSchema); derive_utoipa!(ToolAnnotations as ToolAnnotationsSchema); derive_utoipa!(Annotations as AnnotationsSchema); derive_utoipa!(ResourceContents as ResourceContentsSchema); +// Create a manual schema for the generic Annotated type +struct AnnotatedSchema {} + +impl<'__s> ToSchema<'__s> for AnnotatedSchema { + fn schema() -> (&'__s str, utoipa::openapi::RefOr) { + let settings = rmcp::schemars::generate::SchemaSettings::openapi3(); + let generator = settings.into_generator(); + let schema = generator.into_root_schema_for::>(); + let schema = convert_schemars_to_utoipa(schema); + ("Annotated", schema) + } + + fn aliases() -> Vec<(&'__s str, utoipa::openapi::schema::Schema)> { + Vec::new() + } +} + #[allow(dead_code)] // Used by utoipa for OpenAPI generation #[derive(OpenApi)] #[openapi( @@ -380,6 +400,10 @@ derive_utoipa!(ResourceContents as ResourceContentsSchema); ImageContentSchema, AnnotationsSchema, TextContentSchema, + RawTextContentSchema, + RawImageContentSchema, + RawEmbeddedResourceSchema, + AnnotatedSchema, ToolResponse, ToolRequest, ToolConfirmationRequest, diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 7f4190fbe85d..8aa6d9192a7e 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1091,6 +1091,38 @@ } } }, + "Annotated": { + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/RawTextContent" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/RawImageContent" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/RawEmbeddedResource" + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/Annotated" + } + ] + } + ] + }, "Annotations": { "type": "object", "properties": { @@ -1188,79 +1220,32 @@ "Content": { "oneOf": [ { - "type": "object", - "required": [ - "text", - "type" - ], - "properties": { - "text": { - "type": "string" - }, - "type": { - "type": "string" + "allOf": [ + { + "$ref": "#/components/schemas/RawTextContent" } - } + ] }, { - "type": "object", - "required": [ - "data", - "mimeType", - "type" - ], - "properties": { - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" + "allOf": [ + { + "$ref": "#/components/schemas/RawImageContent" } - } + ] }, { - "type": "object", - "required": [ - "resource", - "type" - ], - "properties": { - "resource": { - "$ref": "#/components/schemas/ResourceContents" - }, - "type": { - "type": "string" + "allOf": [ + { + "$ref": "#/components/schemas/RawEmbeddedResource" } - } + ] }, { - "type": "object", - "required": [ - "data", - "mimeType", - "type" - ], - "properties": { - "annotations": { - "allOf": [ - { - "$ref": "#/components/schemas/Annotations" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" + "allOf": [ + { + "$ref": "#/components/schemas/Annotated" } - } + ] } ] }, @@ -1427,9 +1412,12 @@ ], "properties": { "annotations": { - "allOf": [ + "anyOf": [ { "$ref": "#/components/schemas/Annotations" + }, + { + "type": "object" } ] }, @@ -1828,9 +1816,12 @@ ], "properties": { "annotations": { - "allOf": [ + "anyOf": [ { "$ref": "#/components/schemas/Annotations" + }, + { + "type": "object" } ] }, @@ -2288,6 +2279,43 @@ } } }, + "RawEmbeddedResource": { + "type": "object", + "required": [ + "resource" + ], + "properties": { + "resource": { + "$ref": "#/components/schemas/ResourceContents" + } + } + }, + "RawImageContent": { + "type": "object", + "required": [ + "data", + "mimeType" + ], + "properties": { + "data": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + } + }, + "RawTextContent": { + "type": "object", + "required": [ + "text" + ], + "properties": { + "text": { + "type": "string" + } + } + }, "Recipe": { "type": "object", "description": "A Recipe represents a personalized, user-generated agent configuration that defines\nspecific behaviors and capabilities within the Goose system.\n\n# Fields\n\n## Required Fields\n* `version` - Semantic version of the Recipe file format (defaults to \"1.0.0\")\n* `title` - Short, descriptive name of the Recipe\n* `description` - Detailed description explaining the Recipe's purpose and functionality\n* `Instructions` - Instructions that defines the Recipe's behavior\n\n## Optional Fields\n* `prompt` - the initial prompt to the session to start with\n* `extensions` - List of extension configurations required by the Recipe\n* `context` - Supplementary context information for the Recipe\n* `activities` - Activity labels that appear when loading the Recipe\n* `author` - Information about the Recipe's creator and metadata\n* `parameters` - Additional parameters for the Recipe\n* `response` - Response configuration including JSON schema validation\n* `retry` - Retry configuration for automated validation and recovery\n# Example\n\n\nuse goose::recipe::Recipe;\n\n// Using the builder pattern\nlet recipe = Recipe::builder()\n.title(\"Example Agent\")\n.description(\"An example Recipe configuration\")\n.instructions(\"Act as a helpful assistant\")\n.build()\n.expect(\"Missing required fields\");\n\n// Or using struct initialization\nlet recipe = Recipe {\nversion: \"1.0.0\".to_string(),\ntitle: \"Example Agent\".to_string(),\ndescription: \"An example Recipe configuration\".to_string(),\ninstructions: Some(\"Act as a helpful assistant\".to_string()),\nprompt: None,\nextensions: None,\ncontext: None,\nactivities: None,\nauthor: None,\nsettings: None,\nparameters: None,\nresponse: None,\nsub_recipes: None,\nretry: None,\n};\n", @@ -2451,11 +2479,11 @@ { "type": "object", "required": [ - "text", - "uri" + "uri", + "text" ], "properties": { - "mime_type": { + "mimeType": { "type": "string" }, "text": { @@ -2469,14 +2497,14 @@ { "type": "object", "required": [ - "blob", - "uri" + "uri", + "blob" ], "properties": { "blob": { "type": "string" }, - "mime_type": { + "mimeType": { "type": "string" }, "uri": { @@ -2898,9 +2926,12 @@ ], "properties": { "annotations": { - "allOf": [ + "anyOf": [ { "$ref": "#/components/schemas/Annotations" + }, + { + "type": "object" } ] }, @@ -2927,14 +2958,17 @@ "Tool": { "type": "object", "required": [ - "inputSchema", - "name" + "name", + "inputSchema" ], "properties": { "annotations": { - "allOf": [ + "anyOf": [ { "$ref": "#/components/schemas/ToolAnnotations" + }, + { + "type": "object" } ] }, diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 19a8a8952032..c8e57ddf2565 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -8,6 +8,8 @@ export type AddSubRecipesResponse = { success: boolean; }; +export type Annotated = RawTextContent | RawImageContent | RawEmbeddedResource | Annotated; + export type Annotations = { audience?: Array; priority?: number; @@ -40,22 +42,7 @@ export type ConfigResponse = { config: {}; }; -export type Content = { - text: string; - type: string; -} | { - data: string; - mimeType: string; - type: string; -} | { - resource: ResourceContents; - type: string; -} | { - annotations?: Annotations; - data: string; - mimeType: string; - type: string; -}; +export type Content = RawTextContent | RawImageContent | RawEmbeddedResource | Annotated; export type ContextLengthExceeded = { msg: string; @@ -118,7 +105,9 @@ export type DecodeRecipeResponse = { }; export type EmbeddedResource = { - annotations?: Annotations; + annotations?: Annotations | { + [key: string]: unknown; + }; resource: ResourceContents; }; @@ -265,7 +254,9 @@ export type FrontendToolRequest = { }; export type ImageContent = { - annotations?: Annotations; + annotations?: Annotations | { + [key: string]: unknown; + }; data: string; mimeType: string; }; @@ -407,6 +398,19 @@ export type ProvidersResponse = { providers: Array; }; +export type RawEmbeddedResource = { + resource: ResourceContents; +}; + +export type RawImageContent = { + data: string; + mimeType: string; +}; + +export type RawTextContent = { + text: string; +}; + /** * A Recipe represents a personalized, user-generated agent configuration that defines * specific behaviors and capabilities within the Goose system. @@ -495,12 +499,12 @@ export type RedactedThinkingContent = { }; export type ResourceContents = { - mime_type?: string; + mimeType?: string; text: string; uri: string; } | { blob: string; - mime_type?: string; + mimeType?: string; uri: string; }; @@ -679,7 +683,9 @@ export type SummarizationRequested = { }; export type TextContent = { - annotations?: Annotations; + annotations?: Annotations | { + [key: string]: unknown; + }; text: string; }; @@ -689,7 +695,9 @@ export type ThinkingContent = { }; export type Tool = { - annotations?: ToolAnnotations; + annotations?: ToolAnnotations | { + [key: string]: unknown; + }; description?: string; inputSchema: { [key: string]: unknown; From 6a351f1bf67b08c80dc6a478cb8cde3edf930cf7 Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Wed, 30 Jul 2025 16:10:31 -0400 Subject: [PATCH 3/5] Fix circular reference in Annotated type by updating Rust OpenAPI schema generation --- crates/goose-server/src/openapi.rs | 19 +++++++++++++------ ui/desktop/openapi.json | 27 ++++----------------------- ui/desktop/src/api/types.gen.ts | 2 +- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index fd084d9679fa..e6e895a6badc 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -13,7 +13,7 @@ use goose::session::info::SessionInfo; use goose::session::SessionMetadata; use rmcp::model::{ Annotations, Content, EmbeddedResource, ImageContent, ResourceContents, Role, TextContent, - Tool, ToolAnnotations, RawTextContent, RawImageContent, RawEmbeddedResource, Annotated, + Tool, ToolAnnotations, RawTextContent, RawImageContent, RawEmbeddedResource, }; use utoipa::{OpenApi, ToSchema}; @@ -325,15 +325,22 @@ derive_utoipa!(Annotations as AnnotationsSchema); derive_utoipa!(ResourceContents as ResourceContentsSchema); // Create a manual schema for the generic Annotated type +// We manually define this to avoid circular references from RawContent::Audio(AudioContent) +// where AudioContent = Annotated struct AnnotatedSchema {} impl<'__s> ToSchema<'__s> for AnnotatedSchema { fn schema() -> (&'__s str, utoipa::openapi::RefOr) { - let settings = rmcp::schemars::generate::SchemaSettings::openapi3(); - let generator = settings.into_generator(); - let schema = generator.into_root_schema_for::>(); - let schema = convert_schemars_to_utoipa(schema); - ("Annotated", schema) + // Create a oneOf schema with only the variants we actually use in the API + // This avoids the circular reference from RawContent::Audio(AudioContent) + let schema = Schema::OneOf( + OneOfBuilder::new() + .item(RefOr::Ref(Ref::new("#/components/schemas/RawTextContent"))) + .item(RefOr::Ref(Ref::new("#/components/schemas/RawImageContent"))) + .item(RefOr::Ref(Ref::new("#/components/schemas/RawEmbeddedResource"))) + .build() + ); + ("Annotated", RefOr::T(schema)) } fn aliases() -> Vec<(&'__s str, utoipa::openapi::schema::Schema)> { diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 8aa6d9192a7e..73fb75a4d2f7 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1094,32 +1094,13 @@ "Annotated": { "oneOf": [ { - "allOf": [ - { - "$ref": "#/components/schemas/RawTextContent" - } - ] - }, - { - "allOf": [ - { - "$ref": "#/components/schemas/RawImageContent" - } - ] + "$ref": "#/components/schemas/RawTextContent" }, { - "allOf": [ - { - "$ref": "#/components/schemas/RawEmbeddedResource" - } - ] + "$ref": "#/components/schemas/RawImageContent" }, { - "allOf": [ - { - "$ref": "#/components/schemas/Annotated" - } - ] + "$ref": "#/components/schemas/RawEmbeddedResource" } ] }, @@ -3145,4 +3126,4 @@ } } } -} \ No newline at end of file +} diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index c8e57ddf2565..79dd33acf3b1 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -8,7 +8,7 @@ export type AddSubRecipesResponse = { success: boolean; }; -export type Annotated = RawTextContent | RawImageContent | RawEmbeddedResource | Annotated; +export type Annotated = RawTextContent | RawImageContent | RawEmbeddedResource; export type Annotations = { audience?: Array; From fee7c943241c67b9442d2e1ddb3b95686787e237 Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Wed, 30 Jul 2025 16:54:54 -0400 Subject: [PATCH 4/5] Fix formatting issues in openapi.rs and FlyingBird.tsx - Reorder imports alphabetically in openapi.rs - Fix line formatting for function calls - Fix Prettier formatting in FlyingBird.tsx - Resolves CI formatting check failures --- crates/goose-server/src/openapi.rs | 10 ++++++---- ui/desktop/src/components/FlyingBird.tsx | 18 +++--------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index e6e895a6badc..38ea0e343e5c 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -12,8 +12,8 @@ use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata}; use goose::session::info::SessionInfo; use goose::session::SessionMetadata; use rmcp::model::{ - Annotations, Content, EmbeddedResource, ImageContent, ResourceContents, Role, TextContent, - Tool, ToolAnnotations, RawTextContent, RawImageContent, RawEmbeddedResource, + Annotations, Content, EmbeddedResource, ImageContent, RawEmbeddedResource, RawImageContent, + RawTextContent, ResourceContents, Role, TextContent, Tool, ToolAnnotations, }; use utoipa::{OpenApi, ToSchema}; @@ -337,8 +337,10 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema { OneOfBuilder::new() .item(RefOr::Ref(Ref::new("#/components/schemas/RawTextContent"))) .item(RefOr::Ref(Ref::new("#/components/schemas/RawImageContent"))) - .item(RefOr::Ref(Ref::new("#/components/schemas/RawEmbeddedResource"))) - .build() + .item(RefOr::Ref(Ref::new( + "#/components/schemas/RawEmbeddedResource", + ))) + .build(), ); ("Annotated", RefOr::T(schema)) } diff --git a/ui/desktop/src/components/FlyingBird.tsx b/ui/desktop/src/components/FlyingBird.tsx index 93baa3f5be6c..d4c8c55ecf5e 100644 --- a/ui/desktop/src/components/FlyingBird.tsx +++ b/ui/desktop/src/components/FlyingBird.tsx @@ -6,26 +6,14 @@ interface FlyingBirdProps { cycleInterval?: number; // milliseconds between bird frame changes } -const birdFrames = [ - Bird1, - Bird2, - Bird3, - Bird4, - Bird5, - Bird6, -]; +const birdFrames = [Bird1, Bird2, Bird3, Bird4, Bird5, Bird6]; -export default function FlyingBird({ - className = '', - cycleInterval = 150 -}: FlyingBirdProps) { +export default function FlyingBird({ className = '', cycleInterval = 150 }: FlyingBirdProps) { const [currentFrameIndex, setCurrentFrameIndex] = useState(0); useEffect(() => { const interval = setInterval(() => { - setCurrentFrameIndex((prevIndex) => - (prevIndex + 1) % birdFrames.length - ); + setCurrentFrameIndex((prevIndex) => (prevIndex + 1) % birdFrames.length); }, cycleInterval); return () => clearInterval(interval); From 03cf84144def60e6a9af30ee2c76f7e255b6a3af Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Thu, 31 Jul 2025 07:23:43 -0400 Subject: [PATCH 5/5] regen openapi.json to fix failed check --- ui/desktop/openapi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 73fb75a4d2f7..34850d03d3c9 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -3126,4 +3126,4 @@ } } } -} +} \ No newline at end of file