Skip to main content
Glama
repositories.ts22.4 kB
import { z } from "zod" import type { Octokit } from "octokit" import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" export function registerRepositoryTools(server: McpServer, octokit: Octokit) { // Tool: Get Repository Details server.tool( "get_repository", "Get detailed information about a GitHub repository including README and file structure", { owner: z.string().describe("Repository owner"), repo: z.string().describe("Repository name"), }, async ({ owner, repo }) => { try { // Get basic repository info const repoResponse = await octokit.rest.repos.get({ owner, repo, }) const repoData = repoResponse.data // Start building markdown let markdown = `# ${repoData.full_name}\n\n` // Add description if available if (repoData.description) { markdown += `> ${repoData.description}\n\n` } // Add basic stats in a single line markdown += `**Language:** ${repoData.language || "Not specified"} | ` markdown += `**Stars:** ${repoData.stargazers_count} | ` markdown += `**Forks:** ${repoData.forks_count} | ` markdown += `**License:** ${repoData.license?.spdx_id || "None"}\n\n` // Get README content try { const readmeResponse = await octokit.rest.repos.getReadme({ owner, repo, }) // Decode README content from base64 const readmeContent = Buffer.from( readmeResponse.data.content, "base64", ).toString("utf-8") markdown += `## README\n\n` markdown += readmeContent markdown += `\n\n` } catch (readmeError) { markdown += `## README\n\n` markdown += `*No README file found*\n\n` } // Get repository file structure (root directory) try { const contentsResponse = await octokit.rest.repos.getContent({ owner, repo, path: "", }) if (Array.isArray(contentsResponse.data)) { markdown += `## Repository Structure\n\n` // Sort contents: directories first, then files const contents = contentsResponse.data.sort((a, b) => { if (a.type === b.type) return a.name.localeCompare(b.name) return a.type === "dir" ? -1 : 1 }) // Group by type const dirs = contents.filter(item => item.type === "dir") const files = contents.filter(item => item.type === "file") if (dirs.length > 0) { markdown += `### Directories\n` dirs.forEach(dir => { markdown += `- **${dir.name}/**\n` }) markdown += `\n` } if (files.length > 0) { markdown += `### Files\n` files.forEach(file => { const size = file.size ? ` (${(file.size / 1024).toFixed(1)} KB)` : "" markdown += `- ${file.name}${size}\n` }) markdown += `\n` } } } catch (contentsError) { markdown += `## Repository Structure\n\n` markdown += `*Unable to fetch repository contents*\n\n` } // Add essential links at the bottom markdown += `## Links\n\n` markdown += `- **GitHub:** ${repoData.html_url}\n` markdown += `- **Clone:** \`git clone ${repoData.clone_url}\`\n` if (repoData.homepage) { markdown += `- **Website:** ${repoData.homepage}\n` } return { content: [{ type: "text", text: markdown }], } } catch (e: any) { return { content: [{ type: "text", text: `Error: ${e.message}` }], } } }, ) // Tool: Get Commit server.tool( "get_commit", "Get details for a commit from a GitHub repository", { owner: z.string().describe("Repository owner"), repo: z.string().describe("Repository name"), sha: z.string().describe("Commit SHA, branch name, or tag name"), }, async ({ owner, repo, sha }) => { try { const response = await octokit.rest.repos.getCommit({ owner, repo, ref: sha, }) const commit = response.data // Format as clean markdown let markdown = `# Commit ${commit.sha.substring(0, 7)}\n\n` markdown += `**Message:** ${commit.commit.message}\n\n` markdown += `**Author:** ${commit.commit.author?.name} <${commit.commit.author?.email}>\n` markdown += `**Date:** ${new Date(commit.commit.author?.date || "").toLocaleDateString()}\n` if (commit.commit.committer?.name !== commit.commit.author?.name) { markdown += `**Committer:** ${commit.commit.committer?.name} <${commit.commit.committer?.email}>\n` } markdown += `\n## Changes\n\n` markdown += `- **Files changed:** ${commit.files?.length || 0}\n` markdown += `- **Additions:** ${commit.stats?.additions || 0}\n` markdown += `- **Deletions:** ${commit.stats?.deletions || 0}\n` if (commit.files && commit.files.length > 0) { markdown += `\n## Files\n\n` commit.files.forEach(file => { const status = file.status === "added" ? "[A]" : file.status === "removed" ? "[D]" : file.status === "modified" ? "[M]" : file.status === "renamed" ? "[R]" : "[?]" markdown += `- ${status} ${file.filename} (+${file.additions} -${file.deletions})\n` }) } markdown += `\n## Links\n\n` markdown += `- **Commit URL:** ${commit.html_url}\n` markdown += `- **Full SHA:** ${commit.sha}\n` return { content: [{ type: "text", text: markdown }], } } catch (e: any) { return { content: [{ type: "text", text: `Error: ${e.message}` }], } } }, ) // Tool: List Commits server.tool( "list_commits", "Get list of commits of a branch in a GitHub repository", { owner: z.string().describe("Repository owner"), repo: z.string().describe("Repository name"), sha: z.string().optional().describe("SHA or Branch name"), per_page: z .number() .optional() .default(10) .describe("Results per page (default 10, max 100)"), page: z .number() .optional() .default(1) .describe("Page number (default 1)"), }, async ({ owner, repo, sha, per_page, page }) => { try { const response = await octokit.rest.repos.listCommits({ owner, repo, sha, per_page, page, }) const commits = response.data if (commits.length === 0) { return { content: [{ type: "text", text: "No commits found." }], } } // Format as clean markdown let markdown = `# Commits for ${owner}/${repo}` if (sha) { markdown += ` (${sha})` } markdown += `\n\n` markdown += `Showing ${commits.length} commit(s) - Page ${page}\n` if (commits.length === per_page) { markdown += `*Note: More commits may be available. Use 'page' parameter to see next page.*\n` } markdown += `\n` commits.forEach(commit => { const shortSha = commit.sha.substring(0, 7) const message = commit.commit.message.split("\n")[0] // First line only const author = commit.commit.author?.name || commit.author?.login || "Unknown" const date = new Date( commit.commit.author?.date || "", ).toLocaleDateString() markdown += `## ${shortSha}: ${message}\n\n` markdown += `- **Author:** ${author}\n` markdown += `- **Date:** ${date}\n` if (commit.commit.comment_count > 0) { markdown += `- **Comments:** ${commit.commit.comment_count}\n` } markdown += `- **URL:** ${commit.html_url}\n` markdown += `\n` }) return { content: [{ type: "text", text: markdown }], } } catch (e: any) { return { content: [{ type: "text", text: `Error: ${e.message}` }], } } }, ) // Tool: List Branches server.tool( "list_branches", "List branches in a GitHub repository", { owner: z.string().describe("Repository owner"), repo: z.string().describe("Repository name"), per_page: z .number() .optional() .default(10) .describe("Results per page (default 10, max 100)"), page: z .number() .optional() .default(1) .describe("Page number (default 1)"), }, async ({ owner, repo, per_page, page }) => { try { const response = await octokit.rest.repos.listBranches({ owner, repo, per_page, page, }) const branches = response.data if (branches.length === 0) { return { content: [{ type: "text", text: "No branches found." }], } } // Get default branch const repoResponse = await octokit.rest.repos.get({ owner, repo }) const defaultBranch = repoResponse.data.default_branch // Format as clean markdown let markdown = `# Branches for ${owner}/${repo}\n\n` markdown += `Showing ${branches.length} branch(es) - Page ${page}\n` if (branches.length === per_page) { markdown += `*Note: More branches may be available. Use 'page' parameter to see next page.*\n` } markdown += `\n` branches.forEach(branch => { const isDefault = branch.name === defaultBranch markdown += `## ${branch.name}${isDefault ? " (default)" : ""}\n\n` markdown += `- **SHA:** ${branch.commit.sha.substring(0, 7)}\n` if (branch.protected) { markdown += `- **Protected:** Yes\n` } markdown += `- **URL:** ${branch.commit.url.replace("api.github.com/repos", "github.com").replace("/commits/", "/tree/")}\n` markdown += `\n` }) return { content: [{ type: "text", text: markdown }], } } catch (e: any) { return { content: [{ type: "text", text: `Error: ${e.message}` }], } } }, ) // Tool: Create or Update File server.tool( "create_or_update_file", "Create or update a single file in a GitHub repository. If updating an existing file, you must provide the current SHA of the file (the full 40-character SHA, not a shortened version).", { owner: z.string().describe("Repository owner (username or organization)"), repo: z.string().describe("Repository name"), path: z.string().describe("Path where to create/update the file"), content: z.string().describe("Content of the file"), message: z.string().describe("Commit message"), branch: z.string().describe("Branch to create/update the file in"), sha: z .string() .optional() .describe( "Full SHA of the current file blob (required for updates, must be the complete 40-character SHA)", ), }, async ({ owner, repo, path, content, message, branch, sha }) => { try { // If SHA is provided, validate it's the full SHA if (sha && sha.length !== 40) { return { content: [ { type: "text", text: `Error: SHA must be the full 40-character blob SHA. Provided SHA "${sha}" is only ${sha.length} characters. Use get_file_contents to retrieve the full SHA.`, }, ], } } const response = await octokit.rest.repos.createOrUpdateFileContents({ owner, repo, path, message, content: Buffer.from(content).toString("base64"), branch, sha, }) // Format response as markdown let markdown = `# File ${response.data.commit.message}\n\n` markdown += `**Path:** ${response.data.content?.path || path}\n` markdown += `**SHA:** ${response.data.content?.sha || "N/A"}\n` markdown += `**Size:** ${response.data.content?.size || 0} bytes\n\n` markdown += `## Commit Details\n\n` markdown += `- **Commit SHA:** ${response.data.commit.sha}\n` markdown += `- **Author:** ${response.data.commit.author?.name} <${response.data.commit.author?.email}>\n` markdown += `- **Date:** ${response.data.commit.author?.date}\n` markdown += `- **URL:** ${response.data.commit.html_url}\n` return { content: [{ type: "text", text: markdown }], } } catch (e: any) { // Provide more helpful error messages if (e.message.includes("does not match")) { return { content: [ { type: "text", text: `Error: SHA mismatch. The provided SHA does not match the current file's SHA. This usually means the file has been modified since you last retrieved it. Use get_file_contents to get the current SHA, then retry the update.\n\nOriginal error: ${e.message}`, }, ], } } return { content: [{ type: "text", text: `Error: ${e.message}` }], } } }, ) // Tool: Create Repository server.tool( "create_repository", "Create a new GitHub repository in your account", { name: z.string().describe("Repository name"), description: z.string().optional().describe("Repository description"), private: z .boolean() .optional() .describe("Whether repo should be private"), autoInit: z.boolean().optional().describe("Initialize with README"), }, async ({ name, description, private: isPrivate, autoInit }) => { try { const response = await octokit.rest.repos.createForAuthenticatedUser({ name, description, private: isPrivate, auto_init: autoInit, }) return { content: [{ type: "text", text: JSON.stringify(response.data) }], } } catch (e: any) { return { content: [{ type: "text", text: `Error: ${e.message}` }], } } }, ) // Tool: Get File Contents server.tool( "get_file_contents", "Get the contents of a file from a GitHub repository", { owner: z.string().describe("Repository owner (username or organization)"), repo: z.string().describe("Repository name"), path: z.string().describe("Path to file"), branch: z .string() .optional() .describe("Branch to get contents from (defaults to default branch)"), mode: z .enum(["overview", "full"]) .optional() .default("overview") .describe( "Mode: 'overview' for truncated preview, 'full' for complete file", ), }, async ({ owner, repo, path, branch, mode }) => { try { const response = await octokit.rest.repos.getContent({ owner, repo, path, ref: branch, }) // Handle only files if (Array.isArray(response.data)) { return { content: [ { type: "text", text: "Error: Path points to a directory, not a file.", }, ], } } if (response.data.type !== "file") { return { content: [ { type: "text", text: `Error: Path points to a ${response.data.type}, not a file.`, }, ], } } // Decode the file content const fullContent = Buffer.from( response.data.content, "base64", ).toString("utf-8") // Get file extension for syntax highlighting const ext = path.split(".").pop() || "" const fileName = path.split("/").pop() || path // Format as markdown let markdown = `# File: ${fileName}\n\n` markdown += `**Path:** ${response.data.path}\n` markdown += `**Size:** ${(response.data.size / 1024).toFixed(2)} KB (${response.data.size} bytes)\n` markdown += `**SHA:** ${response.data.sha.substring(0, 7)} (full: ${response.data.sha})\n\n` if (mode === "overview") { // Truncated overview mode const lines = fullContent.split("\n") const totalLines = lines.length markdown += `**Lines:** ${totalLines}\n\n` markdown += `## Preview (first 50 lines)\n\n` const previewLines = lines.slice(0, 50) const preview = previewLines.join("\n") markdown += `\`\`\`${ext}\n${preview}\n` if (totalLines > 50) { markdown += `\n... (${totalLines - 50} more lines)\n` } markdown += `\`\`\`\n\n` if (totalLines > 50) { markdown += `*Showing first 50 lines of ${totalLines} total. Use mode='full' to see complete file.*\n` } } else { // Full content mode const lines = fullContent.split("\n").length markdown += `**Lines:** ${lines}\n\n` markdown += `## Full Content\n\n` markdown += `\`\`\`${ext}\n${fullContent}\n\`\`\`` } return { content: [{ type: "text", text: markdown }], } } catch (e: any) { return { content: [{ type: "text", text: `Error: ${e.message}` }], } } }, ) // Tool: Fork Repository server.tool( "fork_repository", "Fork a GitHub repository to your account or specified organization", { owner: z.string().describe("Repository owner"), repo: z.string().describe("Repository name"), organization: z.string().optional().describe("Organization to fork to"), }, async ({ owner, repo, organization }) => { try { const response = await octokit.rest.repos.createFork({ owner, repo, organization, }) return { content: [{ type: "text", text: JSON.stringify(response.data) }], } } catch (e: any) { return { content: [{ type: "text", text: `Error: ${e.message}` }], } } }, ) // Tool: Create Branch server.tool( "create_branch", "Create a new branch in a GitHub repository", { owner: z.string().describe("Repository owner"), repo: z.string().describe("Repository name"), branch: z.string().describe("Name for new branch"), from_branch: z .string() .optional() .describe("Source branch (defaults to repo default)"), }, async ({ owner, repo, branch, from_branch }) => { try { // Get the source branch ref let sourceBranch = from_branch if (!sourceBranch) { const repoResp = await octokit.rest.repos.get({ owner, repo }) sourceBranch = repoResp.data.default_branch } const refResp = await octokit.rest.git.getRef({ owner, repo, ref: `heads/${sourceBranch}`, }) const sha = refResp.data.object.sha // Create new branch const response = await octokit.rest.git.createRef({ owner, repo, ref: `refs/heads/${branch}`, sha, }) return { content: [{ type: "text", text: JSON.stringify(response.data) }], } } catch (e: any) { return { content: [{ type: "text", text: `Error: ${e.message}` }], } } }, ) // Tool: List Tags server.tool( "list_tags", "List git tags in a GitHub repository", { owner: z.string().describe("Repository owner"), repo: z.string().describe("Repository name"), per_page: z .number() .optional() .default(10) .describe("Results per page (default 10, max 100)"), page: z .number() .optional() .default(1) .describe("Page number (default 1)"), }, async ({ owner, repo, per_page, page }) => { try { const response = await octokit.rest.repos.listTags({ owner, repo, per_page, page, }) const tags = response.data if (tags.length === 0) { return { content: [{ type: "text", text: "No tags found." }], } } // Format as clean markdown let markdown = `# Tags for ${owner}/${repo}\n\n` markdown += `Showing ${tags.length} tag(s) - Page ${page}\n` if (tags.length === per_page) { markdown += `*Note: More tags may be available. Use 'page' parameter to see next page.*\n` } markdown += `\n` tags.forEach(tag => { markdown += `## ${tag.name}\n\n` markdown += `- **SHA:** ${tag.commit.sha.substring(0, 7)}\n` markdown += `- **URL:** ${tag.commit.url.replace("api.github.com/repos", "github.com").replace("/commits/", "/releases/tag/")}\n` if (tag.zipball_url) { markdown += `- **Download:** [ZIP](${tag.zipball_url}) | [TAR](${tag.tarball_url})\n` } markdown += `\n` }) return { content: [{ type: "text", text: markdown }], } } catch (e: any) { return { content: [{ type: "text", text: `Error: ${e.message}` }], } } }, ) // Tool: Get Tag server.tool( "get_tag", "Get details about a specific git tag in a GitHub repository", { owner: z.string().describe("Repository owner"), repo: z.string().describe("Repository name"), tag: z.string().describe("Tag name"), }, async ({ owner, repo, tag }) => { try { // Get the tag reference const refResp = await octokit.rest.git.getRef({ owner, repo, ref: `tags/${tag}`, }) const sha = refResp.data.object.sha // Get the tag object const tagResp = await octokit.rest.git.getTag({ owner, repo, tag_sha: sha, }) return { content: [{ type: "text", text: JSON.stringify(tagResp.data) }], } } catch (e: any) { return { content: [{ type: "text", text: `Error: ${e.message}` }], } } }, ) // Tool: Push Files server.tool( "push_files", "Push multiple files to a GitHub repository in a single commit", { owner: z.string().describe("Repository owner"), repo: z.string().describe("Repository name"), branch: z.string().describe("Branch to push to"), files: z .array(z.object({ path: z.string(), content: z.string() })) .describe( "Array of file objects to push, each object with path (string) and content (string)", ), message: z.string().describe("Commit message"), }, async ({ owner, repo, branch, files, message }) => { try { // Get the reference for the branch const refResp = await octokit.rest.git.getRef({ owner, repo, ref: `heads/${branch}`, }) const baseSha = refResp.data.object.sha // Get the commit object that the branch points to const baseCommit = await octokit.rest.git.getCommit({ owner, repo, commit_sha: baseSha, }) // Create tree entries for all files const treeItems = files.map(file => ({ path: file.path, mode: "100644" as const, // Regular file mode type: "blob" as const, content: file.content, })) // Create a new tree with the file entries const newTree = await octokit.rest.git.createTree({ owner, repo, base_tree: baseCommit.data.tree.sha, tree: treeItems, }) // Create a new commit const newCommit = await octokit.rest.git.createCommit({ owner, repo, message, tree: newTree.data.sha, parents: [baseSha], }) // Update the reference to point to the new commit const updatedRef = await octokit.rest.git.updateRef({ owner, repo, ref: `heads/${branch}`, sha: newCommit.data.sha, force: false, }) return { content: [{ type: "text", text: JSON.stringify(updatedRef.data) }], } } catch (e: any) { return { content: [{ type: "text", text: `Error: ${e.message}` }], } } }, ) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/hithereiamaliff/mcp-github'

If you have feedback or need assistance with the MCP directory API, please join our Discord server