diff --git a/CLAUDE.md b/CLAUDE.md index 196e5c2..22ad980 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,3 +56,12 @@ src/ - The action creates branches for issues and pushes to PR branches directly - All actions create OIDC tokens for secure authentication - Progress is tracked through dynamic comment updates with checkboxes + +## MCP Tool Development + +When adding new MCP tools: + +1. **Add to MCP Server**: Implement the tool in the appropriate MCP server file (e.g., `src/mcp/local-git-ops-server.ts`) +2. **Expose to Claude**: Add the tool name to `BASE_ALLOWED_TOOLS` array in `src/create-prompt/index.ts` +3. **Tool Naming**: Follow the pattern `mcp__server_name__tool_name` (e.g., `mcp__local_git_ops__checkout_branch`) +4. **Documentation**: Update the prompt's "What You CAN Do" section if the tool adds new capabilities diff --git a/README.md b/README.md index cd35d91..bdca7cf 100644 --- a/README.md +++ b/README.md @@ -68,21 +68,21 @@ jobs: ## Inputs -| Input | Description | Required | Default | -| --------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- | ---------- | -| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | -| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - | -| `timeout_minutes` | Timeout in minutes for execution | No | `30` | -| `gitea_token` | Gitea token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | -| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - | -| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - | -| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | -| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | -| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" | -| `disallowed_tools` | Tools that Claude should never use | No | "" | -| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" | -| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | -| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | +| Input | Description | Required | Default | +| --------------------- | ------------------------------------------------------------------------------------------------------------------- | -------- | --------- | +| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | +| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - | +| `timeout_minutes` | Timeout in minutes for execution | No | `30` | +| `gitea_token` | Gitea token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | +| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - | +| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - | +| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | +| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | +| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" | +| `disallowed_tools` | Tools that Claude should never use | No | "" | +| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" | +| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | +| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | \*Required when using direct Anthropic API (default and when not using Bedrock or Vertex) diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index 30d742e..fc14ef2 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -33,6 +33,8 @@ const BASE_ALLOWED_TOOLS = [ "mcp__local_git_ops__delete_files", "mcp__local_git_ops__push_branch", "mcp__local_git_ops__create_pull_request", + "mcp__local_git_ops__checkout_branch", + "mcp__local_git_ops__create_branch", "mcp__local_git_ops__git_status", ]; const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"]; @@ -512,7 +514,20 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov - For implementation requests, assess if they are straightforward or complex. - Mark this todo as complete by checking the box. -4. Execute Actions: +${ + !eventData.isPR || !eventData.claudeBranch + ? ` +4. Check for Existing Branch (for issues and closed PRs): + - Before implementing changes, check if there's already a claude branch for this ${eventData.isPR ? "PR" : "issue"}. + - Use Bash to run \`git branch -r | grep "claude/${eventData.isPR ? "pr" : "issue"}-${eventData.isPR ? eventData.prNumber : eventData.issueNumber}"\` to search for existing branches. + - If found, use mcp__local_git_ops__checkout_branch to switch to the existing branch (set fetch_remote=true). + - If not found, you'll create a new branch when making changes (see Execute Actions section). + - Mark this todo as complete by checking the box. + +5. Execute Actions:` + : ` +4. Execute Actions:` +} - Continually update your todo list as you discover new requirements or realize tasks can be broken down. A. For Answering Questions and Code Reviews: @@ -537,15 +552,14 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov - Use mcp__local_git_ops__commit_files to commit files atomically in a single commit (supports single or multiple files). - CRITICAL: After committing, you MUST push the branch to the remote repository using mcp__local_git_ops__push_branch - When pushing changes with this tool and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.local>" line in the commit message.` - : ` - - You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch. + : eventData.claudeBranch + ? ` + - You are already on the correct branch (${eventData.claudeBranch}). Do not create a new branch. - Commit changes using mcp__local_git_ops__commit_files (works for both new and existing files) - Use mcp__local_git_ops__commit_files to commit files atomically in a single commit (supports single or multiple files). - CRITICAL: After committing, you MUST push the branch to the remote repository using mcp__local_git_ops__push_branch - When pushing changes and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.local>" line in the commit message. - ${ - eventData.claudeBranch - ? `- Provide a URL to create a PR manually in this format: + - Provide a URL to create a PR manually in this format: [Create a PR](${GITEA_SERVER_URL}/${context.repository}/compare/${eventData.baseBranch}...?quick_pull=1&title=&body=) - IMPORTANT: Use THREE dots (...) between branch names, not two (..) Example: ${GITEA_SERVER_URL}/${context.repository}/compare/main...feature-branch (correct) @@ -559,8 +573,34 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov - Reference to the original ${eventData.isPR ? "PR" : "issue"} - The signature: "Generated with [Claude Code](https://claude.ai/code)" - Just include the markdown link with text "Create a PR" - do not add explanatory text before it like "You can create a PR using this link"` - : "" - }` + : ` + - IMPORTANT: You are currently on the base branch (${eventData.baseBranch}). Before making changes, you should first check if there's already an existing claude branch for this ${eventData.isPR ? "PR" : "issue"}. + - FIRST: Use Bash to run \`git branch -r | grep "claude/${eventData.isPR ? "pr" : "issue"}-${eventData.isPR ? eventData.prNumber : eventData.issueNumber}"\` to check for existing branches. + - If an existing claude branch is found: + - Use mcp__local_git_ops__checkout_branch to switch to the existing branch (set fetch_remote=true) + - Continue working on that branch rather than creating a new one + - If NO existing claude branch is found: + - Create a new branch using mcp__local_git_ops__create_branch + - Use a descriptive branch name following the pattern: claude/${eventData.isPR ? "pr" : "issue"}-${eventData.isPR ? eventData.prNumber : eventData.issueNumber}- + - Example: claude/issue-123-fix-login-bug or claude/issue-456-add-user-profile + - After being on the correct branch (existing or new), commit changes using mcp__local_git_ops__commit_files (works for both new and existing files) + - Use mcp__local_git_ops__commit_files to commit files atomically in a single commit (supports single or multiple files). + - CRITICAL: After committing, you MUST push the branch to the remote repository using mcp__local_git_ops__push_branch + - When pushing changes and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.local>" line in the commit message. + - Provide a URL to create a PR manually in this format: + [Create a PR](${GITEA_SERVER_URL}/${context.repository}/compare/${eventData.baseBranch}...?quick_pull=1&title=&body=) + - IMPORTANT: Use THREE dots (...) between branch names, not two (..) + Example: ${GITEA_SERVER_URL}/${context.repository}/compare/main...feature-branch (correct) + NOT: ${GITEA_SERVER_URL}/${context.repository}/compare/main..feature-branch (incorrect) + - IMPORTANT: Ensure all URL parameters are properly encoded - spaces should be encoded as %20, not left as spaces + Example: Instead of "fix: update welcome message", use "fix%3A%20update%20welcome%20message" + - The target-branch should be '${eventData.baseBranch}'. + - The branch-name is your created branch name + - The body should include: + - A clear description of the changes + - Reference to the original ${eventData.isPR ? "PR" : "issue"} + - The signature: "Generated with [Claude Code](https://claude.ai/code)" + - Just include the markdown link with text "Create a PR" - do not add explanatory text before it like "You can create a PR using this link"` } C. For Complex Changes: @@ -572,12 +612,12 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov - Follow the same pushing strategy as for straightforward changes (see section B above). - Or explain why it's too complex: mark todo as completed in checklist with explanation. -5. Final Update: +${!eventData.isPR || !eventData.claudeBranch ? `6. Final Update:` : `5. Final Update:`} - Always update the GitHub comment to reflect the current todo state. - When all todos are completed, remove the spinner and add a brief summary of what was accomplished, and what was not done. - Note: If you see previous Claude comments with headers like "**Claude finished @user's task**" followed by "---", do not include this in your comment. The system adds this automatically. - If you changed any files locally, you must commit them using mcp__local_git_ops__commit_files AND push the branch using mcp__local_git_ops__push_branch before saying that you're done. - ${eventData.claudeBranch ? `- If you created anything in your branch, your comment must include the PR URL with prefilled title and body mentioned above.` : ""} + ${!eventData.isPR || !eventData.claudeBranch ? `- If you created a branch and made changes, your comment must include the PR URL with prefilled title and body mentioned above.` : ""} Important Notes: - All communication must happen through GitHub PR comments. @@ -585,7 +625,7 @@ Important Notes: - This includes ALL responses: code reviews, answers to questions, progress updates, and final results.${eventData.isPR ? "\n- PR CRITICAL: After reading files and forming your response, you MUST post it by calling mcp__github__update_issue_comment. Do NOT just respond with a normal response, the user will not see it." : ""} - You communicate exclusively by editing your single comment - not through any other means. - Use this spinner HTML when work is in progress: -${eventData.isPR && !eventData.claudeBranch ? `- Always push to the existing branch when triggered on a PR.` : `- IMPORTANT: You are already on the correct branch (${eventData.claudeBranch || "the created branch"}). Never create new branches when triggered on issues or closed/merged PRs.`} +${eventData.isPR && !eventData.claudeBranch ? `- Always push to the existing branch when triggered on a PR.` : eventData.claudeBranch ? `- IMPORTANT: You are already on the correct branch (${eventData.claudeBranch}). Do not create additional branches.` : `- IMPORTANT: You are currently on the base branch (${eventData.baseBranch}). First check for existing claude branches for this ${eventData.isPR ? "PR" : "issue"} and use them if found, otherwise create a new branch using mcp__local_git_ops__create_branch.`} - Use mcp__local_git_ops__commit_files for making commits (works for both new and existing files, single or multiple). Use mcp__local_git_ops__delete_files for deleting files (supports deleting single or multiple files atomically), or mcp__github__delete_file for deleting a single file. Edit files locally, and the tool will read the content from the same path on disk. Tool usage examples: - mcp__local_git_ops__commit_files: {"files": ["path/to/file1.js", "path/to/file2.py"], "message": "feat: add new feature"} @@ -606,9 +646,10 @@ What You CAN Do: - Implement code changes (simple to moderate complexity) when explicitly requested - Create pull requests for changes to human-authored code - Smart branch handling: - - When triggered on an issue: Always create a new branch - - When triggered on an open PR: Always push directly to the existing PR branch - - When triggered on a closed PR: Create a new branch + - When triggered on an issue: Create a new branch using mcp__local_git_ops__create_branch + - When triggered on an open PR: Push directly to the existing PR branch + - When triggered on a closed PR: Create a new branch using mcp__local_git_ops__create_branch +- Create new branches when needed using the create_branch tool What You CANNOT Do: - Submit formal GitHub PR reviews @@ -616,7 +657,7 @@ What You CANNOT Do: - Post multiple comments (you only update your initial comment) - Execute commands outside the repository context - Run arbitrary Bash commands (unless explicitly allowed via allowed_tools configuration) -- Perform branch operations (cannot merge branches, rebase, or perform other git operations beyond pushing commits) +- Perform advanced branch operations (cannot merge branches, rebase, or perform other complex git operations beyond creating, checking out, and pushing branches) - Modify files in the .github/workflows directory (GitHub App permissions do not allow workflow modifications) - View CI/CD results or workflow run outputs (cannot access GitHub Actions logs or test results) diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index 14918e2..2f3ec12 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -73,7 +73,7 @@ async function run() { core.setOutput("CLAUDE_BRANCH", branchInfo.claudeBranch); } - // Step 9: Update initial comment with branch link (only for issues that created a new branch) + // Step 9: Update initial comment with branch link (only if a claude branch was created) if (branchInfo.claudeBranch) { await updateTrackingComment( client, diff --git a/src/entrypoints/update-comment-link.ts b/src/entrypoints/update-comment-link.ts index d00fcc4..d171cb2 100644 --- a/src/entrypoints/update-comment-link.ts +++ b/src/entrypoints/update-comment-link.ts @@ -32,7 +32,7 @@ async function run() { const client = createClient(githubToken); const serverUrl = GITEA_SERVER_URL; - const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; + const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_NUMBER}`; let comment; let isPRReviewComment = false; diff --git a/src/github/context.ts b/src/github/context.ts index 0fb7f65..1c94518 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -40,7 +40,7 @@ export function parseGitHubContext(): ParsedGitHubContext { const context = github.context; const commonFields = { - runId: process.env.GITHUB_RUN_ID!, + runId: process.env.GITHUB_RUN_NUMBER!, eventName: context.eventName, eventAction: context.payload.action, repository: { diff --git a/src/github/operations/branch.ts b/src/github/operations/branch.ts index c525b6e..e02e884 100644 --- a/src/github/operations/branch.ts +++ b/src/github/operations/branch.ts @@ -29,6 +29,18 @@ export async function setupBranch( const { baseBranch } = context.inputs; const isPR = context.isPR; + // Determine base branch - use baseBranch if provided, otherwise fetch default + let sourceBranch: string; + + if (baseBranch) { + // Use provided base branch for source + sourceBranch = baseBranch; + } else { + // No base branch provided, fetch the default branch to use as source + const repoResponse = await client.api.getRepo(owner, repo); + sourceBranch = repoResponse.data.default_branch; + } + if (isPR) { const prData = githubData.contextData as GitHubPullRequest; const prState = prData.state; @@ -36,9 +48,18 @@ export async function setupBranch( // Check if PR is closed or merged if (prState === "CLOSED" || prState === "MERGED") { console.log( - `PR #${entityNumber} is ${prState}, creating new branch from source...`, + `PR #${entityNumber} is ${prState}, will let Claude create a new branch when needed`, ); - // Fall through to create a new branch like we do for issues + + // Check out the base branch and let Claude create branches as needed + await $`git fetch origin ${sourceBranch}`; + await $`git checkout ${sourceBranch}`; + await $`git pull origin ${sourceBranch}`; + + return { + baseBranch: sourceBranch, + currentBranch: sourceBranch, + }; } else { // Handle open PR: Checkout the PR branch console.log("This is an open PR, checking out PR branch..."); @@ -62,97 +83,54 @@ export async function setupBranch( } } - // Determine source branch - use baseBranch if provided, otherwise fetch default - let sourceBranch: string; - - if (baseBranch) { - // Use provided base branch for source - sourceBranch = baseBranch; - } else { - // No base branch provided, fetch the default branch to use as source - const repoResponse = await client.api.getRepo(owner, repo); - sourceBranch = repoResponse.data.default_branch; - } - - // Creating a new branch for either an issue or closed/merged PR - const entityType = isPR ? "pr" : "issue"; + // For issues, check out the base branch and let Claude create branches as needed console.log( - `Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`, + `Setting up base branch ${sourceBranch} for issue #${entityNumber}, Claude will create branch when needed...`, ); - const timestamp = new Date() - .toISOString() - .replace(/[:-]/g, "") - .replace(/\.\d{3}Z/, "") - .split("T") - .join("_"); - - const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`; - try { - // Use local git operations instead of API since Gitea's API is unreliable - console.log( - `Setting up local git branch: ${newBranch} from: ${sourceBranch}`, - ); - // Ensure we're in the repository directory const repoDir = process.env.GITHUB_WORKSPACE || process.cwd(); console.log(`Working in directory: ${repoDir}`); - try { - // Check if we're in a git repository - console.log(`Checking if we're in a git repository...`); - await $`git status`; + // Check if we're in a git repository + console.log(`Checking if we're in a git repository...`); + await $`git status`; - // Ensure we have the latest version of the source branch - console.log(`Fetching latest ${sourceBranch}...`); - await $`git fetch origin ${sourceBranch}`; + // Ensure we have the latest version of the source branch + console.log(`Fetching latest ${sourceBranch}...`); + await $`git fetch origin ${sourceBranch}`; - // Checkout the source branch - console.log(`Checking out ${sourceBranch}...`); - await $`git checkout ${sourceBranch}`; + // Checkout the source branch + console.log(`Checking out ${sourceBranch}...`); + await $`git checkout ${sourceBranch}`; - // Pull latest changes - console.log(`Pulling latest changes for ${sourceBranch}...`); - await $`git pull origin ${sourceBranch}`; + // Pull latest changes + console.log(`Pulling latest changes for ${sourceBranch}...`); + await $`git pull origin ${sourceBranch}`; - // Create and checkout the new branch - console.log(`Creating new branch: ${newBranch}`); - await $`git checkout -b ${newBranch}`; + // Verify the branch was checked out + const currentBranch = await $`git branch --show-current`; + const branchName = currentBranch.text().trim(); + console.log(`Current branch: ${branchName}`); - // Verify the branch was created - const currentBranch = await $`git branch --show-current`; - const branchName = currentBranch.text().trim(); - console.log(`Current branch after creation: ${branchName}`); - - if (branchName === newBranch) { - console.log( - `✅ Successfully created and checked out branch: ${newBranch}`, - ); - } else { - throw new Error( - `Branch creation failed. Expected ${newBranch}, got ${branchName}`, - ); - } - } catch (gitError: any) { - console.error(`❌ Git operations failed:`, gitError); - console.error(`Error message: ${gitError.message || gitError}`); - - // This is a critical failure - the branch MUST be created for Claude to work + if (branchName === sourceBranch) { + console.log(`✅ Successfully checked out base branch: ${sourceBranch}`); + } else { throw new Error( - `Failed to create branch ${newBranch}: ${gitError.message || gitError}`, + `Branch checkout failed. Expected ${sourceBranch}, got ${branchName}`, ); } - console.log(`Branch setup completed for: ${newBranch}`); + console.log( + `Branch setup completed, ready for Claude to create branches as needed`, + ); // Set outputs for GitHub Actions - core.setOutput("CLAUDE_BRANCH", newBranch); core.setOutput("BASE_BRANCH", sourceBranch); return { baseBranch: sourceBranch, - claudeBranch: newBranch, - currentBranch: newBranch, + currentBranch: sourceBranch, }; } catch (error) { console.error("Error setting up branch:", error); diff --git a/src/mcp/github-file-ops-server.ts b/src/mcp/github-file-ops-server.ts deleted file mode 100644 index 2b87ff7..0000000 --- a/src/mcp/github-file-ops-server.ts +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env node -// GitHub File Operations MCP Server -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { z } from "zod"; -import { readFile } from "fs/promises"; -import { join } from "path"; -import fetch from "node-fetch"; -import { GITEA_API_URL } from "../github/api/config"; - -type GitHubRef = { - object: { - sha: string; - }; -}; - -type GitHubCommit = { - tree: { - sha: string; - }; -}; - -type GitHubTree = { - sha: string; -}; - -type GitHubNewCommit = { - sha: string; - message: string; - author: { - name: string; - date: string; - }; -}; - -// Get repository information from environment variables -const REPO_OWNER = process.env.REPO_OWNER; -const REPO_NAME = process.env.REPO_NAME; -const BRANCH_NAME = process.env.BRANCH_NAME; -const REPO_DIR = process.env.REPO_DIR || process.cwd(); - -if (!REPO_OWNER || !REPO_NAME || !BRANCH_NAME) { - console.error( - "Error: REPO_OWNER, REPO_NAME, and BRANCH_NAME environment variables are required", - ); - process.exit(1); -} - -const server = new McpServer({ - name: "GitHub File Operations Server", - version: "0.0.1", -}); - -// Commit files tool -server.tool( - "commit_files", - "Commit one or more files to a repository in a single commit (this will commit them atomically in the remote repository)", - { - files: z - .array(z.string()) - .describe( - 'Array of file paths relative to repository root (e.g. ["src/main.js", "README.md"]). All files must exist locally.', - ), - message: z.string().describe("Commit message"), - }, - async ({ files, message }) => { - const owner = REPO_OWNER; - const repo = REPO_NAME; - const branch = BRANCH_NAME; - try { - const githubToken = process.env.GITHUB_TOKEN; - if (!githubToken) { - throw new Error("GITHUB_TOKEN environment variable is required"); - } - - const processedFiles = files.map((filePath) => { - if (filePath.startsWith("/")) { - return filePath.slice(1); - } - return filePath; - }); - - // NOTE: Gitea does not support GitHub's low-level git API operations - // (creating trees, commits, etc.). We need to use the contents API instead. - // For now, throw an error indicating this functionality is not available. - throw new Error( - "Multi-file commits are not supported with Gitea. " + - "Gitea does not provide the low-level git API operations (trees, commits) " + - "that are required for atomic multi-file commits. " + - "Please commit files individually using the contents API.", - ); - - return { - content: [ - { - type: "text", - text: JSON.stringify(simplifiedResult, null, 2), - }, - ], - }; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - return { - content: [ - { - type: "text", - text: `Error: ${errorMessage}`, - }, - ], - error: errorMessage, - isError: true, - }; - } - }, -); - -// Delete files tool -server.tool( - "delete_files", - "Delete one or more files from a repository in a single commit", - { - paths: z - .array(z.string()) - .describe( - 'Array of file paths to delete relative to repository root (e.g. ["src/old-file.js", "docs/deprecated.md"])', - ), - message: z.string().describe("Commit message"), - }, - async ({ paths, message }) => { - const owner = REPO_OWNER; - const repo = REPO_NAME; - const branch = BRANCH_NAME; - try { - const githubToken = process.env.GITHUB_TOKEN; - if (!githubToken) { - throw new Error("GITHUB_TOKEN environment variable is required"); - } - - // Convert absolute paths to relative if they match CWD - const cwd = process.cwd(); - const processedPaths = paths.map((filePath) => { - if (filePath.startsWith("/")) { - if (filePath.startsWith(cwd)) { - // Strip CWD from absolute path - return filePath.slice(cwd.length + 1); - } else { - throw new Error( - `Path '${filePath}' must be relative to repository root or within current working directory`, - ); - } - } - return filePath; - }); - - // NOTE: Gitea does not support GitHub's low-level git API operations - // (creating trees, commits, etc.). We need to use the contents API instead. - // For now, throw an error indicating this functionality is not available. - throw new Error( - "Multi-file deletions are not supported with Gitea. " + - "Gitea does not provide the low-level git API operations (trees, commits) " + - "that are required for atomic multi-file operations. " + - "Please delete files individually using the contents API.", - ); - - return { - content: [ - { - type: "text", - text: JSON.stringify(simplifiedResult, null, 2), - }, - ], - }; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - return { - content: [ - { - type: "text", - text: `Error: ${errorMessage}`, - }, - ], - error: errorMessage, - isError: true, - }; - } - }, -); - -async function runServer() { - const transport = new StdioServerTransport(); - await server.connect(transport); - process.on("exit", () => { - server.close(); - }); -} - -runServer().catch(console.error); diff --git a/src/mcp/local-git-ops-server.ts b/src/mcp/local-git-ops-server.ts index 11876fb..f0d380f 100644 --- a/src/mcp/local-git-ops-server.ts +++ b/src/mcp/local-git-ops-server.ts @@ -127,6 +127,102 @@ server.tool( }, ); +// Checkout branch tool +server.tool( + "checkout_branch", + "Checkout an existing branch using local git operations", + { + branch_name: z.string().describe("Name of the existing branch to checkout"), + create_if_missing: z + .boolean() + .optional() + .describe( + "Create branch if it doesn't exist locally (defaults to false)", + ), + fetch_remote: z + .boolean() + .optional() + .describe( + "Fetch from remote if branch doesn't exist locally (defaults to true)", + ), + }, + async ({ branch_name, create_if_missing = false, fetch_remote = true }) => { + try { + // Check if branch exists locally + let branchExists = false; + try { + runGitCommand(`git rev-parse --verify ${branch_name}`); + branchExists = true; + } catch (error) { + console.log( + `[LOCAL-GIT-MCP] Branch ${branch_name} doesn't exist locally`, + ); + } + + // If branch doesn't exist locally, try to fetch from remote + if (!branchExists && fetch_remote) { + try { + console.log( + `[LOCAL-GIT-MCP] Attempting to fetch ${branch_name} from remote`, + ); + runGitCommand(`git fetch origin ${branch_name}:${branch_name}`); + branchExists = true; + } catch (error) { + console.log( + `[LOCAL-GIT-MCP] Branch ${branch_name} doesn't exist on remote`, + ); + } + } + + // If branch still doesn't exist and create_if_missing is true, create it + if (!branchExists && create_if_missing) { + console.log(`[LOCAL-GIT-MCP] Creating new branch ${branch_name}`); + runGitCommand(`git checkout -b ${branch_name}`); + return { + content: [ + { + type: "text", + text: `Successfully created and checked out new branch: ${branch_name}`, + }, + ], + }; + } + + // If branch doesn't exist and we can't/won't create it, throw error + if (!branchExists) { + throw new Error( + `Branch '${branch_name}' does not exist locally or on remote. Use create_if_missing=true to create it.`, + ); + } + + // Checkout the existing branch + runGitCommand(`git checkout ${branch_name}`); + + return { + content: [ + { + type: "text", + text: `Successfully checked out branch: ${branch_name}`, + }, + ], + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + return { + content: [ + { + type: "text", + text: `Error checking out branch: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + // Commit files tool server.tool( "commit_files",