ACP Migration: MCP/Goose Apps (#9988)
Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -54,6 +54,9 @@ module.exports = [
|
||||
__dirname: 'readonly',
|
||||
require: 'readonly',
|
||||
module: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
TextDecoder: 'readonly',
|
||||
TextEncoder: 'readonly',
|
||||
// Browser globals
|
||||
window: 'readonly',
|
||||
document: 'readonly',
|
||||
|
||||
@@ -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>',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user