Git Repo Browser MCP
by bsreeram08
Verified
- git-commands-mcp
- src
- handlers
import {
execPromise,
cloneRepo,
getDirectoryTree,
simpleGit,
path,
fs,
} from "./common.js";
/**
* Handles the git_directory_structure tool request
* @param {Object} params - Tool parameters
* @param {string} params.repo_url - Repository URL
* @returns {Object} - Tool response
*/
export async function handleGitDirectoryStructure({ repo_url }) {
try {
const repoPath = await cloneRepo(repo_url);
const tree = await getDirectoryTree(repoPath);
return {
content: [
{
type: "text",
text: tree,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
}
/**
* Handles the git_read_files tool request
* @param {Object} params - Tool parameters
* @param {string} params.repo_url - Repository URL
* @param {string[]} params.file_paths - File paths to read
* @returns {Object} - Tool response
*/
export async function handleGitReadFiles({ repo_url, file_paths }) {
try {
const repoPath = await cloneRepo(repo_url);
const results = {};
for (const filePath of file_paths) {
const fullPath = path.join(repoPath, filePath);
try {
if (await fs.pathExists(fullPath)) {
results[filePath] = await fs.readFile(fullPath, "utf8");
} else {
results[filePath] = "Error: File not found";
}
} catch (error) {
results[filePath] = `Error reading file: ${error.message}`;
}
}
return {
content: [
{
type: "text",
text: JSON.stringify(results, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{ error: `Failed to process repository: ${error.message}` },
null,
2
),
},
],
isError: true,
};
}
}
/**
* Handles the git_search_code tool request
* @param {Object} params - Tool parameters
* @param {string} params.repo_url - Repository URL
* @param {string} params.pattern - Search pattern (regex or string)
* @param {string[]} params.file_patterns - Optional file patterns to filter (e.g., "*.js")
* @param {boolean} params.case_sensitive - Whether the search is case sensitive
* @param {number} params.context_lines - Number of context lines to include
* @returns {Object} - Tool response
*/
export async function handleGitSearchCode({
repo_url,
pattern,
file_patterns = [],
case_sensitive = false,
context_lines = 2,
}) {
try {
const repoPath = await cloneRepo(repo_url);
// Build the grep command
let grepCommand = `cd "${repoPath}" && git grep`;
// Add options
if (!case_sensitive) {
grepCommand += " -i";
}
// Add context lines
grepCommand += ` -n -C${context_lines}`;
// Add pattern (escape quotes in the pattern)
const escapedPattern = pattern.replace(/"/g, '\\"');
grepCommand += ` "${escapedPattern}"`;
// Add file patterns if provided
if (file_patterns && file_patterns.length > 0) {
grepCommand += ` -- ${file_patterns.join(" ")}`;
}
// Execute the command
const { stdout, stderr } = await execPromise(grepCommand);
if (stderr) {
console.error(`Search error: ${stderr}`);
}
// Process the results
const results = [];
if (stdout) {
// Split by file sections (git grep output format)
const fileMatches = stdout.split(/^(?=\S[^:]*:)/m);
for (const fileMatch of fileMatches) {
if (!fileMatch.trim()) continue;
// Extract file name and matches
const lines = fileMatch.split("\n");
const firstLine = lines[0];
const fileNameMatch = firstLine.match(/^([^:]+):/);
if (fileNameMatch) {
const fileName = fileNameMatch[1];
const matches = [];
// Process each line
let currentMatch = null;
let contextLines = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Skip empty lines
if (!line.trim()) continue;
// Check if this is a line number indicator
const lineNumberMatch = line.match(/^([^-][^:]+):(\d+):(.*)/);
if (lineNumberMatch) {
// If we have a previous match, add it to the results
if (currentMatch) {
currentMatch.context_after = contextLines;
matches.push(currentMatch);
contextLines = [];
}
// Start a new match
currentMatch = {
file: fileName,
line_number: parseInt(lineNumberMatch[2]),
content: lineNumberMatch[3],
context_before: contextLines,
context_after: [],
};
contextLines = [];
} else {
// This is a context line
const contextMatch = line.match(/^([^:]+)-(\d+)-(.*)/);
if (contextMatch) {
contextLines.push({
line_number: parseInt(contextMatch[2]),
content: contextMatch[3],
});
}
}
}
// Add the last match if there is one
if (currentMatch) {
currentMatch.context_after = contextLines;
matches.push(currentMatch);
}
if (matches.length > 0) {
results.push({
file: fileName,
matches: matches,
});
}
}
}
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{
pattern: pattern,
case_sensitive: case_sensitive,
context_lines: context_lines,
file_patterns: file_patterns,
results: results,
total_matches: results.reduce(
(sum, file) => sum + file.matches.length,
0
),
total_files: results.length,
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{ error: `Failed to search repository: ${error.message}` },
null,
2
),
},
],
isError: true,
};
}
}
/**
* Handles the git_local_changes tool request
* @param {Object} params - Tool parameters
* @param {string} params.repo_path - Local repository path
* @returns {Object} - Tool response
*/
export async function handleGitLocalChanges({ repo_path }) {
try {
// Use the provided local repo path
const git = simpleGit(repo_path);
// Get status information
const status = await git.status();
// Get detailed diff for modified files
let diffs = {};
for (const file of status.modified) {
diffs[file] = await git.diff([file]);
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{
branch: status.current,
staged_files: status.staged,
modified_files: status.modified,
new_files: status.not_added,
deleted_files: status.deleted,
conflicted_files: status.conflicted,
diffs: diffs,
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{ error: `Failed to get local changes: ${error.message}` },
null,
2
),
},
],
isError: true,
};
}
}