1
- use std:: sync:: { Arc , Mutex } ;
1
+ use std:: {
2
+ path:: Path ,
3
+ sync:: { Arc , Mutex } ,
4
+ } ;
2
5
3
6
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 ;
4
11
use rmcp:: {
5
12
Error as McpError , ServerHandler , ServiceExt ,
6
13
model:: {
7
14
CallToolResult , Content , Implementation , ProtocolVersion , ServerCapabilities , ServerInfo ,
8
15
} ,
9
16
schemars, tool,
10
17
} ;
11
- use tracing_subscriber:: { self , EnvFilter } ;
12
-
13
- pub async fn start ( ) -> Result < ( ) > {
14
- tracing_subscriber:: fmt ( )
15
- . with_env_filter ( EnvFilter :: from_default_env ( ) . add_directive ( tracing:: Level :: DEBUG . into ( ) ) )
16
- . with_writer ( std:: io:: stderr)
17
- . with_ansi ( false )
18
- . init ( ) ;
19
-
20
- tracing:: info!( "Starting MCP server" ) ;
21
18
19
+ pub async fn start ( repo_path : & Path ) -> Result < ( ) > {
20
+ let project = Project :: from_path ( repo_path) . expect ( "Failed to create project from path" ) ;
22
21
let client_info = Arc :: new ( Mutex :: new ( None ) ) ;
23
22
let transport = ( tokio:: io:: stdin ( ) , tokio:: io:: stdout ( ) ) ;
24
- let service = Mcp :: default ( ) . serve ( transport) . await ?;
23
+ let server = Mcp { project } ;
24
+ let service = server. serve ( transport) . await ?;
25
25
let info = service. peer_info ( ) ;
26
26
if let Ok ( mut guard) = client_info. lock ( ) {
27
27
guard. replace ( info. client_info . clone ( ) ) ;
@@ -31,27 +31,105 @@ pub async fn start() -> Result<()> {
31
31
}
32
32
33
33
#[ derive( Debug , Clone , Default ) ]
34
- pub struct Mcp { }
34
+ pub struct Mcp {
35
+ project : Project ,
36
+ }
35
37
36
38
#[ tool( tool_box) ]
37
39
impl Mcp {
38
- #[ tool( description = "Permission check - approve if the input contains allow, otherwise deny. " ) ]
40
+ #[ tool( description = "Permission check for tool calls " ) ]
39
41
pub fn approval_prompt (
40
42
& self ,
41
- #[ tool( aggr) ] request : PermissionRequest ,
43
+ #[ tool( aggr) ] request : McpPermissionRequest ,
42
44
) -> Result < CallToolResult , McpError > {
43
- let result = Ok ( PermissionResponse {
44
- behavior : Behavior :: Allow ,
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
+
49
+ let result = Ok ( McpPermissionResponse {
50
+ behavior : if approved {
51
+ Behavior :: Allow
52
+ } else {
53
+ Behavior :: Deny
54
+ } ,
45
55
updated_input : Some ( request. input ) ,
46
- message : None ,
56
+ message : if approved {
57
+ None
58
+ } else {
59
+ Some ( "Rejected by user" . to_string ( ) )
60
+ } ,
47
61
} ) ;
48
62
result. map ( |outcome| Ok ( CallToolResult :: success ( vec ! [ Content :: json( outcome) ?] ) ) ) ?
49
63
}
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
+ }
50
116
}
51
117
52
- #[ derive( Debug , serde:: Serialize , serde:: Deserialize , schemars:: JsonSchema ) ]
53
- #[ serde( rename_all = "camelCase" ) ]
54
- pub struct PermissionRequest {
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
+ }
129
+ }
130
+
131
+ #[ derive( Debug , Clone , serde:: Serialize , serde:: Deserialize , schemars:: JsonSchema ) ]
132
+ pub struct McpPermissionRequest {
55
133
#[ schemars( description = "The name of the tool requesting permission" ) ]
56
134
tool_name : String ,
57
135
#[ schemars( description = "The input for the tool" ) ]
@@ -60,17 +138,17 @@ pub struct PermissionRequest {
60
138
tool_use_id : String ,
61
139
}
62
140
63
- #[ derive( Debug , Clone , serde:: Serialize , serde:: Deserialize , strum :: Display ) ]
141
+ #[ derive( Debug , Clone , serde:: Serialize , serde:: Deserialize ) ]
64
142
pub enum Behavior {
65
- #[ strum ( serialize = "allow" ) ]
143
+ #[ serde ( rename = "allow" ) ]
66
144
Allow ,
67
- #[ strum ( serialize = "deny" ) ]
145
+ #[ serde ( rename = "deny" ) ]
68
146
Deny ,
69
147
}
70
148
71
149
#[ derive( Debug , Clone , serde:: Serialize , serde:: Deserialize ) ]
72
150
#[ serde( rename_all = "camelCase" ) ]
73
- pub struct PermissionResponse {
151
+ pub struct McpPermissionResponse {
74
152
behavior : Behavior ,
75
153
updated_input : Option < serde_json:: Value > ,
76
154
message : Option < String > ,
0 commit comments