Skip to content

Commit 4f2e431

Browse files
committed
Claude permissions endpoint persists requests and polls for updates
1 parent ef45536 commit 4f2e431

File tree

3 files changed

+140
-7
lines changed

3 files changed

+140
-7
lines changed

crates/but-claude/src/mcp.rs

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
use std::sync::{Arc, Mutex};
1+
use std::{
2+
path::Path,
3+
sync::{Arc, Mutex},
4+
};
25

36
use anyhow::Result;
7+
use but_db::poll::ItemKind;
8+
use but_settings::AppSettings;
9+
use gitbutler_command_context::CommandContext;
10+
use gitbutler_project::Project;
411
use rmcp::{
512
Error as McpError, ServerHandler, ServiceExt,
613
model::{
@@ -9,10 +16,12 @@ use rmcp::{
916
schemars, tool,
1017
};
1118

12-
pub async fn start() -> Result<()> {
19+
pub async fn start(repo_path: &Path) -> Result<()> {
20+
let project = Project::from_path(repo_path).expect("Failed to create project from path");
1321
let client_info = Arc::new(Mutex::new(None));
1422
let transport = (tokio::io::stdin(), tokio::io::stdout());
15-
let service = Mcp::default().serve(transport).await?;
23+
let server = Mcp { project };
24+
let service = server.serve(transport).await?;
1625
let info = service.peer_info();
1726
if let Ok(mut guard) = client_info.lock() {
1827
guard.replace(info.client_info.clone());
@@ -22,7 +31,9 @@ pub async fn start() -> Result<()> {
2231
}
2332

2433
#[derive(Debug, Clone, Default)]
25-
pub struct Mcp {}
34+
pub struct Mcp {
35+
project: Project,
36+
}
2637

2738
#[tool(tool_box)]
2839
impl Mcp {
@@ -31,13 +42,90 @@ impl Mcp {
3142
&self,
3243
#[tool(aggr)] request: McpPermissionRequest,
3344
) -> Result<CallToolResult, McpError> {
45+
let approved = self
46+
.approval_inner(request.clone().into(), std::time::Duration::from_secs(60))
47+
.map_err(|e| McpError::internal_error(e.to_string(), None))?;
48+
3449
let result = Ok(McpPermissionResponse {
35-
behavior: Behavior::Allow,
50+
behavior: if approved {
51+
Behavior::Allow
52+
} else {
53+
Behavior::Deny
54+
},
3655
updated_input: Some(request.input),
37-
message: None,
56+
message: if approved {
57+
None
58+
} else {
59+
Some("Rejected by user".to_string())
60+
},
3861
});
3962
result.map(|outcome| Ok(CallToolResult::success(vec![Content::json(outcome)?])))?
4063
}
64+
65+
fn approval_inner(
66+
&self,
67+
req: crate::ClaudePermissionRequest,
68+
timeout: std::time::Duration,
69+
) -> anyhow::Result<bool> {
70+
let ctx = &mut CommandContext::open(
71+
&self.project,
72+
AppSettings::load_from_default_path_creating()?,
73+
)?;
74+
// Create a record that will be seen by the user in the UI
75+
ctx.db()?
76+
.claude_permission_requests()
77+
.insert(req.clone().try_into()?)?;
78+
// Poll for user approval
79+
let rx = ctx.db()?.poll_changes(
80+
ItemKind::Actions
81+
| ItemKind::Workflows
82+
| ItemKind::Assignments
83+
| ItemKind::Rules
84+
| ItemKind::ClaudePermissionRequests,
85+
std::time::Duration::from_millis(500),
86+
)?;
87+
let mut approved_state = false;
88+
let start_time = std::time::Instant::now();
89+
for item in rx {
90+
if start_time.elapsed() > timeout {
91+
eprintln!("Timeout waiting for permission approval (60 seconds)");
92+
break;
93+
}
94+
match item {
95+
Ok(ItemKind::ClaudePermissionRequests) => {
96+
if let Some(updated) = ctx.db()?.claude_permission_requests().get(&req.id)? {
97+
if let Some(approved) = updated.approved {
98+
approved_state = approved;
99+
break;
100+
}
101+
} else {
102+
eprintln!("Permission request not found: {}", req.id);
103+
break;
104+
}
105+
}
106+
Ok(_) => continue, // Ignore other item kinds
107+
Err(e) => {
108+
eprintln!("Error polling for changes: {e}");
109+
break;
110+
}
111+
}
112+
}
113+
ctx.db()?.claude_permission_requests().delete(&req.id)?;
114+
Ok(approved_state)
115+
}
116+
}
117+
118+
impl From<McpPermissionRequest> for crate::ClaudePermissionRequest {
119+
fn from(request: McpPermissionRequest) -> Self {
120+
crate::ClaudePermissionRequest {
121+
id: request.tool_use_id,
122+
created_at: chrono::Utc::now().naive_utc(),
123+
updated_at: chrono::Utc::now().naive_utc(),
124+
tool_name: request.tool_name,
125+
input: request.input,
126+
approved: None,
127+
}
128+
}
41129
}
42130

43131
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]

crates/but-db/src/poll.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ bitflags! {
1111
const Workflows = 1 << 1;
1212
const Assignments = 1 << 2;
1313
const Rules = 1 << 3;
14+
const ClaudePermissionRequests = 1 << 4;
1415
}
1516
}
1617

@@ -36,6 +37,8 @@ impl DbHandle {
3637
let mut prev_assignments = Vec::new();
3738
let mut prev_workflows = Vec::new();
3839
let mut prev_actions = Vec::new();
40+
let mut prev_rules = Vec::new();
41+
let mut prev_claude_requests = Vec::new();
3942
'outer: loop {
4043
std::thread::sleep(interval);
4144
for to_check in ItemKind::all().iter() {
@@ -78,6 +81,32 @@ impl DbHandle {
7881
}
7982
Err(e) => tx.send(Err(e)),
8083
}
84+
} else if kind & to_check == ItemKind::Rules {
85+
let res = this.workspace_rules().list();
86+
match res {
87+
Ok(items) => {
88+
if items != prev_rules {
89+
prev_rules = items;
90+
tx.send(Ok(ItemKind::Rules))
91+
} else {
92+
continue;
93+
}
94+
}
95+
Err(e) => tx.send(Err(anyhow::Error::from(e))),
96+
}
97+
} else if kind & to_check == ItemKind::ClaudePermissionRequests {
98+
let res = this.claude_permission_requests().list();
99+
match res {
100+
Ok(items) => {
101+
if items != prev_claude_requests {
102+
prev_claude_requests = items;
103+
tx.send(Ok(ItemKind::ClaudePermissionRequests))
104+
} else {
105+
continue;
106+
}
107+
}
108+
Err(e) => tx.send(Err(anyhow::Error::from(e))),
109+
}
81110
} else {
82111
eprintln!("BUG: didn't implement a branch for {to_check:?}");
83112
break 'outer;
@@ -112,6 +141,7 @@ impl DbHandle {
112141
let mut prev_workflows = Vec::new();
113142
let mut prev_actions = Vec::new();
114143
let mut prev_rules = Vec::new();
144+
let mut prev_claude_requests = Vec::new();
115145
let mut ticker = tokio::time::interval(interval);
116146
loop {
117147
ticker.tick().await;
@@ -168,6 +198,19 @@ impl DbHandle {
168198
}
169199
Err(e) => tx.send(Err(anyhow::Error::from(e))).await,
170200
}
201+
} else if kind & to_check == ItemKind::ClaudePermissionRequests {
202+
let res = this.claude_permission_requests().list();
203+
match res {
204+
Ok(items) => {
205+
if items != prev_claude_requests {
206+
prev_claude_requests = items;
207+
tx.send(Ok(ItemKind::ClaudePermissionRequests)).await
208+
} else {
209+
continue;
210+
}
211+
}
212+
Err(e) => tx.send(Err(anyhow::Error::from(e))).await,
213+
}
171214
} else {
172215
eprintln!("BUG: didn't implement a branch for {to_check:?}");
173216
return;

crates/but/src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ async fn main() -> Result<()> {
7575
metrics_if_configured(app_settings, CommandName::ClaudeStop, p).ok();
7676
Ok(())
7777
}
78-
claude::Subcommands::PermissionPromptMcp => but_claude::mcp::start().await,
78+
claude::Subcommands::PermissionPromptMcp => {
79+
but_claude::mcp::start(&args.current_dir).await
80+
}
7981
},
8082
Subcommands::Log => {
8183
let result = log::commit_graph(&args.current_dir, args.json);

0 commit comments

Comments
 (0)