Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/goose-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,10 @@ derive_utoipa!(Icon as IconSchema);
super::tunnel::TunnelInfo,
super::tunnel::TunnelState,
super::routes::telemetry::TelemetryEventRequest,
goose::goose_apps::McpAppResource,
goose::goose_apps::CspMetadata,
goose::goose_apps::UiMetadata,
goose::goose_apps::ResourceMetadata,
))
)]
pub struct ApiDoc;
Expand Down
3 changes: 3 additions & 0 deletions crates/goose-server/src/routes/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ pub struct CallToolResponse {
content: Vec<Content>,
structured_content: Option<Value>,
is_error: bool,
#[serde(skip_serializing_if = "Option::is_none")]
_meta: Option<Value>,
}

#[utoipa::path(
Expand Down Expand Up @@ -670,6 +672,7 @@ async fn call_tool(
content: result.content,
structured_content: result.structured_content,
is_error: result.is_error.unwrap_or(false),
_meta: None,
}))
}

Expand Down
9 changes: 9 additions & 0 deletions crates/goose/src/goose_apps/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! goose Apps module
//!
//! This module contains types and utilities for working with goose Apps,
//! which are UI resources that can be rendered in an MCP server or native
//! goose apps, or something in between.

pub mod resource;

pub use resource::{CspMetadata, McpAppResource, ResourceMetadata, UiMetadata};
117 changes: 117 additions & 0 deletions crates/goose/src/goose_apps/resource.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

/// Content Security Policy metadata for MCP Apps
/// Specifies allowed domains for network connections and resource loading
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CspMetadata {
/// Domains allowed for connect-src (fetch, XHR, WebSocket)
#[serde(skip_serializing_if = "Option::is_none")]
pub connect_domains: Option<Vec<String>>,
/// Domains allowed for resource loading (scripts, styles, images, fonts, media)
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_domains: Option<Vec<String>>,
}

/// UI-specific metadata for MCP resources
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct UiMetadata {
/// Content Security Policy configuration
#[serde(skip_serializing_if = "Option::is_none")]
pub csp: Option<CspMetadata>,
/// Preferred domain for the app (used for CORS)
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
/// Whether the app prefers to have a border around it
#[serde(skip_serializing_if = "Option::is_none")]
pub prefers_border: Option<bool>,
}

/// Resource metadata containing UI configuration
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ResourceMetadata {
/// UI-specific configuration
#[serde(skip_serializing_if = "Option::is_none")]
pub ui: Option<UiMetadata>,
}

/// MCP App Resource
/// Represents a UI resource that can be rendered in an MCP App
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct McpAppResource {
/// URI of the resource (must use ui:// scheme)
pub uri: String,
/// Human-readable name of the resource
pub name: String,
/// Optional description of what this resource does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// MIME type (should be "text/html;profile=mcp-app" for MCP Apps)
pub mime_type: String,
/// Text content of the resource (HTML for MCP Apps)
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
/// Base64-encoded binary content (alternative to text)
#[serde(skip_serializing_if = "Option::is_none")]
pub blob: Option<String>,
/// Resource metadata including UI configuration
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<ResourceMetadata>,
}

impl McpAppResource {
pub fn new_html(uri: String, name: String, html: String) -> Self {
Self {
uri,
name,
description: None,
mime_type: "text/html;profile=mcp-app".to_string(),
text: Some(html),
blob: None,
meta: None,
}
}

pub fn new_html_with_csp(
uri: String,
name: String,
html: String,
csp: CspMetadata,
) -> Self {
Self {
uri,
name,
description: None,
mime_type: "text/html;profile=mcp-app".to_string(),
text: Some(html),
blob: None,
meta: Some(ResourceMetadata {
ui: Some(UiMetadata {
csp: Some(csp),
domain: None,
prefers_border: None,
}),
}),
}
}

pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}

pub fn with_ui_metadata(mut self, ui_metadata: UiMetadata) -> Self {
if let Some(meta) = &mut self.meta {
meta.ui = Some(ui_metadata);
} else {
self.meta = Some(ResourceMetadata {
ui: Some(ui_metadata),
});
}
self
}
}
1 change: 1 addition & 0 deletions crates/goose/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod config;
pub mod context_mgmt;
pub mod conversation;
pub mod execution;
pub mod goose_apps;
pub mod hints;
pub mod logging;
pub mod mcp_utils;
Expand Down
109 changes: 109 additions & 0 deletions ui/desktop/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2668,6 +2668,9 @@
"is_error"
],
"properties": {
"_meta": {
"nullable": true
},
"content": {
"type": "array",
"items": {
Expand Down Expand Up @@ -2908,6 +2911,28 @@
}
}
},
"CspMetadata": {
"type": "object",
"description": "Content Security Policy metadata for MCP Apps\nSpecifies allowed domains for network connections and resource loading",
"properties": {
"connectDomains": {
"type": "array",
"items": {
"type": "string"
},
"description": "Domains allowed for connect-src (fetch, XHR, WebSocket)",
"nullable": true
},
"resourceDomains": {
"type": "array",
"items": {
"type": "string"
},
"description": "Domains allowed for resource loading (scripts, styles, images, fonts, media)",
"nullable": true
}
}
},
"DeclarativeProviderConfig": {
"type": "object",
"required": [
Expand Down Expand Up @@ -3700,6 +3725,52 @@
}
}
},
"McpAppResource": {
"type": "object",
"description": "MCP App Resource\nRepresents a UI resource that can be rendered in an MCP App",
"required": [
"uri",
"name",
"mimeType"
],
"properties": {
"_meta": {
"allOf": [
{
"$ref": "#/components/schemas/ResourceMetadata"
}
],
"nullable": true
},
"blob": {
"type": "string",
"description": "Base64-encoded binary content (alternative to text)",
"nullable": true
},
"description": {
"type": "string",
"description": "Optional description of what this resource does",
"nullable": true
},
"mimeType": {
"type": "string",
"description": "MIME type (should be \"text/html;profile=mcp-app\" for MCP Apps)"
},
"name": {
"type": "string",
"description": "Human-readable name of the resource"
},
"text": {
"type": "string",
"description": "Text content of the resource (HTML for MCP Apps)",
"nullable": true
},
"uri": {
"type": "string",
"description": "URI of the resource (must use ui:// scheme)"
}
}
},
"Message": {
"type": "object",
"description": "A message to or from an LLM",
Expand Down Expand Up @@ -4705,6 +4776,20 @@
}
]
},
"ResourceMetadata": {
"type": "object",
"description": "Resource metadata containing UI configuration",
"properties": {
"ui": {
"allOf": [
{
"$ref": "#/components/schemas/UiMetadata"
}
],
"nullable": true
}
}
},
"Response": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -5618,6 +5703,30 @@
"disabled"
]
},
"UiMetadata": {
"type": "object",
"description": "UI-specific metadata for MCP resources",
"properties": {
"csp": {
"allOf": [
{
"$ref": "#/components/schemas/CspMetadata"
}
],
"nullable": true
},
"domain": {
"type": "string",
"description": "Preferred domain for the app (used for CORS)",
"nullable": true
},
"prefersBorder": {
"type": "boolean",
"description": "Whether the app prefers to have a border around it",
"nullable": true
}
}
},
"UpdateCustomProviderRequest": {
"type": "object",
"required": [
Expand Down
Loading