diff --git a/.gitignore b/.gitignore index 38ff4cd0..7ecb7109 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 1ae76b8b..0676a34c 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; - }; + 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 }; + } - const fork = await forkRepository(args.owner, args.repo, args.organization); - return { toolResult: fork }; - } + 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" + } + } + ); - 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 = GitHubReferenceSchema.parse(await response.json()); + sha = data.object.sha; + } else { + sha = await getDefaultBranchSHA(args.owner, args.repo); } - ); - if (!response.ok) { - throw new Error(`Source branch '${args.from_branch}' not found`); + const branch = await createBranch(args.owner, args.repo, { + ref: args.branch, + sha + }); + + return { toolResult: branch }; } - 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") { - 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 }; + } + + 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 }; + } + + 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 }; + } + + 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 }; + } + + 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 }; + } + + 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 }; + } + + 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; } - - - 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 === "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() { diff --git a/src/github/interfaces.ts b/src/github/interfaces.ts deleted file mode 100644 index ce3518e6..00000000 --- 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 00000000..213458eb --- /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