From 194fca8b05f044ca6678c516c60c73d6b1d6dbae Mon Sep 17 00:00:00 2001 From: Chris Lloyd Date: Tue, 19 Aug 2025 17:18:05 -0700 Subject: [PATCH] feat: preserve file permissions when committing via GitHub API (#469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add file permission detection to github-file-ops-server.ts to properly preserve file modes (regular, executable, symlink) when committing files through the GitHub API. This ensures executable scripts and other special files maintain their correct permissions. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude --- src/mcp/github-file-ops-server.ts | 38 ++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/mcp/github-file-ops-server.ts b/src/mcp/github-file-ops-server.ts index e3da6f4..b4e8a19 100644 --- a/src/mcp/github-file-ops-server.ts +++ b/src/mcp/github-file-ops-server.ts @@ -3,8 +3,9 @@ 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 { readFile, stat } from "fs/promises"; import { join } from "path"; +import { constants } from "fs"; import fetch from "node-fetch"; import { GITHUB_API_URL } from "../github/api/config"; import { retryWithBackoff } from "../utils/retry"; @@ -162,6 +163,34 @@ async function getOrCreateBranchRef( return baseSha; } +// Get the appropriate Git file mode for a file +async function getFileMode(filePath: string): Promise { + try { + const fileStat = await stat(filePath); + if (fileStat.isFile()) { + // Check if execute bit is set for user + if (fileStat.mode & constants.S_IXUSR) { + return "100755"; // Executable file + } else { + return "100644"; // Regular file + } + } else if (fileStat.isDirectory()) { + return "040000"; // Directory (tree) + } else if (fileStat.isSymbolicLink()) { + return "120000"; // Symbolic link + } else { + // Fallback for unknown file types + return "100644"; + } + } catch (error) { + // If we can't stat the file, default to regular file + console.warn( + `Could not determine file mode for ${filePath}, using default: ${error}`, + ); + return "100644"; + } +} + // Commit files tool server.tool( "commit_files", @@ -223,6 +252,9 @@ server.tool( ? filePath : join(REPO_DIR, filePath); + // Get the proper file mode based on file permissions + const fileMode = await getFileMode(fullPath); + // Check if file is binary (images, etc.) const isBinaryFile = /\.(png|jpg|jpeg|gif|webp|ico|pdf|zip|tar|gz|exe|bin|woff|woff2|ttf|eot)$/i.test( @@ -261,7 +293,7 @@ server.tool( // Return tree entry with blob SHA return { path: filePath, - mode: "100644", + mode: fileMode, type: "blob", sha: blobData.sha, }; @@ -270,7 +302,7 @@ server.tool( const content = await readFile(fullPath, "utf-8"); return { path: filePath, - mode: "100644", + mode: fileMode, type: "blob", content: content, };