Skip to content

Commit 730af3d

Browse files
committed
simple gitbutler-mcp server binary
Instead of needing to set env vars and run a node server, this implements a simple Rust compiled binary that has direct access to all the GitButler apis. Should be possible to build this into a single binary that can be run as an MCP server that doesn’t need anything else.
1 parent 15cc30a commit 730af3d

File tree

6 files changed

+306
-0
lines changed

6 files changed

+306
-0
lines changed

Cargo.lock

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

crates/gitbutler-mcp/Cargo.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "gitbutler-mcp"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "Model Context Protocol server for GitButler"
6+
authors = ["GitButler <gitbutler@gitbutler.com>"]
7+
publish = false
8+
9+
[dependencies]
10+
tokio = { version = "1", features = [
11+
"macros",
12+
"rt",
13+
"rt-multi-thread",
14+
"io-std",
15+
"signal",
16+
] }
17+
anyhow = "1.0"
18+
async-trait = "0.1"
19+
clap = { version = "4.4", features = ["derive"] }
20+
rmcp = { version = "0.1.5", features = ["server", "transport-io"] }
21+
serde = { version = "1.0", features = ["derive"] }
22+
serde_json = "1.0"
23+
thiserror = "1.0"
24+
tracing = "0.1"
25+
tracing-subscriber = { version = "0.3", features = [
26+
"env-filter",
27+
"std",
28+
"fmt",
29+
] }
30+
31+
[dev-dependencies]
32+
temp-dir = "0.1"

