Implement switch branch
This commit is contained in:
@@ -56,3 +56,12 @@ src/
|
|||||||
- The action creates branches for issues and pushes to PR branches directly
|
- The action creates branches for issues and pushes to PR branches directly
|
||||||
- All actions create OIDC tokens for secure authentication
|
- All actions create OIDC tokens for secure authentication
|
||||||
- Progress is tracked through dynamic comment updates with checkboxes
|
- 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
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const BASE_ALLOWED_TOOLS = [
|
|||||||
"mcp__local_git_ops__delete_files",
|
"mcp__local_git_ops__delete_files",
|
||||||
"mcp__local_git_ops__push_branch",
|
"mcp__local_git_ops__push_branch",
|
||||||
"mcp__local_git_ops__create_pull_request",
|
"mcp__local_git_ops__create_pull_request",
|
||||||
|
"mcp__local_git_ops__checkout_branch",
|
||||||
"mcp__local_git_ops__git_status",
|
"mcp__local_git_ops__git_status",
|
||||||
];
|
];
|
||||||
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -127,6 +127,90 @@ 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
|
// Commit files tool
|
||||||
server.tool(
|
server.tool(
|
||||||
"commit_files",
|
"commit_files",
|
||||||
|
|||||||
Reference in New Issue
Block a user