diff --git a/package-lock.json b/package-lock.json index c73f44ee..e06112f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4001,8 +4001,10 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.0.1", + "@types/node": "^20.11.0", "@types/node-fetch": "^2.6.12", "node-fetch": "^3.3.2", + "zod": "^3.22.4", "zod-to-json-schema": "^3.23.5" }, "bin": { @@ -4023,6 +4025,14 @@ "zod": "^3.23.8" } }, + "src/github/node_modules/@types/node": { + "version": "20.17.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz", + "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, "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", diff --git a/src/github/README.md b/src/github/README.md index cfd268a8..b5b0bfa6 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -1,6 +1,6 @@ # GitHub MCP Server -MCP Server for the GitHub API, enabling file operations, repository management, and more. +MCP Server for the GitHub API, enabling file operations, repository management, search functionality, and more. ### Features @@ -8,6 +8,7 @@ MCP Server for the GitHub API, enabling file operations, repository management, - **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 +- **Advanced Search**: Support for searching code, issues/PRs, and users ## Tools @@ -102,6 +103,60 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `from_branch` (optional string): Source branch (defaults to repo default) - Returns: Created branch reference +10. `search_code` + - Search for code across GitHub repositories + - Inputs: + - `q` (string): Search query using GitHub code search syntax + - `sort` (optional string): Sort field ('indexed' only) + - `order` (optional string): Sort order ('asc' or 'desc') + - `per_page` (optional number): Results per page (max 100) + - `page` (optional number): Page number + - Returns: Code search results with repository context + +11. `search_issues` + - Search for issues and pull requests + - Inputs: + - `q` (string): Search query using GitHub issues search syntax + - `sort` (optional string): Sort field (comments, reactions, created, etc.) + - `order` (optional string): Sort order ('asc' or 'desc') + - `per_page` (optional number): Results per page (max 100) + - `page` (optional number): Page number + - Returns: Issue and pull request search results + +12. `search_users` + - Search for GitHub users + - Inputs: + - `q` (string): Search query using GitHub users search syntax + - `sort` (optional string): Sort field (followers, repositories, joined) + - `order` (optional string): Sort order ('asc' or 'desc') + - `per_page` (optional number): Results per page (max 100) + - `page` (optional number): Page number + - Returns: User search results + +## Search Query Syntax + +### Code Search +- `language:javascript`: Search by programming language +- `repo:owner/name`: Search in specific repository +- `path:app/src`: Search in specific path +- `extension:js`: Search by file extension +- Example: `q: "import express" language:typescript path:src/` + +### Issues Search +- `is:issue` or `is:pr`: Filter by type +- `is:open` or `is:closed`: Filter by state +- `label:bug`: Search by label +- `author:username`: Search by author +- Example: `q: "memory leak" is:issue is:open label:bug` + +### Users Search +- `type:user` or `type:org`: Filter by account type +- `followers:>1000`: Filter by followers +- `location:London`: Search by location +- Example: `q: "fullstack developer" location:London followers:>100` + +For detailed search syntax, see [GitHub's searching documentation](https://docs.github.com/en/search-github/searching-on-github). + ## Setup ### Personal Access Token diff --git a/src/github/index.ts b/src/github/index.ts index 800bce83..2d73dee4 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -41,19 +41,32 @@ import { 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", - version: "0.1.0", -}, { - capabilities: { - tools: {} + CreateBranchSchema, + SearchCodeSchema, + SearchIssuesSchema, + SearchUsersSchema, + SearchCodeResponseSchema, + SearchIssuesResponseSchema, + SearchUsersResponseSchema, + type SearchCodeResponse, + type SearchIssuesResponse, + type SearchUsersResponse, +} from "./schemas.js"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from "zod"; +import type { CallToolRequest } from "@modelcontextprotocol/sdk/types.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; @@ -67,17 +80,17 @@ async function forkRepository( repo: string, organization?: string ): Promise { - const url = organization + 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" - } + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, }); if (!response.ok) { @@ -93,21 +106,21 @@ async function createBranch( options: z.infer ): Promise { 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", + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", "User-Agent": "github-mcp-server", - "Content-Type": "application/json" + "Content-Type": "application/json", }, body: JSON.stringify({ ref: fullRef, - sha: options.sha - }) + sha: options.sha, + }), } ); @@ -126,10 +139,10 @@ async function getDefaultBranchSHA( `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" - } + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, } ); @@ -138,15 +151,17 @@ async function getDefaultBranchSHA( `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" - } + 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')"); + throw new Error( + "Could not find default branch (tried 'main' and 'master')" + ); } const data = GitHubReferenceSchema.parse(await masterResponse.json()); @@ -170,10 +185,10 @@ async function getFileContents( const response = await fetch(url, { headers: { - "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, - "Accept": "application/vnd.github.v3+json", - "User-Agent": "github-mcp-server" - } + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, }); if (!response.ok) { @@ -184,7 +199,7 @@ async function getFileContents( // If it's a file, decode the content if (!Array.isArray(data) && data.content) { - data.content = Buffer.from(data.content, 'base64').toString('utf8'); + data.content = Buffer.from(data.content, "base64").toString("utf8"); } return data; @@ -200,12 +215,12 @@ async function createIssue( { method: "POST", headers: { - "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, - "Accept": "application/vnd.github.v3+json", + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", "User-Agent": "github-mcp-server", - "Content-Type": "application/json" + "Content-Type": "application/json", }, - body: JSON.stringify(options) + body: JSON.stringify(options), } ); @@ -226,12 +241,12 @@ async function createPullRequest( { method: "POST", headers: { - "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, - "Accept": "application/vnd.github.v3+json", + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", "User-Agent": "github-mcp-server", - "Content-Type": "application/json" + "Content-Type": "application/json", }, - body: JSON.stringify(options) + body: JSON.stringify(options), } ); @@ -251,7 +266,7 @@ async function createOrUpdateFile( branch: string, sha?: string ): Promise { - const encodedContent = Buffer.from(content).toString('base64'); + const encodedContent = Buffer.from(content).toString("base64"); let currentSha = sha; if (!currentSha) { @@ -261,28 +276,30 @@ async function createOrUpdateFile( currentSha = existingFile.sha; } } catch (error) { - console.error('Note: File does not exist in branch, will create new file'); + 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: encodedContent, branch, - ...(currentSha ? { sha: currentSha } : {}) + ...(currentSha ? { sha: currentSha } : {}), }; const response = await fetch(url, { method: "PUT", headers: { - "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, - "Accept": "application/vnd.github.v3+json", + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", "User-Agent": "github-mcp-server", - "Content-Type": "application/json" + "Content-Type": "application/json", }, - body: JSON.stringify(body) + body: JSON.stringify(body), }); if (!response.ok) { @@ -298,11 +315,11 @@ async function createTree( files: FileOperation[], baseTree?: string ): Promise { - const tree = files.map(file => ({ + const tree = files.map((file) => ({ path: file.path, - mode: '100644' as const, - type: 'blob' as const, - content: file.content + mode: "100644" as const, + type: "blob" as const, + content: file.content, })); const response = await fetch( @@ -310,15 +327,15 @@ async function createTree( { method: "POST", headers: { - "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, - "Accept": "application/vnd.github.v3+json", + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", "User-Agent": "github-mcp-server", - "Content-Type": "application/json" + "Content-Type": "application/json", }, body: JSON.stringify({ tree, - base_tree: baseTree - }) + base_tree: baseTree, + }), } ); @@ -341,16 +358,16 @@ async function createCommit( { method: "POST", headers: { - "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, - "Accept": "application/vnd.github.v3+json", + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", "User-Agent": "github-mcp-server", - "Content-Type": "application/json" + "Content-Type": "application/json", }, body: JSON.stringify({ message, tree, - parents - }) + parents, + }), } ); @@ -372,15 +389,15 @@ async function updateReference( { method: "PATCH", headers: { - "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, - "Accept": "application/vnd.github.v3+json", + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", "User-Agent": "github-mcp-server", - "Content-Type": "application/json" + "Content-Type": "application/json", }, body: JSON.stringify({ sha, - force: true - }) + force: true, + }), } ); @@ -402,10 +419,10 @@ async function pushFiles( `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" - } + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, } ); @@ -417,7 +434,9 @@ async function pushFiles( const commitSha = ref.object.sha; const tree = await createTree(owner, repo, files, commitSha); - const commit = await createCommit(owner, repo, message, tree.sha, [commitSha]); + const commit = await createCommit(owner, repo, message, tree.sha, [ + commitSha, + ]); return await updateReference(owner, repo, `heads/${branch}`, commit.sha); } @@ -433,10 +452,10 @@ async function searchRepositories( const response = await fetch(url.toString(), { headers: { - "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, - "Accept": "application/vnd.github.v3+json", - "User-Agent": "github-mcp-server" - } + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, }); if (!response.ok) { @@ -452,12 +471,12 @@ async function createRepository( 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", + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", "User-Agent": "github-mcp-server", - "Content-Type": "application/json" + "Content-Type": "application/json", }, - body: JSON.stringify(options) + body: JSON.stringify(options), }); if (!response.ok) { @@ -467,55 +486,149 @@ async function createRepository( return GitHubRepositorySchema.parse(await response.json()); } +async function searchCode( + params: z.infer +): Promise { + const url = new URL("https://api.github.com/search/code"); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.append(key, value.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 SearchCodeResponseSchema.parse(await response.json()); +} + +async function searchIssues( + params: z.infer +): Promise { + const url = new URL("https://api.github.com/search/issues"); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.append(key, value.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 SearchIssuesResponseSchema.parse(await response.json()); +} + +async function searchUsers( + params: z.infer +): Promise { + const url = new URL("https://api.github.com/search/users"); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.append(key, value.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 SearchUsersResponseSchema.parse(await response.json()); +} + server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "create_or_update_file", description: "Create or update a single file in a GitHub repository", - inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema) + inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema), }, { name: "search_repositories", description: "Search for GitHub repositories", - inputSchema: zodToJsonSchema(SearchRepositoriesSchema) + inputSchema: zodToJsonSchema(SearchRepositoriesSchema), }, { name: "create_repository", description: "Create a new GitHub repository in your account", - inputSchema: zodToJsonSchema(CreateRepositorySchema) + inputSchema: zodToJsonSchema(CreateRepositorySchema), }, { name: "get_file_contents", - description: "Get the contents of a file or directory from a GitHub repository", - inputSchema: zodToJsonSchema(GetFileContentsSchema) + description: + "Get the contents of a file or directory from a GitHub repository", + inputSchema: zodToJsonSchema(GetFileContentsSchema), }, { name: "push_files", - description: "Push multiple files to a GitHub repository in a single commit", - inputSchema: zodToJsonSchema(PushFilesSchema) + description: + "Push multiple files to a GitHub repository in a single commit", + inputSchema: zodToJsonSchema(PushFilesSchema), }, { name: "create_issue", description: "Create a new issue in a GitHub repository", - inputSchema: zodToJsonSchema(CreateIssueSchema) + inputSchema: zodToJsonSchema(CreateIssueSchema), }, { name: "create_pull_request", description: "Create a new pull request in a GitHub repository", - inputSchema: zodToJsonSchema(CreatePullRequestSchema) + inputSchema: zodToJsonSchema(CreatePullRequestSchema), }, { name: "fork_repository", - description: "Fork a GitHub repository to your account or specified organization", - inputSchema: zodToJsonSchema(ForkRepositorySchema) + description: + "Fork a GitHub repository to your account or specified organization", + inputSchema: zodToJsonSchema(ForkRepositorySchema), }, { name: "create_branch", description: "Create a new branch in a GitHub repository", - inputSchema: zodToJsonSchema(CreateBranchSchema) - } - ] + inputSchema: zodToJsonSchema(CreateBranchSchema), + }, + { + name: "search_code", + description: "Search for code across GitHub repositories", + inputSchema: zodToJsonSchema(SearchCodeSchema), + }, + { + name: "search_issues", + description: + "Search for issues and pull requests across GitHub repositories", + inputSchema: zodToJsonSchema(SearchIssuesSchema), + }, + { + name: "search_users", + description: "Search for users on GitHub", + inputSchema: zodToJsonSchema(SearchUsersSchema), + }, + ], }; }); @@ -528,8 +641,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { 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 { content: [{ type: "text", text: JSON.stringify(fork, null, 2) }] }; + const fork = await forkRepository( + args.owner, + args.repo, + args.organization + ); + return { + content: [{ type: "text", text: JSON.stringify(fork, null, 2) }], + }; } case "create_branch": { @@ -540,10 +659,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { `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" - } + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, } ); @@ -559,28 +678,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const branch = await createBranch(args.owner, args.repo, { ref: args.branch, - sha + sha, }); - return { content: [{ type: "text", text: JSON.stringify(branch, null, 2) }] }; + return { + content: [{ type: "text", text: JSON.stringify(branch, null, 2) }], + }; } case "search_repositories": { const args = SearchRepositoriesSchema.parse(request.params.arguments); - const results = await searchRepositories(args.query, args.page, args.perPage); - return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] }; + const results = await searchRepositories( + args.query, + args.page, + args.perPage + ); + return { + content: [{ type: "text", text: JSON.stringify(results, null, 2) }], + }; } case "create_repository": { const args = CreateRepositorySchema.parse(request.params.arguments); const repository = await createRepository(args); - return { content: [{ type: "text", text: JSON.stringify(repository, null, 2) }] }; + return { + content: [ + { type: "text", text: JSON.stringify(repository, null, 2) }, + ], + }; } case "get_file_contents": { const args = GetFileContentsSchema.parse(request.params.arguments); - const contents = await getFileContents(args.owner, args.repo, args.path, args.branch); - return { content: [{ type: "text", text: JSON.stringify(contents, null, 2) }] }; + const contents = await getFileContents( + args.owner, + args.repo, + args.path, + args.branch + ); + return { + content: [{ type: "text", text: JSON.stringify(contents, null, 2) }], + }; } case "create_or_update_file": { @@ -594,7 +732,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { args.branch, args.sha ); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; } case "push_files": { @@ -606,21 +746,53 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { args.files, args.message ); - return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; } case "create_issue": { const args = CreateIssueSchema.parse(request.params.arguments); const { owner, repo, ...options } = args; const issue = await createIssue(owner, repo, options); - return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }] }; + return { + content: [{ type: "text", text: JSON.stringify(issue, null, 2) }], + }; } case "create_pull_request": { const args = CreatePullRequestSchema.parse(request.params.arguments); const { owner, repo, ...options } = args; const pullRequest = await createPullRequest(owner, repo, options); - return { content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }] }; + return { + content: [ + { type: "text", text: JSON.stringify(pullRequest, null, 2) }, + ], + }; + } + + case "search_code": { + const args = SearchCodeSchema.parse(request.params.arguments); + const results = await searchCode(args); + return { + content: [{ type: "text", text: JSON.stringify(results, null, 2) }], + }; + } + + case "search_issues": { + const args = SearchIssuesSchema.parse(request.params.arguments); + const results = await searchIssues(args); + return { + content: [{ type: "text", text: JSON.stringify(results, null, 2) }], + }; + } + + case "search_users": { + const args = SearchUsersSchema.parse(request.params.arguments); + const results = await searchUsers(args); + return { + content: [{ type: "text", text: JSON.stringify(results, null, 2) }], + }; } default: @@ -628,7 +800,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } } catch (error) { if (error instanceof z.ZodError) { - throw new Error(`Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`); + throw new Error( + `Invalid arguments: ${error.errors + .map( + (e: z.ZodError["errors"][number]) => + `${e.path.join(".")}: ${e.message}` + ) + .join(", ")}` + ); } throw error; } @@ -643,4 +822,4 @@ async function runServer() { runServer().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); -}); \ No newline at end of file +}); diff --git a/src/github/package.json b/src/github/package.json index a9dc0b33..0fc2aaeb 100644 --- a/src/github/package.json +++ b/src/github/package.json @@ -20,8 +20,10 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "1.0.1", + "@types/node": "^20.11.0", "@types/node-fetch": "^2.6.12", "node-fetch": "^3.3.2", + "zod": "^3.22.4", "zod-to-json-schema": "^3.23.5" }, "devDependencies": { diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 213458eb..b9c81a15 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -1,10 +1,10 @@ -import { z } from 'zod'; +import { z } from "zod"; // Base schemas for common types export const GitHubAuthorSchema = z.object({ name: z.string(), email: z.string(), - date: z.string() + date: z.string(), }); // Repository related schemas @@ -15,7 +15,7 @@ export const GitHubOwnerSchema = z.object({ avatar_url: z.string(), url: z.string(), html_url: z.string(), - type: z.string() + type: z.string(), }); export const GitHubRepositorySchema = z.object({ @@ -35,7 +35,7 @@ export const GitHubRepositorySchema = z.object({ git_url: z.string(), ssh_url: z.string(), clone_url: z.string(), - default_branch: z.string() + default_branch: z.string(), }); // File content schemas @@ -50,7 +50,7 @@ export const GitHubFileContentSchema = z.object({ url: z.string(), git_url: z.string(), html_url: z.string(), - download_url: z.string() + download_url: z.string(), }); export const GitHubDirectoryContentSchema = z.object({ @@ -62,35 +62,35 @@ export const GitHubDirectoryContentSchema = z.object({ url: z.string(), git_url: z.string(), html_url: z.string(), - download_url: z.string().nullable() + download_url: z.string().nullable(), }); export const GitHubContentSchema = z.union([ GitHubFileContentSchema, - z.array(GitHubDirectoryContentSchema) + z.array(GitHubDirectoryContentSchema), ]); // Operation schemas export const FileOperationSchema = z.object({ path: z.string(), - content: 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']), + mode: z.enum(["100644", "100755", "040000", "160000", "120000"]), + type: z.enum(["blob", "tree", "commit"]), size: z.number().optional(), sha: z.string(), - url: z.string() + url: z.string(), }); export const GitHubTreeSchema = z.object({ sha: z.string(), url: z.string(), tree: z.array(GitHubTreeEntrySchema), - truncated: z.boolean() + truncated: z.boolean(), }); export const GitHubCommitSchema = z.object({ @@ -102,12 +102,14 @@ export const GitHubCommitSchema = z.object({ message: z.string(), tree: z.object({ sha: z.string(), - url: z.string() + url: z.string(), }), - parents: z.array(z.object({ - sha: z.string(), - url: z.string() - })) + parents: z.array( + z.object({ + sha: z.string(), + url: z.string(), + }) + ), }); // Reference schema @@ -118,8 +120,8 @@ export const GitHubReferenceSchema = z.object({ object: z.object({ sha: z.string(), type: z.string(), - url: z.string() - }) + url: z.string(), + }), }); // Input schemas for operations @@ -127,7 +129,7 @@ export const CreateRepositoryOptionsSchema = z.object({ name: z.string(), description: z.string().optional(), private: z.boolean().optional(), - auto_init: z.boolean().optional() + auto_init: z.boolean().optional(), }); export const CreateIssueOptionsSchema = z.object({ @@ -135,7 +137,7 @@ export const CreateIssueOptionsSchema = z.object({ body: z.string().optional(), assignees: z.array(z.string()).optional(), milestone: z.number().optional(), - labels: z.array(z.string()).optional() + labels: z.array(z.string()).optional(), }); export const CreatePullRequestOptionsSchema = z.object({ @@ -144,12 +146,12 @@ export const CreatePullRequestOptionsSchema = z.object({ head: z.string(), base: z.string(), maintainer_can_modify: z.boolean().optional(), - draft: z.boolean().optional() + draft: z.boolean().optional(), }); export const CreateBranchOptionsSchema = z.object({ ref: z.string(), - sha: z.string() + sha: z.string(), }); // Response schemas for operations @@ -164,21 +166,23 @@ export const GitHubCreateUpdateFileResponseSchema = z.object({ 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() - })) - }) + }), + 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) + items: z.array(GitHubRepositorySchema), }); // Fork related schemas @@ -188,14 +192,14 @@ export const GitHubForkParentSchema = z.object({ owner: z.object({ login: z.string(), id: z.number(), - avatar_url: z.string() + avatar_url: z.string(), }), - html_url: z.string() + html_url: z.string(), }); export const GitHubForkSchema = GitHubRepositorySchema.extend({ parent: GitHubForkParentSchema, - source: GitHubForkParentSchema + source: GitHubForkParentSchema, }); // Issue related schemas @@ -206,7 +210,7 @@ export const GitHubLabelSchema = z.object({ name: z.string(), color: z.string(), default: z.boolean(), - description: z.string().optional() + description: z.string().optional(), }); export const GitHubIssueAssigneeSchema = z.object({ @@ -214,7 +218,7 @@ export const GitHubIssueAssigneeSchema = z.object({ id: z.number(), avatar_url: z.string(), url: z.string(), - html_url: z.string() + html_url: z.string(), }); export const GitHubMilestoneSchema = z.object({ @@ -226,7 +230,7 @@ export const GitHubMilestoneSchema = z.object({ number: z.number(), title: z.string(), description: z.string(), - state: z.string() + state: z.string(), }); export const GitHubIssueSchema = z.object({ @@ -251,7 +255,7 @@ export const GitHubIssueSchema = z.object({ created_at: z.string(), updated_at: z.string(), closed_at: z.string().nullable(), - body: z.string() + body: z.string(), }); // Pull Request related schemas @@ -260,7 +264,7 @@ export const GitHubPullRequestHeadSchema = z.object({ ref: z.string(), sha: z.string(), user: GitHubIssueAssigneeSchema, - repo: GitHubRepositorySchema + repo: GitHubRepositorySchema, }); export const GitHubPullRequestSchema = z.object({ @@ -285,12 +289,12 @@ export const GitHubPullRequestSchema = z.object({ assignee: GitHubIssueAssigneeSchema.nullable(), assignees: z.array(GitHubIssueAssigneeSchema), head: GitHubPullRequestHeadSchema, - base: GitHubPullRequestHeadSchema + base: GitHubPullRequestHeadSchema, }); const RepoParamsSchema = z.object({ owner: z.string().describe("Repository owner (username or organization)"), - repo: z.string().describe("Repository name") + repo: z.string().describe("Repository name"), }); export const CreateOrUpdateFileSchema = RepoParamsSchema.extend({ @@ -298,81 +302,350 @@ export const CreateOrUpdateFileSchema = RepoParamsSchema.extend({ 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)") + 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)") + 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") + 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") + 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") + 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"), + 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") + 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") + 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)") + 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)") + from_branch: z + .string() + .optional() + .describe( + "Optional: source branch to create from (defaults to the repository's default branch)" + ), +}); + +/** + * Response schema for a code search result item + * @see https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-code + */ +export const SearchCodeItemSchema = z.object({ + name: z.string().describe("The name of the file"), + path: z.string().describe("The path to the file in the repository"), + sha: z.string().describe("The SHA hash of the file"), + url: z.string().describe("The API URL for this file"), + git_url: z.string().describe("The Git URL for this file"), + html_url: z.string().describe("The HTML URL to view this file on GitHub"), + repository: GitHubRepositorySchema.describe( + "The repository where this file was found" + ), + score: z.number().describe("The search result score"), +}); + +/** + * Response schema for code search results + */ +export const SearchCodeResponseSchema = z.object({ + total_count: z.number().describe("Total number of matching results"), + incomplete_results: z + .boolean() + .describe("Whether the results are incomplete"), + items: z.array(SearchCodeItemSchema).describe("The search results"), +}); + +/** + * Response schema for an issue search result item + * @see https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-issues-and-pull-requests + */ +export const SearchIssueItemSchema = z.object({ + url: z.string().describe("The API URL for this issue"), + repository_url: z + .string() + .describe("The API URL for the repository where this issue was found"), + labels_url: z.string().describe("The API URL for the labels of this issue"), + comments_url: z.string().describe("The API URL for comments of this issue"), + events_url: z.string().describe("The API URL for events of this issue"), + html_url: z.string().describe("The HTML URL to view this issue on GitHub"), + id: z.number().describe("The ID of this issue"), + node_id: z.string().describe("The Node ID of this issue"), + number: z.number().describe("The number of this issue"), + title: z.string().describe("The title of this issue"), + user: GitHubIssueAssigneeSchema.describe("The user who created this issue"), + labels: z.array(GitHubLabelSchema).describe("The labels of this issue"), + state: z.string().describe("The state of this issue"), + locked: z.boolean().describe("Whether this issue is locked"), + assignee: GitHubIssueAssigneeSchema.nullable().describe( + "The assignee of this issue" + ), + assignees: z + .array(GitHubIssueAssigneeSchema) + .describe("The assignees of this issue"), + comments: z.number().describe("The number of comments on this issue"), + created_at: z.string().describe("The creation time of this issue"), + updated_at: z.string().describe("The last update time of this issue"), + closed_at: z.string().nullable().describe("The closure time of this issue"), + body: z.string().describe("The body of this issue"), + score: z.number().describe("The search result score"), + pull_request: z + .object({ + url: z.string().describe("The API URL for this pull request"), + html_url: z.string().describe("The HTML URL to view this pull request"), + diff_url: z.string().describe("The URL to view the diff"), + patch_url: z.string().describe("The URL to view the patch"), + }) + .optional() + .describe("Pull request details if this is a PR"), +}); + +/** + * Response schema for issue search results + */ +export const SearchIssuesResponseSchema = z.object({ + total_count: z.number().describe("Total number of matching results"), + incomplete_results: z + .boolean() + .describe("Whether the results are incomplete"), + items: z.array(SearchIssueItemSchema).describe("The search results"), +}); + +/** + * Response schema for a user search result item + * @see https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-users + */ +export const SearchUserItemSchema = z.object({ + login: z.string().describe("The username of the user"), + id: z.number().describe("The ID of the user"), + node_id: z.string().describe("The Node ID of the user"), + avatar_url: z.string().describe("The avatar URL of the user"), + gravatar_id: z.string().describe("The Gravatar ID of the user"), + url: z.string().describe("The API URL for this user"), + html_url: z.string().describe("The HTML URL to view this user on GitHub"), + followers_url: z.string().describe("The API URL for followers of this user"), + following_url: z.string().describe("The API URL for following of this user"), + gists_url: z.string().describe("The API URL for gists of this user"), + starred_url: z + .string() + .describe("The API URL for starred repositories of this user"), + subscriptions_url: z + .string() + .describe("The API URL for subscriptions of this user"), + organizations_url: z + .string() + .describe("The API URL for organizations of this user"), + repos_url: z.string().describe("The API URL for repositories of this user"), + events_url: z.string().describe("The API URL for events of this user"), + received_events_url: z + .string() + .describe("The API URL for received events of this user"), + type: z.string().describe("The type of this user"), + site_admin: z.boolean().describe("Whether this user is a site administrator"), + score: z.number().describe("The search result score"), +}); + +/** + * Response schema for user search results + */ +export const SearchUsersResponseSchema = z.object({ + total_count: z.number().describe("Total number of matching results"), + incomplete_results: z + .boolean() + .describe("Whether the results are incomplete"), + items: z.array(SearchUserItemSchema).describe("The search results"), +}); + +/** + * Input schema for code search + * @see https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-code--parameters + */ +export const SearchCodeSchema = z.object({ + q: z + .string() + .describe( + "Search query. See GitHub code search syntax: https://docs.github.com/en/search-github/searching-on-github/searching-code" + ), + order: z + .enum(["asc", "desc"]) + .optional() + .describe("Sort order (asc or desc)"), + per_page: z + .number() + .min(1) + .max(100) + .optional() + .describe("Results per page (max 100)"), + page: z.number().min(1).optional().describe("Page number"), +}); + +/** + * Input schema for issues search + * @see https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-issues-and-pull-requests--parameters + */ +export const SearchIssuesSchema = z.object({ + q: z + .string() + .describe( + "Search query. See GitHub issues search syntax: https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests" + ), + sort: z + .enum([ + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated", + ]) + .optional() + .describe("Sort field"), + order: z + .enum(["asc", "desc"]) + .optional() + .describe("Sort order (asc or desc)"), + per_page: z + .number() + .min(1) + .max(100) + .optional() + .describe("Results per page (max 100)"), + page: z.number().min(1).optional().describe("Page number"), +}); + +/** + * Input schema for users search + * @see https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-users--parameters + */ +export const SearchUsersSchema = z.object({ + q: z + .string() + .describe( + "Search query. See GitHub users search syntax: https://docs.github.com/en/search-github/searching-on-github/searching-users" + ), + sort: z + .enum(["followers", "repositories", "joined"]) + .optional() + .describe("Sort field"), + order: z + .enum(["asc", "desc"]) + .optional() + .describe("Sort order (asc or desc)"), + per_page: z + .number() + .min(1) + .max(100) + .optional() + .describe("Results per page (max 100)"), + page: z.number().min(1).optional().describe("Page number"), }); // 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 GitHubPullRequest = z.infer; +export type GitHubRepository = z.infer; export type GitHubFileContent = z.infer; -export type GitHubDirectoryContent = z.infer; +export type GitHubDirectoryContent = z.infer< + typeof GitHubDirectoryContentSchema +>; 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 CreateRepositoryOptions = z.infer< + typeof CreateRepositoryOptionsSchema +>; export type CreateIssueOptions = z.infer; -export type CreatePullRequestOptions = z.infer; +export type CreatePullRequestOptions = z.infer< + typeof CreatePullRequestOptionsSchema +>; export type CreateBranchOptions = z.infer; -export type GitHubCreateUpdateFileResponse = z.infer; -export type GitHubSearchResponse = z.infer; \ No newline at end of file +export type GitHubCreateUpdateFileResponse = z.infer< + typeof GitHubCreateUpdateFileResponseSchema +>; +export type GitHubSearchResponse = z.infer; +export type SearchCodeItem = z.infer; +export type SearchCodeResponse = z.infer; +export type SearchIssueItem = z.infer; +export type SearchIssuesResponse = z.infer; +export type SearchUserItem = z.infer; +export type SearchUsersResponse = z.infer;