fix(api): use camelCase in CallToolResponse and add type discriminators to ContentBlock (#7487)
Co-authored-by: Goose <opensource@block.xyz> Co-authored-by: Jack Amadeo <jackamadeo@block.xyz>
This commit is contained in:
@@ -11,8 +11,8 @@ use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata, ProviderTyp
|
|||||||
use goose::session::{Session, SessionInsights, SessionType, SystemInfo};
|
use goose::session::{Session, SessionInsights, SessionType, SystemInfo};
|
||||||
use rmcp::model::{
|
use rmcp::model::{
|
||||||
Annotations, Content, EmbeddedResource, Icon, ImageContent, JsonObject, RawAudioContent,
|
Annotations, Content, EmbeddedResource, Icon, ImageContent, JsonObject, RawAudioContent,
|
||||||
RawEmbeddedResource, RawImageContent, RawResource, RawTextContent, ResourceContents, Role,
|
RawContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent,
|
||||||
TaskSupport, TextContent, Tool, ToolAnnotations, ToolExecution,
|
ResourceContents, Role, TaskSupport, TextContent, Tool, ToolAnnotations, ToolExecution,
|
||||||
};
|
};
|
||||||
use utoipa::{OpenApi, ToSchema};
|
use utoipa::{OpenApi, ToSchema};
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ use utoipa::openapi::{AllOfBuilder, Ref, RefOr};
|
|||||||
|
|
||||||
macro_rules! derive_utoipa {
|
macro_rules! derive_utoipa {
|
||||||
($inner_type:ident as $schema_name:ident) => {
|
($inner_type:ident as $schema_name:ident) => {
|
||||||
struct $schema_name {}
|
pub struct $schema_name {}
|
||||||
|
|
||||||
impl<'__s> ToSchema<'__s> for $schema_name {
|
impl<'__s> ToSchema<'__s> for $schema_name {
|
||||||
fn schema() -> (&'__s str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
|
fn schema() -> (&'__s str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
|
||||||
@@ -47,6 +47,23 @@ macro_rules! derive_utoipa {
|
|||||||
(stringify!($inner_type), schema)
|
(stringify!($inner_type), schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn aliases() -> Vec<(&'__s str, utoipa::openapi::schema::Schema)> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($inner_type:ident as $schema_name:ident => $output_name:expr) => {
|
||||||
|
pub struct $schema_name {}
|
||||||
|
|
||||||
|
impl<'__s> ToSchema<'__s> for $schema_name {
|
||||||
|
fn schema() -> (&'__s str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
|
||||||
|
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);
|
||||||
|
($output_name, schema)
|
||||||
|
}
|
||||||
|
|
||||||
fn aliases() -> Vec<(&'__s str, utoipa::openapi::schema::Schema)> {
|
fn aliases() -> Vec<(&'__s str, utoipa::openapi::schema::Schema)> {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
@@ -89,7 +106,26 @@ fn convert_json_object_to_utoipa(
|
|||||||
return RefOr::T(Schema::OneOf(builder.build()));
|
return RefOr::T(Schema::OneOf(builder.build()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle the discriminated union pattern from schemars: an object with
|
||||||
|
// `type`, `properties`, `required` AND `allOf` (e.g. each variant of a
|
||||||
|
// `#[serde(tag = "type")]` enum). We merge the inline object (which carries
|
||||||
|
// the discriminator property) with the `allOf` refs into a single `allOf`.
|
||||||
if let Some(Value::Array(all_of)) = obj.get("allOf") {
|
if let Some(Value::Array(all_of)) = obj.get("allOf") {
|
||||||
|
let has_inline_properties = obj.contains_key("properties") || obj.contains_key("type");
|
||||||
|
if has_inline_properties {
|
||||||
|
let mut builder = AllOfBuilder::new();
|
||||||
|
// Build an object schema from the inline properties/required
|
||||||
|
let mut obj_without_allof = obj.clone();
|
||||||
|
obj_without_allof.remove("allOf");
|
||||||
|
builder = builder.item(convert_json_object_to_utoipa(&obj_without_allof));
|
||||||
|
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(builder.build()));
|
||||||
|
}
|
||||||
|
|
||||||
let mut builder = AllOfBuilder::new();
|
let mut builder = AllOfBuilder::new();
|
||||||
for item in all_of {
|
for item in all_of {
|
||||||
if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) {
|
if let Ok(schema) = rmcp::schemars::Schema::try_from(item.clone()) {
|
||||||
@@ -215,6 +251,22 @@ fn convert_typed_schema(
|
|||||||
"string" => {
|
"string" => {
|
||||||
let mut object_builder = ObjectBuilder::new().schema_type(SchemaType::String);
|
let mut object_builder = ObjectBuilder::new().schema_type(SchemaType::String);
|
||||||
|
|
||||||
|
if let Some(Value::Array(enum_values)) = obj.get("enum") {
|
||||||
|
let values: Vec<serde_json::Value> = enum_values
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| {
|
||||||
|
if let Value::String(s) = v {
|
||||||
|
Some(Value::String(s.clone()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if !values.is_empty() {
|
||||||
|
object_builder = object_builder.enum_values(Some(values));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(Value::Number(min_length)) = obj.get("minLength") {
|
if let Some(Value::Number(min_length)) = obj.get("minLength") {
|
||||||
if let Some(min) = min_length.as_u64() {
|
if let Some(min) = min_length.as_u64() {
|
||||||
object_builder = object_builder.min_length(Some(min as usize));
|
object_builder = object_builder.min_length(Some(min as usize));
|
||||||
@@ -310,6 +362,7 @@ fn convert_typed_schema(
|
|||||||
|
|
||||||
derive_utoipa!(Role as RoleSchema);
|
derive_utoipa!(Role as RoleSchema);
|
||||||
derive_utoipa!(Content as ContentSchema);
|
derive_utoipa!(Content as ContentSchema);
|
||||||
|
derive_utoipa!(RawContent as ContentBlockSchema => "ContentBlock");
|
||||||
derive_utoipa!(EmbeddedResource as EmbeddedResourceSchema);
|
derive_utoipa!(EmbeddedResource as EmbeddedResourceSchema);
|
||||||
derive_utoipa!(ImageContent as ImageContentSchema);
|
derive_utoipa!(ImageContent as ImageContentSchema);
|
||||||
derive_utoipa!(TextContent as TextContentSchema);
|
derive_utoipa!(TextContent as TextContentSchema);
|
||||||
@@ -577,6 +630,7 @@ derive_utoipa!(Icon as IconSchema);
|
|||||||
super::routes::agent::ReadResourceResponse,
|
super::routes::agent::ReadResourceResponse,
|
||||||
super::routes::agent::CallToolRequest,
|
super::routes::agent::CallToolRequest,
|
||||||
super::routes::agent::CallToolResponse,
|
super::routes::agent::CallToolResponse,
|
||||||
|
ContentBlockSchema,
|
||||||
super::routes::agent::ListAppsRequest,
|
super::routes::agent::ListAppsRequest,
|
||||||
super::routes::agent::ListAppsResponse,
|
super::routes::agent::ListAppsResponse,
|
||||||
super::routes::agent::ImportAppRequest,
|
super::routes::agent::ImportAppRequest,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ use goose::{
|
|||||||
agents::{extension::ToolInfo, extension_manager::get_parameter_names},
|
agents::{extension::ToolInfo, extension_manager::get_parameter_names},
|
||||||
config::permission::PermissionLevel,
|
config::permission::PermissionLevel,
|
||||||
};
|
};
|
||||||
use rmcp::model::{CallToolRequestParams, Content};
|
use rmcp::model::CallToolRequestParams;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
@@ -134,13 +134,31 @@ pub struct CallToolRequest {
|
|||||||
arguments: Value,
|
arguments: Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ref-only alias so utoipa emits `$ref: "#/components/schemas/ContentBlock"`.
|
||||||
|
/// The actual schema is registered via `derive_utoipa!(RawContent as ContentBlockSchema => "ContentBlock")`.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum ContentBlock {}
|
||||||
|
|
||||||
|
impl<'s> utoipa::ToSchema<'s> for ContentBlock {
|
||||||
|
fn schema() -> (
|
||||||
|
&'s str,
|
||||||
|
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
|
||||||
|
) {
|
||||||
|
// Delegate to the auto-generated schema
|
||||||
|
crate::openapi::ContentBlockSchema::schema()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CallToolResponse {
|
pub struct CallToolResponse {
|
||||||
content: Vec<Content>,
|
#[schema(value_type = Vec<ContentBlock>)]
|
||||||
|
content: Vec<Value>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
structured_content: Option<Value>,
|
structured_content: Option<Value>,
|
||||||
is_error: bool,
|
is_error: bool,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "_meta")]
|
||||||
_meta: Option<Value>,
|
_meta: Option<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -992,8 +1010,15 @@ async fn call_tool(
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
|
let content = result
|
||||||
|
.content
|
||||||
|
.into_iter()
|
||||||
|
.map(serde_json::to_value)
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|
||||||
Ok(Json(CallToolResponse {
|
Ok(Json(CallToolResponse {
|
||||||
content: result.content,
|
content,
|
||||||
structured_content: result.structured_content,
|
structured_content: result.structured_content,
|
||||||
is_error: result.is_error.unwrap_or(false),
|
is_error: result.is_error.unwrap_or(false),
|
||||||
_meta: result.meta.and_then(|m| serde_json::to_value(m).ok()),
|
_meta: result.meta.and_then(|m| serde_json::to_value(m).ok()),
|
||||||
|
|||||||
+203
-9
@@ -3871,7 +3871,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"content",
|
"content",
|
||||||
"is_error"
|
"isError"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"_meta": {
|
"_meta": {
|
||||||
@@ -3880,13 +3880,13 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/Content"
|
"$ref": "#/components/schemas/ContentBlock"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"is_error": {
|
"isError": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"structured_content": {
|
"structuredContent": {
|
||||||
"nullable": true
|
"nullable": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4030,6 +4030,20 @@
|
|||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"allOf": [
|
"allOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"text"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/RawTextContent"
|
"$ref": "#/components/schemas/RawTextContent"
|
||||||
}
|
}
|
||||||
@@ -4037,6 +4051,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allOf": [
|
"allOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/RawImageContent"
|
"$ref": "#/components/schemas/RawImageContent"
|
||||||
}
|
}
|
||||||
@@ -4044,6 +4072,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allOf": [
|
"allOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"resource"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/RawEmbeddedResource"
|
"$ref": "#/components/schemas/RawEmbeddedResource"
|
||||||
}
|
}
|
||||||
@@ -4051,6 +4093,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allOf": [
|
"allOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"audio"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/RawAudioContent"
|
"$ref": "#/components/schemas/RawAudioContent"
|
||||||
}
|
}
|
||||||
@@ -4058,6 +4114,129 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allOf": [
|
"allOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"resource_link"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/RawResource"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ContentBlock": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"text"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/RawTextContent"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/RawImageContent"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"resource"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/RawEmbeddedResource"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"audio"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/RawAudioContent"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"resource_link"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/RawResource"
|
"$ref": "#/components/schemas/RawResource"
|
||||||
}
|
}
|
||||||
@@ -7029,10 +7208,16 @@
|
|||||||
"Role": {
|
"Role": {
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"user"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"assistant"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -7765,13 +7950,22 @@
|
|||||||
"TaskSupport": {
|
"TaskSupport": {
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"forbidden"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"optional"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"required"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -54,9 +54,9 @@ export type CallToolRequest = {
|
|||||||
|
|
||||||
export type CallToolResponse = {
|
export type CallToolResponse = {
|
||||||
_meta?: unknown;
|
_meta?: unknown;
|
||||||
content: Array<Content>;
|
content: Array<ContentBlock>;
|
||||||
is_error: boolean;
|
isError: boolean;
|
||||||
structured_content?: unknown;
|
structuredContent?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChatRequest = {
|
export type ChatRequest = {
|
||||||
@@ -128,7 +128,29 @@ export type ConfirmToolActionRequest = {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Content = RawTextContent | RawImageContent | RawEmbeddedResource | RawAudioContent | RawResource;
|
export type Content = ({
|
||||||
|
type: 'text';
|
||||||
|
} & RawTextContent) | ({
|
||||||
|
type: 'image';
|
||||||
|
} & RawImageContent) | ({
|
||||||
|
type: 'resource';
|
||||||
|
} & RawEmbeddedResource) | ({
|
||||||
|
type: 'audio';
|
||||||
|
} & RawAudioContent) | ({
|
||||||
|
type: 'resource_link';
|
||||||
|
} & RawResource);
|
||||||
|
|
||||||
|
export type ContentBlock = ({
|
||||||
|
type: 'text';
|
||||||
|
} & RawTextContent) | ({
|
||||||
|
type: 'image';
|
||||||
|
} & RawImageContent) | ({
|
||||||
|
type: 'resource';
|
||||||
|
} & RawEmbeddedResource) | ({
|
||||||
|
type: 'audio';
|
||||||
|
} & RawAudioContent) | ({
|
||||||
|
type: 'resource_link';
|
||||||
|
} & RawResource);
|
||||||
|
|
||||||
export type Conversation = Array<Message>;
|
export type Conversation = Array<Message>;
|
||||||
|
|
||||||
@@ -1112,7 +1134,7 @@ export type RetryConfig = {
|
|||||||
timeout_seconds?: number | null;
|
timeout_seconds?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Role = string;
|
export type Role = 'user' | 'assistant';
|
||||||
|
|
||||||
export type RunNowResponse = {
|
export type RunNowResponse = {
|
||||||
session_id: string;
|
session_id: string;
|
||||||
@@ -1317,7 +1339,7 @@ export type SystemNotificationContent = {
|
|||||||
|
|
||||||
export type SystemNotificationType = 'thinkingMessage' | 'inlineMessage' | 'creditsExhausted';
|
export type SystemNotificationType = 'thinkingMessage' | 'inlineMessage' | 'creditsExhausted';
|
||||||
|
|
||||||
export type TaskSupport = string;
|
export type TaskSupport = 'forbidden' | 'optional' | 'required';
|
||||||
|
|
||||||
export type TelemetryEventRequest = {
|
export type TelemetryEventRequest = {
|
||||||
event_name: string;
|
event_name: string;
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import {
|
|||||||
McpAppToolCancelled,
|
McpAppToolCancelled,
|
||||||
McpAppToolInput,
|
McpAppToolInput,
|
||||||
McpAppToolInputPartial,
|
McpAppToolInputPartial,
|
||||||
McpAppToolResult,
|
|
||||||
DimensionLayout,
|
DimensionLayout,
|
||||||
OnDisplayModeChange,
|
OnDisplayModeChange,
|
||||||
SamplingCreateMessageParams,
|
SamplingCreateMessageParams,
|
||||||
@@ -142,7 +141,7 @@ interface McpAppRendererProps {
|
|||||||
sessionId?: string | null;
|
sessionId?: string | null;
|
||||||
toolInput?: McpAppToolInput;
|
toolInput?: McpAppToolInput;
|
||||||
toolInputPartial?: McpAppToolInputPartial;
|
toolInputPartial?: McpAppToolInputPartial;
|
||||||
toolResult?: McpAppToolResult;
|
toolResult?: CallToolResult;
|
||||||
toolCancelled?: McpAppToolCancelled;
|
toolCancelled?: McpAppToolCancelled;
|
||||||
append?: (text: string) => void;
|
append?: (text: string) => void;
|
||||||
displayMode?: GooseDisplayMode;
|
displayMode?: GooseDisplayMode;
|
||||||
@@ -505,14 +504,13 @@ export default function McpAppRenderer({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// rmcp serializes Content with a `type` discriminator via #[serde(tag = "type")].
|
|
||||||
// Our generated TS types don't reflect this, but the wire format matches CallToolResult.content.
|
|
||||||
return {
|
return {
|
||||||
content: (response.data?.content || []) as unknown as CallToolResult['content'],
|
content: (response.data?.content || []) as unknown as CallToolResult['content'],
|
||||||
isError: response.data?.is_error || false,
|
isError: response.data?.isError || false,
|
||||||
structuredContent: response.data?.structured_content as
|
structuredContent: response.data?.structuredContent as
|
||||||
| { [key: string]: unknown }
|
| { [key: string]: unknown }
|
||||||
| undefined,
|
| undefined,
|
||||||
|
_meta: response.data?._meta as { [key: string]: unknown } | undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[sessionId, extensionName]
|
[sessionId, extensionName]
|
||||||
@@ -685,17 +683,6 @@ export default function McpAppRenderer({
|
|||||||
effectiveDisplayModes,
|
effectiveDisplayModes,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const appToolResult = useMemo((): CallToolResult | undefined => {
|
|
||||||
if (!toolResult) return undefined;
|
|
||||||
// rmcp serializes Content with a `type` discriminator via #[serde(tag = "type")].
|
|
||||||
// Our generated TS types don't reflect this, but the wire format matches CallToolResult.content.
|
|
||||||
return {
|
|
||||||
content: toolResult.content as unknown as CallToolResult['content'],
|
|
||||||
structuredContent: toolResult.structuredContent as { [key: string]: unknown } | undefined,
|
|
||||||
_meta: toolResult._meta,
|
|
||||||
};
|
|
||||||
}, [toolResult]);
|
|
||||||
|
|
||||||
const isToolCancelled = !!toolCancelled;
|
const isToolCancelled = !!toolCancelled;
|
||||||
const isError = state.status === 'error';
|
const isError = state.status === 'error';
|
||||||
const isReady = state.status === 'ready';
|
const isReady = state.status === 'ready';
|
||||||
@@ -736,7 +723,7 @@ export default function McpAppRenderer({
|
|||||||
toolInputPartial={toolInputPartial ? { arguments: toolInputPartial.arguments } : undefined}
|
toolInputPartial={toolInputPartial ? { arguments: toolInputPartial.arguments } : undefined}
|
||||||
toolCancelled={isToolCancelled}
|
toolCancelled={isToolCancelled}
|
||||||
hostContext={hostContext}
|
hostContext={hostContext}
|
||||||
toolResult={appToolResult}
|
toolResult={toolResult}
|
||||||
onOpenLink={handleOpenLink}
|
onOpenLink={handleOpenLink}
|
||||||
onMessage={handleMessage}
|
onMessage={handleMessage}
|
||||||
onCallTool={handleCallTool}
|
onCallTool={handleCallTool}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type {
|
|||||||
McpUiToolCancelledNotification,
|
McpUiToolCancelledNotification,
|
||||||
McpUiDisplayMode,
|
McpUiDisplayMode,
|
||||||
} from '@modelcontextprotocol/ext-apps/app-bridge';
|
} from '@modelcontextprotocol/ext-apps/app-bridge';
|
||||||
import type { Content } from '../../api';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Space-separated sandbox tokens for iframe permissions.
|
* Space-separated sandbox tokens for iframe permissions.
|
||||||
@@ -37,12 +36,6 @@ export type McpAppToolInputPartial = McpUiToolInputPartialNotification['params']
|
|||||||
|
|
||||||
export type McpAppToolCancelled = McpUiToolCancelledNotification['params'];
|
export type McpAppToolCancelled = McpUiToolCancelledNotification['params'];
|
||||||
|
|
||||||
export type McpAppToolResult = {
|
|
||||||
content: Content[];
|
|
||||||
structuredContent?: unknown;
|
|
||||||
_meta?: { [key: string]: unknown };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback fired when the display mode changes, either via user-initiated
|
* Callback fired when the display mode changes, either via user-initiated
|
||||||
* host-side controls or app-initiated `ui/request-display-mode` changes.
|
* host-side controls or app-initiated `ui/request-display-mode` changes.
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ import { ChevronRight, FlaskConical } from 'lucide-react';
|
|||||||
import { TooltipWrapper } from './settings/providers/subcomponents/buttons/TooltipWrapper';
|
import { TooltipWrapper } from './settings/providers/subcomponents/buttons/TooltipWrapper';
|
||||||
import MCPUIResourceRenderer from './MCPUIResourceRenderer';
|
import MCPUIResourceRenderer from './MCPUIResourceRenderer';
|
||||||
import { isUIResource } from '@mcp-ui/client';
|
import { isUIResource } from '@mcp-ui/client';
|
||||||
import { CallToolResponse, Content, EmbeddedResource } from '../api';
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import { CallToolResponse, ContentBlock, EmbeddedResource } from '../api';
|
||||||
|
|
||||||
import McpAppRenderer from './McpApps/McpAppRenderer';
|
import McpAppRenderer from './McpApps/McpAppRenderer';
|
||||||
import ToolApprovalButtons from './ToolApprovalButtons';
|
import ToolApprovalButtons from './ToolApprovalButtons';
|
||||||
|
|
||||||
@@ -64,7 +66,7 @@ interface ToolCallWithResponseProps {
|
|||||||
isApprovalClicked?: boolean;
|
isApprovalClicked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getToolResultContent(toolResult: Record<string, unknown>): Content[] {
|
function getToolResultContent(toolResult: Record<string, unknown>): ContentBlock[] {
|
||||||
if (toolResult.status !== 'success') {
|
if (toolResult.status !== 'success') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -75,8 +77,11 @@ function getToolResultContent(toolResult: Record<string, unknown>): Content[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEmbeddedResource(content: Content): content is EmbeddedResource {
|
function isEmbeddedResource(
|
||||||
return 'resource' in content && typeof (content as Record<string, unknown>).resource === 'object';
|
content: ContentBlock
|
||||||
|
): content is EmbeddedResource & { type: 'resource' } {
|
||||||
|
const c = content as Record<string, unknown>;
|
||||||
|
return c.type === 'resource' && typeof c.resource === 'object' && c.resource !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface McpAppWrapperProps {
|
interface McpAppWrapperProps {
|
||||||
@@ -120,7 +125,9 @@ function McpAppWrapper({
|
|||||||
|
|
||||||
const resultWithMeta = toolResponse?.toolResult as ToolResultWithMeta | undefined;
|
const resultWithMeta = toolResponse?.toolResult as ToolResultWithMeta | undefined;
|
||||||
const toolResult =
|
const toolResult =
|
||||||
resultWithMeta?.status === 'success' && resultWithMeta.value ? resultWithMeta.value : undefined;
|
resultWithMeta?.status === 'success' && resultWithMeta.value
|
||||||
|
? (resultWithMeta.value as unknown as CallToolResult)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (!resourceUri) return null;
|
if (!resourceUri) return null;
|
||||||
if (requestWithMeta.toolCall.status !== 'success') return null;
|
if (requestWithMeta.toolCall.status !== 'success') return null;
|
||||||
@@ -217,13 +224,11 @@ export default function ToolCallWithResponse({
|
|||||||
!hasMcpAppResourceURI &&
|
!hasMcpAppResourceURI &&
|
||||||
toolResponse?.toolResult &&
|
toolResponse?.toolResult &&
|
||||||
getToolResultContent(toolResponse.toolResult).map((content, index) => {
|
getToolResultContent(toolResponse.toolResult).map((content, index) => {
|
||||||
const resourceContent = isEmbeddedResource(content)
|
if (!isEmbeddedResource(content)) return null;
|
||||||
? { ...content, type: 'resource' as const }
|
if (isUIResource(content)) {
|
||||||
: null;
|
|
||||||
if (resourceContent && isUIResource(resourceContent)) {
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="mt-3">
|
<div key={index} className="mt-3">
|
||||||
<MCPUIResourceRenderer content={resourceContent} appendPromptToChat={append} />
|
<MCPUIResourceRenderer content={content} appendPromptToChat={append} />
|
||||||
<div className="mt-3 p-4 py-3 border border-border-primary rounded-lg bg-background-secondary flex items-center">
|
<div className="mt-3 p-4 py-3 border border-border-primary rounded-lg bg-background-secondary flex items-center">
|
||||||
<FlaskConical className="mr-2" size={20} />
|
<FlaskConical className="mr-2" size={20} />
|
||||||
<div className="text-sm font-sans">
|
<div className="text-sm font-sans">
|
||||||
@@ -857,21 +862,22 @@ interface ToolResultViewProps {
|
|||||||
name: string;
|
name: string;
|
||||||
arguments: Record<string, unknown>;
|
arguments: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
result: Content;
|
result: ContentBlock;
|
||||||
isStartExpanded: boolean;
|
isStartExpanded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolResultView({ toolCall, result, isStartExpanded }: ToolResultViewProps) {
|
function ToolResultView({ toolCall, result, isStartExpanded }: ToolResultViewProps) {
|
||||||
const hasText = (c: Content): c is Content & { text: string } =>
|
const hasText = (c: ContentBlock): c is ContentBlock & { text: string } =>
|
||||||
'text' in c && typeof (c as Record<string, unknown>).text === 'string';
|
'text' in c && typeof (c as Record<string, unknown>).text === 'string';
|
||||||
|
|
||||||
const hasImage = (c: Content): c is Content & { data: string; mimeType: string } => {
|
const hasImage = (c: ContentBlock): c is ContentBlock & { data: string; mimeType: string } => {
|
||||||
if (!('data' in c && 'mimeType' in c)) return false;
|
if (!('data' in c && 'mimeType' in c)) return false;
|
||||||
const mimeType = (c as Record<string, unknown>).mimeType;
|
const mimeType = (c as Record<string, unknown>).mimeType;
|
||||||
return typeof mimeType === 'string' && mimeType.startsWith('image');
|
return typeof mimeType === 'string' && mimeType.startsWith('image');
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasResource = (c: Content): c is Content & { resource: unknown } => 'resource' in c;
|
const hasResource = (c: ContentBlock): c is ContentBlock & { resource: unknown } =>
|
||||||
|
'resource' in c;
|
||||||
|
|
||||||
const wrapMarkdown = (text: string): string => {
|
const wrapMarkdown = (text: string): string => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
Reference in New Issue
Block a user