sacp_cookbook/lib.rs
1//! Cookbook of common patterns for building ACP components.
2//!
3//! This crate contains guides and examples for the three main things you can build with sacp:
4//!
5//! - **Clients** - Connect to an existing agent and send prompts
6//! - **Proxies** - Sit between client and agent to add capabilities (like MCP tools)
7//! - **Agents** - Respond to prompts with AI-powered responses
8//!
9//! See the [`sacp::concepts`] module for detailed explanations of
10//! the concepts behind the API.
11//!
12//! # Building Clients
13//!
14//! A client connects to an agent, sends requests, and handles responses. Use
15//! [`ClientToAgent`] as your link type.
16//!
17//! - [`one_shot_prompt`] - Send a single prompt and get a response (simplest pattern)
18//! - [`connecting_as_client`] - More details on connection setup and permission handling
19//!
20//! # Building Proxies
21//!
22//! A proxy sits between client and agent, intercepting and optionally modifying
23//! messages. The most common use case is adding MCP tools. Use [`ProxyToConductor`]
24//! as your link type.
25//!
26//! **Important:** Proxies don't run standalone—they need the [`sacp-conductor`] to
27//! orchestrate the connection between client, proxies, and agent. See
28//! [`running_proxies_with_conductor`] for how to put the pieces together.
29//!
30//! - [`global_mcp_server`] - Add tools that work across all sessions
31//! - [`per_session_mcp_server`] - Add tools with session-specific state
32//! - [`filtering_tools`] - Enable or disable tools dynamically
33//! - [`reusable_components`] - Package your proxy as a [`Component`] for composition
34//! - [`running_proxies_with_conductor`] - Run your proxy with an agent
35//!
36//! [`sacp-conductor`]: https://crates.io/crates/sacp-conductor
37//!
38//! # Building Agents
39//!
40//! An agent receives prompts and generates responses. Use [`AgentToClient`] as
41//! your link type.
42//!
43//! - [`building_an_agent`] - Handle initialization, sessions, and prompts
44//! - [`reusable_components`] - Package your agent as a [`Component`]
45//! - [`custom_message_handlers`] - Fine-grained control over message routing
46//!
47//! [`sacp::concepts`]: sacp::concepts
48//! [`ClientToAgent`]: sacp::ClientToAgent
49//! [`AgentToClient`]: sacp::AgentToClient
50//! [`ProxyToConductor`]: sacp::ProxyToConductor
51//! [`Component`]: sacp::Component
52
53pub mod one_shot_prompt {
54 //! Pattern: You Only Prompt Once.
55 //!
56 //! The simplest client pattern: connect to an agent, send one prompt, get the
57 //! response. This is useful for CLI tools, scripts, or any case where you just
58 //! need a single interaction with an agent.
59 //!
60 //! # Example
61 //!
62 //! ```
63 //! use sacp::{ClientToAgent, AgentToClient, Component};
64 //! use sacp::schema::{InitializeRequest, ProtocolVersion};
65 //!
66 //! async fn ask_agent(
67 //! transport: impl Component<AgentToClient> + 'static,
68 //! prompt: &str,
69 //! ) -> Result<String, sacp::Error> {
70 //! ClientToAgent::builder()
71 //! .name("my-client")
72 //! .connect_to(transport)?
73 //! .run_until(async |cx| {
74 //! // Initialize the connection
75 //! cx.send_request(InitializeRequest::new(ProtocolVersion::LATEST))
76 //! .block_task().await?;
77 //!
78 //! // Create a session, send prompt, read response
79 //! let mut session = cx.build_session_cwd()?
80 //! .block_task()
81 //! .start_session()
82 //! .await?;
83 //!
84 //! session.send_prompt(prompt)?;
85 //! session.read_to_string().await
86 //! })
87 //! .await
88 //! }
89 //! ```
90 //!
91 //! # How it works
92 //!
93 //! 1. **[`connect_to`]** establishes the transport connection
94 //! 2. **[`run_until`]** runs your code while the connection handles messages
95 //! in the background
96 //! 3. **[`send_request`]** + **[`block_task`]** sends the initialize request
97 //! and waits for the response
98 //! 4. **[`build_session_cwd`]** creates a session builder using the current working directory
99 //! 5. **[`start_session`]** sends the `NewSessionRequest` and returns an
100 //! [`ActiveSession`] handle
101 //! 6. **[`send_prompt`]** queues the prompt to send to the agent
102 //! 7. **[`read_to_string`]** reads all text chunks until the agent finishes
103 //!
104 //! # Handling permission requests
105 //!
106 //! Most agents will ask for permission before taking actions like running
107 //! commands or writing files. See [`connecting_as_client`] for how to handle
108 //! [`RequestPermissionRequest`] messages.
109 //!
110 //! [`connect_to`]: sacp::JrConnectionBuilder::connect_to
111 //! [`run_until`]: sacp::JrConnection::run_until
112 //! [`send_request`]: sacp::JrConnectionCx::send_request
113 //! [`block_task`]: sacp::JrResponse::block_task
114 //! [`build_session_cwd`]: sacp::JrConnectionCx::build_session_cwd
115 //! [`start_session`]: sacp::SessionBuilder::start_session
116 //! [`ActiveSession`]: sacp::ActiveSession
117 //! [`send_prompt`]: sacp::ActiveSession::send_prompt
118 //! [`read_to_string`]: sacp::ActiveSession::read_to_string
119 //! [`connecting_as_client`]: super::connecting_as_client
120 //! [`RequestPermissionRequest`]: sacp::schema::RequestPermissionRequest
121}
122
123pub mod connecting_as_client {
124 //! Pattern: Connecting as a client.
125 //!
126 //! To connect to an ACP agent and send requests, use [`run_until`].
127 //! This runs your code while the connection handles incoming messages
128 //! in the background.
129 //!
130 //! # Basic Example
131 //!
132 //! ```
133 //! use sacp::{ClientToAgent, AgentToClient, Component};
134 //! use sacp::schema::{InitializeRequest, ProtocolVersion};
135 //!
136 //! async fn connect_to_agent(transport: impl Component<AgentToClient>) -> Result<(), sacp::Error> {
137 //! ClientToAgent::builder()
138 //! .name("my-client")
139 //! .run_until(transport, async |cx| {
140 //! // Initialize the connection
141 //! cx.send_request(InitializeRequest::new(ProtocolVersion::LATEST))
142 //! .block_task().await?;
143 //!
144 //! // Create a session and send a prompt
145 //! cx.build_session_cwd()?
146 //! .block_task()
147 //! .run_until(async |mut session| {
148 //! session.send_prompt("Hello, agent!")?;
149 //! let response = session.read_to_string().await?;
150 //! println!("Agent said: {}", response);
151 //! Ok(())
152 //! })
153 //! .await
154 //! })
155 //! .await
156 //! }
157 //! ```
158 //!
159 //! # Using the Session Builder
160 //!
161 //! The [`build_session`] method creates a [`SessionBuilder`] that handles
162 //! session creation and provides convenient methods for interacting with
163 //! the session:
164 //!
165 //! - [`send_prompt`] - Send a text prompt to the agent
166 //! - [`read_update`] - Read the next update (text chunk, tool call, etc.)
167 //! - [`read_to_string`] - Read all text until the turn ends
168 //!
169 //! The session builder also supports adding MCP servers with [`with_mcp_server`].
170 //!
171 //! # Handling Permission Requests
172 //!
173 //! Agents may send [`RequestPermissionRequest`] to ask for user approval
174 //! before taking actions. Handle these with [`on_receive_request`]:
175 //!
176 //! ```ignore
177 //! ClientToAgent::builder()
178 //! .on_receive_request(async |req: RequestPermissionRequest, request_cx, _cx| {
179 //! // Auto-approve by selecting the first option (YOLO mode)
180 //! let option_id = req.options.first().map(|opt| opt.id.clone());
181 //! request_cx.respond(RequestPermissionResponse {
182 //! outcome: match option_id {
183 //! Some(id) => RequestPermissionOutcome::Selected { option_id: id },
184 //! None => RequestPermissionOutcome::Cancelled,
185 //! },
186 //! meta: None,
187 //! })
188 //! }, sacp::on_receive_request!())
189 //! .run_until(transport, async |cx| { /* ... */ })
190 //! .await
191 //! ```
192 //!
193 //! # Note on `block_task`
194 //!
195 //! Using [`block_task`] is safe inside `run_until` because the closure runs
196 //! as a spawned task, not on the event loop. The event loop continues processing
197 //! messages (including the response you're waiting for) while your task blocks.
198 //!
199 //! [`run_until`]: sacp::JrConnectionBuilder::run_until
200 //! [`block_task`]: sacp::JrResponse::block_task
201 //! [`build_session`]: sacp::JrConnectionCx::build_session
202 //! [`SessionBuilder`]: sacp::SessionBuilder
203 //! [`send_prompt`]: sacp::ActiveSession::send_prompt
204 //! [`read_update`]: sacp::ActiveSession::read_update
205 //! [`read_to_string`]: sacp::ActiveSession::read_to_string
206 //! [`with_mcp_server`]: sacp::SessionBuilder::with_mcp_server
207 //! [`RequestPermissionRequest`]: sacp::schema::RequestPermissionRequest
208 //! [`on_receive_request`]: sacp::JrConnectionBuilder::on_receive_request
209}
210
211pub mod building_an_agent {
212 //! Pattern: Building an agent.
213 //!
214 //! An agent handles prompts and generates responses. At minimum, an agent must:
215 //!
216 //! 1. Handle [`InitializeRequest`] to establish the connection
217 //! 2. Handle [`NewSessionRequest`] to create sessions
218 //! 3. Handle [`PromptRequest`] to process prompts
219 //!
220 //! Use [`AgentToClient`] as your link type.
221 //!
222 //! # Minimal Example
223 //!
224 //! ```
225 //! use sacp::{AgentToClient, Component, MessageCx, JrConnectionCx};
226 //! use sacp::link::JrLink;
227 //! use sacp::schema::{
228 //! InitializeRequest, InitializeResponse, AgentCapabilities,
229 //! NewSessionRequest, NewSessionResponse, SessionId,
230 //! PromptRequest, PromptResponse, StopReason,
231 //! };
232 //!
233 //! async fn run_agent(transport: impl Component<sacp::ClientToAgent>) -> Result<(), sacp::Error> {
234 //! AgentToClient::builder()
235 //! .name("my-agent")
236 //! // Handle initialization
237 //! .on_receive_request(async |req: InitializeRequest, request_cx, _cx| {
238 //! request_cx.respond(
239 //! InitializeResponse::new(req.protocol_version)
240 //! .agent_capabilities(AgentCapabilities::new())
241 //! )
242 //! }, sacp::on_receive_request!())
243 //! // Handle session creation
244 //! .on_receive_request(async |req: NewSessionRequest, request_cx, _cx| {
245 //! request_cx.respond(NewSessionResponse::new(SessionId::new("session-1")))
246 //! }, sacp::on_receive_request!())
247 //! // Handle prompts
248 //! .on_receive_request(async |req: PromptRequest, request_cx, cx| {
249 //! // Send streaming updates via notifications
250 //! // cx.send_notification(SessionNotification { ... })?;
251 //!
252 //! // Return final response
253 //! request_cx.respond(PromptResponse::new(StopReason::EndTurn))
254 //! }, sacp::on_receive_request!())
255 //! // Reject unknown messages
256 //! .on_receive_message(async |message: MessageCx, cx: JrConnectionCx<AgentToClient>| {
257 //! message.respond_with_error(sacp::Error::method_not_found(), cx)
258 //! }, sacp::on_receive_message!())
259 //! .serve(transport)
260 //! .await
261 //! }
262 //! ```
263 //!
264 //! # Streaming Responses
265 //!
266 //! To stream text or other updates to the client, send [`SessionNotification`]s
267 //! while processing a prompt:
268 //!
269 //! ```ignore
270 //! .on_receive_request(async |req: PromptRequest, request_cx, cx| {
271 //! // Stream some text
272 //! cx.send_notification(SessionNotification {
273 //! session_id: req.session_id.clone(),
274 //! update: SessionUpdate::Text(TextUpdate {
275 //! text: "Hello, ".into(),
276 //! // ...
277 //! }),
278 //! meta: None,
279 //! })?;
280 //!
281 //! cx.send_notification(SessionNotification {
282 //! session_id: req.session_id.clone(),
283 //! update: SessionUpdate::Text(TextUpdate {
284 //! text: "world!".into(),
285 //! // ...
286 //! }),
287 //! meta: None,
288 //! })?;
289 //!
290 //! request_cx.respond(PromptResponse {
291 //! stop_reason: StopReason::EndTurn,
292 //! meta: None,
293 //! })
294 //! }, sacp::on_receive_request!())
295 //! ```
296 //!
297 //! # Requesting Permissions
298 //!
299 //! Before taking actions that require user approval (like running commands
300 //! or writing files), send a [`RequestPermissionRequest`]:
301 //!
302 //! ```ignore
303 //! let response = cx.send_request(RequestPermissionRequest {
304 //! session_id: session_id.clone(),
305 //! action: PermissionAction::Bash { command: "rm -rf /".into() },
306 //! options: vec![
307 //! PermissionOption { id: "allow".into(), label: "Allow".into() },
308 //! PermissionOption { id: "deny".into(), label: "Deny".into() },
309 //! ],
310 //! meta: None,
311 //! }).block_task().await?;
312 //!
313 //! match response.outcome {
314 //! RequestPermissionOutcome::Selected { option_id } if option_id == "allow" => {
315 //! // User approved, proceed with action
316 //! }
317 //! _ => {
318 //! // User denied or cancelled
319 //! }
320 //! }
321 //! ```
322 //!
323 //! # As a Reusable Component
324 //!
325 //! For agents that will be composed with proxies, implement [`Component`].
326 //! See [`reusable_components`] for the pattern.
327 //!
328 //! [`InitializeRequest`]: sacp::schema::InitializeRequest
329 //! [`NewSessionRequest`]: sacp::schema::NewSessionRequest
330 //! [`PromptRequest`]: sacp::schema::PromptRequest
331 //! [`SessionNotification`]: sacp::schema::SessionNotification
332 //! [`RequestPermissionRequest`]: sacp::schema::RequestPermissionRequest
333 //! [`AgentToClient`]: sacp::AgentToClient
334 //! [`Component`]: sacp::Component
335 //! [`reusable_components`]: super::reusable_components
336}
337
338pub mod reusable_components {
339 //! Pattern: Defining reusable components.
340 //!
341 //! When building agents or proxies that will be composed together (for example,
342 //! with [`sacp-conductor`]), define a struct that implements [`Component`].
343 //! This allows your component to be connected to other components in a type-safe way.
344 //!
345 //! # Example
346 //!
347 //! ```
348 //! use sacp::{Component, AgentToClient};
349 //! use sacp::link::JrLink;
350 //! use sacp::schema::{
351 //! InitializeRequest, InitializeResponse, AgentCapabilities,
352 //! };
353 //!
354 //! struct MyAgent {
355 //! name: String,
356 //! }
357 //!
358 //! impl Component<AgentToClient> for MyAgent {
359 //! async fn serve(self, client: impl Component<<AgentToClient as JrLink>::ConnectsTo>) -> Result<(), sacp::Error> {
360 //! AgentToClient::builder()
361 //! .name(&self.name)
362 //! .on_receive_request(async move |req: InitializeRequest, request_cx, _cx| {
363 //! request_cx.respond(
364 //! InitializeResponse::new(req.protocol_version)
365 //! .agent_capabilities(AgentCapabilities::new())
366 //! )
367 //! }, sacp::on_receive_request!())
368 //! .serve(client)
369 //! .await
370 //! }
371 //! }
372 //!
373 //! let agent = MyAgent { name: "my-agent".into() };
374 //! ```
375 //!
376 //! # Important: Don't block the event loop
377 //!
378 //! Message handlers run on the event loop. Blocking in a handler prevents the
379 //! connection from processing new messages. For expensive work:
380 //!
381 //! - Use [`JrConnectionCx::spawn`] to offload work to a background task
382 //! - Use [`on_receiving_result`] to schedule work when a response arrives
383 //!
384 //! [`Component`]: sacp::Component
385 //! [`JrConnectionCx::spawn`]: sacp::JrConnectionCx::spawn
386 //! [`on_receiving_result`]: sacp::JrResponse::on_receiving_result
387 //! [`sacp-conductor`]: https://crates.io/crates/sacp-conductor
388}
389
390pub mod custom_message_handlers {
391 //! Pattern: Custom message handlers.
392 //!
393 //! For reusable message handling logic, implement [`JrMessageHandler`] and use
394 //! [`MatchMessage`] or [`MatchMessageFrom`] for type-safe dispatching.
395 //!
396 //! This is useful when you need to:
397 //! - Share message handling logic across multiple components
398 //! - Build complex routing logic that doesn't fit the builder pattern
399 //! - Integrate with existing handler infrastructure
400 //!
401 //! # Example
402 //!
403 //! ```
404 //! use sacp::{JrMessageHandler, MessageCx, Handled, JrConnectionCx};
405 //! use sacp::schema::{InitializeRequest, InitializeResponse, AgentCapabilities};
406 //! use sacp::util::MatchMessage;
407 //!
408 //! struct MyHandler;
409 //!
410 //! impl JrMessageHandler for MyHandler {
411 //! type Link = sacp::link::UntypedLink;
412 //!
413 //! async fn handle_message(
414 //! &mut self,
415 //! message: MessageCx,
416 //! _cx: JrConnectionCx<Self::Link>,
417 //! ) -> Result<Handled<MessageCx>, sacp::Error> {
418 //! MatchMessage::new(message)
419 //! .if_request(async |req: InitializeRequest, request_cx| {
420 //! request_cx.respond(
421 //! InitializeResponse::new(req.protocol_version)
422 //! .agent_capabilities(AgentCapabilities::new())
423 //! )
424 //! })
425 //! .await
426 //! .done()
427 //! }
428 //!
429 //! fn describe_chain(&self) -> impl std::fmt::Debug {
430 //! "MyHandler"
431 //! }
432 //! }
433 //! ```
434 //!
435 //! # When to use `MatchMessage` vs `MatchMessageFrom`
436 //!
437 //! - [`MatchMessage`] - Use when you don't need peer-aware handling
438 //! - [`MatchMessageFrom`] - Use in proxies where messages come from different
439 //! peers (`ClientPeer` vs `AgentPeer`) and may need different handling
440 //!
441 //! [`JrMessageHandler`]: sacp::JrMessageHandler
442 //! [`MatchMessage`]: sacp::util::MatchMessage
443 //! [`MatchMessageFrom`]: sacp::util::MatchMessageFrom
444}
445
446pub mod global_mcp_server {
447 //! Pattern: Global MCP server in handler chain.
448 //!
449 //! Use this pattern when you want a single MCP server that handles tool calls
450 //! for all sessions. The server is added to the connection's handler chain and
451 //! automatically injects itself into every `NewSessionRequest` that passes through.
452 //!
453 //! # When to use
454 //!
455 //! - The MCP server provides stateless tools (no per-session state needed)
456 //! - You want the simplest setup with minimal boilerplate
457 //! - Tools don't need access to session-specific context
458 //!
459 //! # Using the builder API
460 //!
461 //! The simplest way to create an MCP server is with [`McpServer::builder`]:
462 //!
463 //! ```
464 //! use sacp::mcp_server::McpServer;
465 //! use sacp::{Component, JrResponder, ProxyToConductor};
466 //! use schemars::JsonSchema;
467 //! use serde::{Deserialize, Serialize};
468 //!
469 //! #[derive(Debug, Deserialize, JsonSchema)]
470 //! struct EchoParams { message: String }
471 //!
472 //! #[derive(Debug, Serialize, JsonSchema)]
473 //! struct EchoOutput { echoed: String }
474 //!
475 //! // Build the MCP server with tools
476 //! let mcp_server = McpServer::builder("my-tools")
477 //! .tool_fn("echo", "Echoes the input",
478 //! async |params: EchoParams, _cx| {
479 //! Ok(EchoOutput { echoed: params.message })
480 //! },
481 //! sacp::tool_fn!())
482 //! .build();
483 //!
484 //! // The proxy component is generic over the MCP server's responder type
485 //! struct MyProxy<R> {
486 //! mcp_server: McpServer<ProxyToConductor, R>,
487 //! }
488 //!
489 //! impl<R: JrResponder<ProxyToConductor> + Send + 'static> Component<ProxyToConductor> for MyProxy<R> {
490 //! async fn serve(self, client: impl Component<sacp::link::ConductorToProxy>) -> Result<(), sacp::Error> {
491 //! ProxyToConductor::builder()
492 //! .with_mcp_server(self.mcp_server)
493 //! .serve(client)
494 //! .await
495 //! }
496 //! }
497 //!
498 //! let proxy = MyProxy { mcp_server };
499 //! ```
500 //!
501 //! # Using rmcp
502 //!
503 //! If you have an existing [rmcp](https://docs.rs/rmcp) server implementation,
504 //! use [`McpServer::from_rmcp`] from the `sacp-rmcp` crate:
505 //!
506 //! ```
507 //! use rmcp::{ServerHandler, tool, tool_router, tool_handler};
508 //! use rmcp::handler::server::router::tool::ToolRouter;
509 //! use rmcp::handler::server::wrapper::Parameters;
510 //! use rmcp::model::*;
511 //! use sacp::mcp_server::McpServer;
512 //! use sacp::ProxyToConductor;
513 //! use sacp_rmcp::McpServerExt;
514 //! use serde::{Deserialize, Serialize};
515 //!
516 //! #[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
517 //! struct EchoParams {
518 //! message: String,
519 //! }
520 //!
521 //! #[derive(Clone)]
522 //! struct MyMcpServer {
523 //! tool_router: ToolRouter<Self>,
524 //! }
525 //!
526 //! impl MyMcpServer {
527 //! fn new() -> Self {
528 //! Self { tool_router: Self::tool_router() }
529 //! }
530 //! }
531 //!
532 //! #[tool_router]
533 //! impl MyMcpServer {
534 //! #[tool(description = "Echoes back the input message")]
535 //! async fn echo(&self, Parameters(params): Parameters<EchoParams>) -> Result<CallToolResult, rmcp::ErrorData> {
536 //! Ok(CallToolResult::success(vec![Content::text(format!("Echo: {}", params.message))]))
537 //! }
538 //! }
539 //!
540 //! #[tool_handler]
541 //! impl ServerHandler for MyMcpServer {
542 //! fn get_info(&self) -> ServerInfo {
543 //! ServerInfo {
544 //! protocol_version: ProtocolVersion::V_2024_11_05,
545 //! capabilities: ServerCapabilities::builder().enable_tools().build(),
546 //! server_info: Implementation::from_build_env(),
547 //! instructions: None,
548 //! }
549 //! }
550 //! }
551 //!
552 //! // Create an MCP server from the rmcp service
553 //! let mcp_server = McpServer::<ProxyToConductor, _>::from_rmcp("my-server", MyMcpServer::new);
554 //! ```
555 //!
556 //! The `from_rmcp` function takes a factory closure that creates a new server
557 //! instance. This allows each MCP connection to get a fresh server instance.
558 //!
559 //! # How it works
560 //!
561 //! When you call [`with_mcp_server`], the MCP server is added as a message
562 //! handler. It:
563 //!
564 //! 1. Intercepts `NewSessionRequest` messages and adds its `acp:UUID` URL to the
565 //! request's `mcp_servers` list
566 //! 2. Passes the modified request through to the next handler
567 //! 3. Handles incoming MCP protocol messages (tool calls, etc.) for its URL
568 //!
569 //! [`McpServer::builder`]: sacp::mcp_server::McpServer::builder
570 //! [`McpServer::from_rmcp`]: sacp_rmcp::McpServerExt::from_rmcp
571 //! [`with_mcp_server`]: sacp::JrConnectionBuilder::with_mcp_server
572}
573
574pub mod per_session_mcp_server {
575 //! Pattern: Per-session MCP server with workspace context.
576 //!
577 //! Use this pattern when each session needs its own MCP server instance
578 //! with access to session-specific context like the working directory.
579 //!
580 //! # When to use
581 //!
582 //! - Tools need access to the session's working directory
583 //! - You want to track active sessions or maintain per-session state
584 //! - Tools need to customize behavior based on session parameters
585 //!
586 //! # Basic pattern with `on_proxy_session_start`
587 //!
588 //! The most common pattern intercepts [`NewSessionRequest`], extracts context,
589 //! creates a per-session MCP server, and uses [`on_proxy_session_start`] to
590 //! run code after the session is established:
591 //!
592 //! ```
593 //! use sacp::mcp_server::McpServer;
594 //! use sacp::schema::NewSessionRequest;
595 //! use sacp::{ClientPeer, Component, ProxyToConductor};
596 //! use sacp::link::ConductorToProxy;
597 //!
598 //! async fn run_proxy(transport: impl Component<ConductorToProxy>) -> Result<(), sacp::Error> {
599 //! ProxyToConductor::builder()
600 //! .on_receive_request_from(ClientPeer, async move |request: NewSessionRequest, request_cx, cx| {
601 //! // Extract session context from the request
602 //! let workspace_path = request.cwd.clone();
603 //!
604 //! // Create tools that capture the workspace path
605 //! let mcp_server = McpServer::builder("workspace-tools")
606 //! .tool_fn("get_workspace", "Returns the session's workspace directory", {
607 //! async move |_params: (), _cx| {
608 //! Ok(workspace_path.display().to_string())
609 //! }
610 //! }, sacp::tool_fn!())
611 //! .build();
612 //!
613 //! // Build the session and run code after it starts
614 //! cx.build_session_from(request)
615 //! .with_mcp_server(mcp_server)?
616 //! .on_proxy_session_start(request_cx, async move |session_id| {
617 //! // This callback runs after the session-id has been sent to the
618 //! // client but before any further messages from the client or agent
619 //! // related to this session have been processed.
620 //! //
621 //! // You can use this to store the `session_id` before processing
622 //! // future messages, or to send a first prompt to the agent before
623 //! // the client has a chance to do so.
624 //! tracing::info!(%session_id, "Session started");
625 //! Ok(())
626 //! })
627 //! }, sacp::on_receive_request!())
628 //! .serve(transport)
629 //! .await
630 //! }
631 //! ```
632 //!
633 //! # How `on_proxy_session_start` works
634 //!
635 //! [`on_proxy_session_start`] is the non-blocking way to set up a proxy session:
636 //!
637 //! 1. Sends `NewSessionRequest` to the agent
638 //! 2. When the response arrives, responds to the client automatically
639 //! 3. Sets up message proxying for the session
640 //! 4. Runs your callback with the `SessionId`
641 //!
642 //! The callback runs after the session is established but doesn't block
643 //! the message handler. This is ideal for proxies that just need to inject
644 //! tools and track sessions.
645 //!
646 //! # Alternative: blocking with `start_session_proxy`
647 //!
648 //! If you need the simpler blocking API (e.g., in a client context where
649 //! blocking is safe), use [`block_task`] + [`start_session_proxy`]:
650 //!
651 //! ```
652 //! # use sacp::mcp_server::McpServer;
653 //! # use sacp::schema::NewSessionRequest;
654 //! # use sacp::{ClientPeer, Component, ProxyToConductor};
655 //! # use sacp::link::ConductorToProxy;
656 //! # async fn run_proxy(transport: impl Component<ConductorToProxy>) -> Result<(), sacp::Error> {
657 //! ProxyToConductor::builder()
658 //! .on_receive_request_from(ClientPeer, async |request: NewSessionRequest, request_cx, cx| {
659 //! let cwd = request.cwd.clone();
660 //! let mcp_server = McpServer::builder("tools")
661 //! .tool_fn("get_cwd", "Returns working directory", {
662 //! async move |_params: (), _cx| Ok(cwd.display().to_string())
663 //! }, sacp::tool_fn!())
664 //! .build();
665 //!
666 //! let session_id = cx.build_session_from(request)
667 //! .with_mcp_server(mcp_server)?
668 //! .block_task()
669 //! .start_session_proxy(request_cx)
670 //! .await?;
671 //!
672 //! tracing::info!(%session_id, "Session started");
673 //! Ok(())
674 //! }, sacp::on_receive_request!())
675 //! .serve(transport)
676 //! .await
677 //! # }
678 //! ```
679 //!
680 //! For patterns where you need to interact with the session before proxying,
681 //! use [`start_session`] + [`proxy_remaining_messages`] instead.
682 //!
683 //! [`start_session`]: sacp::SessionBuilder::start_session
684 //! [`proxy_remaining_messages`]: sacp::ActiveSession::proxy_remaining_messages
685 //!
686 //! [`NewSessionRequest`]: sacp::schema::NewSessionRequest
687 //! [`on_proxy_session_start`]: sacp::SessionBuilder::on_proxy_session_start
688 //! [`block_task`]: sacp::SessionBuilder::block_task
689 //! [`start_session_proxy`]: sacp::SessionBuilder::start_session_proxy
690}
691
692pub mod filtering_tools {
693 //! Pattern: Filtering which tools are available.
694 //!
695 //! Use [`disable_tool`] and [`enable_tool`] to control which tools are
696 //! visible to clients. This is useful when:
697 //!
698 //! - Some tools should only be available in certain configurations
699 //! - You want to conditionally expose tools based on runtime settings
700 //! - You need to restrict access to sensitive tools
701 //!
702 //! # Disabling specific tools (deny-list)
703 //!
704 //! By default, all registered tools are enabled. Use [`disable_tool`] to
705 //! hide specific tools:
706 //!
707 //! ```
708 //! use sacp::mcp_server::McpServer;
709 //! use sacp::ProxyToConductor;
710 //! use schemars::JsonSchema;
711 //! use serde::Deserialize;
712 //!
713 //! #[derive(Debug, Deserialize, JsonSchema)]
714 //! struct Params {}
715 //!
716 //! fn build_server(enable_admin: bool) -> Result<McpServer<ProxyToConductor, impl sacp::JrResponder<ProxyToConductor>>, sacp::Error> {
717 //! let mut builder = McpServer::builder("my-server")
718 //! .tool_fn("echo", "Echo a message",
719 //! async |_p: Params, _cx| Ok("echoed"),
720 //! sacp::tool_fn!())
721 //! .tool_fn("admin", "Admin-only tool",
722 //! async |_p: Params, _cx| Ok("admin action"),
723 //! sacp::tool_fn!());
724 //!
725 //! // Conditionally disable the admin tool
726 //! if !enable_admin {
727 //! builder = builder.disable_tool("admin")?;
728 //! }
729 //!
730 //! Ok(builder.build())
731 //! }
732 //! ```
733 //!
734 //! Disabled tools:
735 //! - Don't appear in `list_tools` responses
736 //! - Return "tool not found" errors if called directly
737 //!
738 //! # Enabling only specific tools (allow-list)
739 //!
740 //! Use [`disable_all_tools`] followed by [`enable_tool`] to create an
741 //! allow-list where only explicitly enabled tools are available:
742 //!
743 //! ```
744 //! use sacp::mcp_server::McpServer;
745 //! use sacp::ProxyToConductor;
746 //! use schemars::JsonSchema;
747 //! use serde::Deserialize;
748 //!
749 //! #[derive(Debug, Deserialize, JsonSchema)]
750 //! struct Params {}
751 //!
752 //! fn build_restricted_server() -> Result<McpServer<ProxyToConductor, impl sacp::JrResponder<ProxyToConductor>>, sacp::Error> {
753 //! McpServer::builder("restricted-server")
754 //! .tool_fn("safe", "Safe operation",
755 //! async |_p: Params, _cx| Ok("safe"),
756 //! sacp::tool_fn!())
757 //! .tool_fn("dangerous", "Dangerous operation",
758 //! async |_p: Params, _cx| Ok("danger!"),
759 //! sacp::tool_fn!())
760 //! .tool_fn("experimental", "Experimental feature",
761 //! async |_p: Params, _cx| Ok("experimental"),
762 //! sacp::tool_fn!())
763 //! // Start with all tools disabled
764 //! .disable_all_tools()
765 //! // Only enable the safe tool
766 //! .enable_tool("safe")
767 //! .map(|b| b.build())
768 //! }
769 //! ```
770 //!
771 //! # Error handling
772 //!
773 //! Both [`enable_tool`] and [`disable_tool`] return `Result` and will error
774 //! if the tool name doesn't match any registered tool. This helps catch typos:
775 //!
776 //! ```
777 //! use sacp::mcp_server::McpServer;
778 //! use sacp::ProxyToConductor;
779 //!
780 //! // This will error because "ech" is not a registered tool
781 //! let result = McpServer::<ProxyToConductor, _>::builder("server")
782 //! .disable_tool("ech"); // Typo! Should be "echo"
783 //!
784 //! assert!(result.is_err());
785 //! ```
786 //!
787 //! Calling enable/disable on an already enabled/disabled tool is not an error -
788 //! the operations are idempotent.
789 //!
790 //! [`disable_tool`]: sacp::mcp_server::McpServerBuilder::disable_tool
791 //! [`enable_tool`]: sacp::mcp_server::McpServerBuilder::enable_tool
792 //! [`disable_all_tools`]: sacp::mcp_server::McpServerBuilder::disable_all_tools
793}
794
795pub mod running_proxies_with_conductor {
796 //! Pattern: Running proxies with the conductor.
797 //!
798 //! Proxies don't run standalone. To add an MCP server (or other proxy behavior)
799 //! to an existing agent, you need the **conductor** to orchestrate the connection.
800 //!
801 //! The conductor:
802 //! 1. Accepts connections from clients
803 //! 2. Chains your proxies together
804 //! 3. Connects to the final agent
805 //! 4. Routes messages through the entire chain
806 //!
807 //! # Using the `sacp-conductor` binary
808 //!
809 //! The simplest way to run a proxy is with the [`sacp-conductor`] binary.
810 //! Configure it with a JSON file:
811 //!
812 //! ```json
813 //! {
814 //! "proxies": [
815 //! { "command": ["cargo", "run", "--bin", "my-proxy"] }
816 //! ],
817 //! "agent": { "command": ["claude-code", "--agent"] }
818 //! }
819 //! ```
820 //!
821 //! Then run:
822 //!
823 //! ```bash
824 //! sacp-conductor --config conductor.json
825 //! ```
826 //!
827 //! # Using the conductor as a library
828 //!
829 //! For more control, use [`sacp-conductor`] as a library with the [`Conductor`] type:
830 //!
831 //! ```ignore
832 //! use sacp_conductor::{Conductor, ProxiesAndAgent};
833 //!
834 //! // Define your proxy as a Component<ProxyToConductor>
835 //! let my_proxy = MyProxy::new();
836 //!
837 //! // Spawn the agent process
838 //! let agent_process = sacp_tokio::spawn_process("claude-code", &["--agent"]).await?;
839 //!
840 //! // Create the conductor with your proxy chain
841 //! let conductor = Conductor::new(ProxiesAndAgent {
842 //! proxies: vec![Box::new(my_proxy)],
843 //! agent: agent_process,
844 //! });
845 //!
846 //! // Run the conductor (it will accept client connections on stdin/stdout)
847 //! conductor.serve(client_transport).await?;
848 //! ```
849 //!
850 //! # Why can't I just connect my proxy directly to an agent?
851 //!
852 //! ACP uses a message envelope format for proxy chains. When a proxy sends a
853 //! message toward the agent, it gets wrapped in a [`SuccessorMessage`] envelope.
854 //! The conductor handles this wrapping/unwrapping automatically.
855 //!
856 //! If you connected directly to an agent, your proxy would send `SuccessorMessage`
857 //! envelopes that the agent doesn't understand.
858 //!
859 //! # Example: Complete proxy with conductor
860 //!
861 //! See the [`sacp-conductor` tests] for complete working examples of proxies
862 //! running with the conductor.
863 //!
864 //! [`sacp-conductor`]: https://crates.io/crates/sacp-conductor
865 //! [`Conductor`]: sacp_conductor::Conductor
866 //! [`SuccessorMessage`]: sacp::schema::SuccessorMessage
867 //! [`sacp-conductor` tests]: https://github.com/symposium-dev/symposium-acp/tree/main/src/sacp-conductor/tests
868}