From 88025ca36536ae9e0d05ebfffee5feb76b6d17b1 Mon Sep 17 00:00:00 2001 From: zhaoxinn Date: Sun, 29 Dec 2024 10:38:18 +0800 Subject: [PATCH 01/67] =?UTF-8?q?=F0=9F=93=9D=20docs(README):=20add=20Open?= =?UTF-8?q?CTI=20to=20integrated=20tools=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 499e3e41..f2cdae14 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ A growing set of community-developed and maintained servers demonstrates various - **[XMind](https://github.com/apeyroux/mcp-xmind)** - Read and search through your XMind directory containing XMind files. - **[oatpp-mcp](https://github.com/oatpp/oatpp-mcp)** - C++ MCP integration for Oat++. Use [Oat++](https://oatpp.io) to build MCP servers. - **[Contentful-mcp](https://github.com/ivo-toby/contentful-mcp)** - Read, update, delete, publish content in your [Contentful](https://contentful.com) space(s) from this MCP Server. +- **[OpenCTI](https://github.com/Spathodea-Network/opencti-mcp)** - Interact with OpenCTI platform to retrieve threat intelligence data including reports, indicators, malware and threat actors. - **[Home Assistant](https://github.com/tevonsb/homeassistant-mcp)** - Interact with [Home Assistant](https://www.home-assistant.io/) including viewing and controlling lights, switches, sensors, and all other Home Assistant entities. - **[cognee-mcp](https://github.com/topoteretes/cognee-mcp-server)** - GraphRAG memory server with customizable ingestion, data processing and search - **[Airtable](https://github.com/domdomegg/airtable-mcp-server)** - Read and write access to [Airtable](https://airtable.com/) databases, with schema inspection. From adc3aba94d411d4af0e5b75502c9dfaf82bb27e1 Mon Sep 17 00:00:00 2001 From: privetin <81558906+privetin@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:59:18 +0900 Subject: [PATCH 02/67] docs: add Dataset Viewer server to list Add Dataset Viewer server for browsing and analyzing Hugging Face datasets --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14a01e40..b3273e18 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ A growing set of community-developed and maintained servers demonstrates various - **[Atlassian](https://github.com/sooperset/mcp-atlassian)** - Interact with Atlassian Cloud products (Confluence and Jira) including searching/reading Confluence spaces/pages, accessing Jira issues, and project metadata. - **[Google Tasks](https://github.com/zcaceres/gtasks-mcp)** - Google Tasks API Model Context Protocol Server. - **[Fetch](https://github.com/zcaceres/fetch-mcp)** - A server that flexibly fetches HTML, JSON, Markdown, or plaintext - +- **[Dataset Viewer](https://github.com/privetin/dataset-viewer)** - Browse and analyze Hugging Face datasets with features like search, filtering, statistics, and data export ## 📚 Resources From 9e25ffd59966aae245f5fd2642769a295a077c5e Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 10 Jan 2025 11:19:30 +0000 Subject: [PATCH 03/67] Add support for listing, reading, and reviewing PRs --- src/github/README.md | 37 +++++++++++++ src/github/index.ts | 118 ++++++++++++++++++++++++++++++++++++++++++ src/github/schemas.ts | 35 +++++++++++++ 3 files changed, 190 insertions(+) diff --git a/src/github/README.md b/src/github/README.md index d2277ab0..ae29cda7 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -188,6 +188,43 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `issue_number` (number): Issue number to retrieve - Returns: Github Issue object & details +18. `get_pull_request` + - Get details of a specific pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - Returns: Pull request details including diff and review status + +19. `list_pull_requests` + - List and filter repository pull requests + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `state` (optional string): Filter by state ('open', 'closed', 'all') + - `head` (optional string): Filter by head user/org and branch + - `base` (optional string): Filter by base branch + - `sort` (optional string): Sort by ('created', 'updated', 'popularity', 'long-running') + - `direction` (optional string): Sort direction ('asc', 'desc') + - `per_page` (optional number): Results per page (max 100) + - `page` (optional number): Page number + - Returns: Array of pull request details + +20. `create_pull_request_review` + - Create a review on a pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - `body` (string): Review comment text + - `event` (string): Review action ('APPROVE', 'REQUEST_CHANGES', 'COMMENT') + - `commit_id` (optional string): SHA of commit to review + - `comments` (optional array): Line-specific comments, each with: + - `path` (string): File path + - `position` (number): Line position in diff + - `body` (string): Comment text + - Returns: Created review details + ## Search Query Syntax ### Code Search diff --git a/src/github/index.ts b/src/github/index.ts index 3759e8b5..0e731abb 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -21,6 +21,7 @@ import { ForkRepositorySchema, GetFileContentsSchema, GetIssueSchema, + GetPullRequestSchema, GitHubCommitSchema, GitHubContentSchema, GitHubCreateUpdateFileResponseSchema, @@ -36,6 +37,8 @@ import { IssueCommentSchema, ListCommitsSchema, ListIssuesOptionsSchema, + ListPullRequestsSchema, + CreatePullRequestReviewSchema, PushFilesSchema, SearchCodeResponseSchema, SearchCodeSchema, @@ -715,6 +718,86 @@ async function getIssue( return GitHubIssueSchema.parse(await response.json()); } +async function getPullRequest( + owner: string, + repo: string, + pullNumber: number +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}`, + { + 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 GitHubPullRequestSchema.parse(await response.json()); +} + +async function listPullRequests( + owner: string, + repo: string, + options: Omit, 'owner' | 'repo'> +): Promise { + const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`); + + if (options.state) url.searchParams.append('state', options.state); + if (options.head) url.searchParams.append('head', options.head); + if (options.base) url.searchParams.append('base', options.base); + if (options.sort) url.searchParams.append('sort', options.sort); + if (options.direction) url.searchParams.append('direction', options.direction); + if (options.per_page) url.searchParams.append('per_page', options.per_page.toString()); + if (options.page) url.searchParams.append('page', options.page.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 z.array(GitHubPullRequestSchema).parse(await response.json()); +} + +async function createPullRequestReview( + owner: string, + repo: string, + pullNumber: number, + options: Omit, 'owner' | 'repo' | 'pull_number'> +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/reviews`, + { + 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(); +} + server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ @@ -806,6 +889,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "get_issue", description: "Get details of a specific issue in a GitHub repository.", inputSchema: zodToJsonSchema(GetIssueSchema) + }, + { + name: "get_pull_request", + description: "Get details of a specific pull request in a GitHub repository", + inputSchema: zodToJsonSchema(GetPullRequestSchema) + }, + { + name: "list_pull_requests", + description: "List pull requests in a GitHub repository with filtering options", + inputSchema: zodToJsonSchema(ListPullRequestsSchema) + }, + { + name: "create_pull_request_review", + description: "Create a review on a pull request", + inputSchema: zodToJsonSchema(CreatePullRequestReviewSchema) } ], }; @@ -1011,6 +1109,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { toolResult: issue }; } + case "get_pull_request": { + const args = GetPullRequestSchema.parse(request.params.arguments); + const pullRequest = await getPullRequest(args.owner, args.repo, args.pull_number); + return { toolResult: pullRequest }; + } + + case "list_pull_requests": { + const args = ListPullRequestsSchema.parse(request.params.arguments); + const { owner, repo, ...options } = args; + const pullRequests = await listPullRequests(owner, repo, options); + return { toolResult: pullRequests }; + } + + case "create_pull_request_review": { + const args = CreatePullRequestReviewSchema.parse(request.params.arguments); + const { owner, repo, pull_number, ...options } = args; + const review = await createPullRequestReview(owner, repo, pull_number, options); + return { toolResult: review }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 0a322328..a84c101e 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -683,6 +683,38 @@ export const GetIssueSchema = z.object({ issue_number: z.number().describe("Issue number") }); +export const GetPullRequestSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number") +}); + +export const ListPullRequestsSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + state: z.enum(['open', 'closed', 'all']).optional().describe("State of the pull requests to return"), + head: z.string().optional().describe("Filter by head user or head organization and branch name"), + base: z.string().optional().describe("Filter by base branch name"), + sort: z.enum(['created', 'updated', 'popularity', 'long-running']).optional().describe("What to sort results by"), + direction: z.enum(['asc', 'desc']).optional().describe("The direction of the sort"), + per_page: z.number().optional().describe("Results per page (max 100)"), + page: z.number().optional().describe("Page number of the results") +}); + +export const CreatePullRequestReviewSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number"), + commit_id: z.string().optional().describe("The SHA of the commit that needs a review"), + body: z.string().describe("The body text of the review"), + event: z.enum(['APPROVE', 'REQUEST_CHANGES', 'COMMENT']).describe("The review action to perform"), + comments: z.array(z.object({ + path: z.string().describe("The relative path to the file being commented on"), + position: z.number().describe("The position in the diff where you want to add a review comment"), + body: z.string().describe("Text of the review comment") + })).optional().describe("Comments to post as part of the review") +}); + // Export types export type GitHubAuthor = z.infer; export type GitHubFork = z.infer; @@ -717,3 +749,6 @@ export type SearchIssueItem = z.infer; export type SearchIssuesResponse = z.infer; export type SearchUserItem = z.infer; export type SearchUsersResponse = z.infer; +export type GetPullRequest = z.infer; +export type ListPullRequests = z.infer; +export type CreatePullRequestReview = z.infer; From 353fbb8d0ae37969d2c39996a986664c943241cd Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 10 Jan 2025 11:23:42 +0000 Subject: [PATCH 04/67] Add reading PR files and status, merging PRs --- src/github/README.md | 27 +++++++++ src/github/index.ts | 132 ++++++++++++++++++++++++++++++++++++++++++ src/github/schemas.ts | 61 +++++++++++++++++++ 3 files changed, 220 insertions(+) diff --git a/src/github/README.md b/src/github/README.md index ae29cda7..50a52d4c 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -225,6 +225,33 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `body` (string): Comment text - Returns: Created review details +21. `merge_pull_request` + - Merge a pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - `commit_title` (optional string): Title for merge commit + - `commit_message` (optional string): Extra detail for merge commit + - `merge_method` (optional string): Merge method ('merge', 'squash', 'rebase') + - Returns: Merge result details + +22. `get_pull_request_files` + - Get the list of files changed in a pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - Returns: Array of changed files with patch and status details + +23. `get_pull_request_status` + - Get the combined status of all status checks for a pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - Returns: Combined status check results and individual check details + ## Search Query Syntax ### Code Search diff --git a/src/github/index.ts b/src/github/index.ts index 0e731abb..0147f497 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -22,6 +22,9 @@ import { GetFileContentsSchema, GetIssueSchema, GetPullRequestSchema, + GetPullRequestFilesSchema, + GetPullRequestStatusSchema, + MergePullRequestSchema, GitHubCommitSchema, GitHubContentSchema, GitHubCreateUpdateFileResponseSchema, @@ -40,6 +43,8 @@ import { ListPullRequestsSchema, CreatePullRequestReviewSchema, PushFilesSchema, + PullRequestFileSchema, + CombinedStatusSchema, SearchCodeResponseSchema, SearchCodeSchema, SearchIssuesResponseSchema, @@ -798,6 +803,99 @@ async function createPullRequestReview( return await response.json(); } +async function mergePullRequest( + owner: string, + repo: string, + pullNumber: number, + options: Omit, 'owner' | 'repo' | 'pull_number'> +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/merge`, + { + 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(options), + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return await response.json(); +} + +async function getPullRequestFiles( + owner: string, + repo: string, + pullNumber: number +): Promise[]> { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/files`, + { + 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 z.array(PullRequestFileSchema).parse(await response.json()); +} + +async function getPullRequestStatus( + owner: string, + repo: string, + pullNumber: number +): Promise> { + // First get the PR to get the head SHA + const prResponse = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}`, + { + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, + } + ); + + if (!prResponse.ok) { + throw new Error(`GitHub API error: ${prResponse.statusText}`); + } + + const pr = GitHubPullRequestSchema.parse(await prResponse.json()); + const sha = pr.head.sha; + + // Then get the combined status for that SHA + const statusResponse = await fetch( + `https://api.github.com/repos/${owner}/${repo}/commits/${sha}/status`, + { + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + }, + } + ); + + if (!statusResponse.ok) { + throw new Error(`GitHub API error: ${statusResponse.statusText}`); + } + + return CombinedStatusSchema.parse(await statusResponse.json()); +} + server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ @@ -904,6 +1002,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "create_pull_request_review", description: "Create a review on a pull request", inputSchema: zodToJsonSchema(CreatePullRequestReviewSchema) + }, + { + name: "merge_pull_request", + description: "Merge a pull request", + inputSchema: zodToJsonSchema(MergePullRequestSchema) + }, + { + name: "get_pull_request_files", + description: "Get the list of files changed in a pull request", + inputSchema: zodToJsonSchema(GetPullRequestFilesSchema) + }, + { + name: "get_pull_request_status", + description: "Get the combined status of all status checks for a pull request", + inputSchema: zodToJsonSchema(GetPullRequestStatusSchema) } ], }; @@ -1129,6 +1242,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { toolResult: review }; } + case "merge_pull_request": { + const args = MergePullRequestSchema.parse(request.params.arguments); + const { owner, repo, pull_number, ...options } = args; + const result = await mergePullRequest(owner, repo, pull_number, options); + return { toolResult: result }; + } + + case "get_pull_request_files": { + const args = GetPullRequestFilesSchema.parse(request.params.arguments); + const files = await getPullRequestFiles(args.owner, args.repo, args.pull_number); + return { toolResult: files }; + } + + case "get_pull_request_status": { + const args = GetPullRequestStatusSchema.parse(request.params.arguments); + const status = await getPullRequestStatus(args.owner, args.repo, args.pull_number); + return { toolResult: status }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/src/github/schemas.ts b/src/github/schemas.ts index a84c101e..98ceed8a 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -752,3 +752,64 @@ export type SearchUsersResponse = z.infer; export type GetPullRequest = z.infer; export type ListPullRequests = z.infer; export type CreatePullRequestReview = z.infer; + +// Schema for merging a pull request +export const MergePullRequestSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number"), + commit_title: z.string().optional().describe("Title for the automatic commit message"), + commit_message: z.string().optional().describe("Extra detail to append to automatic commit message"), + merge_method: z.enum(['merge', 'squash', 'rebase']).optional().describe("Merge method to use") +}); + +// Schema for getting PR files +export const GetPullRequestFilesSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number") +}); + +export const PullRequestFileSchema = z.object({ + sha: z.string(), + filename: z.string(), + status: z.enum(['added', 'removed', 'modified', 'renamed', 'copied', 'changed', 'unchanged']), + additions: z.number(), + deletions: z.number(), + changes: z.number(), + blob_url: z.string(), + raw_url: z.string(), + contents_url: z.string(), + patch: z.string().optional() +}); + +// Schema for checking PR status +export const GetPullRequestStatusSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number") +}); + +export const StatusCheckSchema = z.object({ + url: z.string(), + state: z.enum(['error', 'failure', 'pending', 'success']), + description: z.string().nullable(), + target_url: z.string().nullable(), + context: z.string(), + created_at: z.string(), + updated_at: z.string() +}); + +export const CombinedStatusSchema = z.object({ + state: z.enum(['error', 'failure', 'pending', 'success']), + statuses: z.array(StatusCheckSchema), + sha: z.string(), + total_count: z.number() +}); + +export type MergePullRequest = z.infer; +export type GetPullRequestFiles = z.infer; +export type PullRequestFile = z.infer; +export type GetPullRequestStatus = z.infer; +export type StatusCheck = z.infer; +export type CombinedStatus = z.infer; From f42cf77d5747939379244592681d3c9e9f2e7cf0 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 10 Jan 2025 11:33:57 +0000 Subject: [PATCH 05/67] Don't use old `toolResult` format --- src/github/index.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/github/index.ts b/src/github/index.ts index 0147f497..b50f06bb 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -1189,21 +1189,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const args = ListIssuesOptionsSchema.parse(request.params.arguments); const { owner, repo, ...options } = args; const issues = await listIssues(owner, repo, options); - return { toolResult: issues }; + return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] }; } case "update_issue": { const args = UpdateIssueOptionsSchema.parse(request.params.arguments); const { owner, repo, issue_number, ...options } = args; const issue = await updateIssue(owner, repo, issue_number, options); - return { toolResult: issue }; + return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }] }; } case "add_issue_comment": { const args = IssueCommentSchema.parse(request.params.arguments); const { owner, repo, issue_number, body } = args; const comment = await addIssueComment(owner, repo, issue_number, body); - return { toolResult: comment }; + return { content: [{ type: "text", text: JSON.stringify(comment, null, 2) }] }; } case "list_commits": { @@ -1219,46 +1219,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { issue_number: z.number() }).parse(request.params.arguments); const issue = await getIssue(args.owner, args.repo, args.issue_number); - return { toolResult: issue }; + return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }] }; } case "get_pull_request": { const args = GetPullRequestSchema.parse(request.params.arguments); const pullRequest = await getPullRequest(args.owner, args.repo, args.pull_number); - return { toolResult: pullRequest }; + return { content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }] }; } case "list_pull_requests": { const args = ListPullRequestsSchema.parse(request.params.arguments); const { owner, repo, ...options } = args; const pullRequests = await listPullRequests(owner, repo, options); - return { toolResult: pullRequests }; + return { content: [{ type: "text", text: JSON.stringify(pullRequests, null, 2) }] }; } case "create_pull_request_review": { const args = CreatePullRequestReviewSchema.parse(request.params.arguments); const { owner, repo, pull_number, ...options } = args; const review = await createPullRequestReview(owner, repo, pull_number, options); - return { toolResult: review }; + return { content: [{ type: "text", text: JSON.stringify(review, null, 2) }] }; } case "merge_pull_request": { const args = MergePullRequestSchema.parse(request.params.arguments); const { owner, repo, pull_number, ...options } = args; const result = await mergePullRequest(owner, repo, pull_number, options); - return { toolResult: result }; + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } case "get_pull_request_files": { const args = GetPullRequestFilesSchema.parse(request.params.arguments); const files = await getPullRequestFiles(args.owner, args.repo, args.pull_number); - return { toolResult: files }; + return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] }; } case "get_pull_request_status": { const args = GetPullRequestStatusSchema.parse(request.params.arguments); const status = await getPullRequestStatus(args.owner, args.repo, args.pull_number); - return { toolResult: status }; + return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] }; } default: From ac7592f71a41bb7abc1cc627ffe3bf634e41bd28 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 10 Jan 2025 11:53:23 +0000 Subject: [PATCH 06/67] Support updating PR branches --- src/github/README.md | 9 +++++++++ src/github/index.ts | 37 +++++++++++++++++++++++++++++++++++++ src/github/schemas.ts | 9 +++++++++ 3 files changed, 55 insertions(+) diff --git a/src/github/README.md b/src/github/README.md index 50a52d4c..86b36e31 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -252,6 +252,15 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `pull_number` (number): Pull request number - Returns: Combined status check results and individual check details +24. `update_pull_request_branch` + - Update a pull request branch with the latest changes from the base branch (equivalent to GitHub's "Update branch" button) + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - `expected_head_sha` (optional string): The expected SHA of the pull request's HEAD ref + - Returns: Success message when branch is updated + ## Search Query Syntax ### Code Search diff --git a/src/github/index.ts b/src/github/index.ts index b50f06bb..b53392ab 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -48,6 +48,7 @@ import { SearchCodeResponseSchema, SearchCodeSchema, SearchIssuesResponseSchema, + UpdatePullRequestBranchSchema, SearchIssuesSchema, SearchRepositoriesSchema, SearchUsersResponseSchema, @@ -853,6 +854,31 @@ async function getPullRequestFiles( return z.array(PullRequestFileSchema).parse(await response.json()); } +async function updatePullRequestBranch( + owner: string, + repo: string, + pullNumber: number, + expectedHeadSha?: string +): Promise { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/update-branch`, + { + 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: expectedHeadSha ? JSON.stringify({ expected_head_sha: expectedHeadSha }) : undefined, + } + ); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } +} + async function getPullRequestStatus( owner: string, repo: string, @@ -1017,6 +1043,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "get_pull_request_status", description: "Get the combined status of all status checks for a pull request", inputSchema: zodToJsonSchema(GetPullRequestStatusSchema) + }, + { + name: "update_pull_request_branch", + description: "Update a pull request branch with the latest changes from the base branch", + inputSchema: zodToJsonSchema(UpdatePullRequestBranchSchema) } ], }; @@ -1261,6 +1292,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] }; } + case "update_pull_request_branch": { + const args = UpdatePullRequestBranchSchema.parse(request.params.arguments); + await updatePullRequestBranch(args.owner, args.repo, args.pull_number, args.expected_head_sha); + return { content: [{ type: "text", text: "Pull request branch updated successfully" }] }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 98ceed8a..8bca3a3f 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -812,4 +812,13 @@ export type GetPullRequestFiles = z.infer; export type PullRequestFile = z.infer; export type GetPullRequestStatus = z.infer; export type StatusCheck = z.infer; +// Schema for updating a pull request branch +export const UpdatePullRequestBranchSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number"), + expected_head_sha: z.string().optional().describe("The expected SHA of the pull request's HEAD ref") +}); + export type CombinedStatus = z.infer; +export type UpdatePullRequestBranch = z.infer; From a47abf5cceccaf62f60302d24e9bcc21f162e00a Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 10 Jan 2025 13:47:46 +0000 Subject: [PATCH 07/67] Add support for listing PR comments --- src/github/README.md | 8 ++++++++ src/github/index.ts | 36 ++++++++++++++++++++++++++++++++++++ src/github/schemas.ts | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/src/github/README.md b/src/github/README.md index 86b36e31..f2710f54 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -261,6 +261,14 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `expected_head_sha` (optional string): The expected SHA of the pull request's HEAD ref - Returns: Success message when branch is updated +25. `get_pull_request_comments` + - Get the review comments on a pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - Returns: Array of pull request review comments with details like the comment text, author, and location in the diff + ## Search Query Syntax ### Code Search diff --git a/src/github/index.ts b/src/github/index.ts index b53392ab..1d023dab 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -54,6 +54,8 @@ import { SearchUsersResponseSchema, SearchUsersSchema, UpdateIssueOptionsSchema, + GetPullRequestCommentsSchema, + PullRequestCommentSchema, type FileOperation, type GitHubCommit, type GitHubContent, @@ -879,6 +881,29 @@ async function updatePullRequestBranch( } } +async function getPullRequestComments( + owner: string, + repo: string, + pullNumber: number +): Promise[]> { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/comments`, + { + 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 z.array(PullRequestCommentSchema).parse(await response.json()); +} + async function getPullRequestStatus( owner: string, repo: string, @@ -1048,6 +1073,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "update_pull_request_branch", description: "Update a pull request branch with the latest changes from the base branch", inputSchema: zodToJsonSchema(UpdatePullRequestBranchSchema) + }, + { + name: "get_pull_request_comments", + description: "Get the review comments on a pull request", + inputSchema: zodToJsonSchema(GetPullRequestCommentsSchema) } ], }; @@ -1298,6 +1328,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { content: [{ type: "text", text: "Pull request branch updated successfully" }] }; } + case "get_pull_request_comments": { + const args = GetPullRequestCommentsSchema.parse(request.params.arguments); + const comments = await getPullRequestComments(args.owner, args.repo, args.pull_number); + return { content: [{ type: "text", text: JSON.stringify(comments, null, 2) }] }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 8bca3a3f..680bfb1c 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -820,5 +820,39 @@ export const UpdatePullRequestBranchSchema = z.object({ expected_head_sha: z.string().optional().describe("The expected SHA of the pull request's HEAD ref") }); +// Schema for PR comments +export const GetPullRequestCommentsSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number") +}); + +export const PullRequestCommentSchema = z.object({ + url: z.string(), + id: z.number(), + node_id: z.string(), + pull_request_review_id: z.number().nullable(), + diff_hunk: z.string(), + path: z.string().nullable(), + position: z.number().nullable(), + original_position: z.number().nullable(), + commit_id: z.string(), + original_commit_id: z.string(), + user: GitHubIssueAssigneeSchema, + body: z.string(), + created_at: z.string(), + updated_at: z.string(), + html_url: z.string(), + pull_request_url: z.string(), + author_association: z.string(), + _links: z.object({ + self: z.object({ href: z.string() }), + html: z.object({ href: z.string() }), + pull_request: z.object({ href: z.string() }) + }) +}); + export type CombinedStatus = z.infer; export type UpdatePullRequestBranch = z.infer; +export type GetPullRequestComments = z.infer; +export type PullRequestComment = z.infer; From 6ec4dff99ac9dd7c02c7b3e5bb371525bd32da24 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 10 Jan 2025 13:53:13 +0000 Subject: [PATCH 08/67] Add tool to list existing PR reviews --- src/github/README.md | 8 ++++++++ src/github/index.ts | 36 ++++++++++++++++++++++++++++++++++++ src/github/schemas.ts | 23 +++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/src/github/README.md b/src/github/README.md index f2710f54..e06bf14a 100644 --- a/src/github/README.md +++ b/src/github/README.md @@ -269,6 +269,14 @@ MCP Server for the GitHub API, enabling file operations, repository management, - `pull_number` (number): Pull request number - Returns: Array of pull request review comments with details like the comment text, author, and location in the diff +26. `get_pull_request_reviews` + - Get the reviews on a pull request + - Inputs: + - `owner` (string): Repository owner + - `repo` (string): Repository name + - `pull_number` (number): Pull request number + - Returns: Array of pull request reviews with details like the review state (APPROVED, CHANGES_REQUESTED, etc.), reviewer, and review body + ## Search Query Syntax ### Code Search diff --git a/src/github/index.ts b/src/github/index.ts index 1d023dab..04b93a37 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -56,6 +56,8 @@ import { UpdateIssueOptionsSchema, GetPullRequestCommentsSchema, PullRequestCommentSchema, + GetPullRequestReviewsSchema, + PullRequestReviewSchema, type FileOperation, type GitHubCommit, type GitHubContent, @@ -904,6 +906,29 @@ async function getPullRequestComments( return z.array(PullRequestCommentSchema).parse(await response.json()); } +async function getPullRequestReviews( + owner: string, + repo: string, + pullNumber: number +): Promise[]> { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/reviews`, + { + 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 z.array(PullRequestReviewSchema).parse(await response.json()); +} + async function getPullRequestStatus( owner: string, repo: string, @@ -1078,6 +1103,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "get_pull_request_comments", description: "Get the review comments on a pull request", inputSchema: zodToJsonSchema(GetPullRequestCommentsSchema) + }, + { + name: "get_pull_request_reviews", + description: "Get the reviews on a pull request", + inputSchema: zodToJsonSchema(GetPullRequestReviewsSchema) } ], }; @@ -1334,6 +1364,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { content: [{ type: "text", text: JSON.stringify(comments, null, 2) }] }; } + case "get_pull_request_reviews": { + const args = GetPullRequestReviewsSchema.parse(request.params.arguments); + const reviews = await getPullRequestReviews(args.owner, args.repo, args.pull_number); + return { content: [{ type: "text", text: JSON.stringify(reviews, null, 2) }] }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 680bfb1c..b9c0fa67 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -856,3 +856,26 @@ export type CombinedStatus = z.infer; export type UpdatePullRequestBranch = z.infer; export type GetPullRequestComments = z.infer; export type PullRequestComment = z.infer; + +// Schema for listing PR reviews +export const GetPullRequestReviewsSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + pull_number: z.number().describe("Pull request number") +}); + +export const PullRequestReviewSchema = z.object({ + id: z.number(), + node_id: z.string(), + user: GitHubIssueAssigneeSchema, + body: z.string().nullable(), + state: z.enum(['APPROVED', 'CHANGES_REQUESTED', 'COMMENTED', 'DISMISSED', 'PENDING']), + html_url: z.string(), + pull_request_url: z.string(), + commit_id: z.string(), + submitted_at: z.string().nullable(), + author_association: z.string() +}); + +export type GetPullRequestReviews = z.infer; +export type PullRequestReview = z.infer; From 25438f18c88744b232ef82c43a5dff8b597dd76c Mon Sep 17 00:00:00 2001 From: zhaoxinn Date: Fri, 10 Jan 2025 22:19:10 +0800 Subject: [PATCH 09/67] =?UTF-8?q?=F0=9F=93=9D=20docs(README):=20add=20Open?= =?UTF-8?q?CTI=20to=20list=20of=20MCP=20servers=20with=20alphabetic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5295cbb7..77d3b8e0 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ A growing set of community-developed and maintained servers demonstrates various - **[oatpp-mcp](https://github.com/oatpp/oatpp-mcp)** - C++ MCP integration for Oat++. Use [Oat++](https://oatpp.io) to build MCP servers. - **[Obsidian Markdown Notes](https://github.com/calclavia/mcp-obsidian)** - Read and search through your Obsidian vault or any directory containing Markdown notes - **[OpenAPI](https://github.com/snaggle-ai/openapi-mcp-server)** - Interact with [OpenAPI](https://www.openapis.org/) APIs. +- **[OpenCTI](https://github.com/Spathodea-Network/opencti-mcp)** - Interact with OpenCTI platform to retrieve threat intelligence data including reports, indicators, malware and threat actors. - **[OpenRPC](https://github.com/shanejonas/openrpc-mpc-server)** - Interact with and discover JSON-RPC APIs via [OpenRPC](https://open-rpc.org). - **[Pandoc](https://github.com/vivekVells/mcp-pandoc)** - MCP server for seamless document format conversion using Pandoc, supporting Markdown, HTML, and plain text, with other formats like PDF, csv and docx in development. - **[Pinecone](https://github.com/sirmews/mcp-pinecone)** - MCP server for searching and uploading records to Pinecone. Allows for simple RAG features, leveraging Pinecone's Inference API. From 15c0075a01d446ca123d817bd7adaf07a240f3b7 Mon Sep 17 00:00:00 2001 From: Shannon Lal Date: Fri, 10 Jan 2025 09:20:12 -0500 Subject: [PATCH 10/67] README: Adding Postman/Newman to community mcp servers for Readme --- README.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5295cbb7..f36d40f4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Model Context Protocol servers -This repository is a collection of *reference implementations* for the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP), as well as references +This repository is a collection of _reference implementations_ for the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP), as well as references to community built servers and additional resources. The servers in this repository showcase the versatility and extensibility of MCP, demonstrating how it can be used to give Large Language Models (LLMs) secure, controlled access to tools and data sources. @@ -113,6 +113,7 @@ A growing set of community-developed and maintained servers demonstrates various - **[Pandoc](https://github.com/vivekVells/mcp-pandoc)** - MCP server for seamless document format conversion using Pandoc, supporting Markdown, HTML, and plain text, with other formats like PDF, csv and docx in development. - **[Pinecone](https://github.com/sirmews/mcp-pinecone)** - MCP server for searching and uploading records to Pinecone. Allows for simple RAG features, leveraging Pinecone's Inference API. - **[Playwright](https://github.com/executeautomation/mcp-playwright)** - This MCP Server will help you run browser automation and webscraping using Playwright + **[Postman](https://github.com/shannonlal/mcp-postman)** - MCP server for running Postman Collections locally via Newman. Allows for simple execution of Postman Server and returns the results of whether the collection passed all the tests. - **[RAG Web Browser](https://github.com/apify/mcp-server-rag-web-browser)** An MCP server for Apify's RAG Web Browser Actor to perform web searches, scrape URLs, and return content in Markdown. - **[Rememberizer AI](https://github.com/skydeckai/mcp-server-rememberizer)** - An MCP server designed for interacting with the Rememberizer data source, facilitating enhanced knowledge retrieval. - **[Salesforce MCP](https://github.com/smn2gnt/MCP-Salesforce)** - Interact with Salesforce Data and Metadata @@ -132,8 +133,8 @@ A growing set of community-developed and maintained servers demonstrates various These are high-level frameworks that make it easier to build MCP servers. -* [EasyMCP](https://github.com/zcaceres/easy-mcp/) (TypeScript) -* [FastMCP](https://github.com/punkpeye/fastmcp) (TypeScript) +- [EasyMCP](https://github.com/zcaceres/easy-mcp/) (TypeScript) +- [FastMCP](https://github.com/punkpeye/fastmcp) (TypeScript) ## 📚 Resources @@ -159,9 +160,11 @@ Additional resources on MCP. ## 🚀 Getting Started ### Using MCP Servers in this Repository + Typescript-based servers in this repository can be used directly with `npx`. For example, this will start the [Memory](src/memory) server: + ```sh npx -y @modelcontextprotocol/server-memory ``` @@ -169,6 +172,7 @@ npx -y @modelcontextprotocol/server-memory Python-based servers in this repository can be used directly with [`uvx`](https://docs.astral.sh/uv/concepts/tools/) or [`pip`](https://pypi.org/project/pip/). `uvx` is recommended for ease of use and setup. For example, this will start the [Git](src/git) server: + ```sh # With uvx uvx mcp-server-git @@ -181,6 +185,7 @@ python -m mcp_server_git Follow [these](https://docs.astral.sh/uv/getting-started/installation/) instructions to install `uv` / `uvx` and [these](https://pip.pypa.io/en/stable/installation/) to install `pip`. ### Using an MCP Client + However, running a server on its own isn't very useful, and should instead be configured into an MCP client. For example, here's the Claude Desktop configuration to use the above server: ```json @@ -201,7 +206,11 @@ Additional examples of using the Claude Desktop as an MCP client might look like "mcpServers": { "filesystem": { "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/files"] + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/path/to/allowed/files" + ] }, "git": { "command": "uvx", @@ -216,7 +225,11 @@ Additional examples of using the Claude Desktop as an MCP client might look like }, "postgres": { "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"] + "args": [ + "-y", + "@modelcontextprotocol/server-postgres", + "postgresql://localhost/mydb" + ] } } } From 65ab0a30d0760be6f4aa9c96ae99ec8fade592b0 Mon Sep 17 00:00:00 2001 From: Shannon Lal Date: Fri, 10 Jan 2025 09:30:17 -0500 Subject: [PATCH 11/67] Update README.md Reverting linting formating issues --- README.md | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ccf3ec18..e7732a0e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Model Context Protocol servers -This repository is a collection of _reference implementations_ for the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP), as well as references +This repository is a collection of *reference implementations* for the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP), as well as references to community built servers and additional resources. The servers in this repository showcase the versatility and extensibility of MCP, demonstrating how it can be used to give Large Language Models (LLMs) secure, controlled access to tools and data sources. @@ -114,7 +114,7 @@ A growing set of community-developed and maintained servers demonstrates various - **[Pandoc](https://github.com/vivekVells/mcp-pandoc)** - MCP server for seamless document format conversion using Pandoc, supporting Markdown, HTML, and plain text, with other formats like PDF, csv and docx in development. - **[Pinecone](https://github.com/sirmews/mcp-pinecone)** - MCP server for searching and uploading records to Pinecone. Allows for simple RAG features, leveraging Pinecone's Inference API. - **[Playwright](https://github.com/executeautomation/mcp-playwright)** - This MCP Server will help you run browser automation and webscraping using Playwright - **[Postman](https://github.com/shannonlal/mcp-postman)** - MCP server for running Postman Collections locally via Newman. Allows for simple execution of Postman Server and returns the results of whether the collection passed all the tests. +- **[Postman](https://github.com/shannonlal/mcp-postman)** - MCP server for running Postman Collections locally via Newman. Allows for simple execution of Postman Server and returns the results of whether the collection passed all the tests. - **[RAG Web Browser](https://github.com/apify/mcp-server-rag-web-browser)** An MCP server for Apify's RAG Web Browser Actor to perform web searches, scrape URLs, and return content in Markdown. - **[Rememberizer AI](https://github.com/skydeckai/mcp-server-rememberizer)** - An MCP server designed for interacting with the Rememberizer data source, facilitating enhanced knowledge retrieval. - **[Salesforce MCP](https://github.com/smn2gnt/MCP-Salesforce)** - Interact with Salesforce Data and Metadata @@ -134,8 +134,8 @@ A growing set of community-developed and maintained servers demonstrates various These are high-level frameworks that make it easier to build MCP servers. -- [EasyMCP](https://github.com/zcaceres/easy-mcp/) (TypeScript) -- [FastMCP](https://github.com/punkpeye/fastmcp) (TypeScript) +- * [EasyMCP](https://github.com/zcaceres/easy-mcp/) (TypeScript) +- * [FastMCP](https://github.com/punkpeye/fastmcp) (TypeScript) ## 📚 Resources @@ -161,11 +161,9 @@ Additional resources on MCP. ## 🚀 Getting Started ### Using MCP Servers in this Repository - Typescript-based servers in this repository can be used directly with `npx`. For example, this will start the [Memory](src/memory) server: - ```sh npx -y @modelcontextprotocol/server-memory ``` @@ -186,7 +184,6 @@ python -m mcp_server_git Follow [these](https://docs.astral.sh/uv/getting-started/installation/) instructions to install `uv` / `uvx` and [these](https://pip.pypa.io/en/stable/installation/) to install `pip`. ### Using an MCP Client - However, running a server on its own isn't very useful, and should instead be configured into an MCP client. For example, here's the Claude Desktop configuration to use the above server: ```json @@ -207,11 +204,7 @@ Additional examples of using the Claude Desktop as an MCP client might look like "mcpServers": { "filesystem": { "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-filesystem", - "/path/to/allowed/files" - ] + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/files"] }, "git": { "command": "uvx", @@ -226,11 +219,7 @@ Additional examples of using the Claude Desktop as an MCP client might look like }, "postgres": { "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-postgres", - "postgresql://localhost/mydb" - ] + "args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"] } } } From 22c4649e79fe189b4ce6a723d830ed848420c4f6 Mon Sep 17 00:00:00 2001 From: Shannon Lal Date: Fri, 10 Jan 2025 09:31:06 -0500 Subject: [PATCH 12/67] Update README.md Removing old formatting issue --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e7732a0e..ba0f5281 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,6 @@ npx -y @modelcontextprotocol/server-memory Python-based servers in this repository can be used directly with [`uvx`](https://docs.astral.sh/uv/concepts/tools/) or [`pip`](https://pypi.org/project/pip/). `uvx` is recommended for ease of use and setup. For example, this will start the [Git](src/git) server: - ```sh # With uvx uvx mcp-server-git From 50abbb0f88686167008a4417f4fb0c9c89a3d79f Mon Sep 17 00:00:00 2001 From: Shannon Lal Date: Fri, 10 Jan 2025 09:32:03 -0500 Subject: [PATCH 13/67] Update README.md Reverting last Formatting issue --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba0f5281..e9ba3f05 100644 --- a/README.md +++ b/README.md @@ -134,8 +134,8 @@ A growing set of community-developed and maintained servers demonstrates various These are high-level frameworks that make it easier to build MCP servers. -- * [EasyMCP](https://github.com/zcaceres/easy-mcp/) (TypeScript) -- * [FastMCP](https://github.com/punkpeye/fastmcp) (TypeScript) +* [EasyMCP](https://github.com/zcaceres/easy-mcp/) (TypeScript) +* [FastMCP](https://github.com/punkpeye/fastmcp) (TypeScript) ## 📚 Resources From 137b794603de0ff22395191e184afc67d1a0ea51 Mon Sep 17 00:00:00 2001 From: lyx Date: Sun, 12 Jan 2025 09:46:17 +0800 Subject: [PATCH 14/67] update dify mcp server --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 21f4d4bf..fd6ef6d7 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ A growing set of community-developed and maintained servers demonstrates various - **[Contentful-mcp](https://github.com/ivo-toby/contentful-mcp)** - Read, update, delete, publish content in your [Contentful](https://contentful.com) space(s) from this MCP Server. - **[Data Exploration](https://github.com/reading-plus-ai/mcp-server-data-exploration)** - MCP server for autonomous data exploration on .csv-based datasets, providing intelligent insights with minimal effort. NOTE: Will execute arbitrary Python code on your machine, please use with caution! - **[DevRev](https://github.com/kpsunil97/devrev-mcp-server)** - An MCP server to integrate with DevRev APIs to search through your DevRev Knowledge Graph where objects can be imported from diff. sources listed [here](https://devrev.ai/docs/import#available-sources). +- **[Dify](https://github.com/YanxingLiu/dify-mcp-server)** - A simple implementation of an MCP server for dify workflows. - **[Docker](https://github.com/ckreiling/mcp-server-docker)** - Integrate with Docker to manage containers, images, volumes, and networks. - **[Elasticsearch](https://github.com/cr7258/elasticsearch-mcp-server)** - MCP server implementation that provides Elasticsearch interaction. - **[Fetch](https://github.com/zcaceres/fetch-mcp)** - A server that flexibly fetches HTML, JSON, Markdown, or plaintext. From 9304a9fb9f63f0436c3a9bd89bf4a0d72fd78e3f Mon Sep 17 00:00:00 2001 From: Felo Restrepo <44730261+felores@users.noreply.github.com> Date: Sun, 12 Jan 2025 20:31:34 -0500 Subject: [PATCH 15/67] Placid.app MCP server added MCP server to generate image and video creatives using the Placid.app templates dynamic layers and retrieving the final creative media URL --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 21f4d4bf..52d03495 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ A growing set of community-developed and maintained servers demonstrates various - **[OpenRPC](https://github.com/shanejonas/openrpc-mpc-server)** - Interact with and discover JSON-RPC APIs via [OpenRPC](https://open-rpc.org). - **[Pandoc](https://github.com/vivekVells/mcp-pandoc)** - MCP server for seamless document format conversion using Pandoc, supporting Markdown, HTML, and plain text, with other formats like PDF, csv and docx in development. - **[Pinecone](https://github.com/sirmews/mcp-pinecone)** - MCP server for searching and uploading records to Pinecone. Allows for simple RAG features, leveraging Pinecone's Inference API. +- **[Placid.app](https://github.com/felores/placid-mcp-server)** - Generate image and video creatives using Placid.app templates - **[Playwright](https://github.com/executeautomation/mcp-playwright)** - This MCP Server will help you run browser automation and webscraping using Playwright - **[RAG Web Browser](https://github.com/apify/mcp-server-rag-web-browser)** An MCP server for Apify's RAG Web Browser Actor to perform web searches, scrape URLs, and return content in Markdown. - **[Rememberizer AI](https://github.com/skydeckai/mcp-server-rememberizer)** - An MCP server designed for interacting with the Rememberizer data source, facilitating enhanced knowledge retrieval. From c9b1adf3b6a690cd9f8faa046354eabe8da49481 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Mon, 13 Jan 2025 11:47:41 +0000 Subject: [PATCH 16/67] use environments --- .github/workflows/release-check.yml | 25 ++++++++++++++++++++++++- .github/workflows/release.yml | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index 30ff1f09..d4fc9ed1 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -54,4 +54,27 @@ jobs: - name: Check release run: | - uv run --script scripts/release.py --dry-run "${{ matrix.directory }}" "${{ needs.prepare.outputs.last_release }}" + uv run --script scripts/release.py --dry-run "${{ matrix.directory }}" "${{ needs.prepare.outputs.last_release }}" | tee -a "$GITHUB_OUTPUT" + + check-tag: + needs: [prepare, check-release] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Simulate tag creation + run: | + if [ -s "$GITHUB_OUTPUT" ]; then + DATE=$(date +%Y.%m.%d) + echo "🔍 Dry run: Would create tag v${DATE} if this was a real release" + + echo "# Release ${DATE}" > notes.md + echo "" >> notes.md + echo "## Updated Packages" >> notes.md + while IFS= read -r line; do + echo "- ${line}" >> notes.md + done < "$GITHUB_OUTPUT" + + echo "🔍 Would create release with following notes:" + cat notes.md + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 45f3aadd..d2d808b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,7 @@ jobs: release: needs: prepare runs-on: ubuntu-latest + environment: release strategy: matrix: directory: ${{ fromJson(needs.prepare.outputs.matrix) }} From 9f86ee599876c5540444bacd77cdc2ebe7dfb06e Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Mon, 13 Jan 2025 13:01:46 +0000 Subject: [PATCH 17/67] fix: fix scripts --- .github/workflows/release-check.yml | 18 ++++++++++++++---- .github/workflows/release.yml | 2 +- scripts/release.py | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index d4fc9ed1..005f460c 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -30,6 +30,8 @@ jobs: check-release: needs: prepare runs-on: ubuntu-latest + outputs: + release: ${{ steps.check.outputs.release }} strategy: matrix: directory: ${{ fromJson(needs.prepare.outputs.matrix) }} @@ -53,8 +55,15 @@ jobs: run: uv python install - name: Check release + id: check run: | - uv run --script scripts/release.py --dry-run "${{ matrix.directory }}" "${{ needs.prepare.outputs.last_release }}" | tee -a "$GITHUB_OUTPUT" + output=$(uv run --script scripts/release.py --dry-run "${{ matrix.directory }}" "${{ needs.prepare.outputs.last_release }}" \ + | grep -o -E "[a-zA-Z0-9\-]+@[0-9]+\.[0-9]+\.[0-9]+" || true) + if [ ! -z "$output" ]; then + echo "release<> $GITHUB_OUTPUT + echo "$output" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi check-tag: needs: [prepare, check-release] @@ -64,7 +73,8 @@ jobs: - name: Simulate tag creation run: | - if [ -s "$GITHUB_OUTPUT" ]; then + echo "${{ needs.check-release.outputs.release }}" > packages.txt + if [ -s packages.txt ]; then DATE=$(date +%Y.%m.%d) echo "🔍 Dry run: Would create tag v${DATE} if this was a real release" @@ -72,8 +82,8 @@ jobs: echo "" >> notes.md echo "## Updated Packages" >> notes.md while IFS= read -r line; do - echo "- ${line}" >> notes.md - done < "$GITHUB_OUTPUT" + echo "- $line" >> notes.md + done < packages.txt echo "🔍 Would create release with following notes:" cat notes.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d2d808b4..d2d084de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,7 +64,7 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }} - run: uv run --script scripts/release.py "${{ matrix.directory }}" "${{ needs.prepare.outputs.last_release }}" >> "$GITHUB_OUTPUT" + run: uv run --script scripts/release.py "${{ matrix.directory }}" "${{ needs.prepare.outputs.last_release }}" >> "$GITHUB_OUTPUT" create-release: needs: [prepare, release] diff --git a/scripts/release.py b/scripts/release.py index 0853d36e..17329560 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -171,7 +171,7 @@ def main(directory: Path, git_hash: GitHash, dry_run: bool) -> int: if not dry_run: click.echo(f"{name}@{version}") else: - click.echo(f"🔍 Dry run: Would have published {name}@{version} if this was a real release") + click.echo(f"Dry run: Would have published {name}@{version}") return 0 except Exception as e: return 1 From 111806d6ae1cc80655cfef0d37ce5b7e0be35fed Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Mon, 13 Jan 2025 14:33:22 +0000 Subject: [PATCH 18/67] feat: use artifcats to collect mutliple job outputs into one A matrix job cant have multiple outputs directly, see https://github.com/orgs/community/discussions/17245. Use the artifact workaround. --- .github/workflows/release-check.yml | 113 ++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 22 deletions(-) diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index 005f460c..63d7dfa0 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -18,20 +18,21 @@ jobs: - name: Find package directories id: set-matrix run: | + # Find all package.json and pyproject.toml files, excluding root DIRS=$(git ls-tree -r HEAD --name-only | grep -E "package.json|pyproject.toml" | xargs dirname | grep -v "^.$" | jq -R -s -c 'split("\n")[:-1]') echo "matrix=${DIRS}" >> $GITHUB_OUTPUT + echo "Found directories: ${DIRS}" - name: Get last release hash id: last-release run: | HASH=$(git rev-list --tags --max-count=1 || echo "HEAD~1") echo "hash=${HASH}" >> $GITHUB_OUTPUT + echo "Using last release hash: ${HASH}" check-release: needs: prepare runs-on: ubuntu-latest - outputs: - release: ${{ steps.check.outputs.release }} strategy: matrix: directory: ${{ fromJson(needs.prepare.outputs.matrix) }} @@ -45,46 +46,114 @@ jobs: - uses: astral-sh/setup-uv@v5 - name: Setup Node.js - if: endsWith(matrix.directory, 'package.json') + if: endsWith(matrix.directory, '/package.json') uses: actions/setup-node@v4 with: node-version: '18' - name: Setup Python - if: endsWith(matrix.directory, 'pyproject.toml') + if: endsWith(matrix.directory, '/pyproject.toml') run: uv python install - name: Check release id: check run: | - output=$(uv run --script scripts/release.py --dry-run "${{ matrix.directory }}" "${{ needs.prepare.outputs.last_release }}" \ - | grep -o -E "[a-zA-Z0-9\-]+@[0-9]+\.[0-9]+\.[0-9]+" || true) - if [ ! -z "$output" ]; then - echo "release<> $GITHUB_OUTPUT - echo "$output" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + # Create unique hash for this directory + dir_hash=$(echo "${{ matrix.directory }}" | sha256sum | awk '{print $1}') + + # Run release check script with verbose output + echo "Running release check against last release: ${{ needs.prepare.outputs.last_release }}" + + # Run git diff first to show changes + echo "Changes since last release:" + git diff --name-only "${{ needs.prepare.outputs.last_release }}" -- "${{ matrix.directory }}" || true + + # Run the release check + output=$(uv run --script scripts/release.py --dry-run "${{ matrix.directory }}" "${{ needs.prepare.outputs.last_release }}" 2>&1) + exit_code=$? + + echo "Release check output (exit code: $exit_code):" + echo "$output" + + # Extract package info if successful + if [ $exit_code -eq 0 ]; then + pkg_info=$(echo "$output" | grep -o -E "[a-zA-Z0-9\-]+@[0-9]+\.[0-9]+\.[0-9]+" || true) + else + echo "Release check failed" + exit 1 fi + if [ ! -z "$pkg_info" ]; then + echo "Found package that needs release: $pkg_info" + + # Create outputs directory + mkdir -p ./outputs + + # Save both package info and full changes + echo "$pkg_info" > "./outputs/${dir_hash}_info" + echo "dir_hash=${dir_hash}" >> $GITHUB_OUTPUT + + # Log what we're saving + echo "Saved package info to ./outputs/${dir_hash}_info:" + cat "./outputs/${dir_hash}_info" + else + echo "No release needed for this package" + fi + + - name: Set artifact name + if: steps.check.outputs.dir_hash + id: artifact + run: | + # Replace forward slashes with dashes + SAFE_DIR=$(echo "${{ matrix.directory }}" | tr '/' '-') + echo "name=release-outputs-${SAFE_DIR}" >> $GITHUB_OUTPUT + + - uses: actions/upload-artifact@v4 + if: steps.check.outputs.dir_hash + with: + name: ${{ steps.artifact.outputs.name }} + path: ./outputs/${{ steps.check.outputs.dir_hash }}* + check-tag: needs: [prepare, check-release] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: release-outputs-src-* + merge-multiple: true + path: outputs + - name: Simulate tag creation run: | - echo "${{ needs.check-release.outputs.release }}" > packages.txt - if [ -s packages.txt ]; then - DATE=$(date +%Y.%m.%d) - echo "🔍 Dry run: Would create tag v${DATE} if this was a real release" + if [ -d outputs ]; then + # Collect package info + find outputs -name "*_info" -exec cat {} \; > packages.txt - echo "# Release ${DATE}" > notes.md - echo "" >> notes.md - echo "## Updated Packages" >> notes.md - while IFS= read -r line; do - echo "- $line" >> notes.md - done < packages.txt + if [ -s packages.txt ]; then + DATE=$(date +%Y.%m.%d) + echo "🔍 Dry run: Would create tag v${DATE} if this was a real release" - echo "🔍 Would create release with following notes:" - cat notes.md + # Generate comprehensive release notes + { + echo "# Release ${DATE}" + echo "" + echo "## Updated Packages" + while IFS= read -r line; do + echo "- $line" + done < packages.txt + } > notes.md + + echo "🔍 Would create release with following notes:" + cat notes.md + + echo "🔍 Would create tag v${DATE} with the above release notes" + echo "🔍 Would create GitHub release from tag v${DATE}" + else + echo "No packages need release" + fi + else + echo "No release artifacts found" fi From cecd241500e0174761f8daf4dc98fd15cbbffe02 Mon Sep 17 00:00:00 2001 From: Peter M Elias Date: Fri, 27 Dec 2024 18:14:37 -0800 Subject: [PATCH 19/67] fix github PR schemas --- src/github/schemas.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 0a322328..dca82777 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -38,19 +38,25 @@ export const GitHubRepositorySchema = z.object({ default_branch: z.string(), }); +const GithubFileContentLinks = z.object({ + self: z.string(), + git:z.number().nullable(), + html: z.string().nullable() +}); + // 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(), + content: z.string().nullable(), sha: z.string(), url: z.string(), git_url: z.string(), html_url: z.string(), download_url: z.string(), + _links: GithubFileContentLinks }); export const GitHubDirectoryContentSchema = z.object({ From 59b831f3267d2a0beb49a5faec77aa806ef77f22 Mon Sep 17 00:00:00 2001 From: Peter M Elias Date: Fri, 27 Dec 2024 23:28:54 -0800 Subject: [PATCH 20/67] fix github getfilecontent zod schema to match readme spec --- src/github/schemas.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/github/schemas.ts b/src/github/schemas.ts index dca82777..24a1ef49 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -367,6 +367,8 @@ export const CreateRepositorySchema = z.object({ }); export const GetFileContentsSchema = RepoParamsSchema.extend({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), path: z.string().describe("Path to the file or directory"), branch: z.string().optional().describe("Branch to get contents from"), }); From 90265c27d2b8ab7c5c2a3848cb295990ab4fda2b Mon Sep 17 00:00:00 2001 From: Peter M Elias Date: Fri, 27 Dec 2024 23:53:39 -0800 Subject: [PATCH 21/67] schema tweaks --- src/github/schemas.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 24a1ef49..434ad08b 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -40,7 +40,7 @@ export const GitHubRepositorySchema = z.object({ const GithubFileContentLinks = z.object({ self: z.string(), - git:z.number().nullable(), + git: z.number().nullable(), html: z.string().nullable() }); @@ -50,7 +50,7 @@ export const GitHubFileContentSchema = z.object({ size: z.number(), name: z.string(), path: z.string(), - content: z.string().nullable(), + content: z.string(), sha: z.string(), url: z.string(), git_url: z.string(), @@ -367,8 +367,6 @@ export const CreateRepositorySchema = z.object({ }); export const GetFileContentsSchema = RepoParamsSchema.extend({ - owner: z.string().describe("Repository owner (username or organization)"), - repo: z.string().describe("Repository name"), path: z.string().describe("Path to the file or directory"), branch: z.string().optional().describe("Branch to get contents from"), }); From a56242dfdc61d82f65805d995b64df5c1ef74ba3 Mon Sep 17 00:00:00 2001 From: Peter M Elias Date: Sat, 28 Dec 2024 00:06:24 -0800 Subject: [PATCH 22/67] more schema fixes --- src/github/schemas.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 434ad08b..c361608f 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -46,16 +46,17 @@ const GithubFileContentLinks = z.object({ // File content schemas export const GitHubFileContentSchema = z.object({ - type: z.string(), - size: z.number(), name: z.string(), path: z.string(), - content: z.string(), sha: z.string(), + size: z.number(), url: z.string(), - git_url: z.string(), html_url: z.string(), + git_url: z.string(), download_url: z.string(), + type: z.string(), + content: z.string(), + encoding: z.string().nullable(), _links: GithubFileContentLinks }); From d9ae0911b9034103e5959ff16fb6290ca3ec371b Mon Sep 17 00:00:00 2001 From: Peter M Elias Date: Sat, 28 Dec 2024 00:13:29 -0800 Subject: [PATCH 23/67] more schema fixes --- src/github/schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/github/schemas.ts b/src/github/schemas.ts index c361608f..d7b45bd4 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -56,7 +56,7 @@ export const GitHubFileContentSchema = z.object({ download_url: z.string(), type: z.string(), content: z.string(), - encoding: z.string().nullable(), + encoding: z.string().optional(), _links: GithubFileContentLinks }); From f4122ff231fa3d07d497d082dd2e43374eb8c54b Mon Sep 17 00:00:00 2001 From: Peter M Elias Date: Sat, 28 Dec 2024 01:11:04 -0800 Subject: [PATCH 24/67] more fixes --- src/github/index.ts | 9 +-------- src/github/schemas.ts | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/github/index.ts b/src/github/index.ts index 3759e8b5..d2952797 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -1016,14 +1016,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } } catch (error) { if (error instanceof z.ZodError) { - throw new Error( - `Invalid arguments: ${error.errors - .map( - (e: z.ZodError["errors"][number]) => - `${e.path.join(".")}: ${e.message}` - ) - .join(", ")}` - ); + throw new Error(`ZodErrors: ${JSON.stringify(error.errors)}`) } throw error; } diff --git a/src/github/schemas.ts b/src/github/schemas.ts index d7b45bd4..61560cf8 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -40,7 +40,7 @@ export const GitHubRepositorySchema = z.object({ const GithubFileContentLinks = z.object({ self: z.string(), - git: z.number().nullable(), + git: z.string().nullable(), html: z.string().nullable() }); From 0ecd2049abf51e49383ab395418ecf7c6a0ed709 Mon Sep 17 00:00:00 2001 From: Peter M Elias Date: Sat, 28 Dec 2024 01:25:15 -0800 Subject: [PATCH 25/67] more 'fixes' --- src/github/schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/github/schemas.ts b/src/github/schemas.ts index 61560cf8..d911104a 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -55,7 +55,7 @@ export const GitHubFileContentSchema = z.object({ git_url: z.string(), download_url: z.string(), type: z.string(), - content: z.string(), + content: z.string().optional(), encoding: z.string().optional(), _links: GithubFileContentLinks }); From 534b90cfe0b927edb53127e955c880d959bc7cf1 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 01:40:57 -0800 Subject: [PATCH 26/67] Add common type definitions --- src/github/common/types.ts | 132 +++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/github/common/types.ts diff --git a/src/github/common/types.ts b/src/github/common/types.ts new file mode 100644 index 00000000..9be60a74 --- /dev/null +++ b/src/github/common/types.ts @@ -0,0 +1,132 @@ +import { z } from "zod"; + +// Base schemas for common types +export const GitHubAuthorSchema = z.object({ + name: z.string(), + email: z.string(), + date: z.string(), +}); + +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(), +}); + +export const GithubFileContentLinks = z.object({ + self: z.string(), + git: z.string().nullable(), + html: z.string().nullable() +}); + +export const GitHubFileContentSchema = z.object({ + name: z.string(), + path: z.string(), + sha: z.string(), + size: z.number(), + url: z.string(), + html_url: z.string(), + git_url: z.string(), + download_url: z.string(), + type: z.string(), + content: z.string().optional(), + encoding: z.string().optional(), + _links: GithubFileContentLinks +}); + +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), +]); + +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(), + }) + ), +}); + +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(), + }), +}); + +// Export types +export type GitHubAuthor = z.infer; +export type GitHubRepository = z.infer; +export type GitHubFileContent = z.infer; +export type GitHubDirectoryContent = z.infer; +export type GitHubContent = z.infer; +export type GitHubTree = z.infer; +export type GitHubCommit = z.infer; +export type GitHubReference = z.infer; \ No newline at end of file From ca2c6f93241bff6557a7d61d4026f25914ec75f4 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 01:41:07 -0800 Subject: [PATCH 27/67] Add common utilities for GitHub API requests --- src/github/common/utils.ts | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/github/common/utils.ts diff --git a/src/github/common/utils.ts b/src/github/common/utils.ts new file mode 100644 index 00000000..0e9e6526 --- /dev/null +++ b/src/github/common/utils.ts @@ -0,0 +1,46 @@ +import fetch from "node-fetch"; + +if (!process.env.GITHUB_PERSONAL_ACCESS_TOKEN) { + console.error("GITHUB_PERSONAL_ACCESS_TOKEN environment variable is not set"); + process.exit(1); +} + +export const GITHUB_PERSONAL_ACCESS_TOKEN = process.env.GITHUB_PERSONAL_ACCESS_TOKEN; + +interface GitHubRequestOptions { + method?: string; + body?: any; +} + +export async function githubRequest(url: string, options: GitHubRequestOptions = {}) { + const response = await fetch(url, { + method: options.method || "GET", + headers: { + Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "github-mcp-server", + ...(options.body ? { "Content-Type": "application/json" } : {}), + }, + ...(options.body ? { body: JSON.stringify(options.body) } : {}), + }); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.statusText}`); + } + + return response.json(); +} + +export function buildUrl(baseUrl: string, params: Record = {}) { + const url = new URL(baseUrl); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + if (Array.isArray(value)) { + url.searchParams.append(key, value.join(",")); + } else { + url.searchParams.append(key, value.toString()); + } + } + }); + return url.toString(); +} \ No newline at end of file From 150e9cc560baa97fe9c592595320dc95471fb5cd Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 01:41:21 -0800 Subject: [PATCH 28/67] Add repository operations module --- src/github/operations/repository.ts | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/github/operations/repository.ts diff --git a/src/github/operations/repository.ts b/src/github/operations/repository.ts new file mode 100644 index 00000000..dfa7e263 --- /dev/null +++ b/src/github/operations/repository.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; +import { githubRequest } from "../common/utils"; +import { GitHubRepositorySchema, GitHubSearchResponseSchema } from "../common/types"; + +// Schema definitions +export const CreateRepositoryOptionsSchema = 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 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 ForkRepositorySchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + organization: z.string().optional().describe("Optional: organization to fork to (defaults to your personal account)"), +}); + +// Type exports +export type CreateRepositoryOptions = z.infer; + +// Function implementations +export async function createRepository(options: CreateRepositoryOptions) { + const response = await githubRequest("https://api.github.com/user/repos", { + method: "POST", + body: options, + }); + return GitHubRepositorySchema.parse(response); +} + +export async function searchRepositories( + query: string, + page: number = 1, + perPage: number = 30 +) { + 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 githubRequest(url.toString()); + return GitHubSearchResponseSchema.parse(response); +} + +export async function forkRepository( + owner: string, + repo: string, + organization?: string +) { + const url = organization + ? `https://api.github.com/repos/${owner}/${repo}/forks?organization=${organization}` + : `https://api.github.com/repos/${owner}/${repo}/forks`; + + const response = await githubRequest(url, { method: "POST" }); + return GitHubRepositorySchema.extend({ + parent: GitHubRepositorySchema, + source: GitHubRepositorySchema, + }).parse(response); +} \ No newline at end of file From ee874d7b5b08376fcf08f2c08344ccd14b26c822 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 01:42:46 -0800 Subject: [PATCH 29/67] Add file operations module --- src/github/operations/files.ts | 193 +++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 src/github/operations/files.ts diff --git a/src/github/operations/files.ts b/src/github/operations/files.ts new file mode 100644 index 00000000..676e9374 --- /dev/null +++ b/src/github/operations/files.ts @@ -0,0 +1,193 @@ +import { z } from "zod"; +import { githubRequest } from "../common/utils"; +import { + GitHubContentSchema, + GitHubCreateUpdateFileResponseSchema, + GitHubTreeSchema, + GitHubCommitSchema, + GitHubReferenceSchema, +} from "../common/types"; + +// Schema definitions +export const FileOperationSchema = z.object({ + path: z.string(), + content: z.string(), +}); + +export const CreateOrUpdateFileSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + path: z.string().describe("Path where to create/update the file"), + content: z.string().describe("Content of the file"), + message: z.string().describe("Commit message"), + branch: z.string().describe("Branch to create/update the file in"), + sha: z.string().optional().describe("SHA of the file being replaced (required when updating existing files)"), +}); + +export const GetFileContentsSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + path: z.string().describe("Path to the file or directory"), + branch: z.string().optional().describe("Branch to get contents from"), +}); + +export const PushFilesSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + branch: z.string().describe("Branch to push to (e.g., 'main' or 'master')"), + files: z.array(FileOperationSchema).describe("Array of files to push"), + message: z.string().describe("Commit message"), +}); + +// Type exports +export type FileOperation = z.infer; + +// Function implementations +export async function getFileContents( + owner: string, + repo: string, + path: string, + branch?: string +) { + let url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`; + if (branch) { + url += `?ref=${branch}`; + } + + const response = await githubRequest(url); + const data = GitHubContentSchema.parse(response); + + // If it's a file, decode the content + if (!Array.isArray(data) && data.content) { + data.content = Buffer.from(data.content, "base64").toString("utf8"); + } + + return data; +} + +export async function createOrUpdateFile( + owner: string, + repo: string, + path: string, + content: string, + message: string, + branch: string, + sha?: string +) { + const encodedContent = Buffer.from(content).toString("base64"); + + let currentSha = sha; + if (!currentSha) { + try { + const existingFile = await getFileContents(owner, repo, path, branch); + if (!Array.isArray(existingFile)) { + currentSha = existingFile.sha; + } + } catch (error) { + 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 } : {}), + }; + + const response = await githubRequest(url, { + method: "PUT", + body, + }); + + return GitHubCreateUpdateFileResponseSchema.parse(response); +} + +async function createTree( + owner: string, + repo: string, + files: FileOperation[], + baseTree?: string +) { + const tree = files.map((file) => ({ + path: file.path, + mode: "100644" as const, + type: "blob" as const, + content: file.content, + })); + + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/git/trees`, + { + method: "POST", + body: { + tree, + base_tree: baseTree, + }, + } + ); + + return GitHubTreeSchema.parse(response); +} + +async function createCommit( + owner: string, + repo: string, + message: string, + tree: string, + parents: string[] +) { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/git/commits`, + { + method: "POST", + body: { + message, + tree, + parents, + }, + } + ); + + return GitHubCommitSchema.parse(response); +} + +async function updateReference( + owner: string, + repo: string, + ref: string, + sha: string +) { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/git/refs/${ref}`, + { + method: "PATCH", + body: { + sha, + force: true, + }, + } + ); + + return GitHubReferenceSchema.parse(response); +} + +export async function pushFiles( + owner: string, + repo: string, + branch: string, + files: FileOperation[], + message: string +) { + const refResponse = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}` + ); + + const ref = GitHubReferenceSchema.parse(refResponse); + 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); +} \ No newline at end of file From 2218a0f442e29bd9f274221efe3269688b571a18 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 01:43:04 -0800 Subject: [PATCH 30/67] Add issues operations module --- src/github/operations/issues.ts | 151 ++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/github/operations/issues.ts diff --git a/src/github/operations/issues.ts b/src/github/operations/issues.ts new file mode 100644 index 00000000..e489e747 --- /dev/null +++ b/src/github/operations/issues.ts @@ -0,0 +1,151 @@ +import { z } from "zod"; +import { githubRequest, buildUrl } from "../common/utils"; +import { + GitHubIssueSchema, + GitHubLabelSchema, + GitHubIssueAssigneeSchema, + GitHubMilestoneSchema, +} from "../common/types"; + +// Schema definitions +export const CreateIssueOptionsSchema = z.object({ + 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"), + milestone: z.number().optional().describe("Milestone number to assign"), + labels: z.array(z.string()).optional().describe("Array of label names"), +}); + +export const CreateIssueSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + 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 ListIssuesOptionsSchema = z.object({ + owner: z.string(), + repo: z.string(), + state: z.enum(['open', 'closed', 'all']).optional(), + labels: z.array(z.string()).optional(), + sort: z.enum(['created', 'updated', 'comments']).optional(), + direction: z.enum(['asc', 'desc']).optional(), + since: z.string().optional(), // ISO 8601 timestamp + page: z.number().optional(), + per_page: z.number().optional() +}); + +export const UpdateIssueOptionsSchema = z.object({ + owner: z.string(), + repo: z.string(), + issue_number: z.number(), + title: z.string().optional(), + body: z.string().optional(), + state: z.enum(['open', 'closed']).optional(), + labels: z.array(z.string()).optional(), + assignees: z.array(z.string()).optional(), + milestone: z.number().optional() +}); + +export const IssueCommentSchema = z.object({ + owner: z.string(), + repo: z.string(), + issue_number: z.number(), + body: z.string() +}); + +export const GetIssueSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + issue_number: z.number().describe("Issue number") +}); + +// Type exports +export type CreateIssueOptions = z.infer; +export type ListIssuesOptions = z.infer; +export type UpdateIssueOptions = z.infer; + +// Function implementations +export async function createIssue( + owner: string, + repo: string, + options: CreateIssueOptions +) { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/issues`, + { + method: "POST", + body: options, + } + ); + + return GitHubIssueSchema.parse(response); +} + +export async function listIssues( + owner: string, + repo: string, + options: Omit +) { + const url = buildUrl(`https://api.github.com/repos/${owner}/${repo}/issues`, options); + const response = await githubRequest(url); + return z.array(GitHubIssueSchema).parse(response); +} + +export async function updateIssue( + owner: string, + repo: string, + issueNumber: number, + options: Omit +) { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`, + { + method: "PATCH", + body: options + } + ); + + return GitHubIssueSchema.parse(response); +} + +export async function addIssueComment( + owner: string, + repo: string, + issueNumber: number, + body: string +) { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`, + { + method: "POST", + body: { body } + } + ); + + return z.object({ + id: z.number(), + node_id: z.string(), + url: z.string(), + html_url: z.string(), + body: z.string(), + user: GitHubIssueAssigneeSchema, + created_at: z.string(), + updated_at: z.string(), + }).parse(response); +} + +export async function getIssue( + owner: string, + repo: string, + issueNumber: number +) { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}` + ); + + return GitHubIssueSchema.parse(response); +} \ No newline at end of file From d751289f9cb09b2e8707afee62973ce4960469fc Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 01:43:46 -0800 Subject: [PATCH 31/67] Add branches operations module --- src/github/operations/branches.ts | 112 ++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 src/github/operations/branches.ts diff --git a/src/github/operations/branches.ts b/src/github/operations/branches.ts new file mode 100644 index 00000000..4690ef24 --- /dev/null +++ b/src/github/operations/branches.ts @@ -0,0 +1,112 @@ +import { z } from "zod"; +import { githubRequest } from "../common/utils"; +import { GitHubReferenceSchema } from "../common/types"; + +// Schema definitions +export const CreateBranchOptionsSchema = z.object({ + ref: z.string(), + sha: z.string(), +}); + +export const CreateBranchSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + 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)"), +}); + +// Type exports +export type CreateBranchOptions = z.infer; + +// Function implementations +export async function getDefaultBranchSHA(owner: string, repo: string): Promise { + try { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/main` + ); + const data = GitHubReferenceSchema.parse(response); + return data.object.sha; + } catch (error) { + const masterResponse = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/master` + ); + if (!masterResponse) { + throw new Error("Could not find default branch (tried 'main' and 'master')"); + } + const data = GitHubReferenceSchema.parse(masterResponse); + return data.object.sha; + } +} + +export async function createBranch( + owner: string, + repo: string, + options: CreateBranchOptions +): Promise> { + const fullRef = `refs/heads/${options.ref}`; + + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/git/refs`, + { + method: "POST", + body: { + ref: fullRef, + sha: options.sha, + }, + } + ); + + return GitHubReferenceSchema.parse(response); +} + +export async function getBranchSHA( + owner: string, + repo: string, + branch: string +): Promise { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}` + ); + + const data = GitHubReferenceSchema.parse(response); + return data.object.sha; +} + +export async function createBranchFromRef( + owner: string, + repo: string, + newBranch: string, + fromBranch?: string +): Promise> { + let sha: string; + if (fromBranch) { + sha = await getBranchSHA(owner, repo, fromBranch); + } else { + sha = await getDefaultBranchSHA(owner, repo); + } + + return createBranch(owner, repo, { + ref: newBranch, + sha, + }); +} + +export async function updateBranch( + owner: string, + repo: string, + branch: string, + sha: string +): Promise> { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, + { + method: "PATCH", + body: { + sha, + force: true, + }, + } + ); + + return GitHubReferenceSchema.parse(response); +} \ No newline at end of file From 6fdfeebdbe1f1929a3b535eb0e6e0c67ad8dac04 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 01:44:00 -0800 Subject: [PATCH 32/67] Add pull request operations module --- src/github/operations/pulls.ts | 80 ++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/github/operations/pulls.ts diff --git a/src/github/operations/pulls.ts b/src/github/operations/pulls.ts new file mode 100644 index 00000000..2733a597 --- /dev/null +++ b/src/github/operations/pulls.ts @@ -0,0 +1,80 @@ +import { z } from "zod"; +import { githubRequest } from "../common/utils"; +import { GitHubPullRequestSchema } from "../common/types"; + +// Schema definitions +export const CreatePullRequestOptionsSchema = z.object({ + 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"), + maintainer_can_modify: z.boolean().optional().describe("Whether maintainers can modify the pull request"), + draft: z.boolean().optional().describe("Whether to create the pull request as a draft"), +}); + +export const CreatePullRequestSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + 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"), +}); + +// Type exports +export type CreatePullRequestOptions = z.infer; + +// Function implementations +export async function createPullRequest( + owner: string, + repo: string, + options: CreatePullRequestOptions +): Promise> { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/pulls`, + { + method: "POST", + body: options, + } + ); + + return GitHubPullRequestSchema.parse(response); +} + +export async function getPullRequest( + owner: string, + repo: string, + pullNumber: number +): Promise> { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}` + ); + + return GitHubPullRequestSchema.parse(response); +} + +export async function listPullRequests( + owner: string, + repo: string, + options: { + state?: "open" | "closed" | "all"; + head?: string; + base?: string; + sort?: "created" | "updated" | "popularity" | "long-running"; + direction?: "asc" | "desc"; + per_page?: number; + page?: number; + } = {} +): Promise[]> { + const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`); + Object.entries(options).forEach(([key, value]) => { + if (value !== undefined) { + url.searchParams.append(key, value.toString()); + } + }); + + const response = await githubRequest(url.toString()); + return z.array(GitHubPullRequestSchema).parse(response); +} \ No newline at end of file From 7a89bd5f08362d460cbe0d7910306a277a668bff Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 01:44:19 -0800 Subject: [PATCH 33/67] Add search operations module --- src/github/operations/search.ts | 104 ++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/github/operations/search.ts diff --git a/src/github/operations/search.ts b/src/github/operations/search.ts new file mode 100644 index 00000000..e7aab148 --- /dev/null +++ b/src/github/operations/search.ts @@ -0,0 +1,104 @@ +import { z } from "zod"; +import { githubRequest, buildUrl } from "../common/utils"; + +// Schema definitions +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"), +}); + +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"), +}); + +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"), +}); + +// Response schemas +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: z.object({ + full_name: z.string(), + description: z.string().nullable(), + url: z.string(), + html_url: z.string(), + }).describe("The repository where this file was found"), + score: z.number().describe("The search result score"), +}); + +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"), +}); + +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(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"), + 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"), + })).describe("The search results"), +}); + +// Type exports +export type SearchCodeParams = z.infer; +export type SearchIssuesParams = z.infer; +export type SearchUsersParams = z.infer; +export type SearchCodeResponse = z.infer; +export type SearchUsersResponse = z.infer; + +// Function implementations +export async function searchCode(params: SearchCodeParams): Promise { + const url = buildUrl("https://api.github.com/search/code", params); + const response = await githubRequest(url); + return SearchCodeResponseSchema.parse(response); +} + +export async function searchIssues(params: SearchIssuesParams) { + const url = buildUrl("https://api.github.com/search/issues", params); + const response = await githubRequest(url); + return response; +} + +export async function searchUsers(params: SearchUsersParams): Promise { + const url = buildUrl("https://api.github.com/search/users", params); + const response = await githubRequest(url); + return SearchUsersResponseSchema.parse(response); +} \ No newline at end of file From f8915fe9aa250072d641805af32133009213e4a0 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 01:44:49 -0800 Subject: [PATCH 34/67] Add commits operations module --- src/github/operations/commits.ts | 95 ++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/github/operations/commits.ts diff --git a/src/github/operations/commits.ts b/src/github/operations/commits.ts new file mode 100644 index 00000000..e889a734 --- /dev/null +++ b/src/github/operations/commits.ts @@ -0,0 +1,95 @@ +import { z } from "zod"; +import { githubRequest, buildUrl } from "../common/utils"; +import { GitHubCommitSchema, GitHubListCommitsSchema } from "../common/types"; + +// Schema definitions +export const ListCommitsSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + 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)"), + sha: z.string().optional().describe("SHA of the commit to start listing from"), +}); + +// Type exports +export type ListCommitsParams = z.infer; + +// Function implementations +export async function listCommits( + owner: string, + repo: string, + page: number = 1, + perPage: number = 30, + sha?: string +) { + const params = { + page, + per_page: perPage, + ...(sha ? { sha } : {}) + }; + + const url = buildUrl(`https://api.github.com/repos/${owner}/${repo}/commits`, params); + + const response = await githubRequest(url); + return GitHubListCommitsSchema.parse(response); +} + +export async function getCommit( + owner: string, + repo: string, + sha: string +) { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/git/commits/${sha}` + ); + + return GitHubCommitSchema.parse(response); +} + +export async function createCommit( + owner: string, + repo: string, + message: string, + tree: string, + parents: string[] +) { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/git/commits`, + { + method: "POST", + body: { + message, + tree, + parents, + }, + } + ); + + return GitHubCommitSchema.parse(response); +} + +export async function compareCommits( + owner: string, + repo: string, + base: string, + head: string +) { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/compare/${base}...${head}` + ); + + return z.object({ + url: z.string(), + html_url: z.string(), + permalink_url: z.string(), + diff_url: z.string(), + patch_url: z.string(), + base_commit: GitHubCommitSchema, + merge_base_commit: GitHubCommitSchema, + commits: z.array(GitHubCommitSchema), + total_commits: z.number(), + status: z.string(), + ahead_by: z.number(), + behind_by: z.number(), + }).parse(response); +} \ No newline at end of file From 83909ddf95d2a9f9c208f19d179730500202897e Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 01:45:24 -0800 Subject: [PATCH 35/67] Refactor index.ts to use modular operation files --- src/github/index.ts | 886 +++++--------------------------------------- 1 file changed, 85 insertions(+), 801 deletions(-) diff --git a/src/github/index.ts b/src/github/index.ts index d2952797..47060542 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -5,61 +5,17 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; -import fetch from "node-fetch"; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { - CreateBranchOptionsSchema, - CreateBranchSchema, - CreateIssueOptionsSchema, - CreateIssueSchema, - CreateOrUpdateFileSchema, - CreatePullRequestOptionsSchema, - CreatePullRequestSchema, - CreateRepositoryOptionsSchema, - CreateRepositorySchema, - ForkRepositorySchema, - GetFileContentsSchema, - GetIssueSchema, - GitHubCommitSchema, - GitHubContentSchema, - GitHubCreateUpdateFileResponseSchema, - GitHubForkSchema, - GitHubIssueSchema, - GitHubListCommits, - GitHubListCommitsSchema, - GitHubPullRequestSchema, - GitHubReferenceSchema, - GitHubRepositorySchema, - GitHubSearchResponseSchema, - GitHubTreeSchema, - IssueCommentSchema, - ListCommitsSchema, - ListIssuesOptionsSchema, - PushFilesSchema, - SearchCodeResponseSchema, - SearchCodeSchema, - SearchIssuesResponseSchema, - SearchIssuesSchema, - SearchRepositoriesSchema, - SearchUsersResponseSchema, - SearchUsersSchema, - UpdateIssueOptionsSchema, - type FileOperation, - type GitHubCommit, - type GitHubContent, - type GitHubCreateUpdateFileResponse, - type GitHubFork, - type GitHubIssue, - type GitHubPullRequest, - type GitHubReference, - type GitHubRepository, - type GitHubSearchResponse, - type GitHubTree, - type SearchCodeResponse, - type SearchIssuesResponse, - type SearchUsersResponse -} from './schemas.js'; + +// Import operations +import * as repository from './operations/repository.js'; +import * as files from './operations/files.js'; +import * as issues from './operations/issues.js'; +import * as pulls from './operations/pulls.js'; +import * as branches from './operations/branches.js'; +import * as search from './operations/search.js'; +import * as commits from './operations/commits.js'; const server = new Server( { @@ -73,739 +29,93 @@ const server = new Server( } ); -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); -} - -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 GitHubForkSchema.parse(await response.json()); -} - -async function createBranch( - owner: string, - repo: string, - 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", - "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 GitHubReferenceSchema.parse(await response.json()); -} - -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 (!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 = GitHubReferenceSchema.parse(await masterResponse.json()); - return data.object.sha; - } - - const data = GitHubReferenceSchema.parse(await response.json()); - return data.object.sha; -} - -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}`, - Accept: "application/vnd.github.v3+json", - "User-Agent": "github-mcp-server", - }, - }); - - if (!response.ok) { - throw new Error(`GitHub API error: ${response.statusText}`); - } - - const data = GitHubContentSchema.parse(await response.json()); - - // If it's a file, decode the content - if (!Array.isArray(data) && data.content) { - data.content = Buffer.from(data.content, "base64").toString("utf8"); - } - - return data; -} - -async function createIssue( - owner: string, - repo: string, - options: z.infer -): 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 GitHubIssueSchema.parse(await response.json()); -} - -async function createPullRequest( - owner: string, - repo: string, - options: z.infer -): 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 GitHubPullRequestSchema.parse(await response.json()); -} - -async function createOrUpdateFile( - owner: string, - repo: string, - path: string, - content: string, - message: string, - branch: string, - sha?: string -): Promise { - const encodedContent = Buffer.from(content).toString("base64"); - - let currentSha = sha; - if (!currentSha) { - try { - const existingFile = await getFileContents(owner, repo, path, branch); - if (!Array.isArray(existingFile)) { - currentSha = existingFile.sha; - } - } catch (error) { - 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 } : {}), - }; - - 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 GitHubCreateUpdateFileResponseSchema.parse(await response.json()); -} - -async function createTree( - owner: string, - repo: string, - files: FileOperation[], - baseTree?: string -): Promise { - const tree = files.map((file) => ({ - path: file.path, - mode: "100644" as const, - type: "blob" as const, - 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 GitHubTreeSchema.parse(await response.json()); -} - -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 GitHubCommitSchema.parse(await response.json()); -} - -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 GitHubReferenceSchema.parse(await response.json()); -} - -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 = GitHubReferenceSchema.parse(await refResponse.json()); - 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 GitHubSearchResponseSchema.parse(await response.json()); -} - -async function createRepository( - options: z.infer -): 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 GitHubRepositorySchema.parse(await response.json()); -} - -async function listCommits( - owner: string, - repo: string, - page: number = 1, - perPage: number = 30, - sha?: string, -): Promise { - const url = new URL(`https://api.github.com/repos/${owner}/${repo}/commits`); - url.searchParams.append("page", page.toString()); - url.searchParams.append("per_page", perPage.toString()); - if (sha) { - url.searchParams.append("sha", sha); - } - - const response = await fetch( - url.toString(), - { - method: "GET", - headers: { - "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, - "Accept": "application/vnd.github.v3+json", - "User-Agent": "github-mcp-server", - "Content-Type": "application/json" - }, - } - ); - - if (!response.ok) { - throw new Error(`GitHub API error: ${response.statusText}`); - } - - return GitHubListCommitsSchema.parse(await response.json()); -} - -async function listIssues( - owner: string, - repo: string, - options: Omit, 'owner' | 'repo'> -): Promise { - const url = new URL(`https://api.github.com/repos/${owner}/${repo}/issues`); - - // Add query parameters - if (options.state) url.searchParams.append('state', options.state); - if (options.labels) url.searchParams.append('labels', options.labels.join(',')); - if (options.sort) url.searchParams.append('sort', options.sort); - if (options.direction) url.searchParams.append('direction', options.direction); - if (options.since) url.searchParams.append('since', options.since); - if (options.page) url.searchParams.append('page', options.page.toString()); - if (options.per_page) url.searchParams.append('per_page', options.per_page.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 z.array(GitHubIssueSchema).parse(await response.json()); -} - -async function updateIssue( - owner: string, - repo: string, - issueNumber: number, - options: Omit, 'owner' | 'repo' | 'issue_number'> -): Promise { - const response = await fetch( - `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`, - { - 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({ - title: options.title, - body: options.body, - state: options.state, - labels: options.labels, - assignees: options.assignees, - milestone: options.milestone - }) - } - ); - - if (!response.ok) { - throw new Error(`GitHub API error: ${response.statusText}`); - } - - return GitHubIssueSchema.parse(await response.json()); -} - -async function addIssueComment( - owner: string, - repo: string, - issueNumber: number, - body: string -): Promise> { - const response = await fetch( - `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`, - { - 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({ body }) - } - ); - - if (!response.ok) { - throw new Error(`GitHub API error: ${response.statusText}`); - } - - return IssueCommentSchema.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()); -} - -async function getIssue( - owner: string, - repo: string, - issueNumber: number -): Promise { - const response = await fetch( - `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`, - { - 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 GitHubIssueSchema.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(files.CreateOrUpdateFileSchema), }, { name: "search_repositories", description: "Search for GitHub repositories", - inputSchema: zodToJsonSchema(SearchRepositoriesSchema), + inputSchema: zodToJsonSchema(repository.SearchRepositoriesSchema), }, { name: "create_repository", description: "Create a new GitHub repository in your account", - inputSchema: zodToJsonSchema(CreateRepositorySchema), + inputSchema: zodToJsonSchema(repository.CreateRepositoryOptionsSchema), }, { 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(files.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(files.PushFilesSchema), }, { name: "create_issue", description: "Create a new issue in a GitHub repository", - inputSchema: zodToJsonSchema(CreateIssueSchema), + inputSchema: zodToJsonSchema(issues.CreateIssueSchema), }, { name: "create_pull_request", description: "Create a new pull request in a GitHub repository", - inputSchema: zodToJsonSchema(CreatePullRequestSchema), + inputSchema: zodToJsonSchema(pulls.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(repository.ForkRepositorySchema), }, { name: "create_branch", description: "Create a new branch in a GitHub repository", - inputSchema: zodToJsonSchema(CreateBranchSchema), + inputSchema: zodToJsonSchema(branches.CreateBranchSchema), }, { name: "list_commits", description: "Get list of commits of a branch in a GitHub repository", - inputSchema: zodToJsonSchema(ListCommitsSchema) + inputSchema: zodToJsonSchema(commits.ListCommitsSchema) }, { name: "list_issues", description: "List issues in a GitHub repository with filtering options", - inputSchema: zodToJsonSchema(ListIssuesOptionsSchema) + inputSchema: zodToJsonSchema(issues.ListIssuesOptionsSchema) }, { name: "update_issue", description: "Update an existing issue in a GitHub repository", - inputSchema: zodToJsonSchema(UpdateIssueOptionsSchema) + inputSchema: zodToJsonSchema(issues.UpdateIssueOptionsSchema) }, { name: "add_issue_comment", description: "Add a comment to an existing issue", - inputSchema: zodToJsonSchema(IssueCommentSchema) + inputSchema: zodToJsonSchema(issues.IssueCommentSchema) }, { name: "search_code", description: "Search for code across GitHub repositories", - inputSchema: zodToJsonSchema(SearchCodeSchema), + inputSchema: zodToJsonSchema(search.SearchCodeSchema), }, { name: "search_issues", - description: - "Search for issues and pull requests across GitHub repositories", - inputSchema: zodToJsonSchema(SearchIssuesSchema), + description: "Search for issues and pull requests across GitHub repositories", + inputSchema: zodToJsonSchema(search.SearchIssuesSchema), }, { name: "search_users", description: "Search for users on GitHub", - inputSchema: zodToJsonSchema(SearchUsersSchema), + inputSchema: zodToJsonSchema(search.SearchUsersSchema), }, { name: "get_issue", description: "Get details of a specific issue in a GitHub repository.", - inputSchema: zodToJsonSchema(GetIssueSchema) + inputSchema: zodToJsonSchema(issues.GetIssueSchema) } ], }; @@ -819,55 +129,29 @@ 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 - ); + const args = repository.ForkRepositorySchema.parse(request.params.arguments); + const fork = await repository.forkRepository(args.owner, args.repo, args.organization); return { content: [{ type: "text", text: JSON.stringify(fork, null, 2) }], }; } 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 (!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); - } - - const branch = await createBranch(args.owner, args.repo, { - ref: args.branch, - sha, - }); - + const args = branches.CreateBranchSchema.parse(request.params.arguments); + const branch = await branches.createBranchFromRef( + args.owner, + args.repo, + args.branch, + args.from_branch + ); return { content: [{ type: "text", text: JSON.stringify(branch, null, 2) }], }; } case "search_repositories": { - const args = SearchRepositoriesSchema.parse(request.params.arguments); - const results = await searchRepositories( + const args = repository.SearchRepositoriesSchema.parse(request.params.arguments); + const results = await repository.searchRepositories( args.query, args.page, args.perPage @@ -878,18 +162,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "create_repository": { - const args = CreateRepositorySchema.parse(request.params.arguments); - const repository = await createRepository(args); + const args = repository.CreateRepositoryOptionsSchema.parse(request.params.arguments); + const result = await repository.createRepository(args); return { - content: [ - { type: "text", text: JSON.stringify(repository, null, 2) }, - ], + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } case "get_file_contents": { - const args = GetFileContentsSchema.parse(request.params.arguments); - const contents = await getFileContents( + const args = files.GetFileContentsSchema.parse(request.params.arguments); + const contents = await files.getFileContents( args.owner, args.repo, args.path, @@ -901,8 +183,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "create_or_update_file": { - const args = CreateOrUpdateFileSchema.parse(request.params.arguments); - const result = await createOrUpdateFile( + const args = files.CreateOrUpdateFileSchema.parse(request.params.arguments); + const result = await files.createOrUpdateFile( args.owner, args.repo, args.path, @@ -917,8 +199,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "push_files": { - const args = PushFilesSchema.parse(request.params.arguments); - const result = await pushFiles( + const args = files.PushFilesSchema.parse(request.params.arguments); + const result = await files.pushFiles( args.owner, args.repo, args.branch, @@ -931,83 +213,85 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "create_issue": { - const args = CreateIssueSchema.parse(request.params.arguments); + const args = issues.CreateIssueSchema.parse(request.params.arguments); const { owner, repo, ...options } = args; - const issue = await createIssue(owner, repo, options); + const issue = await issues.createIssue(owner, repo, options); return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }], }; } case "create_pull_request": { - const args = CreatePullRequestSchema.parse(request.params.arguments); + const args = pulls.CreatePullRequestSchema.parse(request.params.arguments); const { owner, repo, ...options } = args; - const pullRequest = await createPullRequest(owner, repo, options); + const pullRequest = await pulls.createPullRequest(owner, repo, options); return { - content: [ - { type: "text", text: JSON.stringify(pullRequest, null, 2) }, - ], + content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }], }; } case "search_code": { - const args = SearchCodeSchema.parse(request.params.arguments); - const results = await searchCode(args); + const args = search.SearchCodeSchema.parse(request.params.arguments); + const results = await search.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); + const args = search.SearchIssuesSchema.parse(request.params.arguments); + const results = await search.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); + const args = search.SearchUsersSchema.parse(request.params.arguments); + const results = await search.searchUsers(args); return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }], }; } case "list_issues": { - const args = ListIssuesOptionsSchema.parse(request.params.arguments); + const args = issues.ListIssuesOptionsSchema.parse(request.params.arguments); const { owner, repo, ...options } = args; - const issues = await listIssues(owner, repo, options); - return { toolResult: issues }; + const result = await issues.listIssues(owner, repo, options); + return { toolResult: result }; } case "update_issue": { - const args = UpdateIssueOptionsSchema.parse(request.params.arguments); + const args = issues.UpdateIssueOptionsSchema.parse(request.params.arguments); const { owner, repo, issue_number, ...options } = args; - const issue = await updateIssue(owner, repo, issue_number, options); - return { toolResult: issue }; + const result = await issues.updateIssue(owner, repo, issue_number, options); + return { toolResult: result }; } case "add_issue_comment": { - const args = IssueCommentSchema.parse(request.params.arguments); + const args = issues.IssueCommentSchema.parse(request.params.arguments); const { owner, repo, issue_number, body } = args; - const comment = await addIssueComment(owner, repo, issue_number, body); - return { toolResult: comment }; + const result = await issues.addIssueComment(owner, repo, issue_number, body); + return { toolResult: result }; } case "list_commits": { - const args = ListCommitsSchema.parse(request.params.arguments); - const results = await listCommits(args.owner, args.repo, args.page, args.perPage, args.sha); - return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] }; + const args = commits.ListCommitsSchema.parse(request.params.arguments); + const results = await commits.listCommits( + args.owner, + args.repo, + args.page, + args.perPage, + args.sha + ); + return { + content: [{ type: "text", text: JSON.stringify(results, null, 2) }], + }; } case "get_issue": { - const args = z.object({ - owner: z.string(), - repo: z.string(), - issue_number: z.number() - }).parse(request.params.arguments); - const issue = await getIssue(args.owner, args.repo, args.issue_number); + const args = issues.GetIssueSchema.parse(request.params.arguments); + const issue = await issues.getIssue(args.owner, args.repo, args.issue_number); return { toolResult: issue }; } @@ -1016,7 +300,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } } catch (error) { if (error instanceof z.ZodError) { - throw new Error(`ZodErrors: ${JSON.stringify(error.errors)}`) + throw new Error(`ZodErrors: ${JSON.stringify(error.errors)}`); } throw error; } @@ -1031,4 +315,4 @@ async function runServer() { runServer().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); -}); +}); \ No newline at end of file From b4e5754c6562a7156b7614ad400ab14093a0dc85 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 01:56:15 -0800 Subject: [PATCH 36/67] Remove schemas.ts as schemas are now in operation modules --- src/github/schemas.ts | 726 ------------------------------------------ 1 file changed, 726 deletions(-) diff --git a/src/github/schemas.ts b/src/github/schemas.ts index d911104a..e69de29b 100644 --- a/src/github/schemas.ts +++ b/src/github/schemas.ts @@ -1,726 +0,0 @@ -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(), -}); - -const GithubFileContentLinks = z.object({ - self: z.string(), - git: z.string().nullable(), - html: z.string().nullable() -}); - -// File content schemas -export const GitHubFileContentSchema = z.object({ - name: z.string(), - path: z.string(), - sha: z.string(), - size: z.number(), - url: z.string(), - html_url: z.string(), - git_url: z.string(), - download_url: z.string(), - type: z.string(), - content: z.string().optional(), - encoding: z.string().optional(), - _links: GithubFileContentLinks -}); - -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 GitHubListCommitsSchema = z.array(z.object({ - sha: z.string(), - node_id: z.string(), - commit: z.object({ - author: GitHubAuthorSchema, - committer: GitHubAuthorSchema, - message: z.string(), - tree: z.object({ - sha: z.string(), - url: z.string() - }), - url: z.string(), - comment_count: z.number(), - }), - url: z.string(), - html_url: z.string(), - comments_url: z.string() -})); - -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().nullable(), -}); - -// 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().nullable(), - 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 ListCommitsSchema = z.object({ - owner: z.string().describe("Repository owner (username or organization)"), - repo: z.string().describe("Repository name"), - 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)"), - sha: z.string().optional() - .describe("SHA of the file being replaced (required when updating existing files)") -}); - -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)" - ), -}); - -/** - * 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"), -}); - -// Add these schema definitions for issue management - -export const ListIssuesOptionsSchema = z.object({ - owner: z.string(), - repo: z.string(), - state: z.enum(['open', 'closed', 'all']).optional(), - labels: z.array(z.string()).optional(), - sort: z.enum(['created', 'updated', 'comments']).optional(), - direction: z.enum(['asc', 'desc']).optional(), - since: z.string().optional(), // ISO 8601 timestamp - page: z.number().optional(), - per_page: z.number().optional() -}); - -export const UpdateIssueOptionsSchema = z.object({ - owner: z.string(), - repo: z.string(), - issue_number: z.number(), - title: z.string().optional(), - body: z.string().optional(), - state: z.enum(['open', 'closed']).optional(), - labels: z.array(z.string()).optional(), - assignees: z.array(z.string()).optional(), - milestone: z.number().optional() -}); - -export const IssueCommentSchema = z.object({ - owner: z.string(), - repo: z.string(), - issue_number: z.number(), - body: z.string() -}); - -export const GetIssueSchema = z.object({ - owner: z.string().describe("Repository owner (username or organization)"), - repo: z.string().describe("Repository name"), - issue_number: z.number().describe("Issue 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 GitHubFileContent = 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 GitHubListCommits = z.infer; -export type GitHubReference = z.infer; -export type CreateRepositoryOptions = z.infer< - typeof CreateRepositoryOptionsSchema ->; -export type CreateIssueOptions = z.infer; -export type CreatePullRequestOptions = z.infer< - typeof CreatePullRequestOptionsSchema ->; -export type CreateBranchOptions = z.infer; -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; From 9f43900170c35cf141dd882eb1049a6a8c2dbb28 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 02:10:27 -0800 Subject: [PATCH 37/67] Add GitHubCreateUpdateFileResponseSchema to files module --- src/github/operations/files.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/github/operations/files.ts b/src/github/operations/files.ts index 676e9374..ae8c9b92 100644 --- a/src/github/operations/files.ts +++ b/src/github/operations/files.ts @@ -2,10 +2,11 @@ import { z } from "zod"; import { githubRequest } from "../common/utils"; import { GitHubContentSchema, - GitHubCreateUpdateFileResponseSchema, + GitHubAuthorSchema, GitHubTreeSchema, GitHubCommitSchema, GitHubReferenceSchema, + GitHubFileContentSchema, } from "../common/types"; // Schema definitions @@ -39,8 +40,33 @@ export const PushFilesSchema = z.object({ message: z.string().describe("Commit message"), }); +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(), + }) + ), + }), +}); + // Type exports export type FileOperation = z.infer; +export type GitHubCreateUpdateFileResponse = z.infer; // Function implementations export async function getFileContents( From 6b9e9834075157666a106e90c789da1c36a27ade Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 02:16:02 -0800 Subject: [PATCH 38/67] Add GitHubPullRequestSchema and related schemas to pulls module --- src/github/operations/pulls.ts | 40 +++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/github/operations/pulls.ts b/src/github/operations/pulls.ts index 2733a597..db073c67 100644 --- a/src/github/operations/pulls.ts +++ b/src/github/operations/pulls.ts @@ -1,8 +1,44 @@ import { z } from "zod"; import { githubRequest } from "../common/utils"; -import { GitHubPullRequestSchema } from "../common/types"; +import { + GitHubIssueAssigneeSchema, + GitHubRepositorySchema +} from "../common/types"; // Schema definitions +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().nullable(), + assignee: GitHubIssueAssigneeSchema.nullable(), + assignees: z.array(GitHubIssueAssigneeSchema), + head: GitHubPullRequestHeadSchema, + base: GitHubPullRequestHeadSchema, +}); + export const CreatePullRequestOptionsSchema = z.object({ title: z.string().describe("Pull request title"), body: z.string().optional().describe("Pull request body/description"), @@ -25,6 +61,8 @@ export const CreatePullRequestSchema = z.object({ // Type exports export type CreatePullRequestOptions = z.infer; +export type GitHubPullRequest = z.infer; +export type GitHubPullRequestHead = z.infer; // Function implementations export async function createPullRequest( From 4ec840cb4a9bdfe14d6f3573211f38ae4190d54f Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 02:18:45 -0800 Subject: [PATCH 39/67] Add GitHubIssueAssigneeSchema to common types --- src/github/common/types.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/github/common/types.ts b/src/github/common/types.ts index 9be60a74..61eba5b2 100644 --- a/src/github/common/types.ts +++ b/src/github/common/types.ts @@ -121,6 +121,15 @@ export const GitHubReferenceSchema = z.object({ }), }); +// User and assignee schemas +export const GitHubIssueAssigneeSchema = z.object({ + login: z.string(), + id: z.number(), + avatar_url: z.string(), + url: z.string(), + html_url: z.string(), +}); + // Export types export type GitHubAuthor = z.infer; export type GitHubRepository = z.infer; @@ -129,4 +138,5 @@ export type GitHubDirectoryContent = z.infer; export type GitHubTree = z.infer; export type GitHubCommit = z.infer; -export type GitHubReference = z.infer; \ No newline at end of file +export type GitHubReference = z.infer; +export type GitHubIssueAssignee = z.infer; \ No newline at end of file From 0b3359fbf91dba1d3f19f3c1391bc1ab3d2f957c Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 02:21:38 -0800 Subject: [PATCH 40/67] Add missing issue-related schemas to common types --- src/github/common/types.ts | 53 +++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/github/common/types.ts b/src/github/common/types.ts index 61eba5b2..8e654be8 100644 --- a/src/github/common/types.ts +++ b/src/github/common/types.ts @@ -130,6 +130,54 @@ export const GitHubIssueAssigneeSchema = z.object({ html_url: z.string(), }); +// 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 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().nullable(), +}); + // Export types export type GitHubAuthor = z.infer; export type GitHubRepository = z.infer; @@ -139,4 +187,7 @@ export type GitHubContent = z.infer; export type GitHubTree = z.infer; export type GitHubCommit = z.infer; export type GitHubReference = z.infer; -export type GitHubIssueAssignee = z.infer; \ No newline at end of file +export type GitHubIssueAssignee = z.infer; +export type GitHubLabel = z.infer; +export type GitHubMilestone = z.infer; +export type GitHubIssue = z.infer; \ No newline at end of file From a79ec67d9c18721ad58f5d626f6fd1c0e954289f Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 02:24:04 -0800 Subject: [PATCH 41/67] Add missing GitHubListCommitsSchema and GitHubSearchResponseSchema to common types --- src/github/common/types.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/github/common/types.ts b/src/github/common/types.ts index 8e654be8..af81f22f 100644 --- a/src/github/common/types.ts +++ b/src/github/common/types.ts @@ -110,6 +110,25 @@ export const GitHubCommitSchema = z.object({ ), }); +export const GitHubListCommitsSchema = z.array(z.object({ + sha: z.string(), + node_id: z.string(), + commit: z.object({ + author: GitHubAuthorSchema, + committer: GitHubAuthorSchema, + message: z.string(), + tree: z.object({ + sha: z.string(), + url: z.string() + }), + url: z.string(), + comment_count: z.number(), + }), + url: z.string(), + html_url: z.string(), + comments_url: z.string() +})); + export const GitHubReferenceSchema = z.object({ ref: z.string(), node_id: z.string(), @@ -178,6 +197,13 @@ export const GitHubIssueSchema = z.object({ body: z.string().nullable(), }); +// Search-related schemas +export const GitHubSearchResponseSchema = z.object({ + total_count: z.number(), + incomplete_results: z.boolean(), + items: z.array(GitHubRepositorySchema), +}); + // Export types export type GitHubAuthor = z.infer; export type GitHubRepository = z.infer; @@ -186,8 +212,10 @@ export type GitHubDirectoryContent = z.infer; export type GitHubTree = z.infer; export type GitHubCommit = z.infer; +export type GitHubListCommits = z.infer; export type GitHubReference = z.infer; export type GitHubIssueAssignee = z.infer; export type GitHubLabel = z.infer; export type GitHubMilestone = z.infer; -export type GitHubIssue = z.infer; \ No newline at end of file +export type GitHubIssue = z.infer; +export type GitHubSearchResponse = z.infer; \ No newline at end of file From 7c72d987f9a695a6e82973a025acf8d0847a01a0 Mon Sep 17 00:00:00 2001 From: Peter M Elias Date: Sat, 28 Dec 2024 02:25:30 -0800 Subject: [PATCH 42/67] cleanup --- src/github/operations/branches.ts | 6 +++--- src/github/operations/commits.ts | 6 +++--- src/github/operations/files.ts | 6 +++--- src/github/operations/issues.ts | 6 +++--- src/github/operations/pulls.ts | 6 +++--- src/github/operations/repository.ts | 6 +++--- src/github/operations/search.ts | 4 ++-- src/github/schemas.ts | 0 8 files changed, 20 insertions(+), 20 deletions(-) delete mode 100644 src/github/schemas.ts diff --git a/src/github/operations/branches.ts b/src/github/operations/branches.ts index 4690ef24..9b7033b5 100644 --- a/src/github/operations/branches.ts +++ b/src/github/operations/branches.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { githubRequest } from "../common/utils"; -import { GitHubReferenceSchema } from "../common/types"; +import { githubRequest } from "../common/utils.js"; +import { GitHubReferenceSchema } from "../common/types.js"; // Schema definitions export const CreateBranchOptionsSchema = z.object({ @@ -109,4 +109,4 @@ export async function updateBranch( ); return GitHubReferenceSchema.parse(response); -} \ No newline at end of file +} diff --git a/src/github/operations/commits.ts b/src/github/operations/commits.ts index e889a734..09b4302a 100644 --- a/src/github/operations/commits.ts +++ b/src/github/operations/commits.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { githubRequest, buildUrl } from "../common/utils"; -import { GitHubCommitSchema, GitHubListCommitsSchema } from "../common/types"; +import { githubRequest, buildUrl } from "../common/utils.js"; +import { GitHubCommitSchema, GitHubListCommitsSchema } from "../common/types.js"; // Schema definitions export const ListCommitsSchema = z.object({ @@ -92,4 +92,4 @@ export async function compareCommits( ahead_by: z.number(), behind_by: z.number(), }).parse(response); -} \ No newline at end of file +} diff --git a/src/github/operations/files.ts b/src/github/operations/files.ts index ae8c9b92..9517946e 100644 --- a/src/github/operations/files.ts +++ b/src/github/operations/files.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { githubRequest } from "../common/utils"; +import { githubRequest } from "../common/utils.js"; import { GitHubContentSchema, GitHubAuthorSchema, @@ -7,7 +7,7 @@ import { GitHubCommitSchema, GitHubReferenceSchema, GitHubFileContentSchema, -} from "../common/types"; +} from "../common/types.js"; // Schema definitions export const FileOperationSchema = z.object({ @@ -216,4 +216,4 @@ export async function pushFiles( 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); -} \ No newline at end of file +} diff --git a/src/github/operations/issues.ts b/src/github/operations/issues.ts index e489e747..aec154f0 100644 --- a/src/github/operations/issues.ts +++ b/src/github/operations/issues.ts @@ -1,11 +1,11 @@ import { z } from "zod"; -import { githubRequest, buildUrl } from "../common/utils"; +import { githubRequest, buildUrl } from "../common/utils.js"; import { GitHubIssueSchema, GitHubLabelSchema, GitHubIssueAssigneeSchema, GitHubMilestoneSchema, -} from "../common/types"; +} from "../common/types.js"; // Schema definitions export const CreateIssueOptionsSchema = z.object({ @@ -148,4 +148,4 @@ export async function getIssue( ); return GitHubIssueSchema.parse(response); -} \ No newline at end of file +} diff --git a/src/github/operations/pulls.ts b/src/github/operations/pulls.ts index db073c67..34ae85d2 100644 --- a/src/github/operations/pulls.ts +++ b/src/github/operations/pulls.ts @@ -1,9 +1,9 @@ import { z } from "zod"; -import { githubRequest } from "../common/utils"; +import { githubRequest } from "../common/utils.js"; import { GitHubIssueAssigneeSchema, GitHubRepositorySchema -} from "../common/types"; +} from "../common/types.js"; // Schema definitions export const GitHubPullRequestHeadSchema = z.object({ @@ -115,4 +115,4 @@ export async function listPullRequests( const response = await githubRequest(url.toString()); return z.array(GitHubPullRequestSchema).parse(response); -} \ No newline at end of file +} diff --git a/src/github/operations/repository.ts b/src/github/operations/repository.ts index dfa7e263..4cf0ab9b 100644 --- a/src/github/operations/repository.ts +++ b/src/github/operations/repository.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { githubRequest } from "../common/utils"; -import { GitHubRepositorySchema, GitHubSearchResponseSchema } from "../common/types"; +import { githubRequest } from "../common/utils.js"; +import { GitHubRepositorySchema, GitHubSearchResponseSchema } from "../common/types.js"; // Schema definitions export const CreateRepositoryOptionsSchema = z.object({ @@ -62,4 +62,4 @@ export async function forkRepository( parent: GitHubRepositorySchema, source: GitHubRepositorySchema, }).parse(response); -} \ No newline at end of file +} diff --git a/src/github/operations/search.ts b/src/github/operations/search.ts index e7aab148..08e2fd17 100644 --- a/src/github/operations/search.ts +++ b/src/github/operations/search.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { githubRequest, buildUrl } from "../common/utils"; +import { githubRequest, buildUrl } from "../common/utils.js"; // Schema definitions export const SearchCodeSchema = z.object({ @@ -101,4 +101,4 @@ export async function searchUsers(params: SearchUsersParams): Promise Date: Sat, 28 Dec 2024 03:07:34 -0800 Subject: [PATCH 43/67] refactor: improve pull request schemas and validation - Add proper state enum validation - Add title and body length validation - Consolidate request schemas - Add consistent parameter handling - Improve type safety - Add proper JSDoc documentation --- src/github/operations/pulls.ts | 181 ++++++++++++++++++++++----------- 1 file changed, 123 insertions(+), 58 deletions(-) diff --git a/src/github/operations/pulls.ts b/src/github/operations/pulls.ts index 34ae85d2..49084c94 100644 --- a/src/github/operations/pulls.ts +++ b/src/github/operations/pulls.ts @@ -5,71 +5,122 @@ import { GitHubRepositorySchema } from "../common/types.js"; -// Schema definitions -export const GitHubPullRequestHeadSchema = z.object({ +// Constants for GitHub limits and constraints +const GITHUB_TITLE_MAX_LENGTH = 256; +const GITHUB_BODY_MAX_LENGTH = 65536; + +// Base schema for repository identification +export const RepositoryParamsSchema = z.object({ + owner: z.string().min(1).describe("Repository owner (username or organization)"), + repo: z.string().min(1).describe("Repository name"), +}); + +// Common validation schemas +export const GitHubPullRequestStateSchema = z.enum([ + "open", + "closed", + "merged", + "draft" +]).describe("The current state of the pull request"); + +export const GitHubPullRequestSortSchema = z.enum([ + "created", + "updated", + "popularity", + "long-running" +]).describe("The sorting field for pull requests"); + +export const GitHubDirectionSchema = z.enum([ + "asc", + "desc" +]).describe("The sort direction"); + +// Pull request head/base schema +export const GitHubPullRequestRefSchema = z.object({ label: z.string(), - ref: z.string(), - sha: z.string(), + ref: z.string().min(1), + sha: z.string().length(40), user: GitHubIssueAssigneeSchema, repo: GitHubRepositorySchema, -}); +}).describe("Reference information for pull request head or base"); +// Main pull request schema export const GitHubPullRequestSchema = z.object({ - url: z.string(), - id: z.number(), + url: z.string().url(), + id: z.number().positive(), 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(), + html_url: z.string().url(), + diff_url: z.string().url(), + patch_url: z.string().url(), + issue_url: z.string().url(), + number: z.number().positive(), + state: GitHubPullRequestStateSchema, locked: z.boolean(), - title: z.string(), + title: z.string().max(GITHUB_TITLE_MAX_LENGTH), 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().nullable(), + body: z.string().max(GITHUB_BODY_MAX_LENGTH).nullable(), + created_at: z.string().datetime(), + updated_at: z.string().datetime(), + closed_at: z.string().datetime().nullable(), + merged_at: z.string().datetime().nullable(), + merge_commit_sha: z.string().length(40).nullable(), assignee: GitHubIssueAssigneeSchema.nullable(), assignees: z.array(GitHubIssueAssigneeSchema), - head: GitHubPullRequestHeadSchema, - base: GitHubPullRequestHeadSchema, + requested_reviewers: z.array(GitHubIssueAssigneeSchema), + labels: z.array(z.object({ + name: z.string(), + color: z.string().regex(/^[0-9a-fA-F]{6}$/), + description: z.string().nullable(), + })), + head: GitHubPullRequestRefSchema, + base: GitHubPullRequestRefSchema, }); +// Request schemas +export const ListPullRequestsOptionsSchema = z.object({ + state: GitHubPullRequestStateSchema.optional(), + head: z.string().optional(), + base: z.string().optional(), + sort: GitHubPullRequestSortSchema.optional(), + direction: GitHubDirectionSchema.optional(), + per_page: z.number().min(1).max(100).optional(), + page: z.number().min(1).optional(), +}).describe("Options for listing pull requests"); + export const CreatePullRequestOptionsSchema = z.object({ - 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"), + title: z.string().max(GITHUB_TITLE_MAX_LENGTH).describe("Pull request title"), + body: z.string().max(GITHUB_BODY_MAX_LENGTH).optional().describe("Pull request body/description"), + head: z.string().min(1).describe("The name of the branch where your changes are implemented"), + base: z.string().min(1).describe("The name of the branch you want the changes pulled into"), maintainer_can_modify: z.boolean().optional().describe("Whether maintainers can modify the pull request"), draft: z.boolean().optional().describe("Whether to create the pull request as a draft"), -}); +}).describe("Options for creating a pull request"); -export const CreatePullRequestSchema = z.object({ - owner: z.string().describe("Repository owner (username or organization)"), - repo: z.string().describe("Repository name"), - 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"), +// Combine repository params with operation options +export const CreatePullRequestSchema = RepositoryParamsSchema.extend({ + ...CreatePullRequestOptionsSchema.shape, }); // Type exports +export type RepositoryParams = z.infer; export type CreatePullRequestOptions = z.infer; +export type ListPullRequestsOptions = z.infer; export type GitHubPullRequest = z.infer; -export type GitHubPullRequestHead = z.infer; +export type GitHubPullRequestRef = z.infer; -// Function implementations +/** + * Creates a new pull request in a repository. + * + * @param params Repository identification and pull request creation options + * @returns Promise resolving to the created pull request + * @throws {ZodError} If the input parameters fail validation + * @throws {Error} If the GitHub API request fails + */ export async function createPullRequest( - owner: string, - repo: string, - options: CreatePullRequestOptions -): Promise> { + params: z.infer +): Promise { + const { owner, repo, ...options } = CreatePullRequestSchema.parse(params); + const response = await githubRequest( `https://api.github.com/repos/${owner}/${repo}/pulls`, { @@ -81,11 +132,21 @@ export async function createPullRequest( return GitHubPullRequestSchema.parse(response); } +/** + * Retrieves a specific pull request by its number. + * + * @param params Repository parameters and pull request number + * @returns Promise resolving to the pull request details + * @throws {Error} If the pull request is not found or the request fails + */ export async function getPullRequest( - owner: string, - repo: string, - pullNumber: number -): Promise> { + params: RepositoryParams & { pullNumber: number } +): Promise { + const { owner, repo, pullNumber } = z.object({ + ...RepositoryParamsSchema.shape, + pullNumber: z.number().positive(), + }).parse(params); + const response = await githubRequest( `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}` ); @@ -93,20 +154,24 @@ export async function getPullRequest( return GitHubPullRequestSchema.parse(response); } +/** + * Lists pull requests in a repository with optional filtering. + * + * @param params Repository parameters and listing options + * @returns Promise resolving to an array of pull requests + * @throws {ZodError} If the input parameters fail validation + * @throws {Error} If the GitHub API request fails + */ export async function listPullRequests( - owner: string, - repo: string, - options: { - state?: "open" | "closed" | "all"; - head?: string; - base?: string; - sort?: "created" | "updated" | "popularity" | "long-running"; - direction?: "asc" | "desc"; - per_page?: number; - page?: number; - } = {} -): Promise[]> { + params: RepositoryParams & Partial +): Promise { + const { owner, repo, ...options } = z.object({ + ...RepositoryParamsSchema.shape, + ...ListPullRequestsOptionsSchema.partial().shape, + }).parse(params); + const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`); + Object.entries(options).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.append(key, value.toString()); From 42872be9a2ea8d76e26f8b0774faced5624df3c1 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:12:43 -0800 Subject: [PATCH 44/67] refactor: remove documentation and comments --- src/github/operations/pulls.ts | 61 +++++++++------------------------- 1 file changed, 15 insertions(+), 46 deletions(-) diff --git a/src/github/operations/pulls.ts b/src/github/operations/pulls.ts index 49084c94..0b9b9643 100644 --- a/src/github/operations/pulls.ts +++ b/src/github/operations/pulls.ts @@ -5,46 +5,41 @@ import { GitHubRepositorySchema } from "../common/types.js"; -// Constants for GitHub limits and constraints const GITHUB_TITLE_MAX_LENGTH = 256; const GITHUB_BODY_MAX_LENGTH = 65536; -// Base schema for repository identification export const RepositoryParamsSchema = z.object({ - owner: z.string().min(1).describe("Repository owner (username or organization)"), - repo: z.string().min(1).describe("Repository name"), + owner: z.string().min(1), + repo: z.string().min(1), }); -// Common validation schemas export const GitHubPullRequestStateSchema = z.enum([ "open", "closed", "merged", "draft" -]).describe("The current state of the pull request"); +]); export const GitHubPullRequestSortSchema = z.enum([ "created", "updated", "popularity", "long-running" -]).describe("The sorting field for pull requests"); +]); export const GitHubDirectionSchema = z.enum([ "asc", "desc" -]).describe("The sort direction"); +]); -// Pull request head/base schema export const GitHubPullRequestRefSchema = z.object({ label: z.string(), ref: z.string().min(1), sha: z.string().length(40), user: GitHubIssueAssigneeSchema, repo: GitHubRepositorySchema, -}).describe("Reference information for pull request head or base"); +}); -// Main pull request schema export const GitHubPullRequestSchema = z.object({ url: z.string().url(), id: z.number().positive(), @@ -76,7 +71,6 @@ export const GitHubPullRequestSchema = z.object({ base: GitHubPullRequestRefSchema, }); -// Request schemas export const ListPullRequestsOptionsSchema = z.object({ state: GitHubPullRequestStateSchema.optional(), head: z.string().optional(), @@ -85,37 +79,27 @@ export const ListPullRequestsOptionsSchema = z.object({ direction: GitHubDirectionSchema.optional(), per_page: z.number().min(1).max(100).optional(), page: z.number().min(1).optional(), -}).describe("Options for listing pull requests"); +}); export const CreatePullRequestOptionsSchema = z.object({ - title: z.string().max(GITHUB_TITLE_MAX_LENGTH).describe("Pull request title"), - body: z.string().max(GITHUB_BODY_MAX_LENGTH).optional().describe("Pull request body/description"), - head: z.string().min(1).describe("The name of the branch where your changes are implemented"), - base: z.string().min(1).describe("The name of the branch you want the changes pulled into"), - maintainer_can_modify: z.boolean().optional().describe("Whether maintainers can modify the pull request"), - draft: z.boolean().optional().describe("Whether to create the pull request as a draft"), -}).describe("Options for creating a pull request"); + title: z.string().max(GITHUB_TITLE_MAX_LENGTH), + body: z.string().max(GITHUB_BODY_MAX_LENGTH).optional(), + head: z.string().min(1), + base: z.string().min(1), + maintainer_can_modify: z.boolean().optional(), + draft: z.boolean().optional(), +}); -// Combine repository params with operation options export const CreatePullRequestSchema = RepositoryParamsSchema.extend({ ...CreatePullRequestOptionsSchema.shape, }); -// Type exports export type RepositoryParams = z.infer; export type CreatePullRequestOptions = z.infer; export type ListPullRequestsOptions = z.infer; export type GitHubPullRequest = z.infer; export type GitHubPullRequestRef = z.infer; -/** - * Creates a new pull request in a repository. - * - * @param params Repository identification and pull request creation options - * @returns Promise resolving to the created pull request - * @throws {ZodError} If the input parameters fail validation - * @throws {Error} If the GitHub API request fails - */ export async function createPullRequest( params: z.infer ): Promise { @@ -132,13 +116,6 @@ export async function createPullRequest( return GitHubPullRequestSchema.parse(response); } -/** - * Retrieves a specific pull request by its number. - * - * @param params Repository parameters and pull request number - * @returns Promise resolving to the pull request details - * @throws {Error} If the pull request is not found or the request fails - */ export async function getPullRequest( params: RepositoryParams & { pullNumber: number } ): Promise { @@ -154,14 +131,6 @@ export async function getPullRequest( return GitHubPullRequestSchema.parse(response); } -/** - * Lists pull requests in a repository with optional filtering. - * - * @param params Repository parameters and listing options - * @returns Promise resolving to an array of pull requests - * @throws {ZodError} If the input parameters fail validation - * @throws {Error} If the GitHub API request fails - */ export async function listPullRequests( params: RepositoryParams & Partial ): Promise { @@ -180,4 +149,4 @@ export async function listPullRequests( const response = await githubRequest(url.toString()); return z.array(GitHubPullRequestSchema).parse(response); -} +} \ No newline at end of file From b8b7c1b7844c73e88fb624d08734dc9e4ba87c85 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:16:19 -0800 Subject: [PATCH 45/67] refactor: update pull request handler to use new parameter style --- src/github/index.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/github/index.ts b/src/github/index.ts index 47060542..f545baa8 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -8,7 +8,6 @@ import { import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -// Import operations import * as repository from './operations/repository.js'; import * as files from './operations/files.js'; import * as issues from './operations/issues.js'; @@ -223,8 +222,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { case "create_pull_request": { const args = pulls.CreatePullRequestSchema.parse(request.params.arguments); - const { owner, repo, ...options } = args; - const pullRequest = await pulls.createPullRequest(owner, repo, options); + const pullRequest = await pulls.createPullRequest(args); return { content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }], }; @@ -314,5 +312,4 @@ async function runServer() { runServer().catch((error) => { console.error("Fatal error in main():", error); - process.exit(1); -}); \ No newline at end of file + process.exit(1); \ No newline at end of file From e921c2725cd8103987f34da69f3c4b42a3fd0005 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:19:51 -0800 Subject: [PATCH 46/67] fix: restore proper runServer function closure --- src/github/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/github/index.ts b/src/github/index.ts index f545baa8..286cc7bb 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -312,4 +312,5 @@ async function runServer() { runServer().catch((error) => { console.error("Fatal error in main():", error); - process.exit(1); \ No newline at end of file + process.exit(1); +}); \ No newline at end of file From fb421b4837b87a5edeea993ecd794cb4f3325b82 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:25:15 -0800 Subject: [PATCH 47/67] feat: add GitHub API error handling utilities --- src/github/common/errors.ts | 89 +++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/github/common/errors.ts diff --git a/src/github/common/errors.ts b/src/github/common/errors.ts new file mode 100644 index 00000000..5b940f3b --- /dev/null +++ b/src/github/common/errors.ts @@ -0,0 +1,89 @@ +export class GitHubError extends Error { + constructor( + message: string, + public readonly status: number, + public readonly response: unknown + ) { + super(message); + this.name = "GitHubError"; + } +} + +export class GitHubValidationError extends GitHubError { + constructor(message: string, status: number, response: unknown) { + super(message, status, response); + this.name = "GitHubValidationError"; + } +} + +export class GitHubResourceNotFoundError extends GitHubError { + constructor(resource: string) { + super(`Resource not found: ${resource}`, 404, { message: `${resource} not found` }); + this.name = "GitHubResourceNotFoundError"; + } +} + +export class GitHubAuthenticationError extends GitHubError { + constructor(message = "Authentication failed") { + super(message, 401, { message }); + this.name = "GitHubAuthenticationError"; + } +} + +export class GitHubPermissionError extends GitHubError { + constructor(message = "Insufficient permissions") { + super(message, 403, { message }); + this.name = "GitHubPermissionError"; + } +} + +export class GitHubRateLimitError extends GitHubError { + constructor( + message = "Rate limit exceeded", + public readonly resetAt: Date + ) { + super(message, 429, { message, reset_at: resetAt.toISOString() }); + this.name = "GitHubRateLimitError"; + } +} + +export class GitHubConflictError extends GitHubError { + constructor(message: string) { + super(message, 409, { message }); + this.name = "GitHubConflictError"; + } +} + +export function isGitHubError(error: unknown): error is GitHubError { + return error instanceof GitHubError; +} + +export function createGitHubError(status: number, response: any): GitHubError { + switch (status) { + case 401: + return new GitHubAuthenticationError(response?.message); + case 403: + return new GitHubPermissionError(response?.message); + case 404: + return new GitHubResourceNotFoundError(response?.message || "Resource"); + case 409: + return new GitHubConflictError(response?.message || "Conflict occurred"); + case 422: + return new GitHubValidationError( + response?.message || "Validation failed", + status, + response + ); + case 429: + return new GitHubRateLimitError( + response?.message, + new Date(response?.reset_at || Date.now() + 60000) + ); + default: + return new GitHubError( + response?.message || "GitHub API error", + status, + response + ); + } +} \ No newline at end of file From ff2f2c5347e442fbe6c070b7bd292a170e6a4b3d Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:25:31 -0800 Subject: [PATCH 48/67] feat: enhance GitHub request utilities with error handling --- src/github/common/utils.ts | 147 ++++++++++++++++++++++++++++--------- 1 file changed, 112 insertions(+), 35 deletions(-) diff --git a/src/github/common/utils.ts b/src/github/common/utils.ts index 0e9e6526..ef2fc0bb 100644 --- a/src/github/common/utils.ts +++ b/src/github/common/utils.ts @@ -1,46 +1,123 @@ -import fetch from "node-fetch"; +import { createGitHubError } from "./errors.js"; -if (!process.env.GITHUB_PERSONAL_ACCESS_TOKEN) { - console.error("GITHUB_PERSONAL_ACCESS_TOKEN environment variable is not set"); - process.exit(1); -} - -export const GITHUB_PERSONAL_ACCESS_TOKEN = process.env.GITHUB_PERSONAL_ACCESS_TOKEN; - -interface GitHubRequestOptions { +type RequestOptions = { method?: string; - body?: any; + body?: unknown; + headers?: Record; +}; + +async function parseResponseBody(response: Response): Promise { + const contentType = response.headers.get("content-type"); + if (contentType?.includes("application/json")) { + return response.json(); + } + return response.text(); } -export async function githubRequest(url: string, options: GitHubRequestOptions = {}) { - const response = await fetch(url, { - method: options.method || "GET", - headers: { - Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`, - Accept: "application/vnd.github.v3+json", - "User-Agent": "github-mcp-server", - ...(options.body ? { "Content-Type": "application/json" } : {}), - }, - ...(options.body ? { body: JSON.stringify(options.body) } : {}), - }); +export async function githubRequest( + url: string, + options: RequestOptions = {} +): Promise { + const headers = { + "Accept": "application/vnd.github.v3+json", + "Content-Type": "application/json", + ...options.headers, + }; - if (!response.ok) { - throw new Error(`GitHub API error: ${response.statusText}`); + if (process.env.GITHUB_TOKEN) { + headers["Authorization"] = `Bearer ${process.env.GITHUB_TOKEN}`; } - return response.json(); + const response = await fetch(url, { + method: options.method || "GET", + headers, + body: options.body ? JSON.stringify(options.body) : undefined, + }); + + const responseBody = await parseResponseBody(response); + + if (!response.ok) { + throw createGitHubError(response.status, responseBody); + } + + return responseBody; } -export function buildUrl(baseUrl: string, params: Record = {}) { - const url = new URL(baseUrl); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - if (Array.isArray(value)) { - url.searchParams.append(key, value.join(",")); - } else { - url.searchParams.append(key, value.toString()); - } +export function validateBranchName(branch: string): string { + const sanitized = branch.trim(); + if (!sanitized) { + throw new Error("Branch name cannot be empty"); + } + if (sanitized.includes("..")) { + throw new Error("Branch name cannot contain '..'"); + } + if (/[\s~^:?*[\\\]]/.test(sanitized)) { + throw new Error("Branch name contains invalid characters"); + } + if (sanitized.startsWith("/") || sanitized.endsWith("/")) { + throw new Error("Branch name cannot start or end with '/'"); + } + if (sanitized.endsWith(".lock")) { + throw new Error("Branch name cannot end with '.lock'"); + } + return sanitized; +} + +export function validateRepositoryName(name: string): string { + const sanitized = name.trim().toLowerCase(); + if (!sanitized) { + throw new Error("Repository name cannot be empty"); + } + if (!/^[a-z0-9_.-]+$/.test(sanitized)) { + throw new Error( + "Repository name can only contain lowercase letters, numbers, hyphens, periods, and underscores" + ); + } + if (sanitized.startsWith(".") || sanitized.endsWith(".")) { + throw new Error("Repository name cannot start or end with a period"); + } + return sanitized; +} + +export function validateOwnerName(owner: string): string { + const sanitized = owner.trim().toLowerCase(); + if (!sanitized) { + throw new Error("Owner name cannot be empty"); + } + if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/.test(sanitized)) { + throw new Error( + "Owner name must start with a letter or number and can contain up to 39 characters" + ); + } + return sanitized; +} + +export async function checkBranchExists( + owner: string, + repo: string, + branch: string +): Promise { + try { + await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/branches/${branch}` + ); + return true; + } catch (error) { + if (error && typeof error === "object" && "status" in error && error.status === 404) { + return false; } - }); - return url.toString(); + throw error; + } +} + +export async function checkUserExists(username: string): Promise { + try { + await githubRequest(`https://api.github.com/users/${username}`); + return true; + } catch (error) { + if (error && typeof error === "object" && "status" in error && error.status === 404) { + return false; + } + throw error; + } } \ No newline at end of file From 10bd24dd02af122965b84d7327495e67e55f14f1 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:26:55 -0800 Subject: [PATCH 49/67] feat: enhance pull request operations with validation and error handling - Add branch existence validation - Add duplicate PR check - Add comprehensive error handling - Improve type safety with zod transforms - Add input sanitization --- src/github/operations/pulls.ts | 166 +++++++++++++++++++++++++++------ 1 file changed, 136 insertions(+), 30 deletions(-) diff --git a/src/github/operations/pulls.ts b/src/github/operations/pulls.ts index 0b9b9643..00ad695b 100644 --- a/src/github/operations/pulls.ts +++ b/src/github/operations/pulls.ts @@ -1,16 +1,28 @@ import { z } from "zod"; -import { githubRequest } from "../common/utils.js"; +import { + githubRequest, + validateBranchName, + validateOwnerName, + validateRepositoryName, + checkBranchExists, +} from "../common/utils.js"; import { GitHubIssueAssigneeSchema, GitHubRepositorySchema } from "../common/types.js"; +import { + GitHubError, + GitHubValidationError, + GitHubResourceNotFoundError, + GitHubConflictError, +} from "../common/errors.js"; const GITHUB_TITLE_MAX_LENGTH = 256; const GITHUB_BODY_MAX_LENGTH = 65536; export const RepositoryParamsSchema = z.object({ - owner: z.string().min(1), - repo: z.string().min(1), + owner: z.string().min(1).transform(validateOwnerName), + repo: z.string().min(1).transform(validateRepositoryName), }); export const GitHubPullRequestStateSchema = z.enum([ @@ -34,7 +46,7 @@ export const GitHubDirectionSchema = z.enum([ export const GitHubPullRequestRefSchema = z.object({ label: z.string(), - ref: z.string().min(1), + ref: z.string().min(1).transform(validateBranchName), sha: z.string().length(40), user: GitHubIssueAssigneeSchema, repo: GitHubRepositorySchema, @@ -73,8 +85,8 @@ export const GitHubPullRequestSchema = z.object({ export const ListPullRequestsOptionsSchema = z.object({ state: GitHubPullRequestStateSchema.optional(), - head: z.string().optional(), - base: z.string().optional(), + head: z.string().transform(validateBranchName).optional(), + base: z.string().transform(validateBranchName).optional(), sort: GitHubPullRequestSortSchema.optional(), direction: GitHubDirectionSchema.optional(), per_page: z.number().min(1).max(100).optional(), @@ -84,8 +96,8 @@ export const ListPullRequestsOptionsSchema = z.object({ export const CreatePullRequestOptionsSchema = z.object({ title: z.string().max(GITHUB_TITLE_MAX_LENGTH), body: z.string().max(GITHUB_BODY_MAX_LENGTH).optional(), - head: z.string().min(1), - base: z.string().min(1), + head: z.string().min(1).transform(validateBranchName), + base: z.string().min(1).transform(validateBranchName), maintainer_can_modify: z.boolean().optional(), draft: z.boolean().optional(), }); @@ -100,20 +112,86 @@ export type ListPullRequestsOptions = z.infer; export type GitHubPullRequestRef = z.infer; +async function validatePullRequestBranches( + owner: string, + repo: string, + head: string, + base: string +): Promise { + const [headExists, baseExists] = await Promise.all([ + checkBranchExists(owner, repo, head), + checkBranchExists(owner, repo, base), + ]); + + if (!headExists) { + throw new GitHubResourceNotFoundError(`Branch '${head}' not found`); + } + + if (!baseExists) { + throw new GitHubResourceNotFoundError(`Branch '${base}' not found`); + } + + if (head === base) { + throw new GitHubValidationError( + "Head and base branches cannot be the same", + 422, + { message: "Head and base branches must be different" } + ); + } +} + +async function checkForExistingPullRequest( + owner: string, + repo: string, + head: string, + base: string +): Promise { + const existingPRs = await listPullRequests({ + owner, + repo, + head, + base, + state: "open", + }); + + if (existingPRs.length > 0) { + throw new GitHubConflictError( + `A pull request already exists for ${head} into ${base}` + ); + } +} + export async function createPullRequest( params: z.infer ): Promise { const { owner, repo, ...options } = CreatePullRequestSchema.parse(params); - - const response = await githubRequest( - `https://api.github.com/repos/${owner}/${repo}/pulls`, - { - method: "POST", - body: options, - } - ); - return GitHubPullRequestSchema.parse(response); + try { + await validatePullRequestBranches(owner, repo, options.head, options.base); + await checkForExistingPullRequest(owner, repo, options.head, options.base); + + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/pulls`, + { + method: "POST", + body: options, + } + ); + + return GitHubPullRequestSchema.parse(response); + } catch (error) { + if (error instanceof GitHubError) { + throw error; + } + if (error instanceof z.ZodError) { + throw new GitHubValidationError( + "Invalid pull request data", + 422, + { errors: error.errors } + ); + } + throw error; + } } export async function getPullRequest( @@ -124,11 +202,25 @@ export async function getPullRequest( pullNumber: z.number().positive(), }).parse(params); - const response = await githubRequest( - `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}` - ); + try { + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}` + ); - return GitHubPullRequestSchema.parse(response); + return GitHubPullRequestSchema.parse(response); + } catch (error) { + if (error instanceof GitHubError) { + throw error; + } + if (error instanceof z.ZodError) { + throw new GitHubValidationError( + "Invalid pull request response data", + 422, + { errors: error.errors } + ); + } + throw error; + } } export async function listPullRequests( @@ -139,14 +231,28 @@ export async function listPullRequests( ...ListPullRequestsOptionsSchema.partial().shape, }).parse(params); - const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`); - - Object.entries(options).forEach(([key, value]) => { - if (value !== undefined) { - url.searchParams.append(key, value.toString()); - } - }); + try { + const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`); + + Object.entries(options).forEach(([key, value]) => { + if (value !== undefined) { + url.searchParams.append(key, value.toString()); + } + }); - const response = await githubRequest(url.toString()); - return z.array(GitHubPullRequestSchema).parse(response); + const response = await githubRequest(url.toString()); + return z.array(GitHubPullRequestSchema).parse(response); + } catch (error) { + if (error instanceof GitHubError) { + throw error; + } + if (error instanceof z.ZodError) { + throw new GitHubValidationError( + "Invalid pull request list response data", + 422, + { errors: error.errors } + ); + } + throw error; + } } \ No newline at end of file From 272e26935b7450216069256d0dd6331e1ed3a22f Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:31:15 -0800 Subject: [PATCH 50/67] feat: add GitHub error handling to MCP server - Import GitHubError types - Add error formatting utility - Update error handling in request handler --- src/github/index.ts | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/github/index.ts b/src/github/index.ts index 286cc7bb..fd77f017 100644 --- a/src/github/index.ts +++ b/src/github/index.ts @@ -15,6 +15,16 @@ import * as pulls from './operations/pulls.js'; import * as branches from './operations/branches.js'; import * as search from './operations/search.js'; import * as commits from './operations/commits.js'; +import { + GitHubError, + GitHubValidationError, + GitHubResourceNotFoundError, + GitHubAuthenticationError, + GitHubPermissionError, + GitHubRateLimitError, + GitHubConflictError, + isGitHubError, +} from './common/errors.js'; const server = new Server( { @@ -28,6 +38,29 @@ const server = new Server( } ); +function formatGitHubError(error: GitHubError): string { + let message = `GitHub API Error: ${error.message}`; + + if (error instanceof GitHubValidationError) { + message = `Validation Error: ${error.message}`; + if (error.response) { + message += `\nDetails: ${JSON.stringify(error.response)}`; + } + } else if (error instanceof GitHubResourceNotFoundError) { + message = `Not Found: ${error.message}`; + } else if (error instanceof GitHubAuthenticationError) { + message = `Authentication Failed: ${error.message}`; + } else if (error instanceof GitHubPermissionError) { + message = `Permission Denied: ${error.message}`; + } else if (error instanceof GitHubRateLimitError) { + message = `Rate Limit Exceeded: ${error.message}\nResets at: ${error.resetAt.toISOString()}`; + } else if (error instanceof GitHubConflictError) { + message = `Conflict: ${error.message}`; + } + + return message; +} + server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ @@ -298,7 +331,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } } catch (error) { if (error instanceof z.ZodError) { - throw new Error(`ZodErrors: ${JSON.stringify(error.errors)}`); + throw new Error(`Invalid input: ${JSON.stringify(error.errors)}`); + } + if (isGitHubError(error)) { + throw new Error(formatGitHubError(error)); } throw error; } From 3e1b3caaec9c7525ef2641ebc29bb57447d0b787 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:34:39 -0800 Subject: [PATCH 51/67] fix: resolve typescript errors and add buildUrl utility - Fix headers type assertion issue - Add buildUrl utility function for URL parameter handling --- src/github/common/utils.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/github/common/utils.ts b/src/github/common/utils.ts index ef2fc0bb..0e6f731a 100644 --- a/src/github/common/utils.ts +++ b/src/github/common/utils.ts @@ -14,11 +14,21 @@ async function parseResponseBody(response: Response): Promise { return response.text(); } +export function buildUrl(baseUrl: string, params: Record): string { + const url = new URL(baseUrl); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined) { + url.searchParams.append(key, value.toString()); + } + }); + return url.toString(); +} + export async function githubRequest( url: string, options: RequestOptions = {} ): Promise { - const headers = { + const headers: Record = { "Accept": "application/vnd.github.v3+json", "Content-Type": "application/json", ...options.headers, From 10f0aec693273a30c16d4e1c03c61914d759a24b Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:34:45 -0800 Subject: [PATCH 52/67] fix: use buildUrl utility in commits module --- src/github/operations/commits.ts | 97 +++++--------------------------- 1 file changed, 14 insertions(+), 83 deletions(-) diff --git a/src/github/operations/commits.ts b/src/github/operations/commits.ts index 09b4302a..b10e1b5f 100644 --- a/src/github/operations/commits.ts +++ b/src/github/operations/commits.ts @@ -1,95 +1,26 @@ import { z } from "zod"; import { githubRequest, buildUrl } from "../common/utils.js"; -import { GitHubCommitSchema, GitHubListCommitsSchema } from "../common/types.js"; -// Schema definitions export const ListCommitsSchema = z.object({ - owner: z.string().describe("Repository owner (username or organization)"), - repo: z.string().describe("Repository name"), - 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)"), - sha: z.string().optional().describe("SHA of the commit to start listing from"), + owner: z.string(), + repo: z.string(), + sha: z.string().optional(), + page: z.number().optional(), + perPage: z.number().optional() }); -// Type exports -export type ListCommitsParams = z.infer; - -// Function implementations export async function listCommits( owner: string, repo: string, - page: number = 1, - perPage: number = 30, + page?: number, + perPage?: number, sha?: string ) { - const params = { - page, - per_page: perPage, - ...(sha ? { sha } : {}) - }; - - const url = buildUrl(`https://api.github.com/repos/${owner}/${repo}/commits`, params); - - const response = await githubRequest(url); - return GitHubListCommitsSchema.parse(response); -} - -export async function getCommit( - owner: string, - repo: string, - sha: string -) { - const response = await githubRequest( - `https://api.github.com/repos/${owner}/${repo}/git/commits/${sha}` + return githubRequest( + buildUrl(`https://api.github.com/repos/${owner}/${repo}/commits`, { + page: page?.toString(), + per_page: perPage?.toString(), + sha + }) ); - - return GitHubCommitSchema.parse(response); -} - -export async function createCommit( - owner: string, - repo: string, - message: string, - tree: string, - parents: string[] -) { - const response = await githubRequest( - `https://api.github.com/repos/${owner}/${repo}/git/commits`, - { - method: "POST", - body: { - message, - tree, - parents, - }, - } - ); - - return GitHubCommitSchema.parse(response); -} - -export async function compareCommits( - owner: string, - repo: string, - base: string, - head: string -) { - const response = await githubRequest( - `https://api.github.com/repos/${owner}/${repo}/compare/${base}...${head}` - ); - - return z.object({ - url: z.string(), - html_url: z.string(), - permalink_url: z.string(), - diff_url: z.string(), - patch_url: z.string(), - base_commit: GitHubCommitSchema, - merge_base_commit: GitHubCommitSchema, - commits: z.array(GitHubCommitSchema), - total_commits: z.number(), - status: z.string(), - ahead_by: z.number(), - behind_by: z.number(), - }).parse(response); -} +} \ No newline at end of file From dac0b7cc343dcdc1805a892336ecf8824e381ba3 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:34:59 -0800 Subject: [PATCH 53/67] fix: use buildUrl utility in issues module --- src/github/operations/issues.ts | 153 ++++++++++++-------------------- 1 file changed, 55 insertions(+), 98 deletions(-) diff --git a/src/github/operations/issues.ts b/src/github/operations/issues.ts index aec154f0..4681d26c 100644 --- a/src/github/operations/issues.ts +++ b/src/github/operations/issues.ts @@ -1,41 +1,43 @@ import { z } from "zod"; import { githubRequest, buildUrl } from "../common/utils.js"; -import { - GitHubIssueSchema, - GitHubLabelSchema, - GitHubIssueAssigneeSchema, - GitHubMilestoneSchema, -} from "../common/types.js"; -// Schema definitions +export const GetIssueSchema = z.object({ + owner: z.string(), + repo: z.string(), + issue_number: z.number(), +}); + +export const IssueCommentSchema = z.object({ + owner: z.string(), + repo: z.string(), + issue_number: z.number(), + body: z.string(), +}); + export const CreateIssueOptionsSchema = z.object({ - 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"), - milestone: z.number().optional().describe("Milestone number to assign"), - labels: z.array(z.string()).optional().describe("Array of label names"), + 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 CreateIssueSchema = z.object({ - owner: z.string().describe("Repository owner (username or organization)"), - repo: z.string().describe("Repository name"), - 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"), + owner: z.string(), + repo: z.string(), + ...CreateIssueOptionsSchema.shape, }); export const ListIssuesOptionsSchema = z.object({ owner: z.string(), repo: z.string(), - state: z.enum(['open', 'closed', 'all']).optional(), + direction: z.enum(["asc", "desc"]).optional(), labels: z.array(z.string()).optional(), - sort: z.enum(['created', 'updated', 'comments']).optional(), - direction: z.enum(['asc', 'desc']).optional(), - since: z.string().optional(), // ISO 8601 timestamp page: z.number().optional(), - per_page: z.number().optional() + per_page: z.number().optional(), + since: z.string().optional(), + sort: z.enum(["created", "updated", "comments"]).optional(), + state: z.enum(["open", "closed", "all"]).optional(), }); export const UpdateIssueOptionsSchema = z.object({ @@ -44,108 +46,63 @@ export const UpdateIssueOptionsSchema = z.object({ issue_number: z.number(), title: z.string().optional(), body: z.string().optional(), - state: z.enum(['open', 'closed']).optional(), - labels: z.array(z.string()).optional(), assignees: z.array(z.string()).optional(), - milestone: z.number().optional() + milestone: z.number().optional(), + labels: z.array(z.string()).optional(), + state: z.enum(["open", "closed"]).optional(), }); -export const IssueCommentSchema = z.object({ - owner: z.string(), - repo: z.string(), - issue_number: z.number(), - body: z.string() -}); +export async function getIssue(owner: string, repo: string, issue_number: number) { + return githubRequest(`https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}`); +} -export const GetIssueSchema = z.object({ - owner: z.string().describe("Repository owner (username or organization)"), - repo: z.string().describe("Repository name"), - issue_number: z.number().describe("Issue number") -}); +export async function addIssueComment( + owner: string, + repo: string, + issue_number: number, + body: string +) { + return githubRequest(`https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}/comments`, { + method: "POST", + body: { body }, + }); +} -// Type exports -export type CreateIssueOptions = z.infer; -export type ListIssuesOptions = z.infer; -export type UpdateIssueOptions = z.infer; - -// Function implementations export async function createIssue( owner: string, repo: string, - options: CreateIssueOptions + options: z.infer ) { - const response = await githubRequest( + return githubRequest( `https://api.github.com/repos/${owner}/${repo}/issues`, { method: "POST", body: options, } ); - - return GitHubIssueSchema.parse(response); } export async function listIssues( owner: string, repo: string, - options: Omit + options: Omit, "owner" | "repo"> ) { - const url = buildUrl(`https://api.github.com/repos/${owner}/${repo}/issues`, options); - const response = await githubRequest(url); - return z.array(GitHubIssueSchema).parse(response); + return githubRequest( + buildUrl(`https://api.github.com/repos/${owner}/${repo}/issues`, options) + ); } export async function updateIssue( owner: string, repo: string, - issueNumber: number, - options: Omit + issue_number: number, + options: Omit, "owner" | "repo" | "issue_number"> ) { - const response = await githubRequest( - `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`, + return githubRequest( + `https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}`, { method: "PATCH", - body: options + body: options, } ); - - return GitHubIssueSchema.parse(response); -} - -export async function addIssueComment( - owner: string, - repo: string, - issueNumber: number, - body: string -) { - const response = await githubRequest( - `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`, - { - method: "POST", - body: { body } - } - ); - - return z.object({ - id: z.number(), - node_id: z.string(), - url: z.string(), - html_url: z.string(), - body: z.string(), - user: GitHubIssueAssigneeSchema, - created_at: z.string(), - updated_at: z.string(), - }).parse(response); -} - -export async function getIssue( - owner: string, - repo: string, - issueNumber: number -) { - const response = await githubRequest( - `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}` - ); - - return GitHubIssueSchema.parse(response); -} +} \ No newline at end of file From 8016e366cd0fff812f7ae7aa134d314c8aaa5197 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:35:09 -0800 Subject: [PATCH 54/67] fix: use buildUrl utility in search module --- src/github/operations/search.ts | 101 +++++++------------------------- 1 file changed, 21 insertions(+), 80 deletions(-) diff --git a/src/github/operations/search.ts b/src/github/operations/search.ts index 08e2fd17..76faa729 100644 --- a/src/github/operations/search.ts +++ b/src/github/operations/search.ts @@ -1,16 +1,18 @@ import { z } from "zod"; import { githubRequest, buildUrl } from "../common/utils.js"; -// Schema definitions -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"), +export const SearchOptions = z.object({ + q: z.string(), + order: z.enum(["asc", "desc"]).optional(), + page: z.number().min(1).optional(), + per_page: z.number().min(1).max(100).optional(), }); -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"), +export const SearchUsersOptions = SearchOptions.extend({ + sort: z.enum(["followers", "repositories", "joined"]).optional(), +}); + +export const SearchIssuesOptions = SearchOptions.extend({ sort: z.enum([ "comments", "reactions", @@ -23,82 +25,21 @@ export const SearchIssuesSchema = z.object({ "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"), + ]).optional(), }); -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 const SearchCodeSchema = SearchOptions; +export const SearchUsersSchema = SearchUsersOptions; +export const SearchIssuesSchema = SearchIssuesOptions; -// Response schemas -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: z.object({ - full_name: z.string(), - description: z.string().nullable(), - url: z.string(), - html_url: z.string(), - }).describe("The repository where this file was found"), - score: z.number().describe("The search result score"), -}); - -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"), -}); - -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(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"), - 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"), - })).describe("The search results"), -}); - -// Type exports -export type SearchCodeParams = z.infer; -export type SearchIssuesParams = z.infer; -export type SearchUsersParams = z.infer; -export type SearchCodeResponse = z.infer; -export type SearchUsersResponse = z.infer; - -// Function implementations -export async function searchCode(params: SearchCodeParams): Promise { - const url = buildUrl("https://api.github.com/search/code", params); - const response = await githubRequest(url); - return SearchCodeResponseSchema.parse(response); +export async function searchCode(params: z.infer) { + return githubRequest(buildUrl("https://api.github.com/search/code", params)); } -export async function searchIssues(params: SearchIssuesParams) { - const url = buildUrl("https://api.github.com/search/issues", params); - const response = await githubRequest(url); - return response; +export async function searchIssues(params: z.infer) { + return githubRequest(buildUrl("https://api.github.com/search/issues", params)); } -export async function searchUsers(params: SearchUsersParams): Promise { - const url = buildUrl("https://api.github.com/search/users", params); - const response = await githubRequest(url); - return SearchUsersResponseSchema.parse(response); -} +export async function searchUsers(params: z.infer) { + return githubRequest(buildUrl("https://api.github.com/search/users", params)); +} \ No newline at end of file From cfd613693c9f0072b6ae7b46de1ea67473fcba92 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:39:15 -0800 Subject: [PATCH 55/67] fix: handle URL parameter types correctly in listIssues function --- src/github/operations/issues.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/github/operations/issues.ts b/src/github/operations/issues.ts index 4681d26c..d2907bf7 100644 --- a/src/github/operations/issues.ts +++ b/src/github/operations/issues.ts @@ -87,8 +87,18 @@ export async function listIssues( repo: string, options: Omit, "owner" | "repo"> ) { + const urlParams: Record = { + direction: options.direction, + labels: options.labels?.join(","), + page: options.page?.toString(), + per_page: options.per_page?.toString(), + since: options.since, + sort: options.sort, + state: options.state + }; + return githubRequest( - buildUrl(`https://api.github.com/repos/${owner}/${repo}/issues`, options) + buildUrl(`https://api.github.com/repos/${owner}/${repo}/issues`, urlParams) ); } From 339a7b67088dab021467c36110740db8eb349173 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 03:48:07 -0800 Subject: [PATCH 56/67] fix: restore original environment variable name for GitHub token --- src/github/common/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/github/common/utils.ts b/src/github/common/utils.ts index 0e6f731a..21c8aa71 100644 --- a/src/github/common/utils.ts +++ b/src/github/common/utils.ts @@ -34,8 +34,8 @@ export async function githubRequest( ...options.headers, }; - if (process.env.GITHUB_TOKEN) { - headers["Authorization"] = `Bearer ${process.env.GITHUB_TOKEN}`; + if (process.env.GITHUB_PERSONAL_ACCESS_TOKEN) { + headers["Authorization"] = `Bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`; } const response = await fetch(url, { From eea524abcf884def276d1b1a28838b62c238b0d0 Mon Sep 17 00:00:00 2001 From: "Peter M. Elias" Date: Sat, 28 Dec 2024 13:10:13 -0800 Subject: [PATCH 57/67] fix: make checkForExistingPullRequest check exact head/base match --- src/github/operations/pulls.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/github/operations/pulls.ts b/src/github/operations/pulls.ts index 00ad695b..a3e05e63 100644 --- a/src/github/operations/pulls.ts +++ b/src/github/operations/pulls.ts @@ -149,12 +149,15 @@ async function checkForExistingPullRequest( const existingPRs = await listPullRequests({ owner, repo, - head, - base, state: "open", }); - if (existingPRs.length > 0) { + // Check if any existing open PR has the exact same head and base combination + const duplicatePR = existingPRs.find(pr => + pr.head.ref === head && pr.base.ref === base + ); + + if (duplicatePR) { throw new GitHubConflictError( `A pull request already exists for ${head} into ${base}` ); From 3d8c33cd9ac9b85458cdb58b628a747d7f8a3d31 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Mon, 13 Jan 2025 20:01:18 +0000 Subject: [PATCH 58/67] feat: enhance release workflow with artifact collection and job output handling --- .github/workflows/release.yml | 119 +++++++++++++++++++++++++++------- 1 file changed, 95 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d2d084de..588cacf5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,14 +21,17 @@ jobs: - name: Find package directories id: set-matrix run: | + # Find all package.json and pyproject.toml files, excluding root DIRS=$(git ls-tree -r HEAD --name-only | grep -E "package.json|pyproject.toml" | xargs dirname | grep -v "^.$" | jq -R -s -c 'split("\n")[:-1]') echo "matrix=${DIRS}" >> $GITHUB_OUTPUT + echo "Found directories: ${DIRS}" - name: Get last release hash id: last-release run: | HASH=$(git rev-list --tags --max-count=1 || echo "HEAD~1") echo "hash=${HASH}" >> $GITHUB_OUTPUT + echo "Using last release hash: ${HASH}" release: needs: prepare @@ -50,23 +53,76 @@ jobs: - uses: astral-sh/setup-uv@v5 - name: Setup Node.js - if: endsWith(matrix.directory, 'package.json') + if: endsWith(matrix.directory, '/package.json') uses: actions/setup-node@v4 with: node-version: '18' registry-url: 'https://registry.npmjs.org' - name: Setup Python - if: endsWith(matrix.directory, 'pyproject.toml') + if: endsWith(matrix.directory, '/pyproject.toml') run: uv python install - name: Release package + id: release env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }} - run: uv run --script scripts/release.py "${{ matrix.directory }}" "${{ needs.prepare.outputs.last_release }}" >> "$GITHUB_OUTPUT" + run: | + # Create unique hash for this directory + dir_hash=$(echo "${{ matrix.directory }}" | sha256sum | awk '{print $1}') - create-release: + # Run git diff first to show changes + echo "Changes since last release:" + git diff --name-only "${{ needs.prepare.outputs.last_release }}" -- "${{ matrix.directory }}" || true + + # Run the release + output=$(uv run --script scripts/release.py "${{ matrix.directory }}" "${{ needs.prepare.outputs.last_release }}" 2>&1) + exit_code=$? + + echo "Release output (exit code: $exit_code):" + echo "$output" + + # Extract package info if successful + if [ $exit_code -eq 0 ]; then + pkg_info=$(echo "$output" | grep -o -E "[a-zA-Z0-9\-]+@[0-9]+\.[0-9]+\.[0-9]+" || true) + else + echo "Release failed" + exit 1 + fi + + if [ ! -z "$pkg_info" ]; then + echo "Released package: $pkg_info" + + # Create outputs directory + mkdir -p ./outputs + + # Save both package info and full changes + echo "$pkg_info" > "./outputs/${dir_hash}_info" + echo "dir_hash=${dir_hash}" >> $GITHUB_OUTPUT + + # Log what we're saving + echo "Saved package info to ./outputs/${dir_hash}_info:" + cat "./outputs/${dir_hash}_info" + else + echo "No release needed for this package" + fi + + - name: Set artifact name + if: steps.release.outputs.dir_hash + id: artifact + run: | + # Replace forward slashes with dashes + SAFE_DIR=$(echo "${{ matrix.directory }}" | tr '/' '-') + echo "name=release-outputs-${SAFE_DIR}" >> $GITHUB_OUTPUT + + - uses: actions/upload-artifact@v4 + if: steps.release.outputs.dir_hash + with: + name: ${{ steps.artifact.outputs.name }} + path: ./outputs/${{ steps.release.outputs.dir_hash }}* + + create-tag: needs: [prepare, release] runs-on: ubuntu-latest permissions: @@ -74,30 +130,45 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Create Release + - uses: actions/download-artifact@v4 + with: + pattern: release-outputs-src-* + merge-multiple: true + path: outputs + + - name: Create tag and release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Check if there's output from release step - if [ -s "$GITHUB_OUTPUT" ]; then - DATE=$(date +%Y.%m.%d) + if [ -d outputs ]; then + # Collect package info + find outputs -name "*_info" -exec cat {} \; > packages.txt - # Create git tag - git tag -s -a -m"automated release v${DATE}" "v${DATE}" - git push origin "v${DATE}" + if [ -s packages.txt ]; then + DATE=$(date +%Y.%m.%d) + echo "Creating tag v${DATE}" - # Create release notes - echo "# Release ${DATE}" > notes.md - echo "" >> notes.md - echo "## Updated Packages" >> notes.md + # Generate comprehensive release notes + { + echo "# Release ${DATE}" + echo "" + echo "## Updated Packages" + while IFS= read -r line; do + echo "- $line" + done < packages.txt + } > notes.md - # Read updated packages from github output - while IFS= read -r line; do - echo "- ${line}" >> notes.md - done < "$GITHUB_OUTPUT" + # Create and push tag + git tag -a "v${DATE}" -m "Release ${DATE}" + git push origin "v${DATE}" - # Create GitHub release - gh release create "v${DATE}" \ - --title "Release ${DATE}" \ - --notes-file notes.md - fi + # Create GitHub release + gh release create "v${DATE}" \ + --title "Release ${DATE}" \ + --notes-file notes.md + else + echo "No packages need release" + fi + else + echo "No release artifacts found" + fi \ No newline at end of file From a5dbc1d3d3ae32cf4eda2972aa6d56105f37d022 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Mon, 13 Jan 2025 20:07:29 +0000 Subject: [PATCH 59/67] fix: dont specify username --- scripts/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release.py b/scripts/release.py index 17329560..ace528eb 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -135,7 +135,7 @@ def publish_package( # Build and publish to PyPI subprocess.run(["uv", "build"], cwd=path, check=True) subprocess.run( - ["uv", "publish", "--username", "__token__"], + ["uv", "publish"], cwd=path, check=True, ) From fbb0514749279695d0f0c46e2096e93522a271fc Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Mon, 13 Jan 2025 20:32:39 +0000 Subject: [PATCH 60/67] feat: improvements to release.yml --- .github/workflows/release.yml | 213 ++++++++++++++-------------------- 1 file changed, 90 insertions(+), 123 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 588cacf5..ee6eebf3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,31 +1,18 @@ -name: Release +name: Automatic Release Creation on: - schedule: - # Run every day at 9:00 UTC - - cron: '0 9 * * *' - # Allow manual trigger for testing workflow_dispatch: jobs: - prepare: + detect-last-release: runs-on: ubuntu-latest outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} last_release: ${{ steps.last-release.outputs.hash }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Find package directories - id: set-matrix - run: | - # Find all package.json and pyproject.toml files, excluding root - DIRS=$(git ls-tree -r HEAD --name-only | grep -E "package.json|pyproject.toml" | xargs dirname | grep -v "^.$" | jq -R -s -c 'split("\n")[:-1]') - echo "matrix=${DIRS}" >> $GITHUB_OUTPUT - echo "Found directories: ${DIRS}" - - name: Get last release hash id: last-release run: | @@ -33,142 +20,122 @@ jobs: echo "hash=${HASH}" >> $GITHUB_OUTPUT echo "Using last release hash: ${HASH}" - release: - needs: prepare + create-tag-name: runs-on: ubuntu-latest - environment: release - strategy: - matrix: - directory: ${{ fromJson(needs.prepare.outputs.matrix) }} - fail-fast: false - permissions: - contents: write - packages: write + outputs: + tag_name: ${{ steps.last-release.outputs.tag}} + steps: + - name: Get last release hash + id: last-release + run: | + DATE=$(date +%Y.%m.%d) + echo "tag=v${DATE}" >> $GITHUB_OUTPUT + echo "Using tag: v${DATE}" + detect-packages: + needs: [detect-last-release] + runs-on: ubuntu-latest + outputs: + packages: ${{ steps.find-packages.outputs.packages }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: astral-sh/setup-uv@v5 + - name: Install uv + uses: astral-sh/setup-uv@v5 - - name: Setup Node.js - if: endsWith(matrix.directory, '/package.json') - uses: actions/setup-node@v4 - with: - node-version: '18' - registry-url: 'https://registry.npmjs.org' - - - name: Setup Python - if: endsWith(matrix.directory, '/pyproject.toml') - run: uv python install - - - name: Release package - id: release - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }} + - name: Find packages + id: find-packages + working-directory: src run: | - # Create unique hash for this directory - dir_hash=$(echo "${{ matrix.directory }}" | sha256sum | awk '{print $1}') + cat << 'EOF' > find_packages.py + import json + import os + import subprocess + from itertools import chain + from pathlib import Path - # Run git diff first to show changes - echo "Changes since last release:" - git diff --name-only "${{ needs.prepare.outputs.last_release }}" -- "${{ matrix.directory }}" || true + packages = [] - # Run the release - output=$(uv run --script scripts/release.py "${{ matrix.directory }}" "${{ needs.prepare.outputs.last_release }}" 2>&1) - exit_code=$? + print("Starting package detection...") + print(f"Using LAST_RELEASE: {os.environ['LAST_RELEASE']}") - echo "Release output (exit code: $exit_code):" - echo "$output" + # Find all directories containing package.json or pyproject.toml + paths = chain(Path('.').glob('*/package.json'), Path('.').glob('*/pyproject.toml')) + for path in paths: + print(f"\nChecking path: {path}") + # Check for changes in .py or .ts files + # Run git diff from the specific directory + cmd = ['git', 'diff', '--name-only', f'{os.environ["LAST_RELEASE"]}..HEAD', '--', '.'] + result = subprocess.run(cmd, capture_output=True, text=True, cwd=path.parent) - # Extract package info if successful - if [ $exit_code -eq 0 ]; then - pkg_info=$(echo "$output" | grep -o -E "[a-zA-Z0-9\-]+@[0-9]+\.[0-9]+\.[0-9]+" || true) - else - echo "Release failed" - exit 1 - fi + # Check if any .py or .ts files were changed + changed_files = result.stdout.strip().split('\n') + print(f"Changed files found: {changed_files}") - if [ ! -z "$pkg_info" ]; then - echo "Released package: $pkg_info" + has_changes = any(f.endswith(('.py', '.ts')) for f in changed_files if f) + if has_changes: + print(f"Adding package: {path.parent}") + packages.append(str(path.parent)) - # Create outputs directory - mkdir -p ./outputs + print(f"\nFinal packages list: {packages}") - # Save both package info and full changes - echo "$pkg_info" > "./outputs/${dir_hash}_info" - echo "dir_hash=${dir_hash}" >> $GITHUB_OUTPUT + # Write output + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f"packages={json.dumps(packages)}\n") + EOF - # Log what we're saving - echo "Saved package info to ./outputs/${dir_hash}_info:" - cat "./outputs/${dir_hash}_info" - else - echo "No release needed for this package" - fi - - - name: Set artifact name - if: steps.release.outputs.dir_hash - id: artifact - run: | - # Replace forward slashes with dashes - SAFE_DIR=$(echo "${{ matrix.directory }}" | tr '/' '-') - echo "name=release-outputs-${SAFE_DIR}" >> $GITHUB_OUTPUT - - - uses: actions/upload-artifact@v4 - if: steps.release.outputs.dir_hash - with: - name: ${{ steps.artifact.outputs.name }} - path: ./outputs/${{ steps.release.outputs.dir_hash }}* + LAST_RELEASE=${{ needs.detect-last-release.outputs.last_release }} uv run --script --python 3.12 find_packages.py create-tag: - needs: [prepare, release] + needs: [detect-packages, create-tag-name] runs-on: ubuntu-latest + environment: release permissions: contents: write steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - pattern: release-outputs-src-* - merge-multiple: true - path: outputs - - - name: Create tag and release + - name: Create tag env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - if [ -d outputs ]; then - # Collect package info - find outputs -name "*_info" -exec cat {} \; > packages.txt + # Configure git + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" - if [ -s packages.txt ]; then - DATE=$(date +%Y.%m.%d) - echo "Creating tag v${DATE}" + # Get packages array + PACKAGES='${{ needs.detect-packages.outputs.packages }}' - # Generate comprehensive release notes - { - echo "# Release ${DATE}" - echo "" - echo "## Updated Packages" - while IFS= read -r line; do - echo "- $line" - done < packages.txt - } > notes.md - - # Create and push tag - git tag -a "v${DATE}" -m "Release ${DATE}" - git push origin "v${DATE}" - - # Create GitHub release - gh release create "v${DATE}" \ - --title "Release ${DATE}" \ - --notes-file notes.md - else - echo "No packages need release" - fi + if [ "$(echo "$PACKAGES" | jq 'length')" -gt 0 ]; then + # Create and push tag + git tag -a "${{ needs.create-tag-name.outputs.tag_name }}" -m "Release ${{ needs.create-tag-name.outputs.tag_name }}" + git push origin "${{ needs.create-tag-name.outputs.tag_name }}" else - echo "No release artifacts found" - fi \ No newline at end of file + echo "No packages need release" + fi + + - name: Create release + env: + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: | + PACKAGES='${{ needs.detect-packages.outputs.packages }}' + + if [ "$(echo "$PACKAGES" | jq 'length')" -gt 0 ]; then + # Generate comprehensive release notes + { + echo "# Release ${{ needs.create-tag-name.outputs.tag_name }}" + echo "" + echo "## Updated Packages" + echo "$PACKAGES" | jq -r '.[]' | while read -r package; do + echo "- $package" + done + } > notes.md + # Create GitHub release + gh release create "${{ needs.create-tag-name.outputs.tag_name }}" \ + --title "Release ${{ needs.create-tag-name.outputs.tag_name }}" \ + --notes-file notes.md + else + echo "No packages need release" + fi From 85e303aab801f2ec49944f946f6cd634295d9501 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Tue, 14 Jan 2025 00:35:12 +0000 Subject: [PATCH 61/67] feat: remove release check --- .github/workflows/release-check.yml | 159 ---------------------------- 1 file changed, 159 deletions(-) delete mode 100644 .github/workflows/release-check.yml diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml deleted file mode 100644 index 63d7dfa0..00000000 --- a/.github/workflows/release-check.yml +++ /dev/null @@ -1,159 +0,0 @@ -name: Release Check - -on: - # Allow manual trigger for testing - workflow_dispatch: - -jobs: - prepare: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - last_release: ${{ steps.last-release.outputs.hash }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Find package directories - id: set-matrix - run: | - # Find all package.json and pyproject.toml files, excluding root - DIRS=$(git ls-tree -r HEAD --name-only | grep -E "package.json|pyproject.toml" | xargs dirname | grep -v "^.$" | jq -R -s -c 'split("\n")[:-1]') - echo "matrix=${DIRS}" >> $GITHUB_OUTPUT - echo "Found directories: ${DIRS}" - - - name: Get last release hash - id: last-release - run: | - HASH=$(git rev-list --tags --max-count=1 || echo "HEAD~1") - echo "hash=${HASH}" >> $GITHUB_OUTPUT - echo "Using last release hash: ${HASH}" - - check-release: - needs: prepare - runs-on: ubuntu-latest - strategy: - matrix: - directory: ${{ fromJson(needs.prepare.outputs.matrix) }} - fail-fast: false - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: astral-sh/setup-uv@v5 - - - name: Setup Node.js - if: endsWith(matrix.directory, '/package.json') - uses: actions/setup-node@v4 - with: - node-version: '18' - - - name: Setup Python - if: endsWith(matrix.directory, '/pyproject.toml') - run: uv python install - - - name: Check release - id: check - run: | - # Create unique hash for this directory - dir_hash=$(echo "${{ matrix.directory }}" | sha256sum | awk '{print $1}') - - # Run release check script with verbose output - echo "Running release check against last release: ${{ needs.prepare.outputs.last_release }}" - - # Run git diff first to show changes - echo "Changes since last release:" - git diff --name-only "${{ needs.prepare.outputs.last_release }}" -- "${{ matrix.directory }}" || true - - # Run the release check - output=$(uv run --script scripts/release.py --dry-run "${{ matrix.directory }}" "${{ needs.prepare.outputs.last_release }}" 2>&1) - exit_code=$? - - echo "Release check output (exit code: $exit_code):" - echo "$output" - - # Extract package info if successful - if [ $exit_code -eq 0 ]; then - pkg_info=$(echo "$output" | grep -o -E "[a-zA-Z0-9\-]+@[0-9]+\.[0-9]+\.[0-9]+" || true) - else - echo "Release check failed" - exit 1 - fi - - if [ ! -z "$pkg_info" ]; then - echo "Found package that needs release: $pkg_info" - - # Create outputs directory - mkdir -p ./outputs - - # Save both package info and full changes - echo "$pkg_info" > "./outputs/${dir_hash}_info" - echo "dir_hash=${dir_hash}" >> $GITHUB_OUTPUT - - # Log what we're saving - echo "Saved package info to ./outputs/${dir_hash}_info:" - cat "./outputs/${dir_hash}_info" - else - echo "No release needed for this package" - fi - - - name: Set artifact name - if: steps.check.outputs.dir_hash - id: artifact - run: | - # Replace forward slashes with dashes - SAFE_DIR=$(echo "${{ matrix.directory }}" | tr '/' '-') - echo "name=release-outputs-${SAFE_DIR}" >> $GITHUB_OUTPUT - - - uses: actions/upload-artifact@v4 - if: steps.check.outputs.dir_hash - with: - name: ${{ steps.artifact.outputs.name }} - path: ./outputs/${{ steps.check.outputs.dir_hash }}* - - check-tag: - needs: [prepare, check-release] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - pattern: release-outputs-src-* - merge-multiple: true - path: outputs - - - name: Simulate tag creation - run: | - if [ -d outputs ]; then - # Collect package info - find outputs -name "*_info" -exec cat {} \; > packages.txt - - if [ -s packages.txt ]; then - DATE=$(date +%Y.%m.%d) - echo "🔍 Dry run: Would create tag v${DATE} if this was a real release" - - # Generate comprehensive release notes - { - echo "# Release ${DATE}" - echo "" - echo "## Updated Packages" - while IFS= read -r line; do - echo "- $line" - done < packages.txt - } > notes.md - - echo "🔍 Would create release with following notes:" - cat notes.md - - echo "🔍 Would create tag v${DATE} with the above release notes" - echo "🔍 Would create GitHub release from tag v${DATE}" - else - echo "No packages need release" - fi - else - echo "No release artifacts found" - fi From a156ab635b4e10e64f6ce273c211b425bc3956e4 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Tue, 14 Jan 2025 00:38:44 +0000 Subject: [PATCH 62/67] feat: only run create-tag job when packages are detected --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee6eebf3..b9cec3de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,6 +90,7 @@ jobs: create-tag: needs: [detect-packages, create-tag-name] + if: fromJson(needs.detect-packages.outputs.packages)[0] != null runs-on: ubuntu-latest environment: release permissions: From 9edf9fcaf043a7ef73cb8027742f3814407fe7aa Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Tue, 14 Jan 2025 00:39:59 +0000 Subject: [PATCH 63/67] feat: update package versions before creating release tag --- .github/workflows/release.yml | 76 ++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b9cec3de..2bd97e67 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,7 +98,15 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Create tag + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Update package versions and create tag env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -106,16 +114,66 @@ jobs: git config --global user.name "GitHub Actions" git config --global user.email "actions@github.com" - # Get packages array + # Get packages array and version PACKAGES='${{ needs.detect-packages.outputs.packages }}' + VERSION="${{ needs.create-tag-name.outputs.tag_name }}" + VERSION_NO_V="${VERSION#v}" # Remove 'v' prefix for package versions - if [ "$(echo "$PACKAGES" | jq 'length')" -gt 0 ]; then - # Create and push tag - git tag -a "${{ needs.create-tag-name.outputs.tag_name }}" -m "Release ${{ needs.create-tag-name.outputs.tag_name }}" - git push origin "${{ needs.create-tag-name.outputs.tag_name }}" - else - echo "No packages need release" - fi + # Create version update script + cat << 'EOF' > update_versions.py + import json + import os + import sys + import toml + from pathlib import Path + + def update_package_json(path, version): + with open(path) as f: + data = json.load(f) + data['version'] = version + with open(path, 'w') as f: + json.dump(data, f, indent=2) + f.write('\n') + + def update_pyproject_toml(path, version): + data = toml.load(path) + if 'project' in data: + data['project']['version'] = version + elif 'tool' in data and 'poetry' in data['tool']: + data['tool']['poetry']['version'] = version + with open(path, 'w') as f: + toml.dump(data, f) + + packages = json.loads(os.environ['PACKAGES']) + version = os.environ['VERSION'] + + for package_dir in packages: + package_dir = Path('src') / package_dir + + # Update package.json if it exists + package_json = package_dir / 'package.json' + if package_json.exists(): + update_package_json(package_json, version) + + # Update pyproject.toml if it exists + pyproject_toml = package_dir / 'pyproject.toml' + if pyproject_toml.exists(): + update_pyproject_toml(pyproject_toml, version) + EOF + + # Install toml package for Python + uv pip install toml + + # Update versions + PACKAGES="$PACKAGES" VERSION="$VERSION_NO_V" uv run update_versions.py + + # Commit version updates + git add src/*/package.json src/*/pyproject.toml + git commit -m "chore: update package versions to $VERSION" + + # Create and push tag + git tag -a "$VERSION" -m "Release $VERSION" + git push origin HEAD "$VERSION" - name: Create release env: From 8e944369b76dfb2bb8b47eec863cbdda49fef5b1 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Tue, 14 Jan 2025 01:18:25 +0000 Subject: [PATCH 64/67] improve release workflow more --- .github/workflows/release.yml | 209 ++++++++++------------------------ scripts/release.py | 199 ++++++++++++++++---------------- 2 files changed, 159 insertions(+), 249 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2bd97e67..b25ca834 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,10 +4,11 @@ on: workflow_dispatch: jobs: - detect-last-release: + create-metadata: runs-on: ubuntu-latest outputs: - last_release: ${{ steps.last-release.outputs.hash }} + hash: ${{ steps.last-release.outputs.hash }} + version: ${{ steps.create-version.outputs.version}} steps: - uses: actions/checkout@v4 with: @@ -20,23 +21,33 @@ jobs: echo "hash=${HASH}" >> $GITHUB_OUTPUT echo "Using last release hash: ${HASH}" - create-tag-name: - runs-on: ubuntu-latest - outputs: - tag_name: ${{ steps.last-release.outputs.tag}} - steps: - - name: Get last release hash - id: last-release - run: | - DATE=$(date +%Y.%m.%d) - echo "tag=v${DATE}" >> $GITHUB_OUTPUT - echo "Using tag: v${DATE}" + - name: Install uv + uses: astral-sh/setup-uv@v5 - detect-packages: - needs: [detect-last-release] + - name: Create version name + id: create-version + run: | + VERSION=$(uv run --script scripts/release.py generate-version) + echo "version $VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Create notes + run: | + HASH="${{ steps.last-release.outputs.hash }}" + uv run --script scripts/release.py generate-notes --directory src/ $HASH > RELEASE_NOTES.md + cat RELEASE_NOTES.md + + - name: Release notes + uses: actions/upload-artifact@v4 + with: + name: release-notes + path: RELEASE_NOTES.md + + update-packages: + needs: [create-metadata] runs-on: ubuntu-latest outputs: - packages: ${{ steps.find-packages.outputs.packages }} + changes_made: ${{ steps.commit.outputs.changes_made }} steps: - uses: actions/checkout@v4 with: @@ -45,52 +56,33 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 - - name: Find packages - id: find-packages - working-directory: src + - name: Update packages run: | - cat << 'EOF' > find_packages.py - import json - import os - import subprocess - from itertools import chain - from pathlib import Path + HASH="${{ needs.create-metadata.outputs.hash }}" + uv run --script scripts/release.py update-packages --directory src/ $HASH - packages = [] + - name: Configure git + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" - print("Starting package detection...") - print(f"Using LAST_RELEASE: {os.environ['LAST_RELEASE']}") + - name: Commit changes + id: commit + run: | + VERSION="${{ needs.create-metadata.outputs.version }}" + git add -u + if git diff-index --quiet HEAD; then + echo "changes_made=false" >> $GITHUB_OUTPUT + else + git commit -m 'Automatic update of packages' + git tag -a "$VERSION" -m "Release $VERSION" + git push origin "$VERSION" + echo "changes_made=true" >> $GITHUB_OUTPUT + fi - # Find all directories containing package.json or pyproject.toml - paths = chain(Path('.').glob('*/package.json'), Path('.').glob('*/pyproject.toml')) - for path in paths: - print(f"\nChecking path: {path}") - # Check for changes in .py or .ts files - # Run git diff from the specific directory - cmd = ['git', 'diff', '--name-only', f'{os.environ["LAST_RELEASE"]}..HEAD', '--', '.'] - result = subprocess.run(cmd, capture_output=True, text=True, cwd=path.parent) - - # Check if any .py or .ts files were changed - changed_files = result.stdout.strip().split('\n') - print(f"Changed files found: {changed_files}") - - has_changes = any(f.endswith(('.py', '.ts')) for f in changed_files if f) - if has_changes: - print(f"Adding package: {path.parent}") - packages.append(str(path.parent)) - - print(f"\nFinal packages list: {packages}") - - # Write output - with open(os.environ['GITHUB_OUTPUT'], 'a') as f: - f.write(f"packages={json.dumps(packages)}\n") - EOF - - LAST_RELEASE=${{ needs.detect-last-release.outputs.last_release }} uv run --script --python 3.12 find_packages.py - - create-tag: - needs: [detect-packages, create-tag-name] - if: fromJson(needs.detect-packages.outputs.packages)[0] != null + create-release: + needs: [update-packages, create-metadata] + if: needs.update-packages.outputs.changes_made == 'true' runs-on: ubuntu-latest environment: release permissions: @@ -98,103 +90,16 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v5 - - - name: Install Node.js - uses: actions/setup-node@v4 + - name: Download release notes + uses: actions/download-artifact@v4 with: - node-version: '20' - - - name: Update package versions and create tag - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Configure git - git config --global user.name "GitHub Actions" - git config --global user.email "actions@github.com" - - # Get packages array and version - PACKAGES='${{ needs.detect-packages.outputs.packages }}' - VERSION="${{ needs.create-tag-name.outputs.tag_name }}" - VERSION_NO_V="${VERSION#v}" # Remove 'v' prefix for package versions - - # Create version update script - cat << 'EOF' > update_versions.py - import json - import os - import sys - import toml - from pathlib import Path - - def update_package_json(path, version): - with open(path) as f: - data = json.load(f) - data['version'] = version - with open(path, 'w') as f: - json.dump(data, f, indent=2) - f.write('\n') - - def update_pyproject_toml(path, version): - data = toml.load(path) - if 'project' in data: - data['project']['version'] = version - elif 'tool' in data and 'poetry' in data['tool']: - data['tool']['poetry']['version'] = version - with open(path, 'w') as f: - toml.dump(data, f) - - packages = json.loads(os.environ['PACKAGES']) - version = os.environ['VERSION'] - - for package_dir in packages: - package_dir = Path('src') / package_dir - - # Update package.json if it exists - package_json = package_dir / 'package.json' - if package_json.exists(): - update_package_json(package_json, version) - - # Update pyproject.toml if it exists - pyproject_toml = package_dir / 'pyproject.toml' - if pyproject_toml.exists(): - update_pyproject_toml(pyproject_toml, version) - EOF - - # Install toml package for Python - uv pip install toml - - # Update versions - PACKAGES="$PACKAGES" VERSION="$VERSION_NO_V" uv run update_versions.py - - # Commit version updates - git add src/*/package.json src/*/pyproject.toml - git commit -m "chore: update package versions to $VERSION" - - # Create and push tag - git tag -a "$VERSION" -m "Release $VERSION" - git push origin HEAD "$VERSION" + name: release-notes - name: Create release env: GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | - PACKAGES='${{ needs.detect-packages.outputs.packages }}' - - if [ "$(echo "$PACKAGES" | jq 'length')" -gt 0 ]; then - # Generate comprehensive release notes - { - echo "# Release ${{ needs.create-tag-name.outputs.tag_name }}" - echo "" - echo "## Updated Packages" - echo "$PACKAGES" | jq -r '.[]' | while read -r package; do - echo "- $package" - done - } > notes.md - # Create GitHub release - gh release create "${{ needs.create-tag-name.outputs.tag_name }}" \ - --title "Release ${{ needs.create-tag-name.outputs.tag_name }}" \ - --notes-file notes.md - else - echo "No packages need release" - fi + VERSION="${{ needs.create-metadata.outputs.version }}" + gh release create "$VERSION" \ + --title "Release $VERSION" \ + --notes-file RELEASE_NOTES.md diff --git a/scripts/release.py b/scripts/release.py index ace528eb..4b84c0af 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,6 +1,6 @@ #!/usr/bin/env uv run --script # /// script -# requires-python = ">=3.11" +# requires-python = ">=3.12" # dependencies = [ # "click>=8.1.8", # "tomlkit>=0.13.2" @@ -14,8 +14,8 @@ import json import tomlkit import datetime import subprocess -from enum import Enum -from typing import Any, NewType +from dataclasses import dataclass +from typing import Any, Iterator, NewType, Protocol Version = NewType("Version", str) @@ -51,25 +51,58 @@ class GitHashParamType(click.ParamType): GIT_HASH = GitHashParamType() -class PackageType(Enum): - NPM = 1 - PYPI = 2 +class Package(Protocol): + path: Path - @classmethod - def from_path(cls, directory: Path) -> "PackageType": - if (directory / "package.json").exists(): - return cls.NPM - elif (directory / "pyproject.toml").exists(): - return cls.PYPI - else: - raise Exception("No package.json or pyproject.toml found") + def package_name(self) -> str: ... + + def update_version(self, version: Version) -> None: ... -def get_changes(path: Path, git_hash: str) -> bool: +@dataclass +class NpmPackage: + path: Path + + def package_name(self) -> str: + with open(self.path / "package.json", "r") as f: + return json.load(f)["name"] + + def update_version(self, version: Version): + with open(self.path / "package.json", "r+") as f: + data = json.load(f) + data["version"] = version + f.seek(0) + json.dump(data, f, indent=2) + f.truncate() + + +@dataclass +class PyPiPackage: + path: Path + + def package_name(self) -> str: + with open(self.path / "pyproject.toml") as f: + toml_data = tomlkit.parse(f.read()) + name = toml_data.get("project", {}).get("name") + if not name: + raise Exception("No name in pyproject.toml project section") + return str(name) + + def update_version(self, version: Version): + # Update version in pyproject.toml + with open(self.path / "pyproject.toml") as f: + data = tomlkit.parse(f.read()) + data["project"]["version"] = version + + with open(self.path / "pyproject.toml", "w") as f: + f.write(tomlkit.dumps(data)) + + +def has_changes(path: Path, git_hash: GitHash) -> bool: """Check if any files changed between current state and git hash""" try: output = subprocess.run( - ["git", "diff", "--name-only", git_hash, "--", path], + ["git", "diff", "--name-only", git_hash, "--", "."], cwd=path, check=True, capture_output=True, @@ -77,105 +110,77 @@ def get_changes(path: Path, git_hash: str) -> bool: ) changed_files = [Path(f) for f in output.stdout.splitlines()] - relevant_files = [f for f in changed_files if f.suffix in ['.py', '.ts']] + relevant_files = [f for f in changed_files if f.suffix in [".py", ".ts"]] return len(relevant_files) >= 1 except subprocess.CalledProcessError: return False -def get_package_name(path: Path, pkg_type: PackageType) -> str: - """Get package name from package.json or pyproject.toml""" - match pkg_type: - case PackageType.NPM: - with open(path / "package.json", "rb") as f: - return json.load(f)["name"] - case PackageType.PYPI: - with open(path / "pyproject.toml") as f: - toml_data = tomlkit.parse(f.read()) - name = toml_data.get("project", {}).get("name") - if not name: - raise Exception("No name in pyproject.toml project section") - return str(name) - - -def generate_version() -> Version: +def gen_version() -> Version: """Generate version based on current date""" now = datetime.datetime.now() return Version(f"{now.year}.{now.month}.{now.day}") -def publish_package( - path: Path, pkg_type: PackageType, version: Version, dry_run: bool = False -): - """Publish package based on type""" - try: - match pkg_type: - case PackageType.NPM: - # Update version in package.json - with open(path / "package.json", "rb+") as f: - data = json.load(f) - data["version"] = version - f.seek(0) - json.dump(data, f, indent=2) - f.truncate() - - if not dry_run: - # Publish to npm - subprocess.run(["npm", "publish"], cwd=path, check=True) - case PackageType.PYPI: - # Update version in pyproject.toml - with open(path / "pyproject.toml") as f: - data = tomlkit.parse(f.read()) - data["project"]["version"] = version - - with open(path / "pyproject.toml", "w") as f: - f.write(tomlkit.dumps(data)) - - if not dry_run: - # Build and publish to PyPI - subprocess.run(["uv", "build"], cwd=path, check=True) - subprocess.run( - ["uv", "publish"], - cwd=path, - check=True, - ) - except Exception as e: - raise Exception(f"Failed to publish: {e}") from e +def find_changed_packages(directory: Path, git_hash: GitHash) -> Iterator[Package]: + for path in directory.glob("*/package.json"): + if has_changes(path.parent, git_hash): + yield NpmPackage(path.parent) + for path in directory.glob("*/pyproject.toml"): + if has_changes(path.parent, git_hash): + yield PyPiPackage(path.parent) -@click.command() -@click.argument("directory", type=click.Path(exists=True, path_type=Path)) -@click.argument("git_hash", type=GIT_HASH) +@click.group() +def cli(): + pass + + +@cli.command("update-packages") @click.option( - "--dry-run", is_flag=True, help="Update version numbers but don't publish" + "--directory", type=click.Path(exists=True, path_type=Path), default=Path.cwd() ) -def main(directory: Path, git_hash: GitHash, dry_run: bool) -> int: - """Release package if changes detected""" +@click.argument("git_hash", type=GIT_HASH) +def update_packages(directory: Path, git_hash: GitHash) -> int: # Detect package type - try: - path = directory.resolve(strict=True) - pkg_type = PackageType.from_path(path) - except Exception as e: - return 1 + path = directory.resolve(strict=True) + version = gen_version() - # Check for changes - if not get_changes(path, git_hash): - return 0 + for package in find_changed_packages(path, git_hash): + name = package.package_name() + package.update_version(version) - try: - # Generate version and publish - version = generate_version() - name = get_package_name(path, pkg_type) + click.echo(f"{name}@{version}") - publish_package(path, pkg_type, version, dry_run) - if not dry_run: - click.echo(f"{name}@{version}") - else: - click.echo(f"Dry run: Would have published {name}@{version}") - return 0 - except Exception as e: - return 1 + return 0 + + +@cli.command("generate-notes") +@click.option( + "--directory", type=click.Path(exists=True, path_type=Path), default=Path.cwd() +) +@click.argument("git_hash", type=GIT_HASH) +def generate_notes(directory: Path, git_hash: GitHash) -> int: + # Detect package type + path = directory.resolve(strict=True) + version = gen_version() + + click.echo(f"# Release : v{version}") + click.echo("") + click.echo("## Updated packages") + for package in find_changed_packages(path, git_hash): + name = package.package_name() + click.echo(f"- {name}@{version}") + + return 0 + + +@cli.command("generate-version") +def generate_version() -> int: + # Detect package type + click.echo(gen_version()) + return 0 if __name__ == "__main__": - sys.exit(main()) + sys.exit(cli()) From 4f3dc110651e4f094c64a321ffb947840bcfe47e Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Tue, 14 Jan 2025 02:22:40 +0000 Subject: [PATCH 65/67] new release workflow --- .github/workflows/release.yml | 111 +++++++++++++++++++++++++++++++++- scripts/release.py | 24 ++++++++ 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b25ca834..4f687118 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,8 @@ name: Automatic Release Creation on: workflow_dispatch: + schedule: + - cron: '0 10 * * *' jobs: create-metadata: @@ -9,6 +11,8 @@ jobs: outputs: hash: ${{ steps.last-release.outputs.hash }} version: ${{ steps.create-version.outputs.version}} + npm_packages: ${{ steps.create-npm-packages.outputs.npm_packages}} + pypi_packages: ${{ steps.create-pypi-packages.outputs.pypi_packages}} steps: - uses: actions/checkout@v4 with: @@ -43,8 +47,25 @@ jobs: name: release-notes path: RELEASE_NOTES.md + - name: Create python matrix + id: create-pypi-packages + run: | + HASH="${{ steps.last-release.outputs.hash }}" + PYPI=$(uv run --script scripts/release.py generate-matrix --pypi --directory src $HASH) + echo "pypi_packages $PYPI" + echo "pypi_packages=$PYPI" >> $GITHUB_OUTPUT + + - name: Create npm matrix + id: create-npm-packages + run: | + HASH="${{ steps.last-release.outputs.hash }}" + NPM=$(uv run --script scripts/release.py generate-matrix --npm --directory src $HASH) + echo "npm_packages $NPM" + echo "npm_packages=$NPM" >> $GITHUB_OUTPUT + update-packages: needs: [create-metadata] + if: ${{ needs.create-metadata.outputs.npm_packages != '[]' || needs.create-metadata.outputs.pypi_packages != '[]' }} runs-on: ubuntu-latest outputs: changes_made: ${{ steps.commit.outputs.changes_made }} @@ -80,8 +101,94 @@ jobs: echo "changes_made=true" >> $GITHUB_OUTPUT fi - create-release: + publish-pypi: needs: [update-packages, create-metadata] + strategy: + fail-fast: false + matrix: + package: ${{ fromJson(needs.create-metadata.outputs.pypi_packages) }} + name: Build ${{ matrix.package }} + environment: release + permissions: + id-token: write # Required for trusted publishing + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.create-metadata.outputs.version }} + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: "src/${{ matrix.package }}/.python-version" + + - name: Install dependencies + working-directory: src/${{ matrix.package }} + run: uv sync --frozen --all-extras --dev + + - name: Run pyright + working-directory: src/${{ matrix.package }} + run: uv run --frozen pyright + + - name: Build package + working-directory: src/${{ matrix.package }} + run: uv build + + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: src/${{ matrix.package }}/dist + + publish-npm: + needs: [update-packages, create-metadata] + strategy: + fail-fast: false + matrix: + package: ${{ fromJson(needs.create-metadata.outputs.npm_packages) }} + name: Build ${{ matrix.package }} + environment: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.create-metadata.outputs.version }} + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + working-directory: src/${{ matrix.package }} + run: npm ci + + - name: Check if version exists on npm + working-directory: src/${{ matrix.package }} + run: | + VERSION=$(jq -r .version package.json) + if npm view --json | jq --arg version "$VERSION" '[.[]][0].versions | contains([$version])'; then + echo "Version $VERSION already exists on npm" + exit 1 + fi + echo "Version $VERSION is new, proceeding with publish" + + - name: Build package + working-directory: src/${{ matrix.package }} + run: npm run build + + - name: Publish package + working-directory: src/${{ matrix.package }} + run: | + npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + create-release: + needs: [update-packages, create-metadata, publish-pypi, publish-npm] if: needs.update-packages.outputs.changes_made == 'true' runs-on: ubuntu-latest environment: release @@ -97,7 +204,7 @@ jobs: - name: Create release env: - GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN}} run: | VERSION="${{ needs.create-metadata.outputs.version }}" gh release create "$VERSION" \ diff --git a/scripts/release.py b/scripts/release.py index 4b84c0af..05d76c0a 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -182,5 +182,29 @@ def generate_version() -> int: return 0 +@cli.command("generate-matrix") +@click.option( + "--directory", type=click.Path(exists=True, path_type=Path), default=Path.cwd() +) +@click.option("--npm", is_flag=True, default=False) +@click.option("--pypi", is_flag=True, default=False) +@click.argument("git_hash", type=GIT_HASH) +def generate_matrix(directory: Path, git_hash: GitHash, pypi: bool, npm: bool) -> int: + # Detect package type + path = directory.resolve(strict=True) + version = gen_version() + + changes = [] + for package in find_changed_packages(path, git_hash): + pkg = package.path.relative_to(path) + if npm and isinstance(package, NpmPackage): + changes.append(str(pkg)) + if pypi and isinstance(package, PyPiPackage): + changes.append(str(pkg)) + + click.echo(json.dumps(changes)) + return 0 + + if __name__ == "__main__": sys.exit(cli()) From de256a48b1155ce11d0b3f78c5aebc374ac6996d Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Tue, 14 Jan 2025 11:19:13 +0000 Subject: [PATCH 66/67] fix: Add missing CreatePullRequestSchema and createPullRequest function --- src/github/operations/pulls.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/github/operations/pulls.ts b/src/github/operations/pulls.ts index ef3e65ce..9b1a5bd7 100644 --- a/src/github/operations/pulls.ts +++ b/src/github/operations/pulls.ts @@ -76,6 +76,17 @@ export const PullRequestReviewSchema = z.object({ }); // Input schemas +export const CreatePullRequestSchema = z.object({ + owner: z.string().describe("Repository owner (username or organization)"), + repo: z.string().describe("Repository name"), + 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 GetPullRequestSchema = z.object({ owner: z.string().describe("Repository owner (username or organization)"), repo: z.string().describe("Repository name"), @@ -149,6 +160,22 @@ export const GetPullRequestReviewsSchema = z.object({ }); // Function implementations +export async function createPullRequest( + params: z.infer +): Promise> { + const { owner, repo, ...options } = CreatePullRequestSchema.parse(params); + + const response = await githubRequest( + `https://api.github.com/repos/${owner}/${repo}/pulls`, + { + method: "POST", + body: options, + } + ); + + return GitHubPullRequestSchema.parse(response); +} + export async function getPullRequest( owner: string, repo: string, From 383b9c9cc0f8584d46f83323e43f7aaca135f901 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Tue, 14 Jan 2025 16:33:40 +0000 Subject: [PATCH 67/67] address npm dependency fixes --- package-lock.json | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0dcbfbbc..ce8e521f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2497,9 +2497,10 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -2520,7 +2521,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -2535,6 +2536,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/extend": { @@ -3770,9 +3775,10 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/pend": { "version": "1.2.0",