From 9be6268005db9b0254071438fc93cecb40c16103 Mon Sep 17 00:00:00 2001 From: Elie Schoppik Date: Wed, 20 Nov 2024 13:08:06 -0500 Subject: [PATCH 1/8] init on github --- src/github/index.ts | 953 +++++++++++++++++++++++++++++++++++ src/github/interfaces.ts | 332 ++++++++++++ src/github/package-lock.json | 551 ++++++++++++++++++++ src/github/package.json | 30 ++ src/github/tsconfig.json | 11 + 5 files changed, 1877 insertions(+) create mode 100644 src/github/index.ts create mode 100644 src/github/interfaces.ts create mode 100644 src/github/package-lock.json create mode 100644 src/github/package.json create mode 100644 src/github/tsconfig.json diff --git a/src/github/index.ts b/src/github/index.ts new file mode 100644 index 000000000..784577b68 --- /dev/null +++ b/src/github/index.ts @@ -0,0 +1,953 @@ +#!/usr/bin/env node + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import fetch from "node-fetch"; +import { + GitHubContent, + GitHubCreateUpdateFileResponse, + GitHubSearchResponse, + GitHubRepository, + GitHubTree, + GitHubCommit, + GitHubReference, + CreateRepositoryOptions, + FileOperation, + CreateTreeParams, + GitHubPullRequest, + CreateIssueOptions, + CreatePullRequestOptions, + GitHubIssue, + GitHubFork, + CreateBranchOptions, +} from './interfaces.js'; + +const server = new Server({ + name: "github-mcp-server", + version: "0.1.0", +}, { + capabilities: { + tools: {} + } +}); + +const GITHUB_PERSONAL_ACCESS_TOKEN = process.env.GITHUB_PERSONAL_ACCESS_TOKEN; + +if (!GITHUB_PERSONAL_ACCESS_TOKEN) { + console.error("GITHUB_PERSONAL_ACCESS_TOKEN environment variable is not set"); + process.exit(1); +} + +// GitHub API helper functions + +// Add these helper functions to your existing code + +async function forkRepository( + owner: string, + repo: string, + organization?: string +): Promise { + const url = organization + ? `https://api.github.com/repos/${owner}/${repo}/forks?organization=${organization}` + : `https://api.github.com/repos/${owner}/${repo}/forks`; + + const response = await fetch(url, { + method: "POST", + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server" + } + }); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json() as GitHubFork; +} + +async function createBranch( + owner: string, + repo: string, + options: CreateBranchOptions +): Promise { + // The ref needs to be in the format "refs/heads/branch-name" + const fullRef = `refs/heads/${options.ref}`; + + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/git/refs`, + { + method: "POST", + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + ref: fullRef, + sha: options.sha + }) + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json() as GitHubReference; +} + +// Helper function to get the default branch SHA +async function getDefaultBranchSHA( + owner: string, + repo: string +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/main`, + { + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server" + } + } + ); + + // If main branch doesn't exist, try master + if (!response.ok) { + const masterResponse = await fetch( + `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/master`, + { + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server" + } + } + ); + + if (!masterResponse.ok) { + throw new Error("Could not find default branch (tried 'main' and 'master')"); + } + + const data = await masterResponse.json() as GitHubReference; + return data.object.sha; + } + + const data = await response.json() as GitHubReference; + return data.object.sha; +} + +async function getFileContents(owner: string, repo: string, path: string): Promise { + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`; + const response = await fetch(url, { + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server" + } + }); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + const data = await response.json() as GitHubContent; + + // If it's a file, decode the content + if (!Array.isArray(data) && data.content) { + return { + ...data, + content: Buffer.from(data.content, 'base64').toString('utf8') + }; + } + + return data; +} + + +async function createIssue( + owner: string, + repo: string, + options: CreateIssueOptions +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/issues`, + { + method: "POST", + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + "Content-Type": "application/json" + }, + body: JSON.stringify(options) + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json() as GitHubIssue; +} + +async function createPullRequest( + owner: string, + repo: string, + options: CreatePullRequestOptions +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls`, + { + method: "POST", + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + "Content-Type": "application/json" + }, + body: JSON.stringify(options) + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json() as GitHubPullRequest; +} + +async function createOrUpdateFile( + owner: string, + repo: string, + path: string, + content: string, + message: string, + sha?: string +): Promise { + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`; + + const body = { + message, + content: Buffer.from(content).toString('base64'), + sha + }; + + const response = await fetch(url, { + method: "PUT", + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + "Content-Type": "application/json" + }, + body: JSON.stringify(body) + }); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json() as GitHubCreateUpdateFileResponse; +} + +async function createTree( + owner: string, + repo: string, + files: FileOperation[], + baseTree?: string +): Promise { + const tree: CreateTreeParams[] = files.map(file => ({ + path: file.path, + mode: '100644', + type: 'blob', + content: file.content + })); + + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/git/trees`, + { + method: "POST", + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + tree, + base_tree: baseTree + }) + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json() as GitHubTree; +} + +async function createCommit( + owner: string, + repo: string, + message: string, + tree: string, + parents: string[] +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/git/commits`, + { + method: "POST", + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + message, + tree, + parents + }) + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json() as GitHubCommit; +} + +async function updateReference( + owner: string, + repo: string, + ref: string, + sha: string +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/git/refs/${ref}`, + { + method: "PATCH", + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + sha, + force: true + }) + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json() as GitHubReference; +} + +async function pushFiles( + owner: string, + repo: string, + branch: string, + files: FileOperation[], + message: string +): Promise { + const refResponse = await fetch( + `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, + { + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server" + } + } + ); + + if (!refResponse.ok) { + throw new Error(`GitHub API error: ${refResponse.statusText}`); + } + + const ref = await refResponse.json() as GitHubReference; + const commitSha = ref.object.sha; + + const tree = await createTree(owner, repo, files, commitSha); + const commit = await createCommit(owner, repo, message, tree.sha, [commitSha]); + return await updateReference(owner, repo, `heads/${branch}`, commit.sha); +} + +async function searchRepositories( + query: string, + page: number = 1, + perPage: number = 30 +): Promise { + const url = new URL("https://api.github.com/search/repositories"); + url.searchParams.append("q", query); + url.searchParams.append("page", page.toString()); + url.searchParams.append("per_page", perPage.toString()); + + const response = await fetch(url.toString(), { + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server" + } + }); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json() as GitHubSearchResponse; +} + +async function createRepository(options: CreateRepositoryOptions): Promise { + const response = await fetch("https://api.github.com/user/repos", { + method: "POST", + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + "Content-Type": "application/json" + }, + body: JSON.stringify(options) + }); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json() as GitHubRepository; +} + +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "search_repositories", + description: "Search for GitHub repositories", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "Search query (see GitHub search syntax)" + }, + page: { + type: "number", + description: "Page number for pagination (default: 1)" + }, + perPage: { + type: "number", + description: "Number of results per page (default: 30, max: 100)" + } + }, + required: ["query"] + } + }, + { + name: "create_repository", + description: "Create a new GitHub repository in your account", + inputSchema: { + type: "object", + properties: { + name: { + type: "string", + description: "Repository name" + }, + description: { + type: "string", + description: "Repository description" + }, + private: { + type: "boolean", + description: "Whether the repository should be private" + }, + autoInit: { + type: "boolean", + description: "Initialize with README.md" + } + }, + required: ["name"] + } + }, + { + name: "get_file_contents", + description: "Get the contents of a file or directory from a GitHub repository", + inputSchema: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner (username or organization)" + }, + repo: { + type: "string", + description: "Repository name" + }, + path: { + type: "string", + description: "Path to the file or directory" + } + }, + required: ["owner", "repo", "path"] + } + }, + { + name: "create_or_update_file", + description: "Create or update a single file in a GitHub repository", + inputSchema: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner (username or organization)" + }, + repo: { + type: "string", + description: "Repository name" + }, + path: { + type: "string", + description: "Path where to create/update the file" + }, + content: { + type: "string", + description: "Content of the file" + }, + message: { + type: "string", + description: "Commit message" + }, + sha: { + type: "string", + description: "SHA of the file being replaced (required when updating existing files)" + } + }, + required: ["owner", "repo", "path", "content", "message"] + } + }, + { + name: "push_files", + description: "Push multiple files to a GitHub repository in a single commit", + inputSchema: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner (username or organization)" + }, + repo: { + type: "string", + description: "Repository name" + }, + branch: { + type: "string", + description: "Branch to push to (e.g., 'main' or 'master')" + }, + files: { + type: "array", + description: "Array of files to push", + items: { + type: "object", + properties: { + path: { + type: "string", + description: "Path where to create the file" + }, + content: { + type: "string", + description: "Content of the file" + } + }, + required: ["path", "content"] + } + }, + message: { + type: "string", + description: "Commit message" + } + }, + required: ["owner", "repo", "branch", "files", "message"] + } + }, + { + name: "open_in_browser", + description: "Open a GitHub repository, file, issue, or pull request in the browser", + inputSchema: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner (username or organization)" + }, + repo: { + type: "string", + description: "Repository name" + }, + type: { + type: "string", + enum: ["repository", "file", "issue", "pull_request"], + description: "Type of resource to open" + }, + path: { + type: "string", + description: "Path to the file (only for type='file')" + }, + number: { + type: "number", + description: "Issue or PR number (only for type='issue' or type='pull_request')" + } + }, + required: ["owner", "repo", "type"] + } + }, + { + name: "create_issue", + description: "Create a new issue in a GitHub repository", + inputSchema: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner (username or organization)" + }, + repo: { + type: "string", + description: "Repository name" + }, + title: { + type: "string", + description: "Issue title" + }, + body: { + type: "string", + description: "Issue body/description" + }, + assignees: { + type: "array", + items: { type: "string" }, + description: "Array of usernames to assign" + }, + labels: { + type: "array", + items: { type: "string" }, + description: "Array of label names" + }, + milestone: { + type: "number", + description: "Milestone number to assign" + } + }, + required: ["owner", "repo", "title"] + } + }, + { + name: "create_pull_request", + description: "Create a new pull request in a GitHub repository", + inputSchema: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner (username or organization)" + }, + repo: { + type: "string", + description: "Repository name" + }, + title: { + type: "string", + description: "Pull request title" + }, + body: { + type: "string", + description: "Pull request body/description" + }, + head: { + type: "string", + description: "The name of the branch where your changes are implemented" + }, + base: { + type: "string", + description: "The name of the branch you want the changes pulled into" + }, + draft: { + type: "boolean", + description: "Whether to create the pull request as a draft" + }, + maintainer_can_modify: { + type: "boolean", + description: "Whether maintainers can modify the pull request" + } + }, + required: ["owner", "repo", "title", "head", "base"] + } + }, + { + name: "fork_repository", + description: "Fork a GitHub repository to your account or specified organization", + inputSchema: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner (username or organization)" + }, + repo: { + type: "string", + description: "Repository name" + }, + organization: { + type: "string", + description: "Optional: organization to fork to (defaults to your personal account)" + } + }, + required: ["owner", "repo"] + } + }, + { + name: "create_branch", + description: "Create a new branch in a GitHub repository", + inputSchema: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner (username or organization)" + }, + repo: { + type: "string", + description: "Repository name" + }, + branch: { + type: "string", + description: "Name for the new branch" + }, + from_branch: { + type: "string", + description: "Optional: source branch to create from (defaults to the repository's default branch)" + } + }, + required: ["owner", "repo", "branch"] + } + } + ] + }; + +}); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + + if (request.params.name === "fork_repository") { + if (!request.params.arguments) { + throw new Error("Arguments are required"); + } + + const args = request.params.arguments as { + owner: string; + repo: string; + organization?: string; + }; + + const fork = await forkRepository(args.owner, args.repo, args.organization); + return { toolResult: fork }; + } + + if (request.params.name === "create_branch") { + if (!request.params.arguments) { + throw new Error("Arguments are required"); + } + + const args = request.params.arguments as { + owner: string; + repo: string; + branch: string; + from_branch?: string; + }; + + // If no source branch is specified, use the default branch + let sha: string; + if (args.from_branch) { + const response = await fetch( + `https://api.github.com/repos/${args.owner}/${args.repo}/git/refs/heads/${args.from_branch}`, + { + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server" + } + } + ); + + if (!response.ok) { + throw new Error(`Source branch '${args.from_branch}' not found`); + } + + const data = await response.json() as GitHubReference; + sha = data.object.sha; + } else { + sha = await getDefaultBranchSHA(args.owner, args.repo); + } + + const branch = await createBranch(args.owner, args.repo, { + ref: args.branch, + sha: sha + }); + + return { toolResult: branch }; + } + if (request.params.name === "search_repositories") { + const { query, page, perPage } = request.params.arguments as { + query: string; + page?: number; + perPage?: number; + }; + + const results = await searchRepositories(query, page, perPage); + return { toolResult: results }; + } + + if (request.params.name === "create_repository") { + const options = request.params.arguments as CreateRepositoryOptions; + const repository = await createRepository(options); + return { toolResult: repository }; + } + + if (request.params.name === "get_file_contents") { + const { owner, repo, path } = request.params.arguments as { + owner: string; + repo: string; + path: string; + }; + + const contents = await getFileContents(owner, repo, path); + return { toolResult: contents }; + } + + if (request.params.name === "create_or_update_file") { + const { owner, repo, path, content, message, sha } = request.params.arguments as { + owner: string; + repo: string; + path: string; + content: string; + message: string; + sha?: string; + }; + + const result = await createOrUpdateFile(owner, repo, path, content, message, sha); + return { toolResult: result }; + } + + if (request.params.name === "push_files") { + const { owner, repo, branch, files, message } = request.params.arguments as { + owner: string; + repo: string; + branch: string; + files: FileOperation[]; + message: string; + }; + + const result = await pushFiles(owner, repo, branch, files, message); + return { toolResult: result }; + } + + +if (request.params.name === "open_in_browser") { + const { owner, repo, type, path, number } = request.params.arguments as { + owner: string; + repo: string; + type: "repository" | "file" | "issue" | "pull_request"; + path?: string; + number?: number; + }; + + let url: string; + switch (type) { + case "repository": + url = `https://github.com/${owner}/${repo}`; + break; + case "file": + if (!path) throw new Error("Path is required for file URLs"); + url = `https://github.com/${owner}/${repo}/blob/main/${path}`; + break; + case "issue": + if (!number) throw new Error("Number is required for issue URLs"); + url = `https://github.com/${owner}/${repo}/issues/${number}`; + break; + case "pull_request": + if (!number) throw new Error("Number is required for pull request URLs"); + url = `https://github.com/${owner}/${repo}/pull/${number}`; + break; + default: + throw new Error(`Invalid type: ${type}`); + } + + return { toolResult: { url } }; +} + +if (request.params.name === "create_issue") { + if (!request.params.arguments) { + throw new Error("Arguments are required"); + } + + const args = request.params.arguments as { + owner: string; + repo: string; + title: string; + body?: string; + assignees?: string[]; + milestone?: number; + labels?: string[]; + }; + + const { owner, repo, ...options } = args; + const issue = await createIssue(owner, repo, options); + return { toolResult: issue }; +} + +if (request.params.name === "create_pull_request") { + if (!request.params.arguments) { + throw new Error("Arguments are required"); + } + + const args = request.params.arguments as { + owner: string; + repo: string; + title: string; + body?: string; + head: string; + base: string; + maintainer_can_modify?: boolean; + draft?: boolean; + }; + + const { owner, repo, ...options } = args; + const pullRequest = await createPullRequest(owner, repo, options); + return { toolResult: pullRequest }; +} + + throw new Error("Tool not found"); +}); + +async function runServer() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("GitHub MCP Server running on stdio"); +} + +runServer().catch((error) => { + console.error("Fatal error in main():", error); + process.exit(1); +}); \ No newline at end of file diff --git a/src/github/interfaces.ts b/src/github/interfaces.ts new file mode 100644 index 000000000..ce3518e66 --- /dev/null +++ b/src/github/interfaces.ts @@ -0,0 +1,332 @@ +// GitHub API Response Types +export interface GitHubErrorResponse { + message: string; + documentation_url?: string; + } + + export interface GitHubFileContent { + type: string; + encoding: string; + size: number; + name: string; + path: string; + content: string; + sha: string; + url: string; + git_url: string; + html_url: string; + download_url: string; + } + + export interface GitHubDirectoryContent { + type: string; + size: number; + name: string; + path: string; + sha: string; + url: string; + git_url: string; + html_url: string; + download_url: string | null; + } + + export type GitHubContent = GitHubFileContent | GitHubDirectoryContent[]; + + export interface GitHubCreateUpdateFileResponse { + content: GitHubFileContent | null; + commit: { + sha: string; + node_id: string; + url: string; + html_url: string; + author: GitHubAuthor; + committer: GitHubAuthor; + message: string; + tree: { + sha: string; + url: string; + }; + parents: Array<{ + sha: string; + url: string; + html_url: string; + }>; + }; + } + + export interface GitHubAuthor { + name: string; + email: string; + date: string; + } + + export interface GitHubTree { + sha: string; + url: string; + tree: Array<{ + path: string; + mode: string; + type: string; + size?: number; + sha: string; + url: string; + }>; + truncated: boolean; + } + + export interface GitHubCommit { + sha: string; + node_id: string; + url: string; + author: GitHubAuthor; + committer: GitHubAuthor; + message: string; + tree: { + sha: string; + url: string; + }; + parents: Array<{ + sha: string; + url: string; + }>; + } + + export interface GitHubReference { + ref: string; + node_id: string; + url: string; + object: { + sha: string; + type: string; + url: string; + }; + } + + export interface GitHubRepository { + id: number; + node_id: string; + name: string; + full_name: string; + private: boolean; + owner: { + login: string; + id: number; + node_id: string; + avatar_url: string; + url: string; + html_url: string; + type: string; + }; + html_url: string; + description: string | null; + fork: boolean; + url: string; + created_at: string; + updated_at: string; + pushed_at: string; + git_url: string; + ssh_url: string; + clone_url: string; + default_branch: string; + } + + export interface GitHubSearchResponse { + total_count: number; + incomplete_results: boolean; + items: GitHubRepository[]; + } + + // Request Types + export interface CreateRepositoryOptions { + name?: string; + description?: string; + private?: boolean; + auto_init?: boolean; + } + + export interface CreateTreeParams { + path: string; + mode: '100644' | '100755' | '040000' | '160000' | '120000'; + type: 'blob' | 'tree' | 'commit'; + content?: string; + sha?: string; + } + + export interface FileOperation { + path: string; + content: string; + } + +export interface GitHubIssue { + url: string; + repository_url: string; + labels_url: string; + comments_url: string; + events_url: string; + html_url: string; + id: number; + node_id: string; + number: number; + title: string; + user: { + login: string; + id: number; + avatar_url: string; + url: string; + html_url: string; + }; + labels: Array<{ + id: number; + node_id: string; + url: string; + name: string; + color: string; + default: boolean; + description?: string; + }>; + state: string; + locked: boolean; + assignee: null | { + login: string; + id: number; + avatar_url: string; + url: string; + html_url: string; + }; + assignees: Array<{ + login: string; + id: number; + avatar_url: string; + url: string; + html_url: string; + }>; + milestone: null | { + url: string; + html_url: string; + labels_url: string; + id: number; + node_id: string; + number: number; + title: string; + description: string; + state: string; + }; + comments: number; + created_at: string; + updated_at: string; + closed_at: string | null; + body: string; + } + + export interface CreateIssueOptions { + title: string; + body?: string; + assignees?: string[]; + milestone?: number; + labels?: string[]; + } + + export interface GitHubPullRequest { + url: string; + id: number; + node_id: string; + html_url: string; + diff_url: string; + patch_url: string; + issue_url: string; + number: number; + state: string; + locked: boolean; + title: string; + user: { + login: string; + id: number; + avatar_url: string; + url: string; + html_url: string; + }; + body: string; + created_at: string; + updated_at: string; + closed_at: string | null; + merged_at: string | null; + merge_commit_sha: string; + assignee: null | { + login: string; + id: number; + avatar_url: string; + url: string; + html_url: string; + }; + assignees: Array<{ + login: string; + id: number; + avatar_url: string; + url: string; + html_url: string; + }>; + head: { + label: string; + ref: string; + sha: string; + user: { + login: string; + id: number; + avatar_url: string; + url: string; + html_url: string; + }; + repo: GitHubRepository; + }; + base: { + label: string; + ref: string; + sha: string; + user: { + login: string; + id: number; + avatar_url: string; + url: string; + html_url: string; + }; + repo: GitHubRepository; + }; + } + + export interface CreatePullRequestOptions { + title: string; + body?: string; + head: string; + base: string; + maintainer_can_modify?: boolean; + draft?: boolean; + } + + export interface GitHubFork extends GitHubRepository { + // Fork specific fields + parent: { + name: string; + full_name: string; + owner: { + login: string; + id: number; + avatar_url: string; + }; + html_url: string; + }; + source: { + name: string; + full_name: string; + owner: { + login: string; + id: number; + avatar_url: string; + }; + html_url: string; + }; + } + + export interface CreateBranchOptions { + ref: string; // The name for the new branch + sha: string; // The SHA of the commit to branch from + } \ No newline at end of file diff --git a/src/github/package-lock.json b/src/github/package-lock.json new file mode 100644 index 000000000..3f8a6b146 --- /dev/null +++ b/src/github/package-lock.json @@ -0,0 +1,551 @@ +{ + "name": "@modelcontextprotocol/server-github", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@modelcontextprotocol/server-github", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "0.6.0", + "@types/node-fetch": "^2.6.12", + "node-fetch": "^3.3.2" + }, + "bin": { + "mcp-server-github": "dist/index.js" + }, + "devDependencies": { + "shx": "^0.3.4", + "typescript": "^5.6.2" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.0.tgz", + "integrity": "sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@types/node": { + "version": "22.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz", + "integrity": "sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/src/github/package.json b/src/github/package.json new file mode 100644 index 000000000..11fe17d6c --- /dev/null +++ b/src/github/package.json @@ -0,0 +1,30 @@ +{ + "name": "@modelcontextprotocol/server-github", + "version": "0.1.0", + "description": "MCP server for using the GitHub API", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/servers/issues", + "type": "module", + "bin": { + "mcp-server-github": "dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && shx chmod +x dist/*.js", + "prepare": "npm run build", + "watch": "tsc --watch" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "0.6.0", + "@types/node-fetch": "^2.6.12", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "shx": "^0.3.4", + "typescript": "^5.6.2" + } +} diff --git a/src/github/tsconfig.json b/src/github/tsconfig.json new file mode 100644 index 000000000..4d33cae1d --- /dev/null +++ b/src/github/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": [ + "./**/*.ts" + ] + } + \ No newline at end of file From 1b33f678da7b58b2330fcb4ae82aae27ef9b2ad9 Mon Sep 17 00:00:00 2001 From: Elie Schoppik Date: Wed, 20 Nov 2024 15:17:15 -0500 Subject: [PATCH 2/8] cleanup --- src/github/index.ts | 216 +++++++++++++++++++++----------------------- 1 file changed, 103 insertions(+), 113 deletions(-) diff --git a/src/github/index.ts b/src/github/index.ts index 784577b68..15c59df59 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -144,8 +144,12 @@ async function getDefaultBranchSHA( return data.object.sha; } -async function getFileContents(owner: string, repo: string, path: string): Promise { - const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`; +async function getFileContents(owner: string, repo: string, path: string, branch?: string): Promise { + let url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`; + if (branch) { + url += `?ref=${branch}`; + } + const response = await fetch(url, { headers: { "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, @@ -155,7 +159,8 @@ async function getFileContents(owner: string, repo: string, path: string): Promi }); if (!response.ok) { - throw new Error(`GitHub API error: ${response.statusText}`); + const errorData = await response.text(); + throw new Error(`GitHub API error (${response.status}): ${errorData}`); } const data = await response.json() as GitHubContent; @@ -230,14 +235,33 @@ async function createOrUpdateFile( path: string, content: string, message: string, + branch: string, sha?: string ): Promise { + // Properly encode content to base64 + const encodedContent = Buffer.from(content).toString('base64'); + + let currentSha = sha; + if (!currentSha) { + // Try to get current file SHA if it exists in the specified branch + try { + const existingFile = await getFileContents(owner, repo, path, branch); + if (!Array.isArray(existingFile)) { + currentSha = existingFile.sha; + } + } catch (error) { + // File doesn't exist in this branch, which is fine for creation + console.error('Note: File does not exist in branch, will create new file'); + } + } + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`; const body = { message, - content: Buffer.from(content).toString('base64'), - sha + content: encodedContent, + branch, + ...(currentSha ? { sha: currentSha } : {}) }; const response = await fetch(url, { @@ -252,7 +276,8 @@ async function createOrUpdateFile( }); if (!response.ok) { - throw new Error(`GitHub API error: ${response.statusText}`); + const errorData = await response.text(); + throw new Error(`GitHub API error (${response.status}): ${errorData}`); } return await response.json() as GitHubCreateUpdateFileResponse; @@ -434,6 +459,44 @@ async function createRepository(options: CreateRepositoryOptions): Promise { return { tools: [ + { + name: "create_or_update_file", + description: "Create or update a single file in a GitHub repository", + inputSchema: { + type: "object", + properties: { + owner: { + type: "string", + description: "Repository owner (username or organization)" + }, + repo: { + type: "string", + description: "Repository name" + }, + path: { + type: "string", + description: "Path where to create/update the file" + }, + content: { + type: "string", + description: "Content of the file" + }, + message: { + type: "string", + description: "Commit message" + }, + branch: { + type: "string", + description: "Branch to create/update the file in" + }, + sha: { + type: "string", + description: "SHA of the file being replaced (required when updating existing files)" + } + }, + required: ["owner", "repo", "path", "content", "message", "branch"] + } + }, { name: "search_repositories", description: "Search for GitHub repositories", @@ -504,40 +567,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { required: ["owner", "repo", "path"] } }, - { - name: "create_or_update_file", - description: "Create or update a single file in a GitHub repository", - inputSchema: { - type: "object", - properties: { - owner: { - type: "string", - description: "Repository owner (username or organization)" - }, - repo: { - type: "string", - description: "Repository name" - }, - path: { - type: "string", - description: "Path where to create/update the file" - }, - content: { - type: "string", - description: "Content of the file" - }, - message: { - type: "string", - description: "Commit message" - }, - sha: { - type: "string", - description: "SHA of the file being replaced (required when updating existing files)" - } - }, - required: ["owner", "repo", "path", "content", "message"] - } - }, { name: "push_files", description: "Push multiple files to a GitHub repository in a single commit", @@ -582,37 +611,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { required: ["owner", "repo", "branch", "files", "message"] } }, - { - name: "open_in_browser", - description: "Open a GitHub repository, file, issue, or pull request in the browser", - inputSchema: { - type: "object", - properties: { - owner: { - type: "string", - description: "Repository owner (username or organization)" - }, - repo: { - type: "string", - description: "Repository name" - }, - type: { - type: "string", - enum: ["repository", "file", "issue", "pull_request"], - description: "Type of resource to open" - }, - path: { - type: "string", - description: "Path to the file (only for type='file')" - }, - number: { - type: "number", - description: "Issue or PR number (only for type='issue' or type='pull_request')" - } - }, - required: ["owner", "repo", "type"] - } - }, { name: "create_issue", description: "Create a new issue in a GitHub repository", @@ -826,30 +824,56 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } if (request.params.name === "get_file_contents") { - const { owner, repo, path } = request.params.arguments as { + if (!request.params.arguments) { + throw new Error("Arguments are required"); + } + + const args = request.params.arguments as { owner: string; repo: string; path: string; + branch?: string; }; - - const contents = await getFileContents(owner, repo, path); + + const contents = await getFileContents(args.owner, args.repo, args.path, args.branch); return { toolResult: contents }; } - + if (request.params.name === "create_or_update_file") { - const { owner, repo, path, content, message, sha } = request.params.arguments as { + if (!request.params.arguments) { + throw new Error("Arguments are required"); + } + + const args = request.params.arguments as { owner: string; repo: string; path: string; content: string; message: string; + branch: string; sha?: string; }; - - const result = await createOrUpdateFile(owner, repo, path, content, message, sha); - return { toolResult: result }; + + try { + const result = await createOrUpdateFile( + args.owner, + args.repo, + args.path, + args.content, + args.message, + args.branch, + args.sha + ); + return { toolResult: result }; + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to create/update file: ${error.message}`); + } + throw error; + } } + if (request.params.name === "push_files") { const { owner, repo, branch, files, message } = request.params.arguments as { owner: string; @@ -863,40 +887,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { toolResult: result }; } - -if (request.params.name === "open_in_browser") { - const { owner, repo, type, path, number } = request.params.arguments as { - owner: string; - repo: string; - type: "repository" | "file" | "issue" | "pull_request"; - path?: string; - number?: number; - }; - - let url: string; - switch (type) { - case "repository": - url = `https://github.com/${owner}/${repo}`; - break; - case "file": - if (!path) throw new Error("Path is required for file URLs"); - url = `https://github.com/${owner}/${repo}/blob/main/${path}`; - break; - case "issue": - if (!number) throw new Error("Number is required for issue URLs"); - url = `https://github.com/${owner}/${repo}/issues/${number}`; - break; - case "pull_request": - if (!number) throw new Error("Number is required for pull request URLs"); - url = `https://github.com/${owner}/${repo}/pull/${number}`; - break; - default: - throw new Error(`Invalid type: ${type}`); - } - - return { toolResult: { url } }; -} - if (request.params.name === "create_issue") { if (!request.params.arguments) { throw new Error("Arguments are required"); From f1e334366c5fca3df38b88586f55f11ff6d30e07 Mon Sep 17 00:00:00 2001 From: Elie Schoppik Date: Wed, 20 Nov 2024 15:29:45 -0500 Subject: [PATCH 3/8] Adding README.md --- src/github/README.md | 137 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/github/README.md diff --git a/src/github/README.md b/src/github/README.md new file mode 100644 index 000000000..20d87919b --- /dev/null +++ b/src/github/README.md @@ -0,0 +1,137 @@ +# GitHub MCP Server + +MCP Server for the GitHub API, enabling file operations, repository management, and more. + +## Tools + +1. `create_or_update_file` + - Create or update a single file in a repository + - Inputs: + - `owner` (string): Repository owner (username or organization) + - `repo` (string): Repository name + - `path` (string): Path where to create/update the file + - `content` (string): Content of the file + - `message` (string): Commit message + - `branch` (string): Branch to create/update the file in + - `sha` (optional string): SHA of file being replaced (for updates) + - Returns: File content and commit details + +2. `push_files` + - Push multiple files in a single commit + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `branch` (string): Branch to push to + - `files` (array): Files to push, each with `path` and `content` + - `message` (string): Commit message + - Returns: Updated branch reference + +3. `search_repositories` + - Search for GitHub repositories + - Inputs: + - `query` (string): Search query + - `page` (optional number): Page number for pagination + - `perPage` (optional number): Results per page (max 100) + - Returns: Repository search results + +4. `create_repository` + - Create a new GitHub repository + - Inputs: + - `name` (string): Repository name + - `description` (optional string): Repository description + - `private` (optional boolean): Whether repo should be private + - `autoInit` (optional boolean): Initialize with README + - Returns: Created repository details + +5. `get_file_contents` + - Get contents of a file or directory + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `path` (string): Path to file/directory + - `branch` (optional string): Branch to get contents from + - Returns: File/directory contents + +6. `create_issue` + - Create a new issue + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `title` (string): Issue title + - `body` (optional string): Issue description + - `assignees` (optional string[]): Usernames to assign + - `labels` (optional string[]): Labels to add + - `milestone` (optional number): Milestone number + - Returns: Created issue details + +7. `create_pull_request` + - Create a new pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `title` (string): PR title + - `body` (optional string): PR description + - `head` (string): Branch containing changes + - `base` (string): Branch to merge into + - `draft` (optional boolean): Create as draft PR + - `maintainer_can_modify` (optional boolean): Allow maintainer edits + - Returns: Created pull request details + +8. `fork_repository` + - Fork a repository + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `organization` (optional string): Organization to fork to + - Returns: Forked repository details + +9. `create_branch` + - Create a new branch + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `branch` (string): Name for new branch + - `from_branch` (optional string): Source branch (defaults to repo default) + - Returns: Created branch reference + +## Setup + +1. Create a GitHub Personal Access Token with appropriate permissions: + - Go to GitHub Settings > Developer settings > Personal access tokens + - Create a token with required permissions (e.g., repo, workflow) + - Copy the generated token + +2. To use this with Claude Desktop, add the following to your `claude_desktop_config.json`: + ```json + { + "mcp-server-github": { + "command": "mcp-server-github", + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "" + } + } + } + ``` + +## Features + +- **Automatic Branch Creation**: When creating/updating files or pushing changes, branches are automatically created if they don't exist +- **Comprehensive Error Handling**: Clear error messages for common issues +- **Git History Preservation**: Operations maintain proper Git history without force pushing +- **Batch Operations**: Support for both single-file and multi-file operations + +## Error Handling + +The server provides detailed error messages for common scenarios: +- Branch doesn't exist +- File not found +- Authentication issues +- API rate limiting +- Invalid input parameters + +## Limitations + +- Maximum file size limit of 100MB (GitHub limitation) +- API rate limits apply based on your GitHub account +- Some operations may require specific repository permissions +- Binary files must be base64 encoded From cc8495407fb30b0a3f6cad39b0dfb431bbf19777 Mon Sep 17 00:00:00 2001 From: Elie Schoppik Date: Wed, 20 Nov 2024 15:42:04 -0500 Subject: [PATCH 4/8] updating lockfile --- package-lock.json | 115 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/package-lock.json b/package-lock.json index 6cb7c9c4b..657e40fb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,10 @@ "resolved": "src/gdrive", "link": true }, + "node_modules/@modelcontextprotocol/server-github": { + "resolved": "src/github", + "link": true + }, "node_modules/@modelcontextprotocol/server-google-maps": { "resolved": "src/google-maps", "link": true @@ -1001,6 +1005,28 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -1032,6 +1058,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1727,6 +1764,24 @@ "node": ">= 0.4.0" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -2774,6 +2829,14 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -2931,6 +2994,58 @@ "typescript": "^5.6.2" } }, + "src/github": { + "name": "@modelcontextprotocol/server-github", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "0.6.0", + "@types/node-fetch": "^2.6.12", + "node-fetch": "^3.3.2" + }, + "bin": { + "mcp-server-github": "dist/index.js" + }, + "devDependencies": { + "shx": "^0.3.4", + "typescript": "^5.6.2" + } + }, + "src/github/node_modules/@modelcontextprotocol/sdk": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.0.tgz", + "integrity": "sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "src/github/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "src/github/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "src/google-maps": { "name": "@modelcontextprotocol/server-google-maps", "version": "0.1.0", From 1a45f95a7c2b26a355128b2525c86e14e6351809 Mon Sep 17 00:00:00 2001 From: Mahesh Murag Date: Wed, 20 Nov 2024 23:54:39 -0500 Subject: [PATCH 5/8] Updated Github --- src/github/README.md | 42 +++++++++++++++--------------------------- src/github/index.ts | 4 ---- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/github/README.md b/src/github/README.md index 20d87919b..3a2c99163 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -2,6 +2,14 @@ MCP Server for the GitHub API, enabling file operations, repository management, and more. +### Features + +- **Automatic Branch Creation**: When creating/updating files or pushing changes, branches are automatically created if they don't exist +- **Comprehensive Error Handling**: Clear error messages for common issues +- **Git History Preservation**: Operations maintain proper Git history without force pushing +- **Batch Operations**: Support for both single-file and multi-file operations + + ## Tools 1. `create_or_update_file` @@ -96,12 +104,15 @@ MCP Server for the GitHub API, enabling file operations, repository management, ## Setup -1. Create a GitHub Personal Access Token with appropriate permissions: - - Go to GitHub Settings > Developer settings > Personal access tokens - - Create a token with required permissions (e.g., repo, workflow) +### Personal Access Token +[Create a GitHub Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with appropriate permissions: + - Go to [Personal access tokens](https://github.com/settings/tokens) (in GitHub Settings > Developer settings) + - Create a token with the `repo` scope ("Full control of private repositories") + - Alternatively, if working only with public repositories, select only the `public_repo` scope - Copy the generated token -2. To use this with Claude Desktop, add the following to your `claude_desktop_config.json`: +### Usage with Claude Desktop +To use this with Claude Desktop, add the following to your `claude_desktop_config.json`: ```json { "mcp-server-github": { @@ -112,26 +123,3 @@ MCP Server for the GitHub API, enabling file operations, repository management, } } ``` - -## Features - -- **Automatic Branch Creation**: When creating/updating files or pushing changes, branches are automatically created if they don't exist -- **Comprehensive Error Handling**: Clear error messages for common issues -- **Git History Preservation**: Operations maintain proper Git history without force pushing -- **Batch Operations**: Support for both single-file and multi-file operations - -## Error Handling - -The server provides detailed error messages for common scenarios: -- Branch doesn't exist -- File not found -- Authentication issues -- API rate limiting -- Invalid input parameters - -## Limitations - -- Maximum file size limit of 100MB (GitHub limitation) -- API rate limits apply based on your GitHub account -- Some operations may require specific repository permissions -- Binary files must be base64 encoded diff --git a/src/github/index.ts b/src/github/index.ts index 15c59df59..1ae76b8b4 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -42,10 +42,6 @@ if (!GITHUB_PERSONAL_ACCESS_TOKEN) { process.exit(1); } -// GitHub API helper functions - -// Add these helper functions to your existing code - async function forkRepository( owner: string, repo: string, From c60d4564f244dfa63106fc4b1e8117eb84f097ac Mon Sep 17 00:00:00 2001 From: Mahesh Murag Date: Wed, 20 Nov 2024 23:57:29 -0500 Subject: [PATCH 6/8] Updated Github --- src/github/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/github/README.md b/src/github/README.md index 3a2c99163..aedba32b8 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -107,6 +107,7 @@ MCP Server for the GitHub API, enabling file operations, repository management, ### Personal Access Token [Create a GitHub Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with appropriate permissions: - Go to [Personal access tokens](https://github.com/settings/tokens) (in GitHub Settings > Developer settings) + - Select which repositories you'd like this token to have access to (Public, All, or Select) - Create a token with the `repo` scope ("Full control of private repositories") - Alternatively, if working only with public repositories, select only the `public_repo` scope - Copy the generated token From 859c7b8520ba070b0abc15e0a7147f0d37e85eb7 Mon Sep 17 00:00:00 2001 From: Mahesh Murag Date: Thu, 21 Nov 2024 00:18:09 -0500 Subject: [PATCH 7/8] Updated Github to Zod --- .gitignore | 4 +- src/github/index.ts | 637 +++++++++++---------------------------- src/github/interfaces.ts | 332 -------------------- src/github/schemas.ts | 378 +++++++++++++++++++++++ 4 files changed, 553 insertions(+), 798 deletions(-) delete mode 100644 src/github/interfaces.ts create mode 100644 src/github/schemas.ts diff --git a/.gitignore b/.gitignore index 38ff4cd07..7ecb7109b 100644 --- a/.gitignore +++ b/.gitignore @@ -290,9 +290,11 @@ dmypy.json # Cython debug symbols cython_debug/ +.DS_Store + # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ diff --git a/src/github/index.ts b/src/github/index.ts index 1ae76b8b4..0676a34c8 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -8,23 +8,43 @@ import { } from "@modelcontextprotocol/sdk/types.js"; import fetch from "node-fetch"; import { - GitHubContent, - GitHubCreateUpdateFileResponse, - GitHubSearchResponse, - GitHubRepository, - GitHubTree, - GitHubCommit, - GitHubReference, - CreateRepositoryOptions, - FileOperation, - CreateTreeParams, - GitHubPullRequest, - CreateIssueOptions, - CreatePullRequestOptions, - GitHubIssue, - GitHubFork, - CreateBranchOptions, -} from './interfaces.js'; + GitHubForkSchema, + GitHubReferenceSchema, + GitHubRepositorySchema, + GitHubIssueSchema, + GitHubPullRequestSchema, + GitHubContentSchema, + GitHubCreateUpdateFileResponseSchema, + GitHubSearchResponseSchema, + GitHubTreeSchema, + GitHubCommitSchema, + CreateRepositoryOptionsSchema, + CreateIssueOptionsSchema, + CreatePullRequestOptionsSchema, + CreateBranchOptionsSchema, + type GitHubFork, + type GitHubReference, + type GitHubRepository, + type GitHubIssue, + type GitHubPullRequest, + type GitHubContent, + type GitHubCreateUpdateFileResponse, + type GitHubSearchResponse, + type GitHubTree, + type GitHubCommit, + type FileOperation, + CreateOrUpdateFileSchema, + SearchRepositoriesSchema, + CreateRepositorySchema, + GetFileContentsSchema, + PushFilesSchema, + CreateIssueSchema, + CreatePullRequestSchema, + ForkRepositorySchema, + CreateBranchSchema +} from './schemas.js'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; const server = new Server({ name: "github-mcp-server", @@ -64,15 +84,14 @@ async function forkRepository( throw new Error(`GitHub API error: ${response.statusText}`); } - return await response.json() as GitHubFork; + return GitHubForkSchema.parse(await response.json()); } async function createBranch( owner: string, repo: string, - options: CreateBranchOptions + options: z.infer ): Promise { - // The ref needs to be in the format "refs/heads/branch-name" const fullRef = `refs/heads/${options.ref}`; const response = await fetch( @@ -96,10 +115,9 @@ async function createBranch( throw new Error(`GitHub API error: ${response.statusText}`); } - return await response.json() as GitHubReference; + return GitHubReferenceSchema.parse(await response.json()); } -// Helper function to get the default branch SHA async function getDefaultBranchSHA( owner: string, repo: string @@ -115,7 +133,6 @@ async function getDefaultBranchSHA( } ); - // If main branch doesn't exist, try master if (!response.ok) { const masterResponse = await fetch( `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/master`, @@ -132,15 +149,20 @@ async function getDefaultBranchSHA( throw new Error("Could not find default branch (tried 'main' and 'master')"); } - const data = await masterResponse.json() as GitHubReference; + const data = GitHubReferenceSchema.parse(await masterResponse.json()); return data.object.sha; } - const data = await response.json() as GitHubReference; + const data = GitHubReferenceSchema.parse(await response.json()); return data.object.sha; } -async function getFileContents(owner: string, repo: string, path: string, branch?: string): Promise { +async function getFileContents( + owner: string, + repo: string, + path: string, + branch?: string +): Promise { let url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`; if (branch) { url += `?ref=${branch}`; @@ -155,28 +177,23 @@ async function getFileContents(owner: string, repo: string, path: string, branch }); if (!response.ok) { - const errorData = await response.text(); - throw new Error(`GitHub API error (${response.status}): ${errorData}`); + throw new Error(`GitHub API error: ${response.statusText}`); } - const data = await response.json() as GitHubContent; - + const data = GitHubContentSchema.parse(await response.json()); + // If it's a file, decode the content if (!Array.isArray(data) && data.content) { - return { - ...data, - content: Buffer.from(data.content, 'base64').toString('utf8') - }; + data.content = Buffer.from(data.content, 'base64').toString('utf8'); } return data; } - async function createIssue( owner: string, repo: string, - options: CreateIssueOptions + options: z.infer ): Promise { const response = await fetch( `https://api.github.com/repos/${owner}/${repo}/issues`, @@ -196,13 +213,13 @@ async function createIssue( throw new Error(`GitHub API error: ${response.statusText}`); } - return await response.json() as GitHubIssue; + return GitHubIssueSchema.parse(await response.json()); } async function createPullRequest( owner: string, repo: string, - options: CreatePullRequestOptions + options: z.infer ): Promise { const response = await fetch( `https://api.github.com/repos/${owner}/${repo}/pulls`, @@ -222,7 +239,7 @@ async function createPullRequest( throw new Error(`GitHub API error: ${response.statusText}`); } - return await response.json() as GitHubPullRequest; + return GitHubPullRequestSchema.parse(await response.json()); } async function createOrUpdateFile( @@ -234,19 +251,16 @@ async function createOrUpdateFile( branch: string, sha?: string ): Promise { - // Properly encode content to base64 const encodedContent = Buffer.from(content).toString('base64'); let currentSha = sha; if (!currentSha) { - // Try to get current file SHA if it exists in the specified branch try { const existingFile = await getFileContents(owner, repo, path, branch); if (!Array.isArray(existingFile)) { currentSha = existingFile.sha; } } catch (error) { - // File doesn't exist in this branch, which is fine for creation console.error('Note: File does not exist in branch, will create new file'); } } @@ -272,11 +286,10 @@ async function createOrUpdateFile( }); if (!response.ok) { - const errorData = await response.text(); - throw new Error(`GitHub API error (${response.status}): ${errorData}`); + throw new Error(`GitHub API error: ${response.statusText}`); } - return await response.json() as GitHubCreateUpdateFileResponse; + return GitHubCreateUpdateFileResponseSchema.parse(await response.json()); } async function createTree( @@ -285,10 +298,10 @@ async function createTree( files: FileOperation[], baseTree?: string ): Promise { - const tree: CreateTreeParams[] = files.map(file => ({ + const tree = files.map(file => ({ path: file.path, - mode: '100644', - type: 'blob', + mode: '100644' as const, + type: 'blob' as const, content: file.content })); @@ -313,7 +326,7 @@ async function createTree( throw new Error(`GitHub API error: ${response.statusText}`); } - return await response.json() as GitHubTree; + return GitHubTreeSchema.parse(await response.json()); } async function createCommit( @@ -345,7 +358,7 @@ async function createCommit( throw new Error(`GitHub API error: ${response.statusText}`); } - return await response.json() as GitHubCommit; + return GitHubCommitSchema.parse(await response.json()); } async function updateReference( @@ -375,7 +388,7 @@ async function updateReference( throw new Error(`GitHub API error: ${response.statusText}`); } - return await response.json() as GitHubReference; + return GitHubReferenceSchema.parse(await response.json()); } async function pushFiles( @@ -400,7 +413,7 @@ async function pushFiles( throw new Error(`GitHub API error: ${refResponse.statusText}`); } - const ref = await refResponse.json() as GitHubReference; + const ref = GitHubReferenceSchema.parse(await refResponse.json()); const commitSha = ref.object.sha; const tree = await createTree(owner, repo, files, commitSha); @@ -430,10 +443,12 @@ async function searchRepositories( throw new Error(`GitHub API error: ${response.statusText}`); } - return await response.json() as GitHubSearchResponse; + return GitHubSearchResponseSchema.parse(await response.json()); } -async function createRepository(options: CreateRepositoryOptions): Promise { +async function createRepository( + options: z.infer +): Promise { const response = await fetch("https://api.github.com/user/repos", { method: "POST", headers: { @@ -449,7 +464,7 @@ async function createRepository(options: CreateRepositoryOptions): Promise { @@ -458,473 +473,165 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { { name: "create_or_update_file", description: "Create or update a single file in a GitHub repository", - inputSchema: { - type: "object", - properties: { - owner: { - type: "string", - description: "Repository owner (username or organization)" - }, - repo: { - type: "string", - description: "Repository name" - }, - path: { - type: "string", - description: "Path where to create/update the file" - }, - content: { - type: "string", - description: "Content of the file" - }, - message: { - type: "string", - description: "Commit message" - }, - branch: { - type: "string", - description: "Branch to create/update the file in" - }, - sha: { - type: "string", - description: "SHA of the file being replaced (required when updating existing files)" - } - }, - required: ["owner", "repo", "path", "content", "message", "branch"] - } - }, + inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema) + }, { name: "search_repositories", description: "Search for GitHub repositories", - inputSchema: { - type: "object", - properties: { - query: { - type: "string", - description: "Search query (see GitHub search syntax)" - }, - page: { - type: "number", - description: "Page number for pagination (default: 1)" - }, - perPage: { - type: "number", - description: "Number of results per page (default: 30, max: 100)" - } - }, - required: ["query"] - } + inputSchema: zodToJsonSchema(SearchRepositoriesSchema) }, { name: "create_repository", description: "Create a new GitHub repository in your account", - inputSchema: { - type: "object", - properties: { - name: { - type: "string", - description: "Repository name" - }, - description: { - type: "string", - description: "Repository description" - }, - private: { - type: "boolean", - description: "Whether the repository should be private" - }, - autoInit: { - type: "boolean", - description: "Initialize with README.md" - } - }, - required: ["name"] - } + inputSchema: zodToJsonSchema(CreateRepositorySchema) }, { name: "get_file_contents", description: "Get the contents of a file or directory from a GitHub repository", - inputSchema: { - type: "object", - properties: { - owner: { - type: "string", - description: "Repository owner (username or organization)" - }, - repo: { - type: "string", - description: "Repository name" - }, - path: { - type: "string", - description: "Path to the file or directory" - } - }, - required: ["owner", "repo", "path"] - } + inputSchema: zodToJsonSchema(GetFileContentsSchema) }, { name: "push_files", description: "Push multiple files to a GitHub repository in a single commit", - inputSchema: { - type: "object", - properties: { - owner: { - type: "string", - description: "Repository owner (username or organization)" - }, - repo: { - type: "string", - description: "Repository name" - }, - branch: { - type: "string", - description: "Branch to push to (e.g., 'main' or 'master')" - }, - files: { - type: "array", - description: "Array of files to push", - items: { - type: "object", - properties: { - path: { - type: "string", - description: "Path where to create the file" - }, - content: { - type: "string", - description: "Content of the file" - } - }, - required: ["path", "content"] - } - }, - message: { - type: "string", - description: "Commit message" - } - }, - required: ["owner", "repo", "branch", "files", "message"] - } + inputSchema: zodToJsonSchema(PushFilesSchema) }, { name: "create_issue", description: "Create a new issue in a GitHub repository", - inputSchema: { - type: "object", - properties: { - owner: { - type: "string", - description: "Repository owner (username or organization)" - }, - repo: { - type: "string", - description: "Repository name" - }, - title: { - type: "string", - description: "Issue title" - }, - body: { - type: "string", - description: "Issue body/description" - }, - assignees: { - type: "array", - items: { type: "string" }, - description: "Array of usernames to assign" - }, - labels: { - type: "array", - items: { type: "string" }, - description: "Array of label names" - }, - milestone: { - type: "number", - description: "Milestone number to assign" - } - }, - required: ["owner", "repo", "title"] - } + inputSchema: zodToJsonSchema(CreateIssueSchema) }, { name: "create_pull_request", description: "Create a new pull request in a GitHub repository", - inputSchema: { - type: "object", - properties: { - owner: { - type: "string", - description: "Repository owner (username or organization)" - }, - repo: { - type: "string", - description: "Repository name" - }, - title: { - type: "string", - description: "Pull request title" - }, - body: { - type: "string", - description: "Pull request body/description" - }, - head: { - type: "string", - description: "The name of the branch where your changes are implemented" - }, - base: { - type: "string", - description: "The name of the branch you want the changes pulled into" - }, - draft: { - type: "boolean", - description: "Whether to create the pull request as a draft" - }, - maintainer_can_modify: { - type: "boolean", - description: "Whether maintainers can modify the pull request" - } - }, - required: ["owner", "repo", "title", "head", "base"] - } + inputSchema: zodToJsonSchema(CreatePullRequestSchema) }, { name: "fork_repository", description: "Fork a GitHub repository to your account or specified organization", - inputSchema: { - type: "object", - properties: { - owner: { - type: "string", - description: "Repository owner (username or organization)" - }, - repo: { - type: "string", - description: "Repository name" - }, - organization: { - type: "string", - description: "Optional: organization to fork to (defaults to your personal account)" - } - }, - required: ["owner", "repo"] - } + inputSchema: zodToJsonSchema(ForkRepositorySchema) }, { name: "create_branch", description: "Create a new branch in a GitHub repository", - inputSchema: { - type: "object", - properties: { - owner: { - type: "string", - description: "Repository owner (username or organization)" - }, - repo: { - type: "string", - description: "Repository name" - }, - branch: { - type: "string", - description: "Name for the new branch" - }, - from_branch: { - type: "string", - description: "Optional: source branch to create from (defaults to the repository's default branch)" - } - }, - required: ["owner", "repo", "branch"] - } + inputSchema: zodToJsonSchema(CreateBranchSchema) } ] }; - }); server.setRequestHandler(CallToolRequestSchema, async (request) => { - - if (request.params.name === "fork_repository") { + try { if (!request.params.arguments) { throw new Error("Arguments are required"); } - const args = request.params.arguments as { - owner: string; - repo: string; - organization?: string; - }; - - const fork = await forkRepository(args.owner, args.repo, args.organization); - return { toolResult: fork }; - } + switch (request.params.name) { + case "fork_repository": { + const args = ForkRepositorySchema.parse(request.params.arguments); + const fork = await forkRepository(args.owner, args.repo, args.organization); + return { toolResult: fork }; + } - if (request.params.name === "create_branch") { - if (!request.params.arguments) { - throw new Error("Arguments are required"); - } + case "create_branch": { + const args = CreateBranchSchema.parse(request.params.arguments); + let sha: string; + if (args.from_branch) { + const response = await fetch( + `https://api.github.com/repos/${args.owner}/${args.repo}/git/refs/heads/${args.from_branch}`, + { + headers: { + "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + "Accept": "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server" + } + } + ); - const args = request.params.arguments as { - owner: string; - repo: string; - branch: string; - from_branch?: string; - }; - - // If no source branch is specified, use the default branch - let sha: string; - if (args.from_branch) { - const response = await fetch( - `https://api.github.com/repos/${args.owner}/${args.repo}/git/refs/heads/${args.from_branch}`, - { - headers: { - "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, - "Accept": "application/vnd.github.v3+json", - "User-Agent": "github-mcp-server" + if (!response.ok) { + throw new Error(`Source branch '${args.from_branch}' not found`); } - } - ); - - if (!response.ok) { - throw new Error(`Source branch '${args.from_branch}' not found`); - } - const data = await response.json() as GitHubReference; - sha = data.object.sha; - } else { - sha = await getDefaultBranchSHA(args.owner, args.repo); - } - - const branch = await createBranch(args.owner, args.repo, { - ref: args.branch, - sha: sha - }); + const data = GitHubReferenceSchema.parse(await response.json()); + sha = data.object.sha; + } else { + sha = await getDefaultBranchSHA(args.owner, args.repo); + } - return { toolResult: branch }; - } - if (request.params.name === "search_repositories") { - const { query, page, perPage } = request.params.arguments as { - query: string; - page?: number; - perPage?: number; - }; - - const results = await searchRepositories(query, page, perPage); - return { toolResult: results }; - } + const branch = await createBranch(args.owner, args.repo, { + ref: args.branch, + sha + }); - if (request.params.name === "create_repository") { - const options = request.params.arguments as CreateRepositoryOptions; - const repository = await createRepository(options); - return { toolResult: repository }; - } + return { toolResult: branch }; + } - if (request.params.name === "get_file_contents") { - if (!request.params.arguments) { - throw new Error("Arguments are required"); - } - - const args = request.params.arguments as { - owner: string; - repo: string; - path: string; - branch?: string; - }; - - const contents = await getFileContents(args.owner, args.repo, args.path, args.branch); - return { toolResult: contents }; - } - - if (request.params.name === "create_or_update_file") { - if (!request.params.arguments) { - throw new Error("Arguments are required"); - } - - const args = request.params.arguments as { - owner: string; - repo: string; - path: string; - content: string; - message: string; - branch: string; - sha?: string; - }; - - try { - const result = await createOrUpdateFile( - args.owner, - args.repo, - args.path, - args.content, - args.message, - args.branch, - args.sha - ); - return { toolResult: result }; - } catch (error) { - if (error instanceof Error) { - throw new Error(`Failed to create/update file: ${error.message}`); + case "search_repositories": { + const args = SearchRepositoriesSchema.parse(request.params.arguments); + const results = await searchRepositories(args.query, args.page, args.perPage); + return { toolResult: results }; } - throw error; - } - } + case "create_repository": { + const args = CreateRepositorySchema.parse(request.params.arguments); + const repository = await createRepository(args); + return { toolResult: repository }; + } - if (request.params.name === "push_files") { - const { owner, repo, branch, files, message } = request.params.arguments as { - owner: string; - repo: string; - branch: string; - files: FileOperation[]; - message: string; - }; + case "get_file_contents": { + const args = GetFileContentsSchema.parse(request.params.arguments); + const contents = await getFileContents(args.owner, args.repo, args.path, args.branch); + return { toolResult: contents }; + } - const result = await pushFiles(owner, repo, branch, files, message); - return { toolResult: result }; - } + case "create_or_update_file": { + const args = CreateOrUpdateFileSchema.parse(request.params.arguments); + const result = await createOrUpdateFile( + args.owner, + args.repo, + args.path, + args.content, + args.message, + args.branch, + args.sha + ); + return { toolResult: result }; + } -if (request.params.name === "create_issue") { - if (!request.params.arguments) { - throw new Error("Arguments are required"); - } + case "push_files": { + const args = PushFilesSchema.parse(request.params.arguments); + const result = await pushFiles( + args.owner, + args.repo, + args.branch, + args.files, + args.message + ); + return { toolResult: result }; + } - const args = request.params.arguments as { - owner: string; - repo: string; - title: string; - body?: string; - assignees?: string[]; - milestone?: number; - labels?: string[]; - }; + case "create_issue": { + const args = CreateIssueSchema.parse(request.params.arguments); + const { owner, repo, ...options } = args; + const issue = await createIssue(owner, repo, options); + return { toolResult: issue }; + } - const { owner, repo, ...options } = args; - const issue = await createIssue(owner, repo, options); - return { toolResult: issue }; -} + case "create_pull_request": { + const args = CreatePullRequestSchema.parse(request.params.arguments); + const { owner, repo, ...options } = args; + const pullRequest = await createPullRequest(owner, repo, options); + return { toolResult: pullRequest }; + } -if (request.params.name === "create_pull_request") { - if (!request.params.arguments) { - throw new Error("Arguments are required"); + default: + throw new Error(`Unknown tool: ${request.params.name}`); + } + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error(`Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`); + } + throw error; } - - const args = request.params.arguments as { - owner: string; - repo: string; - title: string; - body?: string; - head: string; - base: string; - maintainer_can_modify?: boolean; - draft?: boolean; - }; - - const { owner, repo, ...options } = args; - const pullRequest = await createPullRequest(owner, repo, options); - return { toolResult: pullRequest }; -} - - throw new Error("Tool not found"); }); async function runServer() { diff --git a/src/github/interfaces.ts b/src/github/interfaces.ts deleted file mode 100644 index ce3518e66..000000000 --- a/src/github/interfaces.ts +++ /dev/null @@ -1,332 +0,0 @@ -// GitHub API Response Types -export interface GitHubErrorResponse { - message: string; - documentation_url?: string; - } - - export interface GitHubFileContent { - type: string; - encoding: string; - size: number; - name: string; - path: string; - content: string; - sha: string; - url: string; - git_url: string; - html_url: string; - download_url: string; - } - - export interface GitHubDirectoryContent { - type: string; - size: number; - name: string; - path: string; - sha: string; - url: string; - git_url: string; - html_url: string; - download_url: string | null; - } - - export type GitHubContent = GitHubFileContent | GitHubDirectoryContent[]; - - export interface GitHubCreateUpdateFileResponse { - content: GitHubFileContent | null; - commit: { - sha: string; - node_id: string; - url: string; - html_url: string; - author: GitHubAuthor; - committer: GitHubAuthor; - message: string; - tree: { - sha: string; - url: string; - }; - parents: Array<{ - sha: string; - url: string; - html_url: string; - }>; - }; - } - - export interface GitHubAuthor { - name: string; - email: string; - date: string; - } - - export interface GitHubTree { - sha: string; - url: string; - tree: Array<{ - path: string; - mode: string; - type: string; - size?: number; - sha: string; - url: string; - }>; - truncated: boolean; - } - - export interface GitHubCommit { - sha: string; - node_id: string; - url: string; - author: GitHubAuthor; - committer: GitHubAuthor; - message: string; - tree: { - sha: string; - url: string; - }; - parents: Array<{ - sha: string; - url: string; - }>; - } - - export interface GitHubReference { - ref: string; - node_id: string; - url: string; - object: { - sha: string; - type: string; - url: string; - }; - } - - export interface GitHubRepository { - id: number; - node_id: string; - name: string; - full_name: string; - private: boolean; - owner: { - login: string; - id: number; - node_id: string; - avatar_url: string; - url: string; - html_url: string; - type: string; - }; - html_url: string; - description: string | null; - fork: boolean; - url: string; - created_at: string; - updated_at: string; - pushed_at: string; - git_url: string; - ssh_url: string; - clone_url: string; - default_branch: string; - } - - export interface GitHubSearchResponse { - total_count: number; - incomplete_results: boolean; - items: GitHubRepository[]; - } - - // Request Types - export interface CreateRepositoryOptions { - name?: string; - description?: string; - private?: boolean; - auto_init?: boolean; - } - - export interface CreateTreeParams { - path: string; - mode: '100644' | '100755' | '040000' | '160000' | '120000'; - type: 'blob' | 'tree' | 'commit'; - content?: string; - sha?: string; - } - - export interface FileOperation { - path: string; - content: string; - } - -export interface GitHubIssue { - url: string; - repository_url: string; - labels_url: string; - comments_url: string; - events_url: string; - html_url: string; - id: number; - node_id: string; - number: number; - title: string; - user: { - login: string; - id: number; - avatar_url: string; - url: string; - html_url: string; - }; - labels: Array<{ - id: number; - node_id: string; - url: string; - name: string; - color: string; - default: boolean; - description?: string; - }>; - state: string; - locked: boolean; - assignee: null | { - login: string; - id: number; - avatar_url: string; - url: string; - html_url: string; - }; - assignees: Array<{ - login: string; - id: number; - avatar_url: string; - url: string; - html_url: string; - }>; - milestone: null | { - url: string; - html_url: string; - labels_url: string; - id: number; - node_id: string; - number: number; - title: string; - description: string; - state: string; - }; - comments: number; - created_at: string; - updated_at: string; - closed_at: string | null; - body: string; - } - - export interface CreateIssueOptions { - title: string; - body?: string; - assignees?: string[]; - milestone?: number; - labels?: string[]; - } - - export interface GitHubPullRequest { - url: string; - id: number; - node_id: string; - html_url: string; - diff_url: string; - patch_url: string; - issue_url: string; - number: number; - state: string; - locked: boolean; - title: string; - user: { - login: string; - id: number; - avatar_url: string; - url: string; - html_url: string; - }; - body: string; - created_at: string; - updated_at: string; - closed_at: string | null; - merged_at: string | null; - merge_commit_sha: string; - assignee: null | { - login: string; - id: number; - avatar_url: string; - url: string; - html_url: string; - }; - assignees: Array<{ - login: string; - id: number; - avatar_url: string; - url: string; - html_url: string; - }>; - head: { - label: string; - ref: string; - sha: string; - user: { - login: string; - id: number; - avatar_url: string; - url: string; - html_url: string; - }; - repo: GitHubRepository; - }; - base: { - label: string; - ref: string; - sha: string; - user: { - login: string; - id: number; - avatar_url: string; - url: string; - html_url: string; - }; - repo: GitHubRepository; - }; - } - - export interface CreatePullRequestOptions { - title: string; - body?: string; - head: string; - base: string; - maintainer_can_modify?: boolean; - draft?: boolean; - } - - export interface GitHubFork extends GitHubRepository { - // Fork specific fields - parent: { - name: string; - full_name: string; - owner: { - login: string; - id: number; - avatar_url: string; - }; - html_url: string; - }; - source: { - name: string; - full_name: string; - owner: { - login: string; - id: number; - avatar_url: string; - }; - html_url: string; - }; - } - - export interface CreateBranchOptions { - ref: string; // The name for the new branch - sha: string; // The SHA of the commit to branch from - } \ No newline at end of file diff --git a/src/github/schemas.ts b/src/github/schemas.ts new file mode 100644 index 000000000..213458eb1 --- /dev/null +++ b/src/github/schemas.ts @@ -0,0 +1,378 @@ +import { z } from 'zod'; + +// Base schemas for common types +export const GitHubAuthorSchema = z.object({ + name: z.string(), + email: z.string(), + date: z.string() +}); + +// Repository related schemas +export const GitHubOwnerSchema = z.object({ + login: z.string(), + id: z.number(), + node_id: z.string(), + avatar_url: z.string(), + url: z.string(), + html_url: z.string(), + type: z.string() +}); + +export const GitHubRepositorySchema = z.object({ + id: z.number(), + node_id: z.string(), + name: z.string(), + full_name: z.string(), + private: z.boolean(), + owner: GitHubOwnerSchema, + html_url: z.string(), + description: z.string().nullable(), + fork: z.boolean(), + url: z.string(), + created_at: z.string(), + updated_at: z.string(), + pushed_at: z.string(), + git_url: z.string(), + ssh_url: z.string(), + clone_url: z.string(), + default_branch: z.string() +}); + +// File content schemas +export const GitHubFileContentSchema = z.object({ + type: z.string(), + encoding: z.string(), + size: z.number(), + name: z.string(), + path: z.string(), + content: z.string(), + sha: z.string(), + url: z.string(), + git_url: z.string(), + html_url: z.string(), + download_url: z.string() +}); + +export const GitHubDirectoryContentSchema = z.object({ + type: z.string(), + size: z.number(), + name: z.string(), + path: z.string(), + sha: z.string(), + url: z.string(), + git_url: z.string(), + html_url: z.string(), + download_url: z.string().nullable() +}); + +export const GitHubContentSchema = z.union([ + GitHubFileContentSchema, + z.array(GitHubDirectoryContentSchema) +]); + +// Operation schemas +export const FileOperationSchema = z.object({ + path: z.string(), + content: z.string() +}); + +// Tree and commit schemas +export const GitHubTreeEntrySchema = z.object({ + path: z.string(), + mode: z.enum(['100644', '100755', '040000', '160000', '120000']), + type: z.enum(['blob', 'tree', 'commit']), + size: z.number().optional(), + sha: z.string(), + url: z.string() +}); + +export const GitHubTreeSchema = z.object({ + sha: z.string(), + url: z.string(), + tree: z.array(GitHubTreeEntrySchema), + truncated: z.boolean() +}); + +export const GitHubCommitSchema = z.object({ + sha: z.string(), + node_id: z.string(), + url: z.string(), + author: GitHubAuthorSchema, + committer: GitHubAuthorSchema, + message: z.string(), + tree: z.object({ + sha: z.string(), + url: z.string() + }), + parents: z.array(z.object({ + sha: z.string(), + url: z.string() + })) +}); + +// Reference schema +export const GitHubReferenceSchema = z.object({ + ref: z.string(), + node_id: z.string(), + url: z.string(), + object: z.object({ + sha: z.string(), + type: z.string(), + url: z.string() + }) +}); + +// Input schemas for operations +export const CreateRepositoryOptionsSchema = z.object({ + name: z.string(), + description: z.string().optional(), + private: z.boolean().optional(), + auto_init: z.boolean().optional() +}); + +export const CreateIssueOptionsSchema = z.object({ + title: z.string(), + body: z.string().optional(), + assignees: z.array(z.string()).optional(), + milestone: z.number().optional(), + labels: z.array(z.string()).optional() +}); + +export const CreatePullRequestOptionsSchema = z.object({ + title: z.string(), + body: z.string().optional(), + head: z.string(), + base: z.string(), + maintainer_can_modify: z.boolean().optional(), + draft: z.boolean().optional() +}); + +export const CreateBranchOptionsSchema = z.object({ + ref: z.string(), + sha: z.string() +}); + +// Response schemas for operations +export const GitHubCreateUpdateFileResponseSchema = z.object({ + content: GitHubFileContentSchema.nullable(), + commit: z.object({ + sha: z.string(), + node_id: z.string(), + url: z.string(), + html_url: z.string(), + author: GitHubAuthorSchema, + committer: GitHubAuthorSchema, + message: z.string(), + tree: z.object({ + sha: z.string(), + url: z.string() + }), + parents: z.array(z.object({ + sha: z.string(), + url: z.string(), + html_url: z.string() + })) + }) +}); + +export const GitHubSearchResponseSchema = z.object({ + total_count: z.number(), + incomplete_results: z.boolean(), + items: z.array(GitHubRepositorySchema) +}); + +// Fork related schemas +export const GitHubForkParentSchema = z.object({ + name: z.string(), + full_name: z.string(), + owner: z.object({ + login: z.string(), + id: z.number(), + avatar_url: z.string() + }), + html_url: z.string() +}); + +export const GitHubForkSchema = GitHubRepositorySchema.extend({ + parent: GitHubForkParentSchema, + source: GitHubForkParentSchema +}); + +// Issue related schemas +export const GitHubLabelSchema = z.object({ + id: z.number(), + node_id: z.string(), + url: z.string(), + name: z.string(), + color: z.string(), + default: z.boolean(), + description: z.string().optional() +}); + +export const GitHubIssueAssigneeSchema = z.object({ + login: z.string(), + id: z.number(), + avatar_url: z.string(), + url: z.string(), + html_url: z.string() +}); + +export const GitHubMilestoneSchema = z.object({ + url: z.string(), + html_url: z.string(), + labels_url: z.string(), + id: z.number(), + node_id: z.string(), + number: z.number(), + title: z.string(), + description: z.string(), + state: z.string() +}); + +export const GitHubIssueSchema = z.object({ + url: z.string(), + repository_url: z.string(), + labels_url: z.string(), + comments_url: z.string(), + events_url: z.string(), + html_url: z.string(), + id: z.number(), + node_id: z.string(), + number: z.number(), + title: z.string(), + user: GitHubIssueAssigneeSchema, + labels: z.array(GitHubLabelSchema), + state: z.string(), + locked: z.boolean(), + assignee: GitHubIssueAssigneeSchema.nullable(), + assignees: z.array(GitHubIssueAssigneeSchema), + milestone: GitHubMilestoneSchema.nullable(), + comments: z.number(), + created_at: z.string(), + updated_at: z.string(), + closed_at: z.string().nullable(), + body: z.string() +}); + +// Pull Request related schemas +export const GitHubPullRequestHeadSchema = z.object({ + label: z.string(), + ref: z.string(), + sha: z.string(), + user: GitHubIssueAssigneeSchema, + repo: GitHubRepositorySchema +}); + +export const GitHubPullRequestSchema = z.object({ + url: z.string(), + id: z.number(), + node_id: z.string(), + html_url: z.string(), + diff_url: z.string(), + patch_url: z.string(), + issue_url: z.string(), + number: z.number(), + state: z.string(), + locked: z.boolean(), + title: z.string(), + user: GitHubIssueAssigneeSchema, + body: z.string(), + created_at: z.string(), + updated_at: z.string(), + closed_at: z.string().nullable(), + merged_at: z.string().nullable(), + merge_commit_sha: z.string(), + assignee: GitHubIssueAssigneeSchema.nullable(), + assignees: z.array(GitHubIssueAssigneeSchema), + head: GitHubPullRequestHeadSchema, + base: GitHubPullRequestHeadSchema +}); + +const RepoParamsSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name") +}); + +export const CreateOrUpdateFileSchema = RepoParamsSchema.extend({ + 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("SHA of the file being replaced (required when updating existing files)") +}); + +export const SearchRepositoriesSchema = z.object({ + query: z.string().describe("Search query (see GitHub search syntax)"), + page: z.number().optional().describe("Page number for pagination (default: 1)"), + perPage: z.number().optional().describe("Number of results per page (default: 30, max: 100)") +}); + +export const CreateRepositorySchema = z.object({ + name: z.string().describe("Repository name"), + description: z.string().optional().describe("Repository description"), + private: z.boolean().optional().describe("Whether the repository should be private"), + autoInit: z.boolean().optional().describe("Initialize with README.md") +}); + +export const GetFileContentsSchema = RepoParamsSchema.extend({ + path: z.string().describe("Path to the file or directory"), + branch: z.string().optional().describe("Branch to get contents from") +}); + +export const PushFilesSchema = RepoParamsSchema.extend({ + branch: z.string().describe("Branch to push to (e.g., 'main' or 'master')"), + files: z.array(z.object({ + path: z.string().describe("Path where to create the file"), + content: z.string().describe("Content of the file") + })).describe("Array of files to push"), + message: z.string().describe("Commit message") +}); + +export const CreateIssueSchema = RepoParamsSchema.extend({ + title: z.string().describe("Issue title"), + body: z.string().optional().describe("Issue body/description"), + assignees: z.array(z.string()).optional().describe("Array of usernames to assign"), + labels: z.array(z.string()).optional().describe("Array of label names"), + milestone: z.number().optional().describe("Milestone number to assign") +}); + +export const CreatePullRequestSchema = RepoParamsSchema.extend({ + title: z.string().describe("Pull request title"), + body: z.string().optional().describe("Pull request body/description"), + head: z.string().describe("The name of the branch where your changes are implemented"), + base: z.string().describe("The name of the branch you want the changes pulled into"), + draft: z.boolean().optional().describe("Whether to create the pull request as a draft"), + maintainer_can_modify: z.boolean().optional() + .describe("Whether maintainers can modify the pull request") +}); + +export const ForkRepositorySchema = RepoParamsSchema.extend({ + organization: z.string().optional() + .describe("Optional: organization to fork to (defaults to your personal account)") +}); + +export const CreateBranchSchema = RepoParamsSchema.extend({ + branch: z.string().describe("Name for the new branch"), + from_branch: z.string().optional() + .describe("Optional: source branch to create from (defaults to the repository's default branch)") +}); + +// Export types +export type GitHubAuthor = z.infer; +export type GitHubFork = z.infer; +export type GitHubIssue = z.infer; +export type GitHubPullRequest = z.infer;export type GitHubRepository = z.infer; +export type GitHubFileContent = z.infer; +export type GitHubDirectoryContent = z.infer; +export type GitHubContent = z.infer; +export type FileOperation = z.infer; +export type GitHubTree = z.infer; +export type GitHubCommit = z.infer; +export type GitHubReference = z.infer; +export type CreateRepositoryOptions = z.infer; +export type CreateIssueOptions = z.infer; +export type CreatePullRequestOptions = z.infer; +export type CreateBranchOptions = z.infer; +export type GitHubCreateUpdateFileResponse = z.infer; +export type GitHubSearchResponse = z.infer; \ No newline at end of file From 5eabc5ecba0dc38d2ec7a4f8ede529e82ef044b7 Mon Sep 17 00:00:00 2001 From: Mahesh Murag Date: Thu, 21 Nov 2024 00:22:34 -0500 Subject: [PATCH 8/8] Undo gitignore newline --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7ecb7109b..d26f8e080 100644 --- a/.gitignore +++ b/.gitignore @@ -297,4 +297,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +#.idea/ \ No newline at end of file