rust acp client for extension methods (#8227)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jack Amadeo
2026-04-02 14:17:51 -04:00
committed by GitHub
parent d18555ca62
commit 78098d034a
15 changed files with 374 additions and 94 deletions

14
Cargo.lock generated
View File

@@ -4459,6 +4459,7 @@ dependencies = [
"goose",
"goose-acp-macros",
"goose-mcp",
"goose-sdk",
"goose-test-support",
"http-body-util",
"regex",
@@ -4570,6 +4571,19 @@ dependencies = [
"url",
]
[[package]]
name = "goose-sdk"
version = "1.29.0"
dependencies = [
"agent-client-protocol-schema",
"sacp",
"schemars 1.2.1",
"serde",
"serde_json",
"tokio",
"tokio-util",
]
[[package]]
name = "goose-server"
version = "1.30.0"

View File

@@ -1,16 +1,20 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse_macro_input, FnArg, GenericArgument, ImplItem, ItemImpl, Lit, Pat, PathArguments,
ReturnType, Type,
parse_macro_input, FnArg, GenericArgument, ImplItem, ItemImpl, Pat, PathArguments, ReturnType,
Type,
};
/// Marks an impl block as containing `#[custom_method("...")]`-annotated handlers.
/// Marks an impl block as containing `#[custom_method(RequestType)]`-annotated handlers.
///
/// The request type must derive `sacp::JsonRpcRequest` with a `#[request(method = "...")]`
/// attribute — the method name is extracted from that type at compile time, eliminating
/// duplication between the request struct and the handler.
///
/// Generates two methods on the impl:
///
/// 1. `handle_custom_request` — a dispatcher that:
/// - Uses each annotation string as the method name (include `_goose/` for goose-only methods)
/// - Uses `<RequestType as sacp::JsonRpcMessage>::matches_method` to match incoming methods
/// - Parses JSON params into the handler's typed parameter (if any)
/// - Serializes the handler's return value to JSON
///
@@ -25,11 +29,11 @@ use syn::{
///
/// ```ignore
/// // No params — called for requests with no/empty params
/// #[custom_method("session/list")]
/// async fn on_list_sessions(&self) -> Result<ListSessionsResponse, sacp::Error> { .. }
/// #[custom_method(GetExtensionsRequest)]
/// async fn on_get_extensions(&self) -> Result<GetExtensionsResponse, sacp::Error> { .. }
///
/// // Typed params — JSON params auto-deserialized
/// #[custom_method("session/get")]
/// #[custom_method(GetSessionRequest)]
/// async fn on_get_session(&self, req: GetSessionRequest) -> Result<GetSessionResponse, sacp::Error> { .. }
/// ```
///
@@ -40,15 +44,15 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut routes: Vec<Route> = Vec::new();
// Collect all #[custom_method("...")] annotations and strip them.
// Collect all #[custom_method(RequestType)] annotations and strip them.
for item in &mut impl_block.items {
if let ImplItem::Fn(method) = item {
let mut route_name = None;
let mut request_type = None;
method.attrs.retain(|attr| {
if attr.path().is_ident("custom_method") {
if let Ok(meta_list) = attr.meta.require_list() {
if let Ok(Lit::Str(s)) = meta_list.parse_args::<Lit>() {
route_name = Some(s.value());
if let Ok(ty) = meta_list.parse_args::<Type>() {
request_type = Some(ty);
}
}
false // strip the attribute
@@ -57,7 +61,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
});
if let Some(name) = route_name {
if let Some(req_type) = request_type {
let fn_ident = method.sig.ident.clone();
let param_type = extract_param_type(&method.sig);
@@ -65,7 +69,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
let ok_type = extract_result_ok_type(&method.sig);
routes.push(Route {
method_name: name,
request_type: req_type,
fn_ident,
param_type,
return_type,
@@ -75,31 +79,31 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
}
// Generate the dispatch arms.
// Generate the dispatch arms using matches_method for routing.
let arms: Vec<_> = routes
.iter()
.map(|route| {
let method = &route.method_name;
let req_type = &route.request_type;
let fn_ident = &route.fn_ident;
match &route.param_type {
Some(_) => {
quote! {
#method => {
if <#req_type as sacp::JsonRpcMessage>::matches_method(method) {
let req = serde_json::from_value(params)
.map_err(|e| sacp::Error::invalid_params().data(e.to_string()))?;
let result = self.#fn_ident(req).await?;
serde_json::to_value(&result)
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))
return serde_json::to_value(&result)
.map_err(|e| sacp::Error::internal_error().data(e.to_string()));
}
}
}
None => {
quote! {
#method => {
if <#req_type as sacp::JsonRpcMessage>::matches_method(method) {
let result = self.#fn_ident().await?;
serde_json::to_value(&result)
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))
return serde_json::to_value(&result)
.map_err(|e| sacp::Error::internal_error().data(e.to_string()));
}
}
}
@@ -111,7 +115,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
let schema_entries: Vec<_> = routes
.iter()
.map(|route| {
let method = &route.method_name;
let req_type = &route.request_type;
let params_expr = if let Some(pt) = &route.param_type {
if is_json_value(pt) {
@@ -120,7 +124,12 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
quote! { Some(generator.subschema_for::<#pt>()) }
}
} else {
quote! { None }
// Even with no handler param, generate schema from the request type
if is_json_value(req_type) {
quote! { None }
} else {
quote! { Some(generator.subschema_for::<#req_type>()) }
}
};
let response_expr = if let Some(ok_ty) = &route.ok_type {
@@ -141,7 +150,8 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
quote! { Some(#name.to_string()) }
}
} else {
quote! { None }
let name = type_name(req_type);
quote! { Some(#name.to_string()) }
};
let response_name_expr = if let Some(ok_ty) = &route.ok_type {
@@ -156,12 +166,15 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
};
quote! {
crate::custom_requests::CustomMethodSchema {
method: #method.to_string(),
params_schema: #params_expr,
params_type_name: #params_name_expr,
response_schema: #response_expr,
response_type_name: #response_name_expr,
{
let dummy = <#req_type as Default>::default();
crate::custom_requests::CustomMethodSchema {
method: sacp::JsonRpcMessage::method(&dummy).to_string(),
params_schema: #params_expr,
params_type_name: #params_name_expr,
response_schema: #response_expr,
response_type_name: #response_name_expr,
}
}
}
})
@@ -174,10 +187,8 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
method: &str,
params: serde_json::Value,
) -> Result<serde_json::Value, sacp::Error> {
match method {
#(#arms)*
_ => Err(sacp::Error::method_not_found()),
}
#(#arms)*
Err(sacp::Error::method_not_found())
}
};
@@ -202,7 +213,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
struct Route {
method_name: String,
request_type: Type,
fn_ident: syn::Ident,
param_type: Option<Type>,
#[allow(dead_code)]

View File

@@ -47,6 +47,7 @@ http-body-util = "0.1.3"
uuid = { workspace = true, features = ["v7"] }
schemars = { workspace = true, features = ["derive"] }
goose-acp-macros = { path = "../goose-acp-macros" }
goose-sdk = { path = "../goose-sdk" }
[dev-dependencies]
async-trait = { workspace = true }

View File

@@ -47,7 +47,7 @@
},
{
"method": "_goose/config/extensions",
"requestType": null,
"requestType": "GetExtensionsRequest",
"responseType": "GetExtensionsResponse"
}
]

View File

@@ -9,12 +9,12 @@
"type": "string"
},
"config": {
"description": "Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform)."
"description": "Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform).",
"default": null
}
},
"required": [
"sessionId",
"config"
"sessionId"
],
"description": "Add an extension to an active session.",
"x-side": "agent",
@@ -69,6 +69,7 @@
"required": [
"tools"
],
"description": "Tools response.",
"x-side": "agent",
"x-method": "_goose/tools"
},
@@ -98,12 +99,11 @@
"type": "object",
"properties": {
"result": {
"description": "The resource result from the extension (MCP ReadResourceResult)."
"description": "The resource result from the extension (MCP ReadResourceResult).",
"default": null
}
},
"required": [
"result"
],
"description": "Resource read response.",
"x-side": "agent",
"x-method": "_goose/resource/read"
},
@@ -147,12 +147,10 @@
"type": "object",
"properties": {
"session": {
"description": "The session object with id, name, working_dir, timestamps, tokens, etc."
"description": "The session object with id, name, working_dir, timestamps, tokens, etc.",
"default": null
}
},
"required": [
"session"
],
"description": "Get a session response.",
"x-side": "agent",
"x-method": "session/get"
@@ -195,6 +193,7 @@
"required": [
"data"
],
"description": "Export session response.",
"x-side": "agent",
"x-method": "_goose/session/export"
},
@@ -216,15 +215,20 @@
"type": "object",
"properties": {
"session": {
"description": "The imported session object."
"description": "The imported session object.",
"default": null
}
},
"required": [
"session"
],
"description": "Import session response.",
"x-side": "agent",
"x-method": "_goose/session/import"
},
"GetExtensionsRequest": {
"type": "object",
"description": "List configured extensions and any warnings.",
"x-side": "agent",
"x-method": "_goose/config/extensions"
},
"GetExtensionsResponse": {
"type": "object",
"properties": {
@@ -340,6 +344,15 @@
],
"description": "Params for _goose/session/import",
"title": "ImportSessionRequest"
},
{
"allOf": [
{
"$ref": "#/$defs/GetExtensionsRequest"
}
],
"description": "Params for _goose/config/extensions",
"title": "GetExtensionsRequest"
}
]
},

View File

@@ -1,7 +1,7 @@
#![recursion_limit = "256"]
mod adapters;
pub mod custom_requests;
pub use goose_sdk::custom_requests;
mod fs;
pub mod server;
pub mod server_factory;

View File

@@ -1303,7 +1303,7 @@ impl GooseAcpAgent {
#[custom_methods]
impl GooseAcpAgent {
#[custom_method("_goose/extensions/add")]
#[custom_method(AddExtensionRequest)]
async fn on_add_extension(
&self,
req: AddExtensionRequest,
@@ -1318,7 +1318,7 @@ impl GooseAcpAgent {
Ok(EmptyResponse {})
}
#[custom_method("_goose/extensions/remove")]
#[custom_method(RemoveExtensionRequest)]
async fn on_remove_extension(
&self,
req: RemoveExtensionRequest,
@@ -1331,7 +1331,7 @@ impl GooseAcpAgent {
Ok(EmptyResponse {})
}
#[custom_method("_goose/tools")]
#[custom_method(GetToolsRequest)]
async fn on_get_tools(&self, req: GetToolsRequest) -> Result<GetToolsResponse, sacp::Error> {
let agent = self.get_session_agent(&req.session_id, None).await?;
let tools = agent.list_tools(&req.session_id, None).await;
@@ -1343,7 +1343,7 @@ impl GooseAcpAgent {
Ok(GetToolsResponse { tools: tools_json })
}
#[custom_method("_goose/resource/read")]
#[custom_method(ReadResourceRequest)]
async fn on_read_resource(
&self,
req: ReadResourceRequest,
@@ -1362,7 +1362,7 @@ impl GooseAcpAgent {
})
}
#[custom_method("_goose/working_dir/update")]
#[custom_method(UpdateWorkingDirRequest)]
async fn on_update_working_dir(
&self,
req: UpdateWorkingDirRequest,
@@ -1394,8 +1394,7 @@ impl GooseAcpAgent {
Ok(EmptyResponse {})
}
// TODO: use typed GetSessionRequest when agent-client-protocol-schema adds it (Discussion #60)
#[custom_method("session/get")]
#[custom_method(GetSessionRequest)]
async fn on_get_session(
&self,
req: GetSessionRequest,
@@ -1412,8 +1411,7 @@ impl GooseAcpAgent {
})
}
// TODO: use typed DeleteSessionRequest when agent-client-protocol-schema adds it (RFD #395)
#[custom_method("session/delete")]
#[custom_method(DeleteSessionRequest)]
async fn on_delete_session(
&self,
req: DeleteSessionRequest,
@@ -1426,7 +1424,7 @@ impl GooseAcpAgent {
Ok(EmptyResponse {})
}
#[custom_method("_goose/session/export")]
#[custom_method(ExportSessionRequest)]
async fn on_export_session(
&self,
req: ExportSessionRequest,
@@ -1439,7 +1437,7 @@ impl GooseAcpAgent {
Ok(ExportSessionResponse { data })
}
#[custom_method("_goose/session/import")]
#[custom_method(ImportSessionRequest)]
async fn on_import_session(
&self,
req: ImportSessionRequest,
@@ -1456,7 +1454,7 @@ impl GooseAcpAgent {
})
}
#[custom_method("_goose/config/extensions")]
#[custom_method(GetExtensionsRequest)]
async fn on_get_extensions(&self) -> Result<GetExtensionsResponse, sacp::Error> {
let extensions = goose::config::extensions::get_all_extensions();
let warnings = goose::config::extensions::get_warnings();

View File

@@ -0,0 +1,23 @@
[package]
name = "goose-sdk"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
description = "Rust SDK for talking to Goose over the Agent Client Protocol (ACP)"
[dependencies]
sacp = { workspace = true, features = ["unstable"] }
agent-client-protocol-schema = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
schemars = { workspace = true, features = ["derive"] }
[dev-dependencies]
tokio = { workspace = true }
tokio-util = { workspace = true, features = ["compat", "rt"] }
[package.metadata.cargo-machete]
# Used to provide extras imports for sacp
ignored = ["agent-client-protocol-schema"]

View File

@@ -0,0 +1,156 @@
//! ACP Client Example
//!
//! Spawns `goose acp` as a child process and sends it a completion request
//! using the Agent Client Protocol over stdio.
//!
//! # Prerequisites
//!
//! You must have goose built and a provider configured (`goose configure`).
//!
//! # Usage
//!
//! ```bash
//! cargo run -p goose-sdk --example acp_client -- "What is 2 + 2?"
//! ```
//!
//! Or with a custom goose binary path:
//!
//! ```bash
//! cargo run -p goose-sdk --example acp_client -- --goose-bin ./target/debug/goose "Explain Rust's ownership model in one sentence"
//! ```
use goose_sdk::custom_requests::GetExtensionsRequest;
use sacp::schema::{
ContentBlock, InitializeRequest, ProtocolVersion, RequestPermissionOutcome,
RequestPermissionRequest, RequestPermissionResponse, SelectedPermissionOutcome,
SessionNotification, SessionUpdate,
};
use sacp::{Client, ConnectionTo};
use std::path::PathBuf;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Parse args: [--goose-bin PATH] PROMPT
let args: Vec<String> = std::env::args().skip(1).collect();
let (goose_bin, prompt) = parse_args(&args)?;
eprintln!("🚀 Spawning: {} acp", goose_bin.display());
let mut child = tokio::process::Command::new(&goose_bin)
.arg("acp")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.spawn()
.map_err(|e| format!("Failed to spawn '{}': {e}", goose_bin.display()))?;
let child_stdin = child.stdin.take().expect("stdin should be piped");
let child_stdout = child.stdout.take().expect("stdout should be piped");
let transport = sacp::ByteStreams::new(child_stdin.compat_write(), child_stdout.compat());
let prompt_clone = prompt.clone();
Client
.builder()
.name("acp-client-example")
// Print session notifications (agent text, tool calls, etc.)
.on_receive_notification(
async move |notification: SessionNotification, _cx| {
match &notification.update {
SessionUpdate::AgentMessageChunk(chunk) => {
if let ContentBlock::Text(text) = &chunk.content {
print!("{}", text.text);
}
}
SessionUpdate::ToolCall(tool_call) => {
eprintln!("🔧 Tool call: {}", tool_call.title);
}
SessionUpdate::ToolCallUpdate(update) => {
if let Some(status) = &update.fields.status {
eprintln!(" Tool status: {:?}", status);
}
}
_ => {}
}
Ok(())
},
sacp::on_receive_notification!(),
)
// Auto-approve all permission requests
.on_receive_request(
async move |request: RequestPermissionRequest, responder, _cx| {
eprintln!("✅ Auto-approving permission request");
let option_id = request.options.first().map(|opt| opt.option_id.clone());
match option_id {
Some(id) => responder.respond(RequestPermissionResponse::new(
RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(id)),
)),
None => responder.respond(RequestPermissionResponse::new(
RequestPermissionOutcome::Cancelled,
)),
}
},
sacp::on_receive_request!(),
)
.connect_with(transport, async move |cx: ConnectionTo<sacp::Agent>| {
// Step 1: Initialize
eprintln!("🤝 Initializing...");
let init_response = cx
.send_request(InitializeRequest::new(ProtocolVersion::LATEST))
.block_task()
.await?;
eprintln!("✓ Agent initialized: {:?}", init_response.agent_info);
let response = cx
.send_request(GetExtensionsRequest {})
.block_task()
.await?;
eprintln!("Extensions: {:?}", response.extensions);
// Step 2: Create a session and send the prompt
eprintln!("💬 Sending prompt: \"{}\"", prompt_clone);
cx.build_session_cwd()?
.block_task()
.run_until(async |mut session| {
session.send_prompt(&prompt_clone)?;
let response = session.read_to_string().await?;
// read_to_string collects text; we already printed chunks above,
// so just print a newline to finish.
println!();
eprintln!("✅ Done ({} chars)", response.len());
Ok(())
})
.await
})
.await?;
let _ = child.kill().await;
Ok(())
}
fn parse_args(args: &[String]) -> Result<(PathBuf, String), String> {
let mut goose_bin = PathBuf::from("goose");
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--goose-bin" => {
i += 1;
goose_bin = PathBuf::from(args.get(i).ok_or("--goose-bin requires a value")?);
}
_ => break,
}
i += 1;
}
let prompt = args[i..].join(" ");
if prompt.is_empty() {
return Err("Usage: acp_client [--goose-bin PATH] PROMPT".into());
}
Ok((goose_bin, prompt))
}

View File

@@ -1,3 +1,4 @@
use sacp::{JsonRpcRequest, JsonRpcResponse};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -20,16 +21,19 @@ pub struct CustomMethodSchema {
}
/// Add an extension to an active session.
#[derive(Debug, Deserialize, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/extensions/add", response = EmptyResponse)]
#[serde(rename_all = "camelCase")]
pub struct AddExtensionRequest {
pub session_id: String,
/// Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform).
#[serde(default)]
pub config: serde_json::Value,
}
/// Remove an extension from an active session.
#[derive(Debug, Deserialize, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/extensions/remove", response = EmptyResponse)]
#[serde(rename_all = "camelCase")]
pub struct RemoveExtensionRequest {
pub session_id: String,
@@ -37,20 +41,23 @@ pub struct RemoveExtensionRequest {
}
/// List all tools available in a session.
#[derive(Debug, Deserialize, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/tools", response = GetToolsResponse)]
#[serde(rename_all = "camelCase")]
pub struct GetToolsRequest {
pub session_id: String,
}
#[derive(Debug, Serialize, JsonSchema)]
/// Tools response.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct GetToolsResponse {
/// Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`.
pub tools: Vec<serde_json::Value>,
}
/// Read a resource from an extension.
#[derive(Debug, Deserialize, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/resource/read", response = ReadResourceResponse)]
#[serde(rename_all = "camelCase")]
pub struct ReadResourceRequest {
pub session_id: String,
@@ -58,14 +65,17 @@ pub struct ReadResourceRequest {
pub extension_name: String,
}
#[derive(Debug, Serialize, JsonSchema)]
/// Resource read response.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct ReadResourceResponse {
/// The resource result from the extension (MCP ReadResourceResult).
#[serde(default)]
pub result: serde_json::Value,
}
/// Update the working directory for a session.
#[derive(Debug, Deserialize, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/working_dir/update", response = EmptyResponse)]
#[serde(rename_all = "camelCase")]
pub struct UpdateWorkingDirRequest {
pub session_id: String,
@@ -73,7 +83,8 @@ pub struct UpdateWorkingDirRequest {
}
/// Get a session by ID.
#[derive(Debug, Deserialize, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "session/get", response = GetSessionResponse)]
#[serde(rename_all = "camelCase")]
pub struct GetSessionRequest {
pub session_id: String,
@@ -82,45 +93,57 @@ pub struct GetSessionRequest {
}
/// Get a session response.
#[derive(Debug, Serialize, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct GetSessionResponse {
/// The session object with id, name, working_dir, timestamps, tokens, etc.
#[serde(default)]
pub session: serde_json::Value,
}
/// Delete a session.
#[derive(Debug, Deserialize, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "session/delete", response = EmptyResponse)]
#[serde(rename_all = "camelCase")]
pub struct DeleteSessionRequest {
pub session_id: String,
}
/// Export a session as a JSON string.
#[derive(Debug, Deserialize, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/session/export", response = ExportSessionResponse)]
#[serde(rename_all = "camelCase")]
pub struct ExportSessionRequest {
pub session_id: String,
}
#[derive(Debug, Serialize, JsonSchema)]
/// Export session response.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct ExportSessionResponse {
pub data: String,
}
/// Import a session from a JSON string.
#[derive(Debug, Deserialize, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/session/import", response = ImportSessionResponse)]
pub struct ImportSessionRequest {
pub data: String,
}
#[derive(Debug, Serialize, JsonSchema)]
/// Import session response.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct ImportSessionResponse {
/// The imported session object.
#[serde(default)]
pub session: serde_json::Value,
}
/// List configured extensions and any warnings.
#[derive(Debug, Serialize, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "_goose/config/extensions", response = GetExtensionsResponse)]
pub struct GetExtensionsRequest {}
/// List configured extensions and any warnings.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct GetExtensionsResponse {
/// Array of ExtensionEntry objects with `enabled` flag and config details.
pub extensions: Vec<serde_json::Value>,
@@ -128,5 +151,5 @@ pub struct GetExtensionsResponse {
}
/// Empty success response for operations that return no data.
#[derive(Debug, Serialize, JsonSchema)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct EmptyResponse {}

View File

@@ -0,0 +1 @@
pub mod custom_requests;

View File

@@ -12,6 +12,7 @@ import type {
DeleteSessionRequest,
ExportSessionRequest,
ExportSessionResponse,
GetExtensionsRequest,
GetExtensionsResponse,
GetSessionRequest,
GetSessionResponse,
@@ -83,8 +84,10 @@ export class GooseExtClient {
return zImportSessionResponse.parse(raw) as ImportSessionResponse;
}
async GooseConfigExtensions(): Promise<GetExtensionsResponse> {
const raw = await this.conn.extMethod("_goose/config/extensions", {});
async GooseConfigExtensions(
params: GetExtensionsRequest,
): Promise<GetExtensionsResponse> {
const raw = await this.conn.extMethod("_goose/config/extensions", params);
return zGetExtensionsResponse.parse(raw) as GetExtensionsResponse;
}
}

View File

@@ -1,6 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts
export type { AddExtensionRequest, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsResponse, GetSessionRequest, GetSessionResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ReadResourceRequest, ReadResourceResponse, RemoveExtensionRequest, UpdateWorkingDirRequest } from './types.gen.js';
export type { AddExtensionRequest, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetSessionRequest, GetSessionResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ReadResourceRequest, ReadResourceResponse, RemoveExtensionRequest, UpdateWorkingDirRequest } from './types.gen.js';
export const GOOSE_EXT_METHODS = [
{
@@ -50,7 +50,7 @@ export const GOOSE_EXT_METHODS = [
},
{
method: "_goose/config/extensions",
requestType: null,
requestType: "GetExtensionsRequest",
responseType: "GetExtensionsResponse",
},
] as const;

View File

@@ -9,7 +9,7 @@ export type AddExtensionRequest = {
/**
* Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform).
*/
config: unknown;
config?: unknown;
};
/**
@@ -34,6 +34,9 @@ export type GetToolsRequest = {
sessionId: string;
};
/**
* Tools response.
*/
export type GetToolsResponse = {
/**
* Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`.
@@ -50,11 +53,14 @@ export type ReadResourceRequest = {
extensionName: string;
};
/**
* Resource read response.
*/
export type ReadResourceResponse = {
/**
* The resource result from the extension (MCP ReadResourceResult).
*/
result: unknown;
result?: unknown;
};
/**
@@ -80,7 +86,7 @@ export type GetSessionResponse = {
/**
* The session object with id, name, working_dir, timestamps, tokens, etc.
*/
session: unknown;
session?: unknown;
};
/**
@@ -97,6 +103,9 @@ export type ExportSessionRequest = {
sessionId: string;
};
/**
* Export session response.
*/
export type ExportSessionResponse = {
data: string;
};
@@ -108,11 +117,21 @@ export type ImportSessionRequest = {
data: string;
};
/**
* Import session response.
*/
export type ImportSessionResponse = {
/**
* The imported session object.
*/
session: unknown;
session?: unknown;
};
/**
* List configured extensions and any warnings.
*/
export type GetExtensionsRequest = {
[key: string]: unknown;
};
/**
@@ -129,7 +148,7 @@ export type GetExtensionsResponse = {
export type ExtRequest = {
id: string;
method: string;
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | GetSessionRequest | DeleteSessionRequest | ExportSessionRequest | ImportSessionRequest | {
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | GetSessionRequest | DeleteSessionRequest | ExportSessionRequest | ImportSessionRequest | GetExtensionsRequest | {
[key: string]: unknown;
} | null;
};

View File

@@ -7,7 +7,7 @@ import { z } from 'zod';
*/
export const zAddExtensionRequest = z.object({
sessionId: z.string(),
config: z.unknown()
config: z.unknown().optional().default(null)
});
/**
@@ -30,6 +30,9 @@ export const zGetToolsRequest = z.object({
sessionId: z.string()
});
/**
* Tools response.
*/
export const zGetToolsResponse = z.object({
tools: z.array(z.unknown())
});
@@ -43,8 +46,11 @@ export const zReadResourceRequest = z.object({
extensionName: z.string()
});
/**
* Resource read response.
*/
export const zReadResourceResponse = z.object({
result: z.unknown()
result: z.unknown().optional().default(null)
});
/**
@@ -67,7 +73,7 @@ export const zGetSessionRequest = z.object({
* Get a session response.
*/
export const zGetSessionResponse = z.object({
session: z.unknown()
session: z.unknown().optional().default(null)
});
/**
@@ -84,6 +90,9 @@ export const zExportSessionRequest = z.object({
sessionId: z.string()
});
/**
* Export session response.
*/
export const zExportSessionResponse = z.object({
data: z.string()
});
@@ -95,10 +104,18 @@ export const zImportSessionRequest = z.object({
data: z.string()
});
/**
* Import session response.
*/
export const zImportSessionResponse = z.object({
session: z.unknown()
session: z.unknown().optional().default(null)
});
/**
* List configured extensions and any warnings.
*/
export const zGetExtensionsRequest = z.record(z.unknown());
/**
* List configured extensions and any warnings.
*/
@@ -120,7 +137,8 @@ export const zExtRequest = z.object({
zGetSessionRequest,
zDeleteSessionRequest,
zExportSessionRequest,
zImportSessionRequest
zImportSessionRequest,
zGetExtensionsRequest
]),
z.union([
z.record(z.unknown()),