use anyhow;
use kodegen_mcp_schema::github::{PushFilesArgs, PushFilesPromptArgs};
use kodegen_mcp_tool::{Tool, error::McpError};
use rmcp::model::{PromptArgument, PromptMessage, PromptMessageRole, PromptMessageContent};
use serde_json::Value;
#[derive(Clone)]
pub struct PushFilesTool;
impl Tool for PushFilesTool {
type Args = PushFilesArgs;
type PromptArgs = PushFilesPromptArgs;
fn name() -> &'static str {
"push_files"
}
fn description() -> &'static str {
"Push multiple files to a GitHub repository in a single commit. All files \
are added atomically (creates tree, commit, and updates ref). File content \
must be base64-encoded. Requires GITHUB_TOKEN environment variable."
}
fn read_only() -> bool {
false }
fn destructive() -> bool {
false }
fn idempotent() -> bool {
false }
fn open_world() -> bool {
true }
async fn execute(&self, args: Self::Args) -> Result<Value, McpError> {
let token = std::env::var("GITHUB_TOKEN")
.map_err(|_| McpError::Other(anyhow::anyhow!(
"GITHUB_TOKEN environment variable not set"
)))?;
let client = crate::GitHubClient::builder()
.personal_token(token)
.build()
.map_err(|e| McpError::Other(anyhow::anyhow!("Failed to create GitHub client: {}", e)))?;
let task_result = client.push_files(
args.owner,
args.repo,
args.branch,
args.files,
args.message,
).await;
let api_result = task_result
.map_err(|e| McpError::Other(anyhow::anyhow!("Task channel error: {}", e)))?;
let commit = api_result
.map_err(|e| McpError::Other(anyhow::anyhow!("GitHub API error: {}", e)))?;
Ok(serde_json::to_value(&commit)?)
}
fn prompt_arguments() -> Vec<PromptArgument> {
vec![]
}
async fn prompt(&self, _args: Self::PromptArgs) -> Result<Vec<PromptMessage>, McpError> {
Ok(vec![
PromptMessage {
role: PromptMessageRole::User,
content: PromptMessageContent::text(
"How do I push multiple files to GitHub at once?"
),
},
PromptMessage {
role: PromptMessageRole::Assistant,
content: PromptMessageContent::text(
"Use push_files to commit multiple files atomically:\n\n\
push_files({\n\
\"owner\": \"octocat\",\n\
\"repo\": \"hello-world\",\n\
\"branch\": \"main\",\n\
\"message\": \"Add multiple files\",\n\
\"files\": {\n\
\"src/file1.rs\": \"ZnVuIG1haW4oKSB7fQ==\", // base64 of content\n\
\"src/file2.rs\": \"ZnVuIGhlbHBlcigpIHt9\",\n\
\"README.md\": \"IyBQcm9qZWN0\"\n\
}\n\
})\n\n\
Important:\n\
- All file content MUST be base64-encoded\n\
- All files are added in a SINGLE commit\n\
- Creates tree, commit, and updates ref atomically\n\
- More efficient than multiple create_or_update_file calls\n\
- Good for bulk operations or initial repo setup\n\n\
To encode content to base64:\n\
- In JavaScript: Buffer.from(text).toString('base64')\n\
- In Python: base64.b64encode(text.encode()).decode()\n\
- In Rust: use base64 crate\n\n\
Workflow:\n\
1. Prepare all file contents\n\
2. Base64-encode each file's content\n\
3. Create a map of {path: base64_content}\n\
4. Call push_files with the map\n\n\
Requirements:\n\
- GITHUB_TOKEN environment variable must be set\n\
- Token needs 'repo' scope for private repos, 'public_repo' for public\n\
- User must have write access to the repository\n\
- Branch must already exist"
),
},
])
}
}