crates/gitbutler-mcp/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# GitButler MCP
2+
3+
A Model Context Protocol server implementation for GitButler that provides AI assistants with branch management capabilities.
4+
5+
## Overview
6+
7+
`gitbutler-mcp` is a Rust crate that implements a Model Context Protocol (MCP) server for GitButler. It enables AI assistants to perform branch-related operations in GitButler repositories through standardized MCP interactions.
8+
9+
## Features
10+
11+
- **Branch Management**: Update branches with summaries and prompts
12+
- **MCP Compliance**: Fully implements the Model Context Protocol specification
13+
- **Tooling Integration**: Provides tools that can be used by AI assistants
14+
15+
## Usage
16+
17+
The MCP server can be integrated with AI assistants that support the Model Context Protocol. It exposes tools that allow these assistants to:
18+
19+
- Update branches with contextual information
20+
- Process prompts and convert them into branch-specific actions
21+
22+
## Tool Reference
23+
24+
### `update_branch`
25+
26+
Updates a GitButler branch with a given prompt and summary.
27+
28+
**Parameters:**
29+
- `working_directory`: Path to the Git repository
30+
- `full_prompt`: Complete prompt that was used for the branch update
31+
- `summary`: Short description of the changes
32+
33+
## Development
34+
35+
To run the MCP server:
36+
37+
```bash
38+
cargo run -p gitbutler-mcp
39+
```
40+
41+
## Integration
42+
43+
This MCP server can be integrated with any AI assistant that supports the Model Context Protocol (MCP) specification.
44+
45+
## License
46+
47+
Same as the GitButler project.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use rmcp::{
2+
const_string, model::*, schemars, service::RequestContext, tool, Error as McpError, RoleServer,
3+
ServerHandler,
4+
};
5+
use serde_json::json;
6+
use std::path::PathBuf;
7+
use tracing;
8+
9+
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
10+
pub struct UpdateBranchRequest {
11+
pub working_directory: String,
12+
pub full_prompt: String,
13+
pub summary: String,
14+
}
15+
16+
#[derive(Clone)]
17+
pub struct Butler {}
18+
19+
#[tool(tool_box)]
20+
impl Butler {
21+
#[allow(dead_code)]
22+
pub fn new() -> Self {
23+
Self {}
24+
}
25+
26+
fn _create_resource_text(&self, uri: &str, name: &str) -> Resource {
27+
RawResource::new(uri, name.to_string()).no_annotation()
28+
}
29+
30+
#[tool(description = "Update a branch with the given prompt and summary")]
31+
fn update_branch(
32+
&self,
33+
#[tool(aggr)] UpdateBranchRequest {
34+
working_directory,
35+
full_prompt,
36+
summary,
37+
}: UpdateBranchRequest,
38+
) -> Result<CallToolResult, McpError> {
39+
tracing::info!("Updating branch with prompt: {}", summary);
40+
41+
// Check if the working directory exists
42+
let project_path = PathBuf::from(&working_directory);
43+
if !project_path.exists() {
44+
return Err(McpError::invalid_params(
45+
"Invalid working directory",
46+
Some(json!({ "error": "Working directory does not exist" })),
47+
));
48+
}
49+
50+
// In a real implementation, we would use GitButler's branch management APIs
51+
// But for now, we'll simulate a successful branch update
52+
tracing::info!(
53+
"Would update branch in {} using prompt: {} with summary: {}",
54+
working_directory,
55+
full_prompt,
56+
summary
57+
);
58+
59+
Ok(CallToolResult::success(vec![Content::text(format!(
60+
"Branch has been updated with summary: {}",
61+
summary
62+
))]))
63+
}
64+
}
65+
66+
const_string!(UpdateBranch = "updateBranch");
67+
68+
#[tool(tool_box)]
69+
impl ServerHandler for Butler {
70+
fn get_info(&self) -> ServerInfo {
71+
ServerInfo {
72+
protocol_version: ProtocolVersion::V_2024_11_05,
73+
capabilities: ServerCapabilities::builder()
74+
.enable_prompts()
75+
.enable_resources()
76+
.enable_tools()
77+
.build(),
78+
server_info: Implementation::from_build_env(),
79+
instructions: Some("This server provides a branch update tool that can process prompts and update branches accordingly.".to_string()),
80+
}
81+
}
82+
83+
async fn list_resources(
84+
&self,
85+
_request: std::option::Option<rmcp::model::PaginatedRequestParamInner>,
86+
_: RequestContext<RoleServer>,
87+
) -> Result<ListResourcesResult, McpError> {
88+
Ok(ListResourcesResult {
89+
resources: vec![],
90+
next_cursor: None,
91+
})
92+
}
93+
94+
async fn read_resource(
95+
&self,
96+
ReadResourceRequestParam { uri }: ReadResourceRequestParam,
97+
_: RequestContext<RoleServer>,
98+
) -> Result<ReadResourceResult, McpError> {
99+
Err(McpError::resource_not_found(
100+
"resource_not_found",
101+
Some(json!({
102+
"uri": uri
103+
})),
104+
))
105+
}
106+
107+
async fn list_prompts(
108+
&self,
109+
_request: std::option::Option<rmcp::model::PaginatedRequestParamInner>,
110+
_: RequestContext<RoleServer>,
111+
) -> Result<ListPromptsResult, McpError> {
112+
Ok(ListPromptsResult {
113+
next_cursor: None,
114+
prompts: vec![],
115+
})
116+
}
117+
118+
async fn get_prompt(
119+
&self,
120+
GetPromptRequestParam {
121+
name: _name,
122+
arguments: _arguments,
123+
}: GetPromptRequestParam,
124+
_: RequestContext<RoleServer>,
125+
) -> Result<GetPromptResult, McpError> {
126+
Err(McpError::invalid_params("prompt not found", None))
127+
}
128+
129+
async fn list_resource_templates(
130+
&self,
131+
_request: std::option::Option<rmcp::model::PaginatedRequestParamInner>,
132+
_: RequestContext<RoleServer>,
133+
) -> Result<ListResourceTemplatesResult, McpError> {
134+
Ok(ListResourceTemplatesResult {
135+
next_cursor: None,
136+
resource_templates: Vec::new(),
137+
})
138+
}
139+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod butler;

crates/gitbutler-mcp/src/main.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use anyhow::Result;
2+
use common::butler::Butler;
3+
4+
use rmcp::{transport::stdio, ServiceExt};
5+
use tracing_subscriber::{self, EnvFilter};
6+
mod common;
7+
/// npx @modelcontextprotocol/inspector cargo run -p mcp-server-examples --example std_io
8+
#[tokio::main]
9+
async fn main() -> Result<()> {
10+
// Initialize the tracing subscriber with file and stdout logging
11+
tracing_subscriber::fmt()
12+
.with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into()))
13+
.with_writer(std::io::stderr)
14+
.with_ansi(false)
15+
.init();
16+
17+
tracing::info!("Starting GitButler MCP server");
18+
19+
// Create an instance of our counter router
20+
let service = Butler::new().serve(stdio()).await.inspect_err(|e| {
21+
tracing::error!("serving error: {:?}", e);
22+
})?;
23+
24+
service.waiting().await?;
25+
Ok(())
26+
}

0 commit comments

Comments
 (0)