use crate::{crate_sources_mcp, state::ResearchState};
use indoc::formatdoc;
use sacp::{
mcp_server::{McpServer, McpServiceRegistry},
schema::{
NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse,
RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse,
SessionNotification,
},
Handled, JrConnectionCx, JrMessageHandler, MessageCx, ProxyToConductor,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{pin::pin, sync::Arc};
use tokio::sync::mpsc;
pub struct PermissionAutoApprover {
state: Arc<ResearchState>,
}
impl PermissionAutoApprover {
pub fn new(state: Arc<ResearchState>) -> Self {
Self { state }
}
}
impl JrMessageHandler for PermissionAutoApprover {
type Role = ProxyToConductor;
fn describe_chain(&self) -> impl std::fmt::Debug {
"permission-auto-approver"
}
async fn handle_message(
&mut self,
message: MessageCx,
_cx: JrConnectionCx<Self::Role>,
) -> Result<Handled<MessageCx>, sacp::Error> {
sacp::util::MatchMessage::new(message)
.if_request(async |request: RequestPermissionRequest, request_cx| {
if self.state.is_research_session(&request.session_id) {
tracing::debug!(
"Auto-approving permission request for research session {}",
request.session_id
);
for option in &request.options {
match option.kind {
sacp::schema::PermissionOptionKind::AllowOnce
| sacp::schema::PermissionOptionKind::AllowAlways => {
request_cx.respond(RequestPermissionResponse {
outcome: RequestPermissionOutcome::Selected {
option_id: option.id.clone(),
},
meta: None,
})?;
return Ok(Handled::Yes);
}
sacp::schema::PermissionOptionKind::RejectOnce
| sacp::schema::PermissionOptionKind::RejectAlways => {}
}
}
}
Ok(Handled::No((request, request_cx)))
})
.await
.if_notification({
let state = self.state.clone();
async move |notification: SessionNotification| {
if state.is_research_session(¬ification.session_id) {
tracing::debug!("Research session notification: {:?}", notification);
return Ok(Handled::Yes);
}
Ok(Handled::No(notification))
}
})
.await
.done()
}
}
pub fn research_agent_session_request(
sub_agent_mcp_registry: McpServiceRegistry<ProxyToConductor>,
) -> Result<NewSessionRequest, sacp::Error> {
let cwd = std::env::current_dir().map_err(|_| sacp::Error::internal_error())?;
let mut new_session_req = NewSessionRequest {
cwd,
mcp_servers: vec![],
meta: None,
};
sub_agent_mcp_registry.add_registered_mcp_servers_to(&mut new_session_req);
Ok(new_session_req)
}
pub fn build_research_prompt(user_prompt: &str) -> String {
formatdoc! {"
<agent_instructions>
You are an expert Rust programmer who has been asked advice on a particular question.
You have available to you an MCP server that can fetch the sources for Rust crates.
When you have completed researching the answer to the question, you can invoke the
`return_response_to_user` tool. If you are answering a question with more than one
answer, you can invoke the tool more than once and all the invocations will be returned.
IMPORTANT: You are a *researcher*, you are not here to make changes. Do NOT edit files,
make git commits, or perform any other permanent changes.
The research prompt provided by the user is as follows. If you encounter critical
ambiguities, use the return_response_to_user tool to request a refined prompt and
describe the ambiguities you encountered.
</agent_instructions>
<research_prompt>
{user_prompt}
</research_prompt>
"}
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct RustCrateQueryParams {
pub crate_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub crate_version: Option<String>,
pub prompt: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct RustCrateQueryOutput {
result: serde_json::Value,
}
pub fn build_server(state: Arc<ResearchState>) -> McpServer<ProxyToConductor> {
McpServer::new()
.instructions(indoc::indoc! {"
Research Rust crate source code and APIs. Essential for working with unfamiliar crates.
When to use:
- Before using a new crate: get usage examples and understand the API
- When compilation fails: verify actual method signatures, available fields, correct types
- When implementation details matter: explore how features work internally
- When documentation is unclear: see concrete code examples
"})
.tool_fn(
"rust_crate_query",
indoc::indoc! {r#"
Research a Rust crate by examining its actual source code.
Examples:
- "Show me how to create a tokio::runtime::Runtime and spawn tasks"
- "What fields are available on serde::Deserialize? I'm getting a compilation error"
- "How do I use async-trait with associated types?"
- "What's the signature of reqwest::Client::get()?"
The research agent will examine the crate sources and return relevant code examples, signatures, and implementation details.
"#},
{
async move |input: RustCrateQueryParams, mcp_cx: sacp::mcp_server::McpContext<ProxyToConductor>| {
let RustCrateQueryParams {
crate_name,
crate_version,
prompt,
} = input;
tracing::info!(
"Received crate query for '{}' version: {:?}",
crate_name,
crate_version
);
tracing::debug!("Research prompt: {}", prompt);
let cx = mcp_cx.connection_cx();
let (response_tx, mut response_rx) = mpsc::channel::<serde_json::Value>(32);
let sub_agent_mcp_registry = McpServiceRegistry::new()
.with_mcp_server(
"rust-crate-sources",
crate_sources_mcp::build_server(response_tx.clone()),
)
.map_err(|e| anyhow::anyhow!("Failed to create MCP registry: {}", e))?;
let NewSessionResponse {
session_id,
modes: _,
meta: _,
} = cx
.send_request_to(sacp::Agent, research_agent_session_request(
sub_agent_mcp_registry,
).map_err(|e| anyhow::anyhow!("Failed to create session request: {}", e))?)
.block_task()
.await
.map_err(|e| anyhow::anyhow!("Failed to spawn research session: {}", e))?;
tracing::info!("Research session created: {}", session_id);
state.register_session(&session_id);
let mut responses = vec![];
let (result, _) = futures::future::select(
pin!(async {
while let Some(response) = response_rx.recv().await {
responses.push(response);
}
Ok::<(), anyhow::Error>(())
}),
pin!(async {
let research_prompt = build_research_prompt(&prompt);
let prompt_request = PromptRequest {
session_id: session_id.clone(),
prompt: vec![research_prompt.into()],
meta: None,
};
let PromptResponse {
stop_reason,
meta: _,
} = cx
.send_request_to(sacp::Agent, prompt_request)
.block_task()
.await
.map_err(|e| anyhow::anyhow!("Prompt request failed: {}", e))?;
tracing::info!(
"Research complete for session {session_id} ({stop_reason:?})"
);
Ok::<(), anyhow::Error>(())
}),
)
.await
.factor_first();
result?;
state.unregister_session(&session_id);
let response = if responses.len() == 1 {
responses.pop().expect("singleton")
} else {
serde_json::Value::Array(responses)
};
tracing::info!("Research complete for '{}'", crate_name);
Ok(RustCrateQueryOutput { result: response })
}
},
|f, args, cx| Box::pin(f(args, cx)),
)
}