Skip to content

Commit 6d6a441

Browse files
nathansoboclaude
andcommitted
Add custom commands support via list_commands and run_command
- Add session/list_commands method to retrieve available custom commands - Add session/run_command method to execute custom commands with arguments - Add CommandInfo type with name, description, and requiresArgument fields - Add supportsCustomCommands to PromptCapabilities - Update schema documentation and generated TypeScript bindings - Implement empty command handlers in example and test agents 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4ca8fe1 commit 6d6a441

File tree

9 files changed

+449
-8
lines changed

9 files changed

+449
-8
lines changed

docs/protocol/schema.mdx

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ See protocol docs: [Initialization](https://agentclientprotocol.com/protocol/ini
9292
<ResponseField name="agentCapabilities" type={<a href="#agentcapabilities">AgentCapabilities</a>} >
9393
Capabilities supported by the agent.
9494

95-
- Default: `{"loadSession":false,"promptCapabilities":{"audio":false,"embeddedContext":false,"image":false}}`
95+
- Default: `{"loadSession":false,"promptCapabilities":{"audio":false,"embeddedContext":false,"image":false,"supportsCustomCommands":false}}`
9696

9797
</ResponseField>
9898
<ResponseField name="authMethods" type={<><span><a href="#authmethod">AuthMethod</a></span><span>[]</span></>} >
@@ -143,6 +143,53 @@ See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/promp
143143
The ID of the session to cancel operations for.
144144
</ResponseField>
145145

146+
<a id="session-list_commands"></a>
147+
### <span class="font-mono">session/list_commands</span>
148+
149+
Lists available custom commands for a session.
150+
151+
Returns all commands available in the agent's `.claude/commands` directory
152+
or equivalent command registry. Commands can be executed via `run_command`.
153+
154+
#### <span class="font-mono">ListCommandsRequest</span>
155+
156+
Request parameters for listing available commands.
157+
158+
**Type:** Object
159+
160+
**Properties:**
161+
162+
<ResponseField
163+
name="sessionId"
164+
type={<a href="#sessionid">SessionId</a>}
165+
required
166+
>
167+
The session ID to list commands for.
168+
</ResponseField>
169+
170+
#### <span class="font-mono">ListCommandsResponse</span>
171+
172+
Response containing available commands.
173+
174+
**Type:** Object
175+
176+
**Properties:**
177+
178+
<ResponseField
179+
name="commands"
180+
type={
181+
<>
182+
<span>
183+
<a href="#commandinfo">CommandInfo</a>
184+
</span>
185+
<span>[]</span>
186+
</>
187+
}
188+
required
189+
>
190+
List of available commands.
191+
</ResponseField>
192+
146193
<a id="session-load"></a>
147194
### <span class="font-mono">session/load</span>
148195

@@ -323,6 +370,36 @@ See protocol docs: [Check for Completion](https://agentclientprotocol.com/protoc
323370
Indicates why the agent stopped processing the turn.
324371
</ResponseField>
325372

373+
<a id="session-run_command"></a>
374+
### <span class="font-mono">session/run_command</span>
375+
376+
Executes a custom command within a session.
377+
378+
Runs the specified command with optional arguments. The agent should
379+
stream results back via session update notifications.
380+
381+
#### <span class="font-mono">RunCommandRequest</span>
382+
383+
Request parameters for executing a command.
384+
385+
**Type:** Object
386+
387+
**Properties:**
388+
389+
<ResponseField name="args" type={"string | null"}>
390+
Optional arguments for the command.
391+
</ResponseField>
392+
<ResponseField name="command" type={"string"} required>
393+
Name of the command to execute.
394+
</ResponseField>
395+
<ResponseField
396+
name="sessionId"
397+
type={<a href="#sessionid">SessionId</a>}
398+
required
399+
>
400+
The session ID to execute the command in.
401+
</ResponseField>
402+
326403
## Client
327404

328405
Defines the interface that ACP-compliant clients must implement.
@@ -539,7 +616,7 @@ See protocol docs: [Agent Capabilities](https://agentclientprotocol.com/protocol
539616
<ResponseField name="promptCapabilities" type={<a href="#promptcapabilities">PromptCapabilities</a>} >
540617
Prompt capabilities supported by the agent.
541618

542-
- Default: `{"audio":false,"embeddedContext":false,"image":false}`
619+
- Default: `{"audio":false,"embeddedContext":false,"image":false,"supportsCustomCommands":false}`
543620

544621
</ResponseField>
545622

@@ -646,6 +723,24 @@ This capability is not part of the spec yet, and may be removed or changed at an
646723

647724
</ResponseField>
648725

726+
## <span class="font-mono">CommandInfo</span>
727+
728+
Information about a custom command.
729+
730+
**Type:** Object
731+
732+
**Properties:**
733+
734+
<ResponseField name="description" type={"string"} required>
735+
Human-readable description of what the command does.
736+
</ResponseField>
737+
<ResponseField name="name" type={"string"} required>
738+
Command name (e.g., "create_plan", "research_codebase").
739+
</ResponseField>
740+
<ResponseField name="requiresArgument" type={"boolean"} required>
741+
Whether this command requires arguments from the user.
742+
</ResponseField>
743+
649744
## <span class="font-mono">ContentBlock</span>
650745

651746
Content blocks represent displayable information in the Agent Client Protocol.
@@ -1151,6 +1246,12 @@ in prompt requests for pieces of context that are referenced in the message.
11511246

11521247
- Default: `false`
11531248

1249+
</ResponseField>
1250+
<ResponseField name="supportsCustomCommands" type={"boolean"} >
1251+
Agent supports custom slash commands via `list_commands` and `run_command`.
1252+
1253+
- Default: `false`
1254+
11541255
</ResponseField>
11551256

11561257
## <span class="font-mono">ProtocolVersion</span>

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/acp.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,27 @@ impl Agent for ClientSideConnection {
216216
Some(ClientNotification::CancelNotification(notification)),
217217
)
218218
}
219+
220+
async fn list_commands(
221+
&self,
222+
arguments: ListCommandsRequest,
223+
) -> Result<ListCommandsResponse, Error> {
224+
self.conn
225+
.request(
226+
SESSION_LIST_COMMANDS,
227+
Some(ClientRequest::ListCommandsRequest(arguments)),
228+
)
229+
.await
230+
}
231+
232+
async fn run_command(&self, arguments: RunCommandRequest) -> Result<(), Error> {
233+
self.conn
234+
.request(
235+
SESSION_RUN_COMMAND,
236+
Some(ClientRequest::RunCommandRequest(arguments)),
237+
)
238+
.await
239+
}
219240
}
220241

221242
/// Marker type representing the client side of an ACP connection.
@@ -509,6 +530,12 @@ impl Side for AgentSide {
509530
SESSION_PROMPT_METHOD_NAME => serde_json::from_str(params.get())
510531
.map(ClientRequest::PromptRequest)
511532
.map_err(Into::into),
533+
SESSION_LIST_COMMANDS => serde_json::from_str(params.get())
534+
.map(ClientRequest::ListCommandsRequest)
535+
.map_err(Into::into),
536+
SESSION_RUN_COMMAND => serde_json::from_str(params.get())
537+
.map(ClientRequest::RunCommandRequest)
538+
.map_err(Into::into),
512539
_ => Err(Error::method_not_found()),
513540
}
514541
}
@@ -551,6 +578,14 @@ impl<T: Agent> MessageHandler<AgentSide> for T {
551578
let response = self.prompt(args).await?;
552579
Ok(AgentResponse::PromptResponse(response))
553580
}
581+
ClientRequest::ListCommandsRequest(args) => {
582+
let response = self.list_commands(args).await?;
583+
Ok(AgentResponse::ListCommandsResponse(response))
584+
}
585+
ClientRequest::RunCommandRequest(args) => {
586+
self.run_command(args).await?;
587+
Ok(AgentResponse::AuthenticateResponse) // No specific response type for run_command
588+
}
554589
}
555590
}
556591

rust/agent.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,21 @@ pub trait Agent {
105105
///
106106
/// See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation)
107107
fn cancel(&self, args: CancelNotification) -> impl Future<Output = Result<(), Error>>;
108+
109+
/// Lists available custom commands for a session.
110+
///
111+
/// Returns all commands available in the agent's `.claude/commands` directory
112+
/// or equivalent command registry. Commands can be executed via `run_command`.
113+
fn list_commands(
114+
&self,
115+
arguments: ListCommandsRequest,
116+
) -> impl Future<Output = Result<ListCommandsResponse, Error>>;
117+
118+
/// Executes a custom command within a session.
119+
///
120+
/// Runs the specified command with optional arguments. The agent should
121+
/// stream results back via session update notifications.
122+
fn run_command(&self, arguments: RunCommandRequest) -> impl Future<Output = Result<(), Error>>;
108123
}
109124

110125
// Initialize
@@ -368,6 +383,54 @@ pub struct PromptCapabilities {
368383
/// in prompt requests for pieces of context that are referenced in the message.
369384
#[serde(default)]
370385
pub embedded_context: bool,
386+
/// Agent supports custom slash commands via `list_commands` and `run_command`.
387+
#[serde(default)]
388+
pub supports_custom_commands: bool,
389+
}
390+
391+
// Slash commands
392+
393+
/// Request parameters for listing available commands.
394+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
395+
#[schemars(extend("x-side" = "agent", "x-method" = "session/list_commands"))]
396+
#[serde(rename_all = "camelCase")]
397+
pub struct ListCommandsRequest {
398+
/// The session ID to list commands for.
399+
pub session_id: SessionId,
400+
}
401+
402+
/// Response containing available commands.
403+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
404+
#[schemars(extend("x-side" = "agent", "x-method" = "session/list_commands"))]
405+
#[serde(rename_all = "camelCase")]
406+
pub struct ListCommandsResponse {
407+
/// List of available commands.
408+
pub commands: Vec<CommandInfo>,
409+
}
410+
411+
/// Information about a custom command.
412+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
413+
#[serde(rename_all = "camelCase")]
414+
pub struct CommandInfo {
415+
/// Command name (e.g., "create_plan", "research_codebase").
416+
pub name: String,
417+
/// Human-readable description of what the command does.
418+
pub description: String,
419+
/// Whether this command requires arguments from the user.
420+
pub requires_argument: bool,
421+
}
422+
423+
/// Request parameters for executing a command.
424+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
425+
#[schemars(extend("x-side" = "agent", "x-method" = "session/run_command"))]
426+
#[serde(rename_all = "camelCase")]
427+
pub struct RunCommandRequest {
428+
/// The session ID to execute the command in.
429+
pub session_id: SessionId,
430+
/// Name of the command to execute.
431+
pub command: String,
432+
/// Optional arguments for the command.
433+
pub args: Option<String>,
371434
}
372435

373436
// Method schema
@@ -413,6 +476,10 @@ pub(crate) const SESSION_LOAD_METHOD_NAME: &str = "session/load";
413476
pub(crate) const SESSION_PROMPT_METHOD_NAME: &str = "session/prompt";
414477
/// Method name for the cancel notification.
415478
pub(crate) const SESSION_CANCEL_METHOD_NAME: &str = "session/cancel";
479+
/// Method name for listing custom commands in a session.
480+
pub const SESSION_LIST_COMMANDS: &str = "session/list_commands";
481+
/// Method name for running a custom command in a session.
482+
pub const SESSION_RUN_COMMAND: &str = "session/run_command";
416483

417484
/// All possible requests that a client can send to an agent.
418485
///
@@ -429,6 +496,8 @@ pub enum ClientRequest {
429496
NewSessionRequest(NewSessionRequest),
430497
LoadSessionRequest(LoadSessionRequest),
431498
PromptRequest(PromptRequest),
499+
ListCommandsRequest(ListCommandsRequest),
500+
RunCommandRequest(RunCommandRequest),
432501
}
433502

434503
/// All possible responses that an agent can send to a client.
@@ -446,6 +515,7 @@ pub enum AgentResponse {
446515
NewSessionResponse(NewSessionResponse),
447516
LoadSessionResponse,
448517
PromptResponse(PromptResponse),
518+
ListCommandsResponse(ListCommandsResponse),
449519
}
450520

451521
/// All possible notifications that a client can send to an agent.

rust/example_agent.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,25 @@ impl acp::Agent for ExampleAgent {
9696
log::info!("Received cancel request {args:?}");
9797
Ok(())
9898
}
99+
100+
async fn list_commands(
101+
&self,
102+
arguments: acp::ListCommandsRequest,
103+
) -> Result<acp::ListCommandsResponse, acp::Error> {
104+
log::info!("Received list_commands request {arguments:?}");
105+
Ok(acp::ListCommandsResponse {
106+
commands: vec![acp::CommandInfo {
107+
name: "test".to_string(),
108+
description: "A test command".to_string(),
109+
requires_argument: false,
110+
}],
111+
})
112+
}
113+
114+
async fn run_command(&self, arguments: acp::RunCommandRequest) -> Result<(), acp::Error> {
115+
log::info!("Received run_command request {arguments:?}");
116+
Ok(())
117+
}
99118
}
100119

101120
#[tokio::main(flavor = "current_thread")]

rust/markdown_generator.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,8 @@ impl SideDocs {
634634
"session/load" => self.agent_methods.get("load_session").unwrap(),
635635
"session/prompt" => self.agent_methods.get("prompt").unwrap(),
636636
"session/cancel" => self.agent_methods.get("cancel").unwrap(),
637+
"session/list_commands" => self.agent_methods.get("list_commands").unwrap(),
638+
"session/run_command" => self.agent_methods.get("run_command").unwrap(),
637639
_ => panic!("Introduced a method? Add it here :)"),
638640
}
639641
}

rust/rpc_tests.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,17 @@ impl Agent for TestAgent {
160160
.push(args.session_id);
161161
Ok(())
162162
}
163+
164+
async fn list_commands(
165+
&self,
166+
_arguments: ListCommandsRequest,
167+
) -> Result<ListCommandsResponse, Error> {
168+
Ok(ListCommandsResponse { commands: vec![] })
169+
}
170+
171+
async fn run_command(&self, _arguments: RunCommandRequest) -> Result<(), Error> {
172+
Ok(())
173+
}
163174
}
164175

165176
// Helper function to create a bidirectional connection

0 commit comments

Comments
 (0)