Merge branch 'main' into patch-1

This commit is contained in:
Mamerto Fabian Jr 2025-01-15 19:01:02 +08:00 • committed by GitHub
commit 7981568df1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1616 additions and 1555 deletions

View file

@ -79,7 +79,9 @@ A growing set of community-developed and maintained servers demonstrates various
- **[coin_api_mcp](https://github.com/longmans/coin_api_mcp)** - Provides access to [coinmarketcap](https://coinmarketcap.com/) cryptocurrency data.
- **[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!
- **[Dataset Viewer](https://github.com/privetin/dataset-viewer)** - Browse and analyze Hugging Face datasets with features like search, filtering, statistics, and data export
- **[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.
- **[Everything Search](https://github.com/mamertofabian/mcp-everything-search)** - Fast file searching capabilities across Windows (using [Everything SDK](https://www.voidtools.com/support/everything/sdk/)), macOS (using mdfind command), and Linux (using locate/plocate command).
@ -111,10 +113,13 @@ 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.
- **[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
- **[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

20
package-lock.json generated
View file

@ -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",

View file

@ -1,4 +1,4 @@
FROM node:22.12-alpine as builder
FROM node:22.12-alpine AS builder
COPY src/aws-kb-retrieval-server /app
COPY tsconfig.json /tsconfig.json

View file

@ -1,4 +1,4 @@
FROM node:22.12-alpine as builder
FROM node:22.12-alpine AS builder
# Must be entire project because `prepare` script is run during `npm install` and requires all files.
COPY src/brave-search /app

View file

@ -1,4 +1,4 @@
FROM node:22.12-alpine as builder
FROM node:22.12-alpine AS builder
COPY src/everart /app
COPY tsconfig.json /tsconfig.json

View file

@ -1,4 +1,4 @@
FROM node:22.12-alpine as builder
FROM node:22.12-alpine AS builder
COPY src/everything /app
COPY tsconfig.json /tsconfig.json

View file

@ -17,7 +17,7 @@ classifiers = [
]
dependencies = [
"markdownify>=0.13.1",
"mcp>=1.0.0",
"mcp>=1.1.3",
"protego>=0.3.1",
"pydantic>=2.0.0",
"readabilipy>=0.2.0",

View file

@ -7,6 +7,7 @@ from mcp.shared.exceptions import McpError
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
ErrorData,
GetPromptResult,
Prompt,
PromptArgument,
@ -79,15 +80,15 @@ async def check_may_autonomously_fetch_url(url: str, user_agent: str) -> None:
headers={"User-Agent": user_agent},
)
except HTTPError:
raise McpError(
raise McpError(ErrorData(
INTERNAL_ERROR,
f"Failed to fetch robots.txt {robot_txt_url} due to a connection issue",
)
))
if response.status_code in (401, 403):
raise McpError(
raise McpError(ErrorData(
INTERNAL_ERROR,
f"When fetching robots.txt ({robot_txt_url}), received status {response.status_code} so assuming that autonomous fetching is not allowed, the user can try manually fetching by using the fetch prompt",
)
))
elif 400 <= response.status_code < 500:
return
robot_txt = response.text
@ -96,7 +97,7 @@ async def check_may_autonomously_fetch_url(url: str, user_agent: str) -> None:
)
robot_parser = Protego.parse(processed_robot_txt)
if not robot_parser.can_fetch(str(url), user_agent):
raise McpError(
raise McpError(ErrorData(
INTERNAL_ERROR,
f"The sites robots.txt ({robot_txt_url}), specifies that autonomous fetching of this page is not allowed, "
f"<useragent>{user_agent}</useragent>\n"
@ -104,7 +105,7 @@ async def check_may_autonomously_fetch_url(url: str, user_agent: str) -> None:
f"<robots>\n{robot_txt}\n</robots>\n"
f"The assistant must let the user know that it failed to view the page. The assistant may provide further guidance based on the above information.\n"
f"The assistant can tell the user that they can try manually fetching the page by using the fetch prompt within their UI.",
)
))
async def fetch_url(
@ -124,12 +125,12 @@ async def fetch_url(
timeout=30,
)
except HTTPError as e:
raise McpError(INTERNAL_ERROR, f"Failed to fetch {url}: {e!r}")
raise McpError(ErrorData(INTERNAL_ERROR, f"Failed to fetch {url}: {e!r}"))
if response.status_code >= 400:
raise McpError(
raise McpError(ErrorData(
INTERNAL_ERROR,
f"Failed to fetch {url} - status code {response.status_code}",
)
))
page_raw = response.text
@ -221,11 +222,11 @@ Although originally you did not have internet access, and were advised to refuse
try:
args = Fetch(**arguments)
except ValueError as e:
raise McpError(INVALID_PARAMS, str(e))
raise McpError(ErrorData(INVALID_PARAMS, str(e)))
url = str(args.url)
if not url:
raise McpError(INVALID_PARAMS, "URL is required")
raise McpError(ErrorData(INVALID_PARAMS, "URL is required"))
if not ignore_robots_txt:
await check_may_autonomously_fetch_url(url, user_agent_autonomous)
@ -253,7 +254,7 @@ Although originally you did not have internet access, and were advised to refuse
@server.get_prompt()
async def get_prompt(name: str, arguments: dict | None) -> GetPromptResult:
if not arguments or "url" not in arguments:
raise McpError(INVALID_PARAMS, "URL is required")
raise McpError(ErrorData(INVALID_PARAMS, "URL is required"))
url = arguments["url"]

View file

@ -1,4 +1,4 @@
FROM node:22.12-alpine as builder
FROM node:22.12-alpine AS builder
WORKDIR /app

View file

@ -1,4 +1,4 @@
FROM node:22.12-alpine as builder
FROM node:22.12-alpine AS builder
COPY src/gdrive /app
COPY tsconfig.json /tsconfig.json

View file

@ -1,4 +1,4 @@
FROM node:22.12-alpine as builder
FROM node:22.12-alpine AS builder
# Must be entire project because `prepare` script is run during `npm install` and requires all files.
COPY src/github /app

View file

@ -188,6 +188,95 @@ 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
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
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
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
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

View file

@ -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
);
}
}

259
src/github/common/types.ts Normal file
View file

@ -0,0 +1,259 @@
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 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(),
url: z.string(),
object: z.object({
sha: z.string(),
type: z.string(),
url: z.string(),
}),
});
// 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(),
});
// 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(),
});
// Search-related schemas
export const GitHubSearchResponseSchema = z.object({
total_count: z.number(),
incomplete_results: z.boolean(),
items: z.array(GitHubRepositorySchema),
});
// Pull request schemas
export const GitHubPullRequestRefSchema = 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().nullable(),
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),
requested_reviewers: z.array(GitHubIssueAssigneeSchema),
labels: z.array(GitHubLabelSchema),
head: GitHubPullRequestRefSchema,
base: GitHubPullRequestRefSchema,
});
// Export types
export type GitHubAuthor = z.infer<typeof GitHubAuthorSchema>;
export type GitHubRepository = z.infer<typeof GitHubRepositorySchema>;
export type GitHubFileContent = z.infer<typeof GitHubFileContentSchema>;
export type GitHubDirectoryContent = z.infer<typeof GitHubDirectoryContentSchema>;
export type GitHubContent = z.infer<typeof GitHubContentSchema>;
export type GitHubTree = z.infer<typeof GitHubTreeSchema>;
export type GitHubCommit = z.infer<typeof GitHubCommitSchema>;
export type GitHubListCommits = z.infer<typeof GitHubListCommitsSchema>;
export type GitHubReference = z.infer<typeof GitHubReferenceSchema>;
export type GitHubIssueAssignee = z.infer<typeof GitHubIssueAssigneeSchema>;
export type GitHubLabel = z.infer<typeof GitHubLabelSchema>;
export type GitHubMilestone = z.infer<typeof GitHubMilestoneSchema>;
export type GitHubIssue = z.infer<typeof GitHubIssueSchema>;
export type GitHubSearchResponse = z.infer<typeof GitHubSearchResponseSchema>;
export type GitHubPullRequest = z.infer<typeof GitHubPullRequestSchema>;
export type GitHubPullRequestRef = z.infer<typeof GitHubPullRequestRefSchema>;

133
src/github/common/utils.ts Normal file
View file

@ -0,0 +1,133 @@
import { createGitHubError } from "./errors.js";
type RequestOptions = {
method?: string;
body?: unknown;
headers?: Record<string, string>;
};
async function parseResponseBody(response: Response): Promise<unknown> {
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
return response.json();
}
return response.text();
}
export function buildUrl(baseUrl: string, params: Record<string, string | number | undefined>): 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<unknown> {
const headers: Record<string, string> = {
"Accept": "application/vnd.github.v3+json",
"Content-Type": "application/json",
...options.headers,
};
if (process.env.GITHUB_PERSONAL_ACCESS_TOKEN) {
headers["Authorization"] = `Bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`;
}
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 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<boolean> {
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;
}
throw error;
}
}
export async function checkUserExists(username: string): Promise<boolean> {
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;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,112 @@
import { z } from "zod";
import { githubRequest } from "../common/utils.js";
import { GitHubReferenceSchema } from "../common/types.js";
// 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<typeof CreateBranchOptionsSchema>;
// Function implementations
export async function getDefaultBranchSHA(owner: string, repo: string): Promise<string> {
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<z.infer<typeof GitHubReferenceSchema>> {
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<string> {
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<z.infer<typeof GitHubReferenceSchema>> {
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<z.infer<typeof GitHubReferenceSchema>> {
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);
}

View file

@ -0,0 +1,26 @@
import { z } from "zod";
import { githubRequest, buildUrl } from "../common/utils.js";
export const ListCommitsSchema = z.object({
owner: z.string(),
repo: z.string(),
sha: z.string().optional(),
page: z.number().optional(),
perPage: z.number().optional()
});
export async function listCommits(
owner: string,
repo: string,
page?: number,
perPage?: number,
sha?: string
) {
return githubRequest(
buildUrl(`https://api.github.com/repos/${owner}/${repo}/commits`, {
page: page?.toString(),
per_page: perPage?.toString(),
sha
})
);
}

View file

@ -0,0 +1,219 @@
import { z } from "zod";
import { githubRequest } from "../common/utils.js";
import {
GitHubContentSchema,
GitHubAuthorSchema,
GitHubTreeSchema,
GitHubCommitSchema,
GitHubReferenceSchema,
GitHubFileContentSchema,
} from "../common/types.js";
// 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"),
});
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<typeof FileOperationSchema>;
export type GitHubCreateUpdateFileResponse = z.infer<typeof GitHubCreateUpdateFileResponseSchema>;
// 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);
}

View file

@ -0,0 +1,118 @@
import { z } from "zod";
import { githubRequest, buildUrl } from "../common/utils.js";
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(),
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(),
repo: z.string(),
...CreateIssueOptionsSchema.shape,
});
export const ListIssuesOptionsSchema = z.object({
owner: z.string(),
repo: z.string(),
direction: z.enum(["asc", "desc"]).optional(),
labels: z.array(z.string()).optional(),
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({
owner: z.string(),
repo: z.string(),
issue_number: z.number(),
title: z.string().optional(),
body: z.string().optional(),
assignees: z.array(z.string()).optional(),
milestone: z.number().optional(),
labels: z.array(z.string()).optional(),
state: z.enum(["open", "closed"]).optional(),
});
export async function getIssue(owner: string, repo: string, issue_number: number) {
return githubRequest(`https://api.github.com/repos/${owner}/${repo}/issues/${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 },
});
}
export async function createIssue(
owner: string,
repo: string,
options: z.infer<typeof CreateIssueOptionsSchema>
) {
return githubRequest(
`https://api.github.com/repos/${owner}/${repo}/issues`,
{
method: "POST",
body: options,
}
);
}
export async function listIssues(
owner: string,
repo: string,
options: Omit<z.infer<typeof ListIssuesOptionsSchema>, "owner" | "repo">
) {
const urlParams: Record<string, string | undefined> = {
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`, urlParams)
);
}
export async function updateIssue(
owner: string,
repo: string,
issue_number: number,
options: Omit<z.infer<typeof UpdateIssueOptionsSchema>, "owner" | "repo" | "issue_number">
) {
return githubRequest(
`https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}`,
{
method: "PATCH",
body: options,
}
);
}

Some files were not shown because too many files have changed in this diff Show more