ACP Migration: MCP/Goose Apps (#9988)

Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
Douwe Osinga
2026-06-25 10:06:49 -04:00
committed by GitHub
parent f1bf611a22
commit dc8aedfd6a
30 changed files with 934 additions and 1366 deletions
@@ -103,6 +103,45 @@ pub struct GooseToolCallResponse {
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/unstable/apps/list", response = AppsListResponse)]
#[serde(rename_all = "camelCase")]
pub struct AppsListRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct AppsListResponse {
#[serde(default)]
pub apps: Vec<serde_json::Value>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/unstable/apps/export", response = AppsExportResponse)]
#[serde(rename_all = "camelCase")]
pub struct AppsExportRequest {
pub name: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct AppsExportResponse {
pub html: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/unstable/apps/import", response = AppsImportResponse)]
#[serde(rename_all = "camelCase")]
pub struct AppsImportRequest {
pub html: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct AppsImportResponse {
pub name: String,
pub message: String,
}
/// Update the working directory for a session.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/unstable/session/working-dir/update", response = EmptyResponse)]
-13
View File
@@ -431,11 +431,6 @@ derive_utoipa!(IconTheme as IconThemeSchema);
super::routes::agent::restart_agent,
super::routes::agent::update_working_dir,
super::routes::agent::get_tools,
super::routes::agent::read_resource,
super::routes::agent::call_tool,
super::routes::agent::list_apps,
super::routes::agent::export_app,
super::routes::agent::import_app,
super::routes::agent::update_from_session,
super::routes::agent::agent_add_extension,
super::routes::agent::agent_remove_extension,
@@ -637,15 +632,7 @@ derive_utoipa!(IconTheme as IconThemeSchema);
super::routes::agent::UpdateProviderRequest,
super::routes::agent::UpdateSessionRequest,
super::routes::agent::GetToolsQuery,
super::routes::agent::ReadResourceRequest,
super::routes::agent::ReadResourceResponse,
super::routes::agent::CallToolRequest,
super::routes::agent::CallToolResponse,
ContentBlockSchema,
super::routes::agent::ListAppsRequest,
super::routes::agent::ListAppsResponse,
super::routes::agent::ImportAppRequest,
super::routes::agent::ImportAppResponse,
super::routes::agent::StartAgentRequest,
super::routes::agent::ResumeAgentRequest,
super::routes::agent::StopAgentRequest,
+1 -520
View File
@@ -12,10 +12,7 @@ use axum::{
Json, Router,
};
use goose::agents::{Container, ExtensionLoadResult};
use goose::goose_apps::{fetch_mcp_apps, GooseApp, McpAppCache};
use base64::Engine;
use goose::agents::reply_parts::is_tool_visible_to_app;
use goose::agents::ExtensionConfig;
use goose::config::resolve_extensions_for_new_session;
use goose::config::{Config, GooseMode};
@@ -28,14 +25,10 @@ use goose::{
agents::{extension::ToolInfo, extension_manager::get_parameter_names},
config::permission::PermissionLevel,
};
use rmcp::model::CallToolRequestParams;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use tracing::{error, warn};
use tracing::error;
#[derive(Deserialize, utoipa::ToSchema)]
pub struct UpdateFromSessionRequest {
@@ -116,59 +109,6 @@ pub struct SetContainerRequest {
container_id: Option<String>,
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ReadResourceRequest {
session_id: String,
extension_name: String,
uri: String,
}
#[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ReadResourceResponse {
uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
mime_type: Option<String>,
text: String,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
meta: Option<serde_json::Map<String, Value>>,
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct CallToolRequest {
session_id: String,
name: String,
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)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResponse {
#[schema(value_type = Vec<ContentBlock>)]
content: Vec<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
structured_content: Option<Value>,
is_error: bool,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "_meta")]
_meta: Option<Value>,
}
#[derive(Serialize, utoipa::ToSchema)]
pub struct ResumeAgentResponse {
pub session: Session,
@@ -1061,376 +1001,6 @@ async fn ensure_extensions_loaded(state: &AppState, session_id: &str) -> Result<
}
}
#[utoipa::path(
post,
path = "/agent/read_resource",
request_body = ReadResourceRequest,
responses(
(status = 200, description = "Resource read successfully", body = ReadResourceResponse),
(status = 401, description = "Unauthorized - invalid secret key"),
(status = 424, description = "Agent not initialized"),
(status = 404, description = "Resource not found"),
(status = 500, description = "Internal server error")
)
)]
async fn read_resource(
State(state): State<Arc<AppState>>,
Json(payload): Json<ReadResourceRequest>,
) -> Result<Json<ReadResourceResponse>, StatusCode> {
use rmcp::model::ResourceContents;
ensure_extensions_loaded(&state, &payload.session_id)
.await
.map_err(|err| err.status)?;
let agent = state
.get_agent_for_route(payload.session_id.clone())
.await?;
let read_result = agent
.extension_manager
.read_resource(
&payload.session_id,
&payload.uri,
&payload.extension_name,
CancellationToken::default(),
)
.await
.map_err(|e| {
tracing::error!(
"read_resource failed for session={}, uri={}, extension={}: {:?}",
payload.session_id,
payload.uri,
payload.extension_name,
e
);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let content = read_result
.contents
.into_iter()
.next()
.ok_or(StatusCode::NOT_FOUND)?;
let (uri, mime_type, text, meta) = match content {
ResourceContents::TextResourceContents {
uri,
mime_type,
text,
meta,
} => (uri, mime_type, text, meta),
ResourceContents::BlobResourceContents {
uri,
mime_type,
blob,
meta,
} => {
let decoded = match base64::engine::general_purpose::STANDARD.decode(&blob) {
Ok(bytes) => {
String::from_utf8(bytes).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
}
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
};
(uri, mime_type, decoded, meta)
}
};
let meta_map = meta.map(|m| m.0);
Ok(Json(ReadResourceResponse {
uri,
mime_type,
text,
meta: meta_map,
}))
}
#[utoipa::path(
post,
path = "/agent/call_tool",
request_body = CallToolRequest,
responses(
(status = 200, description = "Resource read successfully", body = CallToolResponse),
(status = 403, description = "Forbidden - tool is not app-visible", body = ErrorResponse),
(status = 401, description = "Unauthorized - invalid secret key"),
(status = 424, description = "Frontend tool execution requires the frontend host", body = ErrorResponse),
(status = 404, description = "Resource not found", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
)
)]
async fn call_tool(
State(state): State<Arc<AppState>>,
Json(payload): Json<CallToolRequest>,
) -> Result<Json<CallToolResponse>, ErrorResponse> {
ensure_extensions_loaded(&state, &payload.session_id).await?;
let agent = state
.get_agent_for_route(payload.session_id.clone())
.await?;
// Check app-side visibility: reject calls to tools that exclude "app"
let tools = agent.list_tools(&payload.session_id, None).await;
if let Some(tool) = tools.iter().find(|t| *t.name == payload.name) {
if !is_tool_visible_to_app(tool) {
warn!(
tool = %payload.name,
"Rejected app call to model-only tool"
);
return Err(ErrorResponse {
message: format!("Tool '{}' cannot be called by the app", payload.name),
status: StatusCode::FORBIDDEN,
});
}
}
if agent.is_frontend_tool(&payload.name).await {
return Err(ErrorResponse {
message: format!(
"Tool '{}' is provided by the frontend and must be executed by the frontend host",
payload.name
),
status: StatusCode::FAILED_DEPENDENCY,
});
}
let arguments = match payload.arguments {
Value::Object(map) => Some(map),
_ => None,
};
let tool_call = {
let mut params = CallToolRequestParams::new(payload.name);
if let Some(args) = arguments {
params = params.with_arguments(args);
}
params
};
let ctx = goose::agents::ToolCallContext::new(payload.session_id.clone(), None, None);
let tool_result = agent
.extension_manager
.dispatch_tool_call(&ctx, tool_call, CancellationToken::default())
.await
.map_err(ErrorResponse::from)?;
let result = tool_result.result.await.map_err(|err| ErrorResponse {
message: err.to_string(),
status: StatusCode::INTERNAL_SERVER_ERROR,
})?;
let content = result
.content
.into_iter()
.map(serde_json::to_value)
.collect::<Result<Vec<_>, _>>()
.map_err(ErrorResponse::from)?;
Ok(Json(CallToolResponse {
content,
structured_content: result.structured_content,
is_error: result.is_error.unwrap_or(false),
_meta: result.meta.and_then(|m| serde_json::to_value(m).ok()),
}))
}
#[derive(Deserialize, utoipa::IntoParams, utoipa::ToSchema)]
pub struct ListAppsRequest {
session_id: Option<String>,
}
#[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ListAppsResponse {
pub apps: Vec<GooseApp>,
}
#[utoipa::path(
get,
path = "/agent/list_apps",
params(
ListAppsRequest
),
responses(
(status = 200, description = "List of apps retrieved successfully", body = ListAppsResponse),
(status = 401, description = "Unauthorized - Invalid or missing API key", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse),
),
security(
("api_key" = [])
),
tag = "Agent"
)]
async fn list_apps(
State(state): State<Arc<AppState>>,
Query(params): Query<ListAppsRequest>,
) -> Result<Json<ListAppsResponse>, ErrorResponse> {
let cache = McpAppCache::new().ok();
let Some(session_id) = params.session_id else {
let apps = cache
.as_ref()
.and_then(|c| c.list_apps().ok())
.unwrap_or_default();
return Ok(Json(ListAppsResponse { apps }));
};
let agent = state
.get_agent_for_route(session_id.clone())
.await
.map_err(|status| ErrorResponse {
message: "Failed to get agent".to_string(),
status,
})?;
let apps = fetch_mcp_apps(&agent.extension_manager, &session_id)
.await
.map_err(|e| ErrorResponse {
message: format!("Failed to list apps: {}", e.message),
status: StatusCode::INTERNAL_SERVER_ERROR,
})?;
if let Some(cache) = cache.as_ref() {
let active_extensions: HashSet<String> = apps
.iter()
.flat_map(|app| app.mcp_servers.iter().cloned())
.collect();
for extension_name in active_extensions {
if let Err(e) = cache.delete_extension_apps(&extension_name) {
warn!(
"Failed to clean cache for extension {}: {}",
extension_name, e
);
}
}
for app in &apps {
if let Err(e) = cache.store_app(app) {
warn!("Failed to cache app {}: {}", app.resource.name, e);
}
}
}
Ok(Json(ListAppsResponse { apps }))
}
#[utoipa::path(
get,
path = "/agent/export_app/{name}",
params(
("name" = String, Path, description = "Name of the app to export")
),
responses(
(status = 200, description = "App HTML exported successfully", body = String),
(status = 404, description = "App not found", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse),
),
security(
("api_key" = [])
),
tag = "Agent"
)]
async fn export_app(
axum::extract::Path(name): axum::extract::Path<String>,
) -> Result<impl IntoResponse, ErrorResponse> {
let cache = McpAppCache::new().map_err(|e| ErrorResponse {
message: format!("Failed to access app cache: {}", e),
status: StatusCode::INTERNAL_SERVER_ERROR,
})?;
let apps = cache.list_apps().map_err(|e| ErrorResponse {
message: format!("Failed to list apps: {}", e),
status: StatusCode::INTERNAL_SERVER_ERROR,
})?;
let app = apps
.into_iter()
.find(|a| a.resource.name == name)
.ok_or_else(|| ErrorResponse {
message: format!("App '{}' not found", name),
status: StatusCode::NOT_FOUND,
})?;
let html = app.to_html().map_err(|e| ErrorResponse {
message: format!("Failed to generate HTML: {}", e),
status: StatusCode::INTERNAL_SERVER_ERROR,
})?;
Ok(html)
}
#[derive(Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportAppRequest {
pub html: String,
}
#[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportAppResponse {
pub name: String,
pub message: String,
}
#[utoipa::path(
post,
path = "/agent/import_app",
request_body = ImportAppRequest,
responses(
(status = 201, description = "App imported successfully", body = ImportAppResponse),
(status = 400, description = "Bad request - Invalid HTML", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse),
),
security(
("api_key" = [])
),
tag = "Agent"
)]
async fn import_app(
Json(body): Json<ImportAppRequest>,
) -> Result<(StatusCode, Json<ImportAppResponse>), ErrorResponse> {
let cache = McpAppCache::new().map_err(|e| ErrorResponse {
message: format!("Failed to access app cache: {}", e),
status: StatusCode::INTERNAL_SERVER_ERROR,
})?;
let mut app = GooseApp::from_html(&body.html).map_err(|e| ErrorResponse {
message: format!("Invalid Goose App HTML: {}", e),
status: StatusCode::BAD_REQUEST,
})?;
let original_name = app.resource.name.clone();
let mut counter = 1;
let existing_apps = cache.list_apps().unwrap_or_default();
let existing_names: HashSet<String> = existing_apps
.iter()
.map(|a| a.resource.name.clone())
.collect();
while existing_names.contains(&app.resource.name) {
app.resource.name = format!("{}_{}", original_name, counter);
app.resource.uri = format!("ui://apps/{}", app.resource.name);
counter += 1;
}
app.mcp_servers = vec!["apps".to_string()];
cache.store_app(&app).map_err(|e| ErrorResponse {
message: format!("Failed to store app: {}", e),
status: StatusCode::INTERNAL_SERVER_ERROR,
})?;
Ok((
StatusCode::CREATED,
Json(ImportAppResponse {
name: app.resource.name.clone(),
message: format!("App '{}' imported successfully", app.resource.name),
}),
))
}
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/agent/start", post(start_agent))
@@ -1438,11 +1008,6 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/agent/restart", post(restart_agent))
.route("/agent/update_working_dir", post(update_working_dir))
.route("/agent/tools", get(get_tools))
.route("/agent/read_resource", post(read_resource))
.route("/agent/call_tool", post(call_tool))
.route("/agent/list_apps", get(list_apps))
.route("/agent/export_app/{name}", get(export_app))
.route("/agent/import_app", post(import_app))
.route("/agent/update_provider", post(update_agent_provider))
.route("/agent/update_session", post(update_session))
.route("/agent/update_from_session", post(update_from_session))
@@ -1452,87 +1017,3 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/agent/stop", post(stop_agent))
.with_state(state)
}
#[cfg(test)]
mod tests {
use super::*;
use goose::config::GooseMode;
use goose::session::session_manager::SessionType;
use rmcp::model::Tool;
use rmcp::object;
fn frontend_extension() -> ExtensionConfig {
ExtensionConfig::Frontend {
name: "frontend-e2e".to_string(),
description: "Frontend test extension".to_string(),
tools: vec![Tool::new(
"frontend__echo".to_string(),
"Echo a string from the frontend".to_string(),
object!({
"type": "object",
"properties": {
"message": { "type": "string" }
},
"required": ["message"]
}),
)],
instructions: Some("Use the frontend echo tool.".to_string()),
bundled: None,
available_tools: vec![],
}
}
#[tokio::test]
async fn frontend_extensions_are_listed_and_rejected_cleanly_by_call_tool() {
let state = AppState::new(true).await.unwrap();
let session = state
.session_manager()
.create_session(
std::env::current_dir().unwrap(),
"frontend-route-test".to_string(),
SessionType::Hidden,
GooseMode::default(),
)
.await
.unwrap();
agent_add_extension(
State(state.clone()),
Json(AddExtensionRequest {
session_id: session.id.clone(),
config: frontend_extension(),
}),
)
.await
.unwrap();
let Json(tools) = get_tools(
State(state.clone()),
Query(GetToolsQuery {
extension_name: None,
session_id: session.id.clone(),
}),
)
.await
.unwrap();
assert!(tools.iter().any(|tool| tool.name == "frontend__echo"));
let error = match call_tool(
State(state),
Json(CallToolRequest {
session_id: session.id,
name: "frontend__echo".to_string(),
arguments: Value::Object(serde_json::Map::new()),
}),
)
.await
{
Ok(_) => panic!("frontend tools should not be callable through /agent/call_tool"),
Err(error) => error,
};
assert_eq!(error.status, StatusCode::FAILED_DEPENDENCY);
assert!(error.message.contains("frontend host"));
}
}
+15
View File
@@ -25,6 +25,21 @@
"requestType": "ReadResourceRequest_unstable",
"responseType": "ReadResourceResponse_unstable"
},
{
"method": "_goose/unstable/apps/list",
"requestType": "AppsListRequest_unstable",
"responseType": "AppsListResponse_unstable"
},
{
"method": "_goose/unstable/apps/export",
"requestType": "AppsExportRequest_unstable",
"responseType": "AppsExportResponse_unstable"
},
{
"method": "_goose/unstable/apps/import",
"requestType": "AppsImportRequest_unstable",
"responseType": "AppsImportResponse_unstable"
},
{
"method": "_goose/unstable/session/working-dir/update",
"requestType": "UpdateWorkingDirRequest_unstable",
+132
View File
@@ -484,6 +484,87 @@
"x-side": "agent",
"x-method": "_goose/unstable/resources/read"
},
"AppsListRequest_unstable": {
"type": "object",
"properties": {
"sessionId": {
"type": [
"string",
"null"
]
}
},
"x-side": "agent",
"x-method": "_goose/unstable/apps/list"
},
"AppsListResponse_unstable": {
"type": "object",
"properties": {
"apps": {
"type": "array",
"items": {},
"default": []
}
},
"x-side": "agent",
"x-method": "_goose/unstable/apps/list"
},
"AppsExportRequest_unstable": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
],
"x-side": "agent",
"x-method": "_goose/unstable/apps/export"
},
"AppsExportResponse_unstable": {
"type": "object",
"properties": {
"html": {
"type": "string"
}
},
"required": [
"html"
],
"x-side": "agent",
"x-method": "_goose/unstable/apps/export"
},
"AppsImportRequest_unstable": {
"type": "object",
"properties": {
"html": {
"type": "string"
}
},
"required": [
"html"
],
"x-side": "agent",
"x-method": "_goose/unstable/apps/import"
},
"AppsImportResponse_unstable": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"name",
"message"
],
"x-side": "agent",
"x-method": "_goose/unstable/apps/import"
},
"UpdateWorkingDirRequest_unstable": {
"type": "object",
"properties": {
@@ -5102,6 +5183,33 @@
"description": "Params for _goose/unstable/resources/read",
"title": "ReadResourceRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/AppsListRequest_unstable"
}
],
"description": "Params for _goose/unstable/apps/list",
"title": "AppsListRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/AppsExportRequest_unstable"
}
],
"description": "Params for _goose/unstable/apps/export",
"title": "AppsExportRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/AppsImportRequest_unstable"
}
],
"description": "Params for _goose/unstable/apps/import",
"title": "AppsImportRequest_unstable"
},
{
"allOf": [
{
@@ -5875,6 +5983,30 @@
],
"title": "ReadResourceResponse_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/AppsListResponse_unstable"
}
],
"title": "AppsListResponse_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/AppsExportResponse_unstable"
}
],
"title": "AppsExportResponse_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/AppsImportResponse_unstable"
}
],
"title": "AppsImportResponse_unstable"
},
{
"allOf": [
{
+8
View File
@@ -83,6 +83,7 @@ use uuid::Uuid;
mod agent_requests;
pub use agent_requests::agent_request_schemas;
mod agent_mentions;
mod apps;
mod config;
mod custom_dispatch;
mod diagnostics;
@@ -1004,6 +1005,7 @@ impl GooseAcpAgent {
fn initial_session_extensions(
&self,
config: &Config,
project_root: &Path,
mcp_servers: Vec<McpServer>,
goose_extensions: Option<Vec<GooseExtension>>,
recipe_extensions: Option<&[ExtensionConfig]>,
@@ -1025,6 +1027,11 @@ impl GooseAcpAgent {
for extension in get_enabled_extensions_with_config(config) {
push_or_replace_extension(&mut extensions, extension);
}
for extension in
crate::plugins::mcp_servers::enabled_plugin_mcp_servers(Some(project_root))
{
push_or_replace_extension(&mut extensions, extension);
}
} else {
for mcp_server in mcp_servers {
let extension = mcp_server_to_extension_config(mcp_server).map_err(|message| {
@@ -1177,6 +1184,7 @@ impl GooseAcpAgent {
) -> Result<ExtensionData, agent_client_protocol::Error> {
let extensions = self.initial_session_extensions(
config,
&session.working_dir,
mcp_servers,
goose_extensions,
recipe_extensions,
+121
View File
@@ -0,0 +1,121 @@
use super::*;
use crate::goose_apps::{fetch_mcp_apps, GooseApp, McpAppCache};
impl GooseAcpAgent {
pub(super) async fn on_list_apps(
&self,
req: AppsListRequest,
) -> Result<AppsListResponse, agent_client_protocol::Error> {
let cache = McpAppCache::new().ok();
let Some(session_id) = req.session_id else {
let apps = cache
.as_ref()
.and_then(|cache| cache.list_apps().ok())
.unwrap_or_default();
return Ok(AppsListResponse {
apps: apps_to_values(apps)?,
});
};
let agent = self.get_session_agent(&session_id).await?;
let apps = fetch_mcp_apps(&agent.extension_manager, &session_id)
.await
.map_err(|error| {
agent_client_protocol::Error::internal_error()
.data(format!("Failed to list apps: {}", error.message))
})?;
if let Some(cache) = cache.as_ref() {
let active_extensions = apps
.iter()
.flat_map(|app| app.mcp_servers.iter().cloned())
.collect::<std::collections::HashSet<_>>();
for extension_name in active_extensions {
if let Err(error) = cache.delete_extension_apps(&extension_name) {
warn!(
extension_name,
%error,
"Failed to clean MCP app cache for extension"
);
}
}
for app in &apps {
if let Err(error) = cache.store_app(app) {
warn!(app = %app.resource.name, %error, "Failed to cache MCP app");
}
}
}
Ok(AppsListResponse {
apps: apps_to_values(apps)?,
})
}
pub(super) async fn on_export_app(
&self,
req: AppsExportRequest,
) -> Result<AppsExportResponse, agent_client_protocol::Error> {
let cache = McpAppCache::new().internal_err_ctx("Failed to access app cache")?;
let apps = cache.list_apps().internal_err_ctx("Failed to list apps")?;
let app = apps
.into_iter()
.find(|app| app.resource.name == req.name)
.ok_or_else(|| {
agent_client_protocol::Error::resource_not_found(Some(req.name.clone()))
.data(format!("App '{}' not found", req.name))
})?;
let html = app
.to_html()
.map_err(|error| agent_client_protocol::Error::internal_error().data(error))?;
Ok(AppsExportResponse { html })
}
pub(super) async fn on_import_app(
&self,
req: AppsImportRequest,
) -> Result<AppsImportResponse, agent_client_protocol::Error> {
let cache = McpAppCache::new().internal_err_ctx("Failed to access app cache")?;
let mut app = GooseApp::from_html(&req.html)
.map_err(|error| agent_client_protocol::Error::invalid_params().data(error))?;
let original_name = app.resource.name.clone();
let mut counter = 1;
let existing_names = cache
.list_apps()
.unwrap_or_default()
.into_iter()
.map(|app| app.resource.name)
.collect::<std::collections::HashSet<_>>();
while existing_names.contains(&app.resource.name) {
app.resource.name = format!("{}_{}", original_name, counter);
app.resource.uri = format!("ui://apps/{}", app.resource.name);
counter += 1;
}
app.mcp_servers = vec!["apps".to_string()];
let name = app.resource.name.clone();
cache
.store_app(&app)
.internal_err_ctx("Failed to store imported app")?;
Ok(AppsImportResponse {
name: name.clone(),
message: format!("App '{}' imported successfully", name),
})
}
}
fn apps_to_values(
apps: Vec<GooseApp>,
) -> Result<Vec<serde_json::Value>, agent_client_protocol::Error> {
apps.into_iter()
.map(serde_json::to_value)
.collect::<Result<Vec<_>, _>>()
.internal_err()
}
@@ -58,6 +58,30 @@ impl GooseAcpAgent {
self.on_read_resource(req).await
}
#[custom_method(AppsListRequest)]
async fn dispatch_list_apps(
&self,
req: AppsListRequest,
) -> Result<AppsListResponse, agent_client_protocol::Error> {
self.on_list_apps(req).await
}
#[custom_method(AppsExportRequest)]
async fn dispatch_export_app(
&self,
req: AppsExportRequest,
) -> Result<AppsExportResponse, agent_client_protocol::Error> {
self.on_export_app(req).await
}
#[custom_method(AppsImportRequest)]
async fn dispatch_import_app(
&self,
req: AppsImportRequest,
) -> Result<AppsImportResponse, agent_client_protocol::Error> {
self.on_import_app(req).await
}
#[custom_method(UpdateWorkingDirRequest)]
async fn dispatch_update_working_dir(
&self,
+3
View File
@@ -54,6 +54,9 @@ module.exports = [
__dirname: 'readonly',
require: 'readonly',
module: 'readonly',
Buffer: 'readonly',
TextDecoder: 'readonly',
TextEncoder: 'readonly',
// Browser globals
window: 'readonly',
document: 'readonly',
-415
View File
@@ -86,288 +86,6 @@
}
}
},
"/agent/call_tool": {
"post": {
"tags": [
"super::routes::agent"
],
"operationId": "call_tool",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CallToolRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Resource read successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CallToolResponse"
}
}
}
},
"401": {
"description": "Unauthorized - invalid secret key"
},
"403": {
"description": "Forbidden - tool is not app-visible",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"404": {
"description": "Resource not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"424": {
"description": "Frontend tool execution requires the frontend host",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/agent/export_app/{name}": {
"get": {
"tags": [
"Agent"
],
"operationId": "export_app",
"parameters": [
{
"name": "name",
"in": "path",
"description": "Name of the app to export",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "App HTML exported successfully",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
},
"404": {
"description": "App not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/agent/import_app": {
"post": {
"tags": [
"Agent"
],
"operationId": "import_app",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ImportAppRequest"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "App imported successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ImportAppResponse"
}
}
}
},
"400": {
"description": "Bad request - Invalid HTML",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/agent/list_apps": {
"get": {
"tags": [
"Agent"
],
"operationId": "list_apps",
"parameters": [
{
"name": "session_id",
"in": "query",
"required": false,
"schema": {
"type": "string",
"nullable": true
}
}
],
"responses": {
"200": {
"description": "List of apps retrieved successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ListAppsResponse"
}
}
}
},
"401": {
"description": "Unauthorized - Invalid or missing API key",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/agent/read_resource": {
"post": {
"tags": [
"super::routes::agent"
],
"operationId": "read_resource",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ReadResourceRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Resource read successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ReadResourceResponse"
}
}
}
},
"401": {
"description": "Unauthorized - invalid secret key"
},
"404": {
"description": "Resource not found"
},
"424": {
"description": "Agent not initialized"
},
"500": {
"description": "Internal server error"
}
}
}
},
"/agent/remove_extension": {
"post": {
"tags": [
@@ -3892,47 +3610,6 @@
}
}
},
"CallToolRequest": {
"type": "object",
"required": [
"session_id",
"name",
"arguments"
],
"properties": {
"arguments": {},
"name": {
"type": "string"
},
"session_id": {
"type": "string"
}
}
},
"CallToolResponse": {
"type": "object",
"required": [
"content",
"isError"
],
"properties": {
"_meta": {
"nullable": true
},
"content": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ContentBlock"
}
},
"isError": {
"type": "boolean"
},
"structuredContent": {
"nullable": true
}
}
},
"CancelRequest": {
"type": "object",
"required": [
@@ -5766,32 +5443,6 @@
}
}
},
"ImportAppRequest": {
"type": "object",
"required": [
"html"
],
"properties": {
"html": {
"type": "string"
}
}
},
"ImportAppResponse": {
"type": "object",
"required": [
"name",
"message"
],
"properties": {
"message": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"ImportSessionNostrRequest": {
"type": "object",
"required": [
@@ -5855,29 +5506,6 @@
}
}
},
"ListAppsRequest": {
"type": "object",
"properties": {
"session_id": {
"type": "string",
"nullable": true
}
}
},
"ListAppsResponse": {
"type": "object",
"required": [
"apps"
],
"properties": {
"apps": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GooseApp"
}
}
}
},
"ListRecipeResponse": {
"type": "object",
"required": [
@@ -7348,49 +6976,6 @@
}
}
},
"ReadResourceRequest": {
"type": "object",
"required": [
"session_id",
"extension_name",
"uri"
],
"properties": {
"extension_name": {
"type": "string"
},
"session_id": {
"type": "string"
},
"uri": {
"type": "string"
}
}
},
"ReadResourceResponse": {
"type": "object",
"required": [
"uri",
"text"
],
"properties": {
"_meta": {
"type": "object",
"additionalProperties": {},
"nullable": true
},
"mimeType": {
"type": "string",
"nullable": true
},
"text": {
"type": "string"
},
"uri": {
"type": "string"
}
}
},
"Recipe": {
"type": "object",
"required": [
@@ -0,0 +1,207 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getAcpClient } from '../acpConnection';
import {
callMcpAppTool,
exportMcpApp,
importMcpApp,
listMcpApps,
listMcpAppTools,
readMcpAppResource,
} from '../mcp-apps';
vi.mock('../acpConnection', () => ({
getAcpClient: vi.fn(),
}));
function createClient() {
return {
goose: {
resourcesRead_unstable: vi.fn(),
toolsCall_unstable: vi.fn(),
toolsList_unstable: vi.fn(),
appsList_unstable: vi.fn(),
appsExport_unstable: vi.fn(),
appsImport_unstable: vi.fn(),
},
};
}
describe('ACP MCP app helpers', () => {
let client: ReturnType<typeof createClient>;
beforeEach(() => {
vi.clearAllMocks();
client = createClient();
vi.mocked(getAcpClient).mockResolvedValue(
client as unknown as Awaited<ReturnType<typeof getAcpClient>>
);
});
it('flattens ACP resource reads into the renderer resource shape', async () => {
client.goose.resourcesRead_unstable.mockResolvedValue({
result: {
contents: [
{
uri: 'ui://weather/panel',
mimeType: 'text/html;profile=mcp-app',
text: '<main>Weather</main>',
_meta: {
ui: {
csp: {
connectDomains: ['https://api.example.com'],
},
prefersBorder: false,
},
},
},
],
},
});
const resource = await readMcpAppResource('session-1', 'weather', 'ui://weather/panel');
expect(client.goose.resourcesRead_unstable).toHaveBeenCalledWith({
sessionId: 'session-1',
extensionName: 'weather',
uri: 'ui://weather/panel',
});
expect(resource).toEqual({
uri: 'ui://weather/panel',
mimeType: 'text/html;profile=mcp-app',
text: '<main>Weather</main>',
_meta: {
ui: {
csp: {
connectDomains: ['https://api.example.com'],
},
prefersBorder: false,
},
},
});
});
it('decodes blob resources as UTF-8 text', async () => {
client.goose.resourcesRead_unstable.mockResolvedValue({
result: {
contents: [
{
uri: 'ui://weather/panel',
mimeType: 'text/html;profile=mcp-app',
blob: Buffer.from('<main>São Paulo 東京</main>', 'utf8').toString('base64'),
},
],
},
});
const resource = await readMcpAppResource('session-1', 'weather', 'ui://weather/panel');
expect(resource.text).toBe('<main>São Paulo 東京</main>');
});
it('prefixes app tool calls before sending them over ACP', async () => {
client.goose.toolsCall_unstable.mockResolvedValue({
content: [{ type: 'text', text: 'done' }],
structuredContent: { ok: true },
isError: false,
_meta: { traceId: 'trace-1' },
});
const result = await callMcpAppTool('session-1', 'weather', 'refresh', { city: 'Amsterdam' });
expect(client.goose.toolsCall_unstable).toHaveBeenCalledWith({
sessionId: 'session-1',
name: 'weather__refresh',
arguments: { city: 'Amsterdam' },
});
expect(result).toEqual({
content: [{ type: 'text', text: 'done' }],
structuredContent: { ok: true },
isError: false,
_meta: { traceId: 'trace-1' },
});
});
it('maps and filters ACP tools for app host context', async () => {
client.goose.toolsList_unstable.mockResolvedValue({
tools: [
{
name: 'weather__refresh',
description: 'Refresh weather',
inputSchema: {
type: 'object',
properties: {
city: { type: 'string' },
},
},
},
{
name: 'calendar__refresh',
description: 'Refresh calendar',
inputSchema: { type: 'object' },
},
],
});
const tools = await listMcpAppTools('session-1', 'weather');
expect(client.goose.toolsList_unstable).toHaveBeenCalledWith({ sessionId: 'session-1' });
expect(tools).toEqual([
{
name: 'weather__refresh',
description: 'Refresh weather',
parameters: [],
input_schema: {
type: 'object',
properties: {
city: { type: 'string' },
},
},
},
]);
});
it('lists apps through ACP', async () => {
client.goose.appsList_unstable.mockResolvedValue({
apps: [
{
uri: 'ui://apps/weather',
name: 'weather',
mimeType: 'text/html;profile=mcp-app',
text: '<main>Weather</main>',
mcpServers: ['apps'],
},
],
});
const apps = await listMcpApps('session-1');
expect(client.goose.appsList_unstable).toHaveBeenCalledWith({ sessionId: 'session-1' });
expect(apps).toEqual([
{
uri: 'ui://apps/weather',
name: 'weather',
mimeType: 'text/html;profile=mcp-app',
text: '<main>Weather</main>',
mcpServers: ['apps'],
},
]);
});
it('imports and exports apps through ACP', async () => {
client.goose.appsExport_unstable.mockResolvedValue({
html: '<html><body>Weather</body></html>',
});
client.goose.appsImport_unstable.mockResolvedValue({
name: 'weather',
message: 'ok',
});
await expect(exportMcpApp('weather')).resolves.toBe('<html><body>Weather</body></html>');
await importMcpApp('<html><body>Weather</body></html>');
expect(client.goose.appsExport_unstable).toHaveBeenCalledWith({ name: 'weather' });
expect(client.goose.appsImport_unstable).toHaveBeenCalledWith({
html: '<html><body>Weather</body></html>',
});
});
});
+9 -2
View File
@@ -3,7 +3,7 @@ import type {
ToolCall,
ToolCallUpdate,
} from '@agentclientprotocol/sdk';
import type { CallToolResponse, ContentBlock as ApiContentBlock, Message } from '../../api';
import type { ContentBlock as ApiContentBlock, Message } from '../../api';
import { findMessageForChunk } from './messages';
import { toolNotificationChange } from './toolNotifications';
import {
@@ -182,7 +182,7 @@ function baseToolMetadata(
function toolResultValue(
update: ToolCallUpdate,
mcpAppMeta: DesktopMcpAppMeta | undefined
): CallToolResponse {
): ToolResultValue {
return {
content: toolResultContent(update),
isError: false,
@@ -302,6 +302,13 @@ interface DesktopMcpAppMeta extends Record<string, unknown> {
toolName?: string;
}
type ToolResultValue = {
content: ApiContentBlock[];
structuredContent?: unknown;
isError: boolean;
_meta?: DesktopMcpAppMeta;
};
function mcpAppMetadata(update: ToolCallUpdate): DesktopMcpAppMeta | undefined {
if (!isRecord(update._meta)) {
return undefined;
+149
View File
@@ -0,0 +1,149 @@
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import type { GooseApp, ToolInfo } from '../api';
import { getAcpClient } from './acpConnection';
type JsonRecord = Record<string, unknown>;
export type McpAppResourceResponse = {
uri: string;
mimeType: string | null;
text: string;
_meta?: Record<string, unknown>;
};
type ToolCallResponseLike = {
content?: Array<unknown>;
structuredContent?: unknown;
isError?: boolean;
_meta?: unknown;
};
function isRecord(value: unknown): value is JsonRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function stringField(record: JsonRecord, key: string): string | undefined {
const value = record[key];
return typeof value === 'string' ? value : undefined;
}
function metaField(record: JsonRecord): McpAppResourceResponse['_meta'] {
const meta = record._meta ?? record.meta;
return isRecord(meta) ? meta : undefined;
}
function decodeBase64Text(blob: string): string {
let bytes: Uint8Array;
if (typeof globalThis.atob === 'function') {
const binary = globalThis.atob(blob);
bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
} else {
bytes = Uint8Array.from(Buffer.from(blob, 'base64'));
}
return new TextDecoder().decode(bytes);
}
function flattenReadResourceResult(result: unknown, fallbackUri: string): McpAppResourceResponse {
const contents = isRecord(result) && Array.isArray(result.contents) ? result.contents : [];
const first = contents.find(isRecord);
if (!first) {
throw new Error(`Resource '${fallbackUri}' returned no contents`);
}
const uri = stringField(first, 'uri') ?? fallbackUri;
const mimeType = stringField(first, 'mimeType') ?? stringField(first, 'mime_type') ?? null;
const text = stringField(first, 'text') ?? decodeBase64Text(stringField(first, 'blob') ?? '');
return {
uri,
mimeType,
text,
_meta: metaField(first),
};
}
function acpToolToToolInfo(value: unknown): ToolInfo | null {
if (!isRecord(value)) return null;
const name = stringField(value, 'name');
if (!name) return null;
const inputSchema = value.inputSchema ?? value.input_schema;
return {
name,
description: stringField(value, 'description') ?? '',
parameters: [],
input_schema: isRecord(inputSchema) ? inputSchema : undefined,
};
}
function acpApp(value: unknown): GooseApp | null {
if (!isRecord(value)) return null;
return value as GooseApp;
}
export async function listMcpApps(sessionId?: string): Promise<GooseApp[]> {
const client = await getAcpClient();
const response = await client.goose.appsList_unstable(sessionId ? { sessionId } : {});
return (response.apps ?? []).map(acpApp).filter((app): app is GooseApp => !!app);
}
export async function exportMcpApp(name: string): Promise<string> {
const client = await getAcpClient();
const response = await client.goose.appsExport_unstable({ name });
return response.html;
}
export async function importMcpApp(html: string): Promise<void> {
const client = await getAcpClient();
await client.goose.appsImport_unstable({ html });
}
export async function listMcpAppTools(
sessionId: string,
extensionName?: string
): Promise<ToolInfo[] | null> {
const client = await getAcpClient();
const response = await client.goose.toolsList_unstable({ sessionId });
const tools = response.tools.map(acpToolToToolInfo).filter((tool): tool is ToolInfo => !!tool);
if (!extensionName) return tools;
const prefix = `${extensionName}__`;
return tools.filter((tool) => tool.name.startsWith(prefix));
}
export async function readMcpAppResource(
sessionId: string,
extensionName: string,
uri: string
): Promise<McpAppResourceResponse> {
const client = await getAcpClient();
const response = await client.goose.resourcesRead_unstable({
sessionId,
uri,
extensionName,
});
return flattenReadResourceResult(response.result, uri);
}
export async function callMcpAppTool(
sessionId: string,
extensionName: string,
name: string,
args: Record<string, unknown> | undefined
): Promise<CallToolResult> {
const fullToolName = `${extensionName}__${name}`;
const client = await getAcpClient();
const response = await client.goose.toolsCall_unstable({
sessionId,
name: fullToolName,
arguments: args || {},
});
return callToolResponseToMcpResult(response);
}
function callToolResponseToMcpResult(response: ToolCallResponseLike | undefined): CallToolResult {
return {
content: (response?.content || []) as unknown as CallToolResult['content'],
isError: response?.isError || false,
structuredContent: response?.structuredContent as { [key: string]: unknown } | undefined,
_meta: response?._meta as { [key: string]: unknown } | undefined,
};
}
+5
View File
@@ -251,6 +251,11 @@ export async function acpDeleteSession(sessionId: string): Promise<void> {
await client.goose.sessionDelete({ sessionId });
}
export async function acpCloseSession(sessionId: string): Promise<void> {
const client = await getAcpClient();
await client.unstable_closeSession({ sessionId });
}
export async function acpRenameSession(sessionId: string, title: string): Promise<void> {
const client = await getAcpClient();
await client.goose.sessionRename_unstable({ sessionId, title });
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-215
View File
@@ -42,19 +42,6 @@ export type Author = {
metadata?: string | null;
};
export type CallToolRequest = {
arguments: unknown;
name: string;
session_id: string;
};
export type CallToolResponse = {
_meta?: unknown;
content: Array<ContentBlock>;
isError: boolean;
structuredContent?: unknown;
};
export type CancelRequest = {
request_id: string;
};
@@ -661,15 +648,6 @@ export type ImageContent = {
mimeType: string;
};
export type ImportAppRequest = {
html: string;
};
export type ImportAppResponse = {
message: string;
name: string;
};
export type ImportSessionNostrRequest = {
deeplink: string;
};
@@ -694,14 +672,6 @@ export type KillJobResponse = {
message: string;
};
export type ListAppsRequest = {
session_id?: string | null;
};
export type ListAppsResponse = {
apps: Array<GooseApp>;
};
export type ListRecipeResponse = {
manifests: Array<RecipeManifest>;
};
@@ -1188,21 +1158,6 @@ export type RawTextContent = {
text: string;
};
export type ReadResourceRequest = {
extension_name: string;
session_id: string;
uri: string;
};
export type ReadResourceResponse = {
_meta?: {
[key: string]: unknown;
} | null;
mimeType?: string | null;
text: string;
uri: string;
};
export type Recipe = {
activities?: Array<string> | null;
author?: Author | null;
@@ -1922,176 +1877,6 @@ export type AgentAddExtensionResponses = {
export type AgentAddExtensionResponse = AgentAddExtensionResponses[keyof AgentAddExtensionResponses];
export type CallToolData = {
body: CallToolRequest;
path?: never;
query?: never;
url: '/agent/call_tool';
};
export type CallToolErrors = {
/**
* Unauthorized - invalid secret key
*/
401: unknown;
/**
* Forbidden - tool is not app-visible
*/
403: ErrorResponse;
/**
* Resource not found
*/
404: ErrorResponse;
/**
* Frontend tool execution requires the frontend host
*/
424: ErrorResponse;
/**
* Internal server error
*/
500: ErrorResponse;
};
export type CallToolError = CallToolErrors[keyof CallToolErrors];
export type CallToolResponses = {
/**
* Resource read successfully
*/
200: CallToolResponse;
};
export type CallToolResponse2 = CallToolResponses[keyof CallToolResponses];
export type ExportAppData = {
body?: never;
path: {
/**
* Name of the app to export
*/
name: string;
};
query?: never;
url: '/agent/export_app/{name}';
};
export type ExportAppErrors = {
/**
* App not found
*/
404: ErrorResponse;
/**
* Internal server error
*/
500: ErrorResponse;
};
export type ExportAppError = ExportAppErrors[keyof ExportAppErrors];
export type ExportAppResponses = {
/**
* App HTML exported successfully
*/
200: string;
};
export type ExportAppResponse = ExportAppResponses[keyof ExportAppResponses];
export type ImportAppData = {
body: ImportAppRequest;
path?: never;
query?: never;
url: '/agent/import_app';
};
export type ImportAppErrors = {
/**
* Bad request - Invalid HTML
*/
400: ErrorResponse;
/**
* Internal server error
*/
500: ErrorResponse;
};
export type ImportAppError = ImportAppErrors[keyof ImportAppErrors];
export type ImportAppResponses = {
/**
* App imported successfully
*/
201: ImportAppResponse;
};
export type ImportAppResponse2 = ImportAppResponses[keyof ImportAppResponses];
export type ListAppsData = {
body?: never;
path?: never;
query?: {
session_id?: string | null;
};
url: '/agent/list_apps';
};
export type ListAppsErrors = {
/**
* Unauthorized - Invalid or missing API key
*/
401: ErrorResponse;
/**
* Internal server error
*/
500: ErrorResponse;
};
export type ListAppsError = ListAppsErrors[keyof ListAppsErrors];
export type ListAppsResponses = {
/**
* List of apps retrieved successfully
*/
200: ListAppsResponse;
};
export type ListAppsResponse2 = ListAppsResponses[keyof ListAppsResponses];
export type ReadResourceData = {
body: ReadResourceRequest;
path?: never;
query?: never;
url: '/agent/read_resource';
};
export type ReadResourceErrors = {
/**
* Unauthorized - invalid secret key
*/
401: unknown;
/**
* Resource not found
*/
404: unknown;
/**
* Agent not initialized
*/
424: unknown;
/**
* Internal server error
*/
500: unknown;
};
export type ReadResourceResponses = {
/**
* Resource read successfully
*/
200: ReadResourceResponse;
};
export type ReadResourceResponse2 = ReadResourceResponses[keyof ReadResourceResponses];
export type AgentRemoveExtensionData = {
body: RemoveExtensionRequest;
path?: never;
@@ -26,7 +26,7 @@ import type {
import type { CallToolResult, JSONRPCRequest, Tool } from '@modelcontextprotocol/sdk/types.js';
import { GripHorizontal, Maximize2, PictureInPicture2, X } from 'lucide-react';
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
import { callTool, readResource } from '../../api';
import { callMcpAppTool, readMcpAppResource } from '../../acp/mcp-apps';
import { getCachedTools } from './toolsCache';
import { AppEvents } from '../../constants/events';
import { useTheme } from '../../contexts/ThemeContext';
@@ -453,18 +453,11 @@ export default function McpAppRenderer({
if (cancelled) return;
try {
const response = await readResource({
body: {
session_id: sessionId,
uri: resourceUri,
extension_name: extensionName,
},
});
const content = await readMcpAppResource(sessionId, extensionName, resourceUri);
if (cancelled) return;
if (response.data) {
const content = response.data;
if (content) {
const rawMeta = content._meta as
| {
ui?: {
@@ -548,33 +541,36 @@ export default function McpAppRenderer({
});
}, [state.status, pendingCsp, intl]);
const handleOpenLink = useCallback(async ({ url }: { url: string }) => {
if (isProtocolSafe(url)) {
const handleOpenLink = useCallback(
async ({ url }: { url: string }) => {
if (isProtocolSafe(url)) {
await window.electron.openExternal(url);
return { status: 'success' as const };
}
const protocol = getProtocol(url);
if (!protocol) {
return { status: 'error' as const, message: intl.formatMessage(i18n.invalidUrl) };
}
const result = await window.electron.showMessageBox({
type: 'question',
buttons: [intl.formatMessage(i18n.cancelButton), intl.formatMessage(i18n.openButton)],
defaultId: 0,
title: intl.formatMessage(i18n.openExternalLinkTitle),
message: intl.formatMessage(i18n.openProtocolLink, { protocol }),
detail: intl.formatMessage(i18n.openLinkDetail, { url }),
});
if (result.response !== 1) {
return { status: 'error' as const, message: 'User cancelled' };
}
await window.electron.openExternal(url);
return { status: 'success' as const };
}
const protocol = getProtocol(url);
if (!protocol) {
return { status: 'error' as const, message: intl.formatMessage(i18n.invalidUrl) };
}
const result = await window.electron.showMessageBox({
type: 'question',
buttons: [intl.formatMessage(i18n.cancelButton), intl.formatMessage(i18n.openButton)],
defaultId: 0,
title: intl.formatMessage(i18n.openExternalLinkTitle),
message: intl.formatMessage(i18n.openProtocolLink, { protocol }),
detail: intl.formatMessage(i18n.openLinkDetail, { url }),
});
if (result.response !== 1) {
return { status: 'error' as const, message: 'User cancelled' };
}
await window.electron.openExternal(url);
return { status: 'success' as const };
}, [intl]);
},
[intl]
);
const handleMessage = useCallback(
async ({ content }: { content: Array<{ type: string; text?: string }> }) => {
@@ -606,24 +602,7 @@ export default function McpAppRenderer({
if (!sessionId) {
throw new Error('Session not initialized for MCP request');
}
const fullToolName = `${extensionName}__${name}`;
const response = await callTool({
body: {
session_id: sessionId,
name: fullToolName,
arguments: args || {},
},
});
return {
content: (response.data?.content || []) as unknown as CallToolResult['content'],
isError: response.data?.isError || false,
structuredContent: response.data?.structuredContent as
| { [key: string]: unknown }
| undefined,
_meta: response.data?._meta as { [key: string]: unknown } | undefined,
};
return callMcpAppTool(sessionId, extensionName, name, args);
},
[sessionId, extensionName]
);
@@ -633,14 +612,7 @@ export default function McpAppRenderer({
if (!sessionId) {
throw new Error('Session not initialized for MCP request');
}
const response = await readResource({
body: {
session_id: sessionId,
uri,
extension_name: extensionName,
},
});
const data = response.data;
const data = await readMcpAppResource(sessionId, extensionName, uri);
if (!data) {
return { contents: [] };
}
@@ -10,7 +10,7 @@
*/
import type { ToolInfo } from '../../api/types.gen';
import { getTools } from '../../api';
import { listMcpAppTools } from '../../acp/mcp-apps';
type ToolsList = Array<ToolInfo>;
@@ -28,15 +28,11 @@ export function getCachedTools(
const existing = cache.get(key);
if (existing) return existing;
const promise = getTools({
query: { session_id: sessionId, extension_name: extensionName || undefined },
})
.then((response) => response.data ?? null)
.catch(() => {
// Evict on failure so the next caller retries
cache.delete(key);
return null;
});
const promise = listMcpAppTools(sessionId, extensionName || undefined).catch(() => {
// Evict on failure so the next caller retries
cache.delete(key);
return null;
});
cache.set(key, promise);
return promise;
@@ -18,7 +18,7 @@ import { TooltipWrapper } from './settings/providers/subcomponents/buttons/Toolt
import MCPUIResourceRenderer from './MCPUIResourceRenderer';
import { isUIResource } from '@mcp-ui/client';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { CallToolResponse, ContentBlock, EmbeddedResource } from '../api';
import type { ContentBlock, EmbeddedResource } from '../api';
import McpAppRenderer from './McpApps/McpAppRenderer';
import ToolApprovalButtons from './ToolApprovalButtons';
@@ -76,9 +76,16 @@ type UiMeta = {
subagent_session_id?: string;
};
type ToolResultValue = {
content: ContentBlock[];
structuredContent?: unknown;
isError: boolean;
_meta?: UiMeta;
};
type ToolResultWithMeta = {
status?: string;
value?: CallToolResponse & {
value?: ToolResultValue & {
_meta?: UiMeta;
};
};
@@ -138,7 +145,7 @@ function getToolResultContent(toolResult: Record<string, unknown>): ContentBlock
if (toolResult.status !== 'success') {
return [];
}
const value = toolResult.value as CallToolResponse;
const value = toolResult.value as ToolResultValue;
return value.content.filter((item) => {
const annotations = (item as { annotations?: { audience?: string[] } }).annotations;
return !annotations?.audience || annotations.audience.includes('user');
@@ -874,7 +881,11 @@ function ToolCallView({
<>
{toolResults.map((result, index) => (
<div key={index} className={cn('border-t border-border-primary')}>
<ToolResultView toolCall={toolCall} result={result} isStartExpanded={isExpandToolDetails} />
<ToolResultView
toolCall={toolCall}
result={result}
isStartExpanded={isExpandToolDetails}
/>
</div>
))}
</>
@@ -1080,9 +1091,10 @@ function ToolLogsView({
// down on the possibility of unwanted runs
const subagentLogCount = logs.filter((l) => l.startsWith('[subagent:')).length;
const labelText = subagentLogCount > 0
? intl.formatMessage(i18n.activityCount, { count: subagentLogCount })
: intl.formatMessage(i18n.logs);
const labelText =
subagentLogCount > 0
? intl.formatMessage(i18n.activityCount, { count: subagentLogCount })
: intl.formatMessage(i18n.logs);
return (
<ToolCallExpandable
+19 -46
View File
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { MainPanelLayout } from '../Layout/MainPanelLayout';
import { Button } from '../ui/button';
import { Download, Play, Upload } from 'lucide-react';
import { exportApp, GooseApp, importApp, listApps } from '../../api';
import type { GooseApp } from '../../api';
import { exportMcpApp, importMcpApp, listMcpApps } from '../../acp/mcp-apps';
import { useChatContext } from '../../contexts/ChatContext';
import { formatAppName } from '../../utils/conversionUtils';
import { errorMessage } from '../../utils/conversionUtils';
@@ -79,10 +80,7 @@ export default function AppsView() {
useEffect(() => {
const loadCachedApps = async () => {
try {
const response = await listApps({
throwOnError: true,
});
const cachedApps = response.data?.apps || [];
const cachedApps = await listMcpApps();
// Only show apps from the "apps" extension (vibe coded apps built by Goose)
setApps(cachedApps.filter((a) => a.mcpServers?.includes('apps')));
} catch (err) {
@@ -101,11 +99,7 @@ export default function AppsView() {
const refreshApps = async () => {
try {
const response = await listApps({
throwOnError: true,
query: { session_id: sessionId },
});
const freshApps = response.data?.apps || [];
const freshApps = await listMcpApps(sessionId);
// Only show apps from the "apps" extension (vibe coded apps built by Goose)
setApps(freshApps.filter((a) => a.mcpServers?.includes('apps')));
setError(null);
@@ -134,13 +128,8 @@ export default function AppsView() {
// Refresh apps list to get latest state
if (eventSessionId) {
listApps({
throwOnError: false,
query: { session_id: eventSessionId },
}).then((response) => {
if (response.data?.apps) {
setApps(response.data.apps.filter((a) => a.mcpServers?.includes('apps')));
}
listMcpApps(eventSessionId).then((apps) => {
setApps(apps.filter((a) => a.mcpServers?.includes('apps')));
});
}
}
@@ -155,11 +144,7 @@ export default function AppsView() {
try {
setLoading(true);
const response = await listApps({
throwOnError: true,
query: { session_id: sessionId },
});
const fetchedApps = response.data?.apps || [];
const fetchedApps = await listMcpApps(sessionId);
// Only show apps from the "apps" extension (vibe coded apps built by Goose)
setApps(fetchedApps.filter((a) => a.mcpServers?.includes('apps')));
setError(null);
@@ -184,22 +169,16 @@ export default function AppsView() {
const handleDownloadApp = async (app: GooseApp) => {
try {
const response = await exportApp({
throwOnError: true,
path: { name: app.name },
});
if (response.data) {
const blob = new Blob([response.data as string], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${app.name}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
const html = await exportMcpApp(app.name);
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${app.name}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (err) {
console.error('Failed to export app:', err);
setError(errorMessage(err, 'Failed to export app'));
@@ -218,15 +197,9 @@ export default function AppsView() {
try {
const text = await file.text();
await importApp({
throwOnError: true,
body: { html: text },
});
await importMcpApp(text);
const response = await listApps({
throwOnError: true,
});
const cachedApps = response.data?.apps || [];
const cachedApps = await listMcpApps();
// Only show apps from the "apps" extension (vibe coded apps built by Goose)
setApps(cachedApps.filter((a) => a.mcpServers?.includes('apps')));
setError(null);
@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import McpAppRenderer from '../McpApps/McpAppRenderer';
import { startAgent, resumeAgent, listApps, stopAgent } from '../../api';
import { listMcpApps } from '../../acp/mcp-apps';
import { acpCloseSession, acpNewSession } from '../../acp/sessions';
import { formatAppName } from '../../utils/conversionUtils';
import { errorMessage } from '../../utils/conversionUtils';
import { defineMessages, useIntl } from '../../i18n';
@@ -48,11 +49,7 @@ export default function StandaloneAppView() {
}
try {
const response = await listApps({
throwOnError: true,
});
const apps = response.data?.apps || [];
const apps = await listMcpApps();
const cachedApp = apps.find(
(app) => app.uri === resourceUri && app.mcpServers?.includes(extensionName)
);
@@ -76,21 +73,7 @@ export default function StandaloneAppView() {
}
try {
const startResponse = await startAgent({
body: { working_dir: workingDir },
throwOnError: true,
});
const sid = startResponse.data.id;
await resumeAgent({
body: {
session_id: sid,
load_model_and_extensions: true,
},
throwOnError: true,
});
const { sessionId: sid } = await acpNewSession(workingDir, []);
setSessionId(sid);
setLoading(false);
} catch (err) {
@@ -115,10 +98,7 @@ export default function StandaloneAppView() {
useEffect(() => {
return () => {
if (sessionId) {
stopAgent({
body: { session_id: sessionId },
throwOnError: false,
}).catch((err: unknown) => {
acpCloseSession(sessionId).catch((err: unknown) => {
console.warn('Failed to stop agent on unmount:', err);
});
}
@@ -139,7 +119,9 @@ export default function StandaloneAppView() {
padding: '24px',
}}
>
<h2 style={{ color: 'var(--text-error, #ef4444)' }}>{intl.formatMessage(i18n.failedToLoad)}</h2>
<h2 style={{ color: 'var(--text-error, #ef4444)' }}>
{intl.formatMessage(i18n.failedToLoad)}
</h2>
<p style={{ color: 'var(--color-text-secondary, #6b7280)' }}>{error}</p>
</div>
);
@@ -156,7 +138,9 @@ export default function StandaloneAppView() {
justifyContent: 'center',
}}
>
<p style={{ color: 'var(--color-text-secondary, #6b7280)' }}>{intl.formatMessage(i18n.initializing)}</p>
<p style={{ color: 'var(--color-text-secondary, #6b7280)' }}>
{intl.formatMessage(i18n.initializing)}
</p>
</div>
);
}
+2 -5
View File
@@ -14,8 +14,8 @@ import {
TokenState,
updateFromSession,
updateSessionUserRecipeValues,
listApps,
} from '../api';
import { listMcpApps } from '../acp/mcp-apps';
import {
createUserMessage,
@@ -823,10 +823,7 @@ export function useChatStream({
});
}
listApps({
throwOnError: true,
query: { session_id: sessionId },
}).catch((err) => {
listMcpApps(sessionId).catch((err) => {
console.warn('Failed to populate apps cache:', err);
});
-1
View File
@@ -249,7 +249,6 @@ export class GitHubUpdater {
// Combine chunks into a single buffer
log.info('GitHubUpdater: Combining chunks into buffer...');
// eslint-disable-next-line no-undef
const buffer = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
log.info(`GitHubUpdater: Buffer created - ${buffer.length} bytes`);
+3 -7
View File
@@ -1,4 +1,5 @@
import { listApps, GooseApp } from '../api';
import type { GooseApp } from '../api';
import { listMcpApps } from '../acp/mcp-apps';
interface PlatformEventData {
extension: string;
@@ -21,12 +22,7 @@ async function handleAppsEvent(eventType: string, eventData: PlatformEventData):
return;
}
const response = await listApps({
throwOnError: false,
query: { session_id: sessionId },
});
const apps = response.data?.apps || [];
const apps = await listMcpApps(sessionId);
const targetApp = apps.find((app: GooseApp) => app.name === app_name);
+40
View File
@@ -11,6 +11,12 @@ import type { Client } from "@agentclientprotocol/sdk";
import type {
AddConfigExtensionRequest_unstable,
AddSessionExtensionRequest_unstable,
AppsExportRequest_unstable,
AppsExportResponse_unstable,
AppsImportRequest_unstable,
AppsImportResponse_unstable,
AppsListRequest_unstable,
AppsListResponse_unstable,
ArchiveSessionRequest_unstable,
CreateScheduleRequest_unstable,
CreateScheduleResponse_unstable,
@@ -151,6 +157,9 @@ import type {
UpdateWorkingDirRequest_unstable,
} from './types.gen.js';
import {
zAppsExportResponse_unstable,
zAppsImportResponse_unstable,
zAppsListResponse_unstable,
zCreateScheduleResponse_unstable,
zCreateSourceResponse_unstable,
zCustomProviderCreateResponse_unstable,
@@ -254,6 +263,37 @@ export class GooseExtClient {
) as ReadResourceResponse_unstable;
}
async appsList_unstable(
params: AppsListRequest_unstable,
): Promise<AppsListResponse_unstable> {
const raw = await this.conn.extMethod("_goose/unstable/apps/list", params);
return zAppsListResponse_unstable.parse(raw) as AppsListResponse_unstable;
}
async appsExport_unstable(
params: AppsExportRequest_unstable,
): Promise<AppsExportResponse_unstable> {
const raw = await this.conn.extMethod(
"_goose/unstable/apps/export",
params,
);
return zAppsExportResponse_unstable.parse(
raw,
) as AppsExportResponse_unstable;
}
async appsImport_unstable(
params: AppsImportRequest_unstable,
): Promise<AppsImportResponse_unstable> {
const raw = await this.conn.extMethod(
"_goose/unstable/apps/import",
params,
);
return zAppsImportResponse_unstable.parse(
raw,
) as AppsImportResponse_unstable;
}
async sessionWorkingDirUpdate_unstable(
params: UpdateWorkingDirRequest_unstable,
): Promise<void> {
File diff suppressed because one or more lines are too long
+27 -2
View File
@@ -250,6 +250,31 @@ export type ReadResourceResponse_unstable = {
result?: unknown;
};
export type AppsListRequest_unstable = {
sessionId?: string | null;
};
export type AppsListResponse_unstable = {
apps?: Array<unknown>;
};
export type AppsExportRequest_unstable = {
name: string;
};
export type AppsExportResponse_unstable = {
html: string;
};
export type AppsImportRequest_unstable = {
html: string;
};
export type AppsImportResponse_unstable = {
name: string;
message: string;
};
/**
* Update the working directory for a session.
*/
@@ -1994,14 +2019,14 @@ export type RecipeParamsAction = 'submit' | 'cancel';
export type ExtRequest = {
id: string;
method: string;
params?: AddSessionExtensionRequest_unstable | RemoveSessionExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DiagnosticsGetRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | EncodeRecipeRequest_unstable | DecodeRecipeRequest_unstable | ScanRecipeRequest_unstable | ListRecipesRequest_unstable | DeleteRecipeRequest_unstable | ScheduleRecipeRequest_unstable | SetRecipeSlashCommandRequest_unstable | SaveRecipeRequest_unstable | ParseRecipeRequest_unstable | RecipeToYamlRequest_unstable | ListSchedulesRequest_unstable | ListScheduleSessionsRequest_unstable | CreateScheduleRequest_unstable | DeleteScheduleRequest_unstable | PauseScheduleRequest_unstable | UnpauseScheduleRequest_unstable | UpdateScheduleRequest_unstable | RunScheduleNowRequest_unstable | KillRunningJobRequest_unstable | InspectRunningJobRequest_unstable | GetSessionInfoRequest_unstable | TruncateSessionConversationRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | ListAgentMentionsRequest_unstable | ListSlashCommandsRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | {
params?: AddSessionExtensionRequest_unstable | RemoveSessionExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | AppsListRequest_unstable | AppsExportRequest_unstable | AppsImportRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DiagnosticsGetRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | EncodeRecipeRequest_unstable | DecodeRecipeRequest_unstable | ScanRecipeRequest_unstable | ListRecipesRequest_unstable | DeleteRecipeRequest_unstable | ScheduleRecipeRequest_unstable | SetRecipeSlashCommandRequest_unstable | SaveRecipeRequest_unstable | ParseRecipeRequest_unstable | RecipeToYamlRequest_unstable | ListSchedulesRequest_unstable | ListScheduleSessionsRequest_unstable | CreateScheduleRequest_unstable | DeleteScheduleRequest_unstable | PauseScheduleRequest_unstable | UnpauseScheduleRequest_unstable | UpdateScheduleRequest_unstable | RunScheduleNowRequest_unstable | KillRunningJobRequest_unstable | InspectRunningJobRequest_unstable | GetSessionInfoRequest_unstable | TruncateSessionConversationRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | ListAgentMentionsRequest_unstable | ListSlashCommandsRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | {
[key: string]: unknown;
} | null;
};
export type ExtResponse = {
id: string;
result?: EmptyResponse | GetToolsResponse_unstable | GooseToolCallResponse_unstable | ReadResourceResponse_unstable | SteerSessionResponse_unstable | DiagnosticsGetResponse_unstable | GetConfigExtensionsResponse_unstable | GetAvailableExtensionsResponse_unstable | GetSessionExtensionsResponse_unstable | ListProvidersResponse_unstable | ProviderSupportedModelsListResponse_unstable | ProviderCatalogListResponse_unstable | ProviderSetupCatalogListResponse_unstable | ProviderCatalogTemplateResponse_unstable | CustomProviderCreateResponse_unstable | CustomProviderReadResponse_unstable | CustomProviderUpdateResponse_unstable | CustomProviderDeleteResponse_unstable | RefreshProviderInventoryResponse_unstable | ProviderConfigReadResponse_unstable | ProviderConfigStatusResponse_unstable | ProviderConfigChangeResponse_unstable | PreferencesReadResponse_unstable | DefaultsReadResponse_unstable | OnboardingImportScanResponse_unstable | OnboardingImportApplyResponse_unstable | ExportSessionResponse_unstable | ImportSessionResponse_unstable | EncodeRecipeResponse_unstable | DecodeRecipeResponse_unstable | ScanRecipeResponse_unstable | ListRecipesResponse_unstable | SaveRecipeResponse_unstable | ParseRecipeResponse_unstable | RecipeToYamlResponse_unstable | ListSchedulesResponse_unstable | ListScheduleSessionsResponse_unstable | CreateScheduleResponse_unstable | UpdateScheduleResponse_unstable | RunScheduleNowResponse_unstable | KillRunningJobResponse_unstable | InspectRunningJobResponse_unstable | GetSessionInfoResponse_unstable | CreateSourceResponse_unstable | ListSourcesResponse_unstable | ListAgentMentionsResponse_unstable | ListSlashCommandsResponse_unstable | UpdateSourceResponse_unstable | ExportSourceResponse_unstable | ImportSourcesResponse_unstable | DictationTranscribeResponse_unstable | DictationConfigResponse_unstable | DictationModelsListResponse_unstable | DictationModelDownloadProgressResponse_unstable | unknown;
result?: EmptyResponse | GetToolsResponse_unstable | GooseToolCallResponse_unstable | ReadResourceResponse_unstable | AppsListResponse_unstable | AppsExportResponse_unstable | AppsImportResponse_unstable | SteerSessionResponse_unstable | DiagnosticsGetResponse_unstable | GetConfigExtensionsResponse_unstable | GetAvailableExtensionsResponse_unstable | GetSessionExtensionsResponse_unstable | ListProvidersResponse_unstable | ProviderSupportedModelsListResponse_unstable | ProviderCatalogListResponse_unstable | ProviderSetupCatalogListResponse_unstable | ProviderCatalogTemplateResponse_unstable | CustomProviderCreateResponse_unstable | CustomProviderReadResponse_unstable | CustomProviderUpdateResponse_unstable | CustomProviderDeleteResponse_unstable | RefreshProviderInventoryResponse_unstable | ProviderConfigReadResponse_unstable | ProviderConfigStatusResponse_unstable | ProviderConfigChangeResponse_unstable | PreferencesReadResponse_unstable | DefaultsReadResponse_unstable | OnboardingImportScanResponse_unstable | OnboardingImportApplyResponse_unstable | ExportSessionResponse_unstable | ImportSessionResponse_unstable | EncodeRecipeResponse_unstable | DecodeRecipeResponse_unstable | ScanRecipeResponse_unstable | ListRecipesResponse_unstable | SaveRecipeResponse_unstable | ParseRecipeResponse_unstable | RecipeToYamlResponse_unstable | ListSchedulesResponse_unstable | ListScheduleSessionsResponse_unstable | CreateScheduleResponse_unstable | UpdateScheduleResponse_unstable | RunScheduleNowResponse_unstable | KillRunningJobResponse_unstable | InspectRunningJobResponse_unstable | GetSessionInfoResponse_unstable | CreateSourceResponse_unstable | ListSourcesResponse_unstable | ListAgentMentionsResponse_unstable | ListSlashCommandsResponse_unstable | UpdateSourceResponse_unstable | ExportSourceResponse_unstable | ImportSourcesResponse_unstable | DictationTranscribeResponse_unstable | DictationConfigResponse_unstable | DictationModelsListResponse_unstable | DictationModelDownloadProgressResponse_unstable | unknown;
} | {
error: {
code: number;
+34
View File
@@ -212,6 +212,34 @@ export const zReadResourceResponse_unstable = z.object({
result: z.unknown().optional().default(null)
});
export const zAppsListRequest_unstable = z.object({
sessionId: z.union([
z.string(),
z.null()
]).optional()
});
export const zAppsListResponse_unstable = z.object({
apps: z.array(z.unknown()).optional().default([])
});
export const zAppsExportRequest_unstable = z.object({
name: z.string()
});
export const zAppsExportResponse_unstable = z.object({
html: z.string()
});
export const zAppsImportRequest_unstable = z.object({
html: z.string()
});
export const zAppsImportResponse_unstable = z.object({
name: z.string(),
message: z.string()
});
/**
* Update the working directory for a session.
*/
@@ -2119,6 +2147,9 @@ export const zExtRequest = z.object({
zGetToolsRequest_unstable,
zGooseToolCallRequest_unstable,
zReadResourceRequest_unstable,
zAppsListRequest_unstable,
zAppsExportRequest_unstable,
zAppsImportRequest_unstable,
zUpdateWorkingDirRequest_unstable,
zSetSessionSystemPromptRequest_unstable,
zSteerSessionRequest_unstable,
@@ -2215,6 +2246,9 @@ export const zExtResponse = z.union([
zGetToolsResponse_unstable,
zGooseToolCallResponse_unstable,
zReadResourceResponse_unstable,
zAppsListResponse_unstable,
zAppsExportResponse_unstable,
zAppsImportResponse_unstable,
zSteerSessionResponse_unstable,
zDiagnosticsGetResponse_unstable,
zGetConfigExtensionsResponse_unstable,
+8
View File
@@ -22,6 +22,8 @@ import {
type ListSessionsResponse,
type ResumeSessionRequest,
type ResumeSessionResponse,
type CloseSessionRequest,
type CloseSessionResponse,
type SetSessionModelRequest,
type SetSessionModelResponse,
} from "@agentclientprotocol/sdk";
@@ -113,6 +115,12 @@ export class GooseClient {
return this.conn.unstable_resumeSession(params);
}
unstable_closeSession(
params: CloseSessionRequest,
): Promise<CloseSessionResponse> {
return this.conn.unstable_closeSession(params);
}
unstable_setSessionModel(
params: SetSessionModelRequest,
): Promise<SetSessionModelResponse> {