Skip to content

Commit 74e2d8e

Browse files
committed
fix(api): use camelCase in CallToolResponse and add type discriminators to ContentBlock
- Add #[serde(rename_all = "camelCase")] to CallToolResponse so fields serialize as isError, structuredContent (matching MCP spec) - Define schema-only ContentBlock types with proper 'type' discriminator fields (text, image, resource, audio, resource_link) for OpenAPI - Serialize content via serde_json::Value to bypass utoipa's inability to reflect rmcp's #[serde(tag = "type")] on external types - Replace old Content type with ContentBlock in ToolCallWithResponse - Remove redundant McpAppToolResult type; use CallToolResult directly in McpAppRenderer
1 parent c8b70b4 commit 74e2d8e

File tree

8 files changed

+437
-48
lines changed

8 files changed

+437
-48
lines changed

crates/goose-server/src/openapi.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,12 @@ derive_utoipa!(Icon as IconSchema);
576576
super::routes::agent::ReadResourceResponse,
577577
super::routes::agent::CallToolRequest,
578578
super::routes::agent::CallToolResponse,
579+
super::routes::agent::ContentBlock,
580+
super::routes::agent::TextContentBlock,
581+
super::routes::agent::ImageContentBlock,
582+
super::routes::agent::ResourceContentBlock,
583+
super::routes::agent::AudioContentBlock,
584+
super::routes::agent::ResourceLinkContentBlock,
579585
super::routes::agent::ListAppsRequest,
580586
super::routes::agent::ListAppsResponse,
581587
super::routes::agent::ImportAppRequest,

crates/goose-server/src/routes/agent.rs

Lines changed: 210 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use goose::{
2727
agents::{extension::ToolInfo, extension_manager::get_parameter_names},
2828
config::permission::PermissionLevel,
2929
};
30-
use rmcp::model::{CallToolRequestParams, Content};
30+
use rmcp::model::CallToolRequestParams;
3131
use serde::{Deserialize, Serialize};
3232
use serde_json::Value;
3333
use std::collections::HashSet;
@@ -134,13 +134,214 @@ pub struct CallToolRequest {
134134
arguments: Value,
135135
}
136136

137+
/// Schema-only types for OpenAPI generation.
138+
///
139+
/// rmcp's `Content` uses `#[serde(tag = "type")]` so the wire format includes a
140+
/// `type` discriminator (e.g. `{"type":"text","text":"..."}`), but utoipa doesn't
141+
/// reflect that in the generated schema. These types exist solely to produce a
142+
/// correct OpenAPI spec — actual serialization goes through rmcp's `Content` via
143+
/// `serde_json::Value`.
144+
#[allow(dead_code)]
145+
pub struct TextContentBlock;
146+
impl<'s> utoipa::ToSchema<'s> for TextContentBlock {
147+
fn schema() -> (
148+
&'s str,
149+
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
150+
) {
151+
use utoipa::openapi::schema::{ObjectBuilder, SchemaType};
152+
(
153+
"TextContentBlock",
154+
ObjectBuilder::new()
155+
.property(
156+
"type",
157+
ObjectBuilder::new()
158+
.schema_type(SchemaType::String)
159+
.enum_values(Some(["text"])),
160+
)
161+
.required("type")
162+
.property("text", ObjectBuilder::new().schema_type(SchemaType::String))
163+
.required("text")
164+
.property(
165+
"_meta",
166+
ObjectBuilder::new().schema_type(SchemaType::Object),
167+
)
168+
.into(),
169+
)
170+
}
171+
}
172+
173+
#[allow(dead_code)]
174+
pub struct ImageContentBlock;
175+
impl<'s> utoipa::ToSchema<'s> for ImageContentBlock {
176+
fn schema() -> (
177+
&'s str,
178+
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
179+
) {
180+
use utoipa::openapi::schema::{ObjectBuilder, SchemaType};
181+
(
182+
"ImageContentBlock",
183+
ObjectBuilder::new()
184+
.property(
185+
"type",
186+
ObjectBuilder::new()
187+
.schema_type(SchemaType::String)
188+
.enum_values(Some(["image"])),
189+
)
190+
.required("type")
191+
.property("data", ObjectBuilder::new().schema_type(SchemaType::String))
192+
.required("data")
193+
.property(
194+
"mimeType",
195+
ObjectBuilder::new().schema_type(SchemaType::String),
196+
)
197+
.required("mimeType")
198+
.property(
199+
"_meta",
200+
ObjectBuilder::new().schema_type(SchemaType::Object),
201+
)
202+
.into(),
203+
)
204+
}
205+
}
206+
207+
#[allow(dead_code)]
208+
pub struct ResourceContentBlock;
209+
impl<'s> utoipa::ToSchema<'s> for ResourceContentBlock {
210+
fn schema() -> (
211+
&'s str,
212+
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
213+
) {
214+
use utoipa::openapi::schema::{ObjectBuilder, SchemaType};
215+
(
216+
"ResourceContentBlock",
217+
ObjectBuilder::new()
218+
.property(
219+
"type",
220+
ObjectBuilder::new()
221+
.schema_type(SchemaType::String)
222+
.enum_values(Some(["resource"])),
223+
)
224+
.required("type")
225+
.property(
226+
"resource",
227+
ObjectBuilder::new().schema_type(SchemaType::Object),
228+
)
229+
.required("resource")
230+
.property(
231+
"_meta",
232+
ObjectBuilder::new().schema_type(SchemaType::Object),
233+
)
234+
.into(),
235+
)
236+
}
237+
}
238+
239+
#[allow(dead_code)]
240+
pub struct AudioContentBlock;
241+
impl<'s> utoipa::ToSchema<'s> for AudioContentBlock {
242+
fn schema() -> (
243+
&'s str,
244+
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
245+
) {
246+
use utoipa::openapi::schema::{ObjectBuilder, SchemaType};
247+
(
248+
"AudioContentBlock",
249+
ObjectBuilder::new()
250+
.property(
251+
"type",
252+
ObjectBuilder::new()
253+
.schema_type(SchemaType::String)
254+
.enum_values(Some(["audio"])),
255+
)
256+
.required("type")
257+
.property("data", ObjectBuilder::new().schema_type(SchemaType::String))
258+
.required("data")
259+
.property(
260+
"mimeType",
261+
ObjectBuilder::new().schema_type(SchemaType::String),
262+
)
263+
.required("mimeType")
264+
.into(),
265+
)
266+
}
267+
}
268+
269+
#[allow(dead_code)]
270+
pub struct ResourceLinkContentBlock;
271+
impl<'s> utoipa::ToSchema<'s> for ResourceLinkContentBlock {
272+
fn schema() -> (
273+
&'s str,
274+
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
275+
) {
276+
use utoipa::openapi::schema::{ObjectBuilder, SchemaType};
277+
(
278+
"ResourceLinkContentBlock",
279+
ObjectBuilder::new()
280+
.property(
281+
"type",
282+
ObjectBuilder::new()
283+
.schema_type(SchemaType::String)
284+
.enum_values(Some(["resource_link"])),
285+
)
286+
.required("type")
287+
.property("uri", ObjectBuilder::new().schema_type(SchemaType::String))
288+
.required("uri")
289+
.property("name", ObjectBuilder::new().schema_type(SchemaType::String))
290+
.required("name")
291+
.property(
292+
"title",
293+
ObjectBuilder::new().schema_type(SchemaType::String),
294+
)
295+
.property(
296+
"description",
297+
ObjectBuilder::new().schema_type(SchemaType::String),
298+
)
299+
.property(
300+
"mimeType",
301+
ObjectBuilder::new().schema_type(SchemaType::String),
302+
)
303+
.property(
304+
"_meta",
305+
ObjectBuilder::new().schema_type(SchemaType::Object),
306+
)
307+
.into(),
308+
)
309+
}
310+
}
311+
312+
/// A content block in a tool response, discriminated by a `type` field.
313+
#[allow(dead_code)]
314+
pub enum ContentBlock {}
315+
316+
impl<'s> utoipa::ToSchema<'s> for ContentBlock {
317+
fn schema() -> (
318+
&'s str,
319+
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
320+
) {
321+
use utoipa::openapi::schema::{OneOfBuilder, Ref};
322+
(
323+
"ContentBlock",
324+
OneOfBuilder::new()
325+
.item(Ref::from_schema_name("TextContentBlock"))
326+
.item(Ref::from_schema_name("ImageContentBlock"))
327+
.item(Ref::from_schema_name("ResourceContentBlock"))
328+
.item(Ref::from_schema_name("AudioContentBlock"))
329+
.item(Ref::from_schema_name("ResourceLinkContentBlock"))
330+
.into(),
331+
)
332+
}
333+
}
334+
137335
#[derive(Serialize, utoipa::ToSchema)]
336+
#[serde(rename_all = "camelCase")]
138337
pub struct CallToolResponse {
139-
content: Vec<Content>,
338+
#[schema(value_type = Vec<ContentBlock>)]
339+
content: Vec<Value>,
140340
#[serde(skip_serializing_if = "Option::is_none")]
141341
structured_content: Option<Value>,
142342
is_error: bool,
143343
#[serde(skip_serializing_if = "Option::is_none")]
344+
#[serde(rename = "_meta")]
144345
_meta: Option<Value>,
145346
}
146347

@@ -968,8 +1169,14 @@ async fn call_tool(
9681169
.await
9691170
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
9701171

1172+
let content = result
1173+
.content
1174+
.into_iter()
1175+
.filter_map(|c| serde_json::to_value(c).ok())
1176+
.collect();
1177+
9711178
Ok(Json(CallToolResponse {
972-
content: result.content,
1179+
content,
9731180
structured_content: result.structured_content,
9741181
is_error: result.is_error.unwrap_or(false),
9751182
_meta: result.meta.and_then(|m| serde_json::to_value(m).ok()),

0 commit comments

Comments
 (0)