@feedmob/github-issues 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/dist/common/errors.js +69 -0
- package/dist/common/types.js +220 -0
- package/dist/common/utils.js +107 -0
- package/dist/common/version.js +3 -0
- package/dist/index.js +195 -0
- package/dist/operations/issues.js +80 -0
- package/dist/operations/search.js +38 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# GitHub MCP Server
|
|
2
|
+
|
|
3
|
+
MCP Server for the GitHub API, enabling issue operations and search functionality.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
The GitHub MCP Server provides the following capabilities:
|
|
8
|
+
- Create and manage GitHub issues
|
|
9
|
+
- Search for issues across repositories
|
|
10
|
+
- List and filter repository issues
|
|
11
|
+
- Update existing issues
|
|
12
|
+
- Get details of specific issues
|
|
13
|
+
|
|
14
|
+
## Tools
|
|
15
|
+
|
|
16
|
+
### `create_issue`
|
|
17
|
+
- Create a new issue in a GitHub repository
|
|
18
|
+
- Inputs:
|
|
19
|
+
- `owner` (string): Repository owner
|
|
20
|
+
- `repo` (string): Repository name
|
|
21
|
+
- `title` (string): Issue title
|
|
22
|
+
- `body` (optional string): Issue description
|
|
23
|
+
- `assignees` (optional string[]): Usernames to assign
|
|
24
|
+
- `labels` (optional string[]): Labels to add
|
|
25
|
+
- `milestone` (optional number): Milestone number
|
|
26
|
+
- Returns: Created issue details
|
|
27
|
+
|
|
28
|
+
### `list_issues`
|
|
29
|
+
- List and filter repository issues
|
|
30
|
+
- Inputs:
|
|
31
|
+
- `owner` (string): Repository owner
|
|
32
|
+
- `repo` (string): Repository name
|
|
33
|
+
- `state` (optional string): Filter by state ('open', 'closed', 'all')
|
|
34
|
+
- `labels` (optional string[]): Filter by labels
|
|
35
|
+
- `sort` (optional string): Sort by ('created', 'updated', 'comments')
|
|
36
|
+
- `direction` (optional string): Sort direction ('asc', 'desc')
|
|
37
|
+
- `since` (optional string): Filter by date (ISO 8601 timestamp)
|
|
38
|
+
- `page` (optional number): Page number
|
|
39
|
+
- `per_page` (optional number): Results per page
|
|
40
|
+
- Returns: Array of issue details
|
|
41
|
+
|
|
42
|
+
### `update_issue`
|
|
43
|
+
- Update an existing issue
|
|
44
|
+
- Inputs:
|
|
45
|
+
- `owner` (string): Repository owner
|
|
46
|
+
- `repo` (string): Repository name
|
|
47
|
+
- `issue_number` (number): Issue number to update
|
|
48
|
+
- `title` (optional string): New title
|
|
49
|
+
- `body` (optional string): New description
|
|
50
|
+
- `state` (optional string): New state ('open' or 'closed')
|
|
51
|
+
- `labels` (optional string[]): New labels
|
|
52
|
+
- `assignees` (optional string[]): New assignees
|
|
53
|
+
- `milestone` (optional number): New milestone number
|
|
54
|
+
- Returns: Updated issue details
|
|
55
|
+
|
|
56
|
+
### `search_issues`
|
|
57
|
+
- Search for issues and pull requests across GitHub repositories
|
|
58
|
+
- Inputs:
|
|
59
|
+
- `q` (string): Search query using GitHub issues search syntax
|
|
60
|
+
- `sort` (optional string): Sort field (comments, reactions, created, etc.)
|
|
61
|
+
- `order` (optional string): Sort order ('asc' or 'desc')
|
|
62
|
+
- `per_page` (optional number): Results per page (max 100)
|
|
63
|
+
- `page` (optional number): Page number
|
|
64
|
+
- Returns: Issue and pull request search results
|
|
65
|
+
|
|
66
|
+
### `get_issue`
|
|
67
|
+
- Gets the contents of an issue within a repository
|
|
68
|
+
- Inputs:
|
|
69
|
+
- `owner` (string): Repository owner
|
|
70
|
+
- `repo` (string): Repository name
|
|
71
|
+
- `issue_number` (number): Issue number to retrieve
|
|
72
|
+
- Returns: GitHub Issue object & details
|
|
73
|
+
|
|
74
|
+
## Search Query Syntax
|
|
75
|
+
|
|
76
|
+
### Issues Search
|
|
77
|
+
- `is:issue` or `is:pr`: Filter by type
|
|
78
|
+
- `is:open` or `is:closed`: Filter by state
|
|
79
|
+
- `label:bug`: Search by label
|
|
80
|
+
- `author:username`: Search by author
|
|
81
|
+
- Example: `q: "memory leak" is:issue is:open label:bug`
|
|
82
|
+
|
|
83
|
+
For detailed search syntax, see [GitHub's searching documentation](https://docs.github.com/en/search-github/searching-on-github).
|
|
84
|
+
|
|
85
|
+
## Setup
|
|
86
|
+
|
|
87
|
+
### Environment Variables
|
|
88
|
+
This server supports the following environment variables:
|
|
89
|
+
- `GITHUB_PERSONAL_ACCESS_TOKEN`: Your GitHub Personal Access Token (required)
|
|
90
|
+
- `GITHUB_DEFAULT_OWNER`: Default repository owner (optional)
|
|
91
|
+
- `GITHUB_DEFAULT_REPO`: Default repository name (optional)
|
|
92
|
+
|
|
93
|
+
### Personal Access Token
|
|
94
|
+
[Create a GitHub Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with appropriate permissions:
|
|
95
|
+
- Go to [Personal access tokens](https://github.com/settings/tokens) (in GitHub Settings > Developer settings)
|
|
96
|
+
- Select which repositories you'd like this token to have access to (Public, All, or Select)
|
|
97
|
+
- Create a token with the `repo` scope ("Full control of private repositories")
|
|
98
|
+
- Alternatively, if working only with public repositories, select only the `public_repo` scope
|
|
99
|
+
- Copy the generated token
|
|
100
|
+
|
|
101
|
+
### Usage with Claude Desktop
|
|
102
|
+
To use this with Claude Desktop, add the following to your `claude_desktop_config.json`:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"github": {
|
|
108
|
+
"command": "npx",
|
|
109
|
+
"args": [
|
|
110
|
+
"-y",
|
|
111
|
+
"@feedmob/github-issues"
|
|
112
|
+
],
|
|
113
|
+
"env": {
|
|
114
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>",
|
|
115
|
+
"GITHUB_DEFAULT_OWNER": "optional-default-owner",
|
|
116
|
+
"GITHUB_DEFAULT_REPO": "optional-default-repo"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export class GitHubError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
response;
|
|
4
|
+
constructor(message, status, response) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.response = response;
|
|
8
|
+
this.name = "GitHubError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class GitHubValidationError extends GitHubError {
|
|
12
|
+
constructor(message, status, response) {
|
|
13
|
+
super(message, status, response);
|
|
14
|
+
this.name = "GitHubValidationError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class GitHubResourceNotFoundError extends GitHubError {
|
|
18
|
+
constructor(resource) {
|
|
19
|
+
super(`Resource not found: ${resource}`, 404, { message: `${resource} not found` });
|
|
20
|
+
this.name = "GitHubResourceNotFoundError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class GitHubAuthenticationError extends GitHubError {
|
|
24
|
+
constructor(message = "Authentication failed") {
|
|
25
|
+
super(message, 401, { message });
|
|
26
|
+
this.name = "GitHubAuthenticationError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class GitHubPermissionError extends GitHubError {
|
|
30
|
+
constructor(message = "Insufficient permissions") {
|
|
31
|
+
super(message, 403, { message });
|
|
32
|
+
this.name = "GitHubPermissionError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class GitHubRateLimitError extends GitHubError {
|
|
36
|
+
resetAt;
|
|
37
|
+
constructor(message = "Rate limit exceeded", resetAt) {
|
|
38
|
+
super(message, 429, { message, reset_at: resetAt.toISOString() });
|
|
39
|
+
this.resetAt = resetAt;
|
|
40
|
+
this.name = "GitHubRateLimitError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export class GitHubConflictError extends GitHubError {
|
|
44
|
+
constructor(message) {
|
|
45
|
+
super(message, 409, { message });
|
|
46
|
+
this.name = "GitHubConflictError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function isGitHubError(error) {
|
|
50
|
+
return error instanceof GitHubError;
|
|
51
|
+
}
|
|
52
|
+
export function createGitHubError(status, response) {
|
|
53
|
+
switch (status) {
|
|
54
|
+
case 401:
|
|
55
|
+
return new GitHubAuthenticationError(response?.message);
|
|
56
|
+
case 403:
|
|
57
|
+
return new GitHubPermissionError(response?.message);
|
|
58
|
+
case 404:
|
|
59
|
+
return new GitHubResourceNotFoundError(response?.message || "Resource");
|
|
60
|
+
case 409:
|
|
61
|
+
return new GitHubConflictError(response?.message || "Conflict occurred");
|
|
62
|
+
case 422:
|
|
63
|
+
return new GitHubValidationError(response?.message || "Validation failed", status, response);
|
|
64
|
+
case 429:
|
|
65
|
+
return new GitHubRateLimitError(response?.message, new Date(response?.reset_at || Date.now() + 60000));
|
|
66
|
+
default:
|
|
67
|
+
return new GitHubError(response?.message || "GitHub API error", status, response);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Base schemas for common types
|
|
3
|
+
export const GitHubAuthorSchema = z.object({
|
|
4
|
+
name: z.string(),
|
|
5
|
+
email: z.string(),
|
|
6
|
+
date: z.string(),
|
|
7
|
+
});
|
|
8
|
+
export const GitHubOwnerSchema = z.object({
|
|
9
|
+
login: z.string(),
|
|
10
|
+
id: z.number(),
|
|
11
|
+
node_id: z.string(),
|
|
12
|
+
avatar_url: z.string(),
|
|
13
|
+
url: z.string(),
|
|
14
|
+
html_url: z.string(),
|
|
15
|
+
type: z.string(),
|
|
16
|
+
});
|
|
17
|
+
export const GitHubRepositorySchema = z.object({
|
|
18
|
+
id: z.number(),
|
|
19
|
+
node_id: z.string(),
|
|
20
|
+
name: z.string(),
|
|
21
|
+
full_name: z.string(),
|
|
22
|
+
private: z.boolean(),
|
|
23
|
+
owner: GitHubOwnerSchema,
|
|
24
|
+
html_url: z.string(),
|
|
25
|
+
description: z.string().nullable(),
|
|
26
|
+
fork: z.boolean(),
|
|
27
|
+
url: z.string(),
|
|
28
|
+
created_at: z.string(),
|
|
29
|
+
updated_at: z.string(),
|
|
30
|
+
pushed_at: z.string(),
|
|
31
|
+
git_url: z.string(),
|
|
32
|
+
ssh_url: z.string(),
|
|
33
|
+
clone_url: z.string(),
|
|
34
|
+
default_branch: z.string(),
|
|
35
|
+
});
|
|
36
|
+
export const GithubFileContentLinks = z.object({
|
|
37
|
+
self: z.string(),
|
|
38
|
+
git: z.string().nullable(),
|
|
39
|
+
html: z.string().nullable()
|
|
40
|
+
});
|
|
41
|
+
export const GitHubFileContentSchema = z.object({
|
|
42
|
+
name: z.string(),
|
|
43
|
+
path: z.string(),
|
|
44
|
+
sha: z.string(),
|
|
45
|
+
size: z.number(),
|
|
46
|
+
url: z.string(),
|
|
47
|
+
html_url: z.string(),
|
|
48
|
+
git_url: z.string(),
|
|
49
|
+
download_url: z.string(),
|
|
50
|
+
type: z.string(),
|
|
51
|
+
content: z.string().optional(),
|
|
52
|
+
encoding: z.string().optional(),
|
|
53
|
+
_links: GithubFileContentLinks
|
|
54
|
+
});
|
|
55
|
+
export const GitHubDirectoryContentSchema = z.object({
|
|
56
|
+
type: z.string(),
|
|
57
|
+
size: z.number(),
|
|
58
|
+
name: z.string(),
|
|
59
|
+
path: z.string(),
|
|
60
|
+
sha: z.string(),
|
|
61
|
+
url: z.string(),
|
|
62
|
+
git_url: z.string(),
|
|
63
|
+
html_url: z.string(),
|
|
64
|
+
download_url: z.string().nullable(),
|
|
65
|
+
});
|
|
66
|
+
export const GitHubContentSchema = z.union([
|
|
67
|
+
GitHubFileContentSchema,
|
|
68
|
+
z.array(GitHubDirectoryContentSchema),
|
|
69
|
+
]);
|
|
70
|
+
export const GitHubTreeEntrySchema = z.object({
|
|
71
|
+
path: z.string(),
|
|
72
|
+
mode: z.enum(["100644", "100755", "040000", "160000", "120000"]),
|
|
73
|
+
type: z.enum(["blob", "tree", "commit"]),
|
|
74
|
+
size: z.number().optional(),
|
|
75
|
+
sha: z.string(),
|
|
76
|
+
url: z.string(),
|
|
77
|
+
});
|
|
78
|
+
export const GitHubTreeSchema = z.object({
|
|
79
|
+
sha: z.string(),
|
|
80
|
+
url: z.string(),
|
|
81
|
+
tree: z.array(GitHubTreeEntrySchema),
|
|
82
|
+
truncated: z.boolean(),
|
|
83
|
+
});
|
|
84
|
+
export const GitHubCommitSchema = z.object({
|
|
85
|
+
sha: z.string(),
|
|
86
|
+
node_id: z.string(),
|
|
87
|
+
url: z.string(),
|
|
88
|
+
author: GitHubAuthorSchema,
|
|
89
|
+
committer: GitHubAuthorSchema,
|
|
90
|
+
message: z.string(),
|
|
91
|
+
tree: z.object({
|
|
92
|
+
sha: z.string(),
|
|
93
|
+
url: z.string(),
|
|
94
|
+
}),
|
|
95
|
+
parents: z.array(z.object({
|
|
96
|
+
sha: z.string(),
|
|
97
|
+
url: z.string(),
|
|
98
|
+
})),
|
|
99
|
+
});
|
|
100
|
+
export const GitHubListCommitsSchema = z.array(z.object({
|
|
101
|
+
sha: z.string(),
|
|
102
|
+
node_id: z.string(),
|
|
103
|
+
commit: z.object({
|
|
104
|
+
author: GitHubAuthorSchema,
|
|
105
|
+
committer: GitHubAuthorSchema,
|
|
106
|
+
message: z.string(),
|
|
107
|
+
tree: z.object({
|
|
108
|
+
sha: z.string(),
|
|
109
|
+
url: z.string()
|
|
110
|
+
}),
|
|
111
|
+
url: z.string(),
|
|
112
|
+
comment_count: z.number(),
|
|
113
|
+
}),
|
|
114
|
+
url: z.string(),
|
|
115
|
+
html_url: z.string(),
|
|
116
|
+
comments_url: z.string()
|
|
117
|
+
}));
|
|
118
|
+
export const GitHubReferenceSchema = z.object({
|
|
119
|
+
ref: z.string(),
|
|
120
|
+
node_id: z.string(),
|
|
121
|
+
url: z.string(),
|
|
122
|
+
object: z.object({
|
|
123
|
+
sha: z.string(),
|
|
124
|
+
type: z.string(),
|
|
125
|
+
url: z.string(),
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
// User and assignee schemas
|
|
129
|
+
export const GitHubIssueAssigneeSchema = z.object({
|
|
130
|
+
login: z.string(),
|
|
131
|
+
id: z.number(),
|
|
132
|
+
avatar_url: z.string(),
|
|
133
|
+
url: z.string(),
|
|
134
|
+
html_url: z.string(),
|
|
135
|
+
});
|
|
136
|
+
// Issue-related schemas
|
|
137
|
+
export const GitHubLabelSchema = z.object({
|
|
138
|
+
id: z.number(),
|
|
139
|
+
node_id: z.string(),
|
|
140
|
+
url: z.string(),
|
|
141
|
+
name: z.string(),
|
|
142
|
+
color: z.string(),
|
|
143
|
+
default: z.boolean(),
|
|
144
|
+
description: z.string().nullable().optional(),
|
|
145
|
+
});
|
|
146
|
+
export const GitHubMilestoneSchema = z.object({
|
|
147
|
+
url: z.string(),
|
|
148
|
+
html_url: z.string(),
|
|
149
|
+
labels_url: z.string(),
|
|
150
|
+
id: z.number(),
|
|
151
|
+
node_id: z.string(),
|
|
152
|
+
number: z.number(),
|
|
153
|
+
title: z.string(),
|
|
154
|
+
description: z.string(),
|
|
155
|
+
state: z.string(),
|
|
156
|
+
});
|
|
157
|
+
export const GitHubIssueSchema = z.object({
|
|
158
|
+
url: z.string(),
|
|
159
|
+
repository_url: z.string(),
|
|
160
|
+
labels_url: z.string(),
|
|
161
|
+
comments_url: z.string(),
|
|
162
|
+
events_url: z.string(),
|
|
163
|
+
html_url: z.string(),
|
|
164
|
+
id: z.number(),
|
|
165
|
+
node_id: z.string(),
|
|
166
|
+
number: z.number(),
|
|
167
|
+
title: z.string(),
|
|
168
|
+
user: GitHubIssueAssigneeSchema,
|
|
169
|
+
labels: z.array(GitHubLabelSchema),
|
|
170
|
+
state: z.string(),
|
|
171
|
+
locked: z.boolean(),
|
|
172
|
+
assignee: GitHubIssueAssigneeSchema.nullable(),
|
|
173
|
+
assignees: z.array(GitHubIssueAssigneeSchema),
|
|
174
|
+
milestone: GitHubMilestoneSchema.nullable(),
|
|
175
|
+
comments: z.number(),
|
|
176
|
+
created_at: z.string(),
|
|
177
|
+
updated_at: z.string(),
|
|
178
|
+
closed_at: z.string().nullable(),
|
|
179
|
+
body: z.string().nullable(),
|
|
180
|
+
});
|
|
181
|
+
// Search-related schemas
|
|
182
|
+
export const GitHubSearchResponseSchema = z.object({
|
|
183
|
+
total_count: z.number(),
|
|
184
|
+
incomplete_results: z.boolean(),
|
|
185
|
+
items: z.array(GitHubRepositorySchema),
|
|
186
|
+
});
|
|
187
|
+
// Pull request schemas
|
|
188
|
+
export const GitHubPullRequestRefSchema = z.object({
|
|
189
|
+
label: z.string(),
|
|
190
|
+
ref: z.string(),
|
|
191
|
+
sha: z.string(),
|
|
192
|
+
user: GitHubIssueAssigneeSchema,
|
|
193
|
+
repo: GitHubRepositorySchema,
|
|
194
|
+
});
|
|
195
|
+
export const GitHubPullRequestSchema = z.object({
|
|
196
|
+
url: z.string(),
|
|
197
|
+
id: z.number(),
|
|
198
|
+
node_id: z.string(),
|
|
199
|
+
html_url: z.string(),
|
|
200
|
+
diff_url: z.string(),
|
|
201
|
+
patch_url: z.string(),
|
|
202
|
+
issue_url: z.string(),
|
|
203
|
+
number: z.number(),
|
|
204
|
+
state: z.string(),
|
|
205
|
+
locked: z.boolean(),
|
|
206
|
+
title: z.string(),
|
|
207
|
+
user: GitHubIssueAssigneeSchema,
|
|
208
|
+
body: z.string().nullable(),
|
|
209
|
+
created_at: z.string(),
|
|
210
|
+
updated_at: z.string(),
|
|
211
|
+
closed_at: z.string().nullable(),
|
|
212
|
+
merged_at: z.string().nullable(),
|
|
213
|
+
merge_commit_sha: z.string().nullable(),
|
|
214
|
+
assignee: GitHubIssueAssigneeSchema.nullable(),
|
|
215
|
+
assignees: z.array(GitHubIssueAssigneeSchema),
|
|
216
|
+
requested_reviewers: z.array(GitHubIssueAssigneeSchema),
|
|
217
|
+
labels: z.array(GitHubLabelSchema),
|
|
218
|
+
head: GitHubPullRequestRefSchema,
|
|
219
|
+
base: GitHubPullRequestRefSchema,
|
|
220
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { getUserAgent } from "universal-user-agent";
|
|
2
|
+
import { createGitHubError } from "./errors.js";
|
|
3
|
+
import { VERSION } from "./version.js";
|
|
4
|
+
async function parseResponseBody(response) {
|
|
5
|
+
const contentType = response.headers.get("content-type");
|
|
6
|
+
if (contentType?.includes("application/json")) {
|
|
7
|
+
return response.json();
|
|
8
|
+
}
|
|
9
|
+
return response.text();
|
|
10
|
+
}
|
|
11
|
+
export function buildUrl(baseUrl, params) {
|
|
12
|
+
const url = new URL(baseUrl);
|
|
13
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
14
|
+
if (value !== undefined) {
|
|
15
|
+
url.searchParams.append(key, value.toString());
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return url.toString();
|
|
19
|
+
}
|
|
20
|
+
const USER_AGENT = `modelcontextprotocol/servers/github/v${VERSION} ${getUserAgent()}`;
|
|
21
|
+
export async function githubRequest(url, options = {}) {
|
|
22
|
+
const headers = {
|
|
23
|
+
"Accept": "application/vnd.github.v3+json",
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"User-Agent": USER_AGENT,
|
|
26
|
+
...options.headers,
|
|
27
|
+
};
|
|
28
|
+
if (process.env.GITHUB_PERSONAL_ACCESS_TOKEN) {
|
|
29
|
+
headers["Authorization"] = `Bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`;
|
|
30
|
+
}
|
|
31
|
+
const response = await fetch(url, {
|
|
32
|
+
method: options.method || "GET",
|
|
33
|
+
headers,
|
|
34
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
35
|
+
});
|
|
36
|
+
const responseBody = await parseResponseBody(response);
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw createGitHubError(response.status, responseBody);
|
|
39
|
+
}
|
|
40
|
+
return responseBody;
|
|
41
|
+
}
|
|
42
|
+
export function validateBranchName(branch) {
|
|
43
|
+
const sanitized = branch.trim();
|
|
44
|
+
if (!sanitized) {
|
|
45
|
+
throw new Error("Branch name cannot be empty");
|
|
46
|
+
}
|
|
47
|
+
if (sanitized.includes("..")) {
|
|
48
|
+
throw new Error("Branch name cannot contain '..'");
|
|
49
|
+
}
|
|
50
|
+
if (/[\s~^:?*[\\\]]/.test(sanitized)) {
|
|
51
|
+
throw new Error("Branch name contains invalid characters");
|
|
52
|
+
}
|
|
53
|
+
if (sanitized.startsWith("/") || sanitized.endsWith("/")) {
|
|
54
|
+
throw new Error("Branch name cannot start or end with '/'");
|
|
55
|
+
}
|
|
56
|
+
if (sanitized.endsWith(".lock")) {
|
|
57
|
+
throw new Error("Branch name cannot end with '.lock'");
|
|
58
|
+
}
|
|
59
|
+
return sanitized;
|
|
60
|
+
}
|
|
61
|
+
export function validateRepositoryName(name) {
|
|
62
|
+
const sanitized = name.trim().toLowerCase();
|
|
63
|
+
if (!sanitized) {
|
|
64
|
+
throw new Error("Repository name cannot be empty");
|
|
65
|
+
}
|
|
66
|
+
if (!/^[a-z0-9_.-]+$/.test(sanitized)) {
|
|
67
|
+
throw new Error("Repository name can only contain lowercase letters, numbers, hyphens, periods, and underscores");
|
|
68
|
+
}
|
|
69
|
+
if (sanitized.startsWith(".") || sanitized.endsWith(".")) {
|
|
70
|
+
throw new Error("Repository name cannot start or end with a period");
|
|
71
|
+
}
|
|
72
|
+
return sanitized;
|
|
73
|
+
}
|
|
74
|
+
export function validateOwnerName(owner) {
|
|
75
|
+
const sanitized = owner.trim().toLowerCase();
|
|
76
|
+
if (!sanitized) {
|
|
77
|
+
throw new Error("Owner name cannot be empty");
|
|
78
|
+
}
|
|
79
|
+
if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/.test(sanitized)) {
|
|
80
|
+
throw new Error("Owner name must start with a letter or number and can contain up to 39 characters");
|
|
81
|
+
}
|
|
82
|
+
return sanitized;
|
|
83
|
+
}
|
|
84
|
+
export async function checkBranchExists(owner, repo, branch) {
|
|
85
|
+
try {
|
|
86
|
+
await githubRequest(`https://api.github.com/repos/${owner}/${repo}/branches/${branch}`);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error && typeof error === "object" && "status" in error && error.status === 404) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function checkUserExists(username) {
|
|
97
|
+
try {
|
|
98
|
+
await githubRequest(`https://api.github.com/users/${username}`);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error && typeof error === "object" && "status" in error && error.status === 404) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
7
|
+
import fetch from 'node-fetch';
|
|
8
|
+
import * as issues from './operations/issues.js';
|
|
9
|
+
import * as search from './operations/search.js';
|
|
10
|
+
import { GitHubValidationError, GitHubResourceNotFoundError, GitHubAuthenticationError, GitHubPermissionError, GitHubRateLimitError, GitHubConflictError, isGitHubError, } from './common/errors.js';
|
|
11
|
+
import { VERSION } from "./common/version.js";
|
|
12
|
+
// If fetch doesn't exist in global scope, add it
|
|
13
|
+
if (!globalThis.fetch) {
|
|
14
|
+
globalThis.fetch = fetch;
|
|
15
|
+
}
|
|
16
|
+
// Default values from environment variables
|
|
17
|
+
const DEFAULT_OWNER = process.env.GITHUB_DEFAULT_OWNER || '';
|
|
18
|
+
const DEFAULT_REPO = process.env.GITHUB_DEFAULT_REPO || '';
|
|
19
|
+
const server = new Server({
|
|
20
|
+
name: "github-mcp-server",
|
|
21
|
+
version: VERSION,
|
|
22
|
+
}, {
|
|
23
|
+
capabilities: {
|
|
24
|
+
tools: {},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
function formatGitHubError(error) {
|
|
28
|
+
let message = `GitHub API Error: ${error.message}`;
|
|
29
|
+
if (error instanceof GitHubValidationError) {
|
|
30
|
+
message = `Validation Error: ${error.message}`;
|
|
31
|
+
if (error.response) {
|
|
32
|
+
message += `\nDetails: ${JSON.stringify(error.response)}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (error instanceof GitHubResourceNotFoundError) {
|
|
36
|
+
message = `Not Found: ${error.message}`;
|
|
37
|
+
}
|
|
38
|
+
else if (error instanceof GitHubAuthenticationError) {
|
|
39
|
+
message = `Authentication Failed: ${error.message}`;
|
|
40
|
+
}
|
|
41
|
+
else if (error instanceof GitHubPermissionError) {
|
|
42
|
+
message = `Permission Denied: ${error.message}`;
|
|
43
|
+
}
|
|
44
|
+
else if (error instanceof GitHubRateLimitError) {
|
|
45
|
+
message = `Rate Limit Exceeded: ${error.message}\nResets at: ${error.resetAt.toISOString()}`;
|
|
46
|
+
}
|
|
47
|
+
else if (error instanceof GitHubConflictError) {
|
|
48
|
+
message = `Conflict: ${error.message}`;
|
|
49
|
+
}
|
|
50
|
+
return message;
|
|
51
|
+
}
|
|
52
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
53
|
+
return {
|
|
54
|
+
tools: [
|
|
55
|
+
{
|
|
56
|
+
name: "create_issue",
|
|
57
|
+
description: "Create a new issue in a GitHub repository",
|
|
58
|
+
inputSchema: zodToJsonSchema(issues.CreateIssueSchema),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "list_issues",
|
|
62
|
+
description: "List issues in a GitHub repository with filtering options",
|
|
63
|
+
inputSchema: zodToJsonSchema(issues.ListIssuesOptionsSchema)
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "update_issue",
|
|
67
|
+
description: "Update an existing issue in a GitHub repository",
|
|
68
|
+
inputSchema: zodToJsonSchema(issues.UpdateIssueOptionsSchema)
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "search_issues",
|
|
72
|
+
description: "Search for issues and pull requests across GitHub repositories",
|
|
73
|
+
inputSchema: zodToJsonSchema(search.SearchIssuesSchema),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "get_issue",
|
|
77
|
+
description: "Get details of a specific issue in a GitHub repository.",
|
|
78
|
+
inputSchema: zodToJsonSchema(issues.GetIssueSchema)
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
84
|
+
try {
|
|
85
|
+
if (!request.params.arguments) {
|
|
86
|
+
throw new Error("Arguments are required");
|
|
87
|
+
}
|
|
88
|
+
switch (request.params.name) {
|
|
89
|
+
case "create_issue": {
|
|
90
|
+
const args = issues.CreateIssueSchema.parse(request.params.arguments);
|
|
91
|
+
// Use default values from environment variables if not provided
|
|
92
|
+
const owner = args.owner || DEFAULT_OWNER;
|
|
93
|
+
const repo = args.repo || DEFAULT_REPO;
|
|
94
|
+
const { ...options } = args;
|
|
95
|
+
if (!owner || !repo) {
|
|
96
|
+
throw new Error("Repository owner and name are required. Either provide them directly or set GITHUB_DEFAULT_OWNER and GITHUB_DEFAULT_REPO environment variables.");
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
console.error(`[DEBUG] Attempting to create issue in ${owner}/${repo}`);
|
|
100
|
+
console.error(`[DEBUG] Issue options:`, JSON.stringify(options, null, 2));
|
|
101
|
+
const issue = await issues.createIssue(owner, repo, options);
|
|
102
|
+
console.error(`[DEBUG] Issue created successfully`);
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
// Type guard for Error objects
|
|
109
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
110
|
+
console.error(`[ERROR] Failed to create issue:`, error);
|
|
111
|
+
if (error instanceof GitHubResourceNotFoundError) {
|
|
112
|
+
throw new Error(`Repository '${owner}/${repo}' not found. Please verify:\n` +
|
|
113
|
+
`1. The repository exists\n` +
|
|
114
|
+
`2. You have correct access permissions\n` +
|
|
115
|
+
`3. The owner and repository names are spelled correctly`);
|
|
116
|
+
}
|
|
117
|
+
// Safely access error properties
|
|
118
|
+
throw new Error(`Failed to create issue: ${error.message}${error.stack ? `\nStack: ${error.stack}` : ''}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
case "search_issues": {
|
|
122
|
+
const args = search.SearchIssuesSchema.parse(request.params.arguments);
|
|
123
|
+
const results = await search.searchIssues(args);
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
case "list_issues": {
|
|
129
|
+
const args = issues.ListIssuesOptionsSchema.parse(request.params.arguments);
|
|
130
|
+
// Use default values from environment variables if not provided
|
|
131
|
+
const owner = args.owner || DEFAULT_OWNER;
|
|
132
|
+
const repo = args.repo || DEFAULT_REPO;
|
|
133
|
+
const { ...options } = args;
|
|
134
|
+
if (!owner || !repo) {
|
|
135
|
+
throw new Error("Repository owner and name are required. Either provide them directly or set GITHUB_DEFAULT_OWNER and GITHUB_DEFAULT_REPO environment variables.");
|
|
136
|
+
}
|
|
137
|
+
const result = await issues.listIssues(owner, repo, options);
|
|
138
|
+
return {
|
|
139
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
case "update_issue": {
|
|
143
|
+
const args = issues.UpdateIssueOptionsSchema.parse(request.params.arguments);
|
|
144
|
+
// Use default values from environment variables if not provided
|
|
145
|
+
const owner = args.owner || DEFAULT_OWNER;
|
|
146
|
+
const repo = args.repo || DEFAULT_REPO;
|
|
147
|
+
const { issue_number, ...options } = args;
|
|
148
|
+
if (!owner || !repo) {
|
|
149
|
+
throw new Error("Repository owner and name are required. Either provide them directly or set GITHUB_DEFAULT_OWNER and GITHUB_DEFAULT_REPO environment variables.");
|
|
150
|
+
}
|
|
151
|
+
const result = await issues.updateIssue(owner, repo, issue_number, options);
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
case "get_issue": {
|
|
157
|
+
const args = issues.GetIssueSchema.parse(request.params.arguments);
|
|
158
|
+
// Use default values from environment variables if not provided
|
|
159
|
+
const owner = args.owner || DEFAULT_OWNER;
|
|
160
|
+
const repo = args.repo || DEFAULT_REPO;
|
|
161
|
+
const { issue_number } = args;
|
|
162
|
+
if (!owner || !repo) {
|
|
163
|
+
throw new Error("Repository owner and name are required. Either provide them directly or set GITHUB_DEFAULT_OWNER and GITHUB_DEFAULT_REPO environment variables.");
|
|
164
|
+
}
|
|
165
|
+
const issue = await issues.getIssue(owner, repo, issue_number);
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
default:
|
|
171
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (error instanceof z.ZodError) {
|
|
176
|
+
throw new Error(`Invalid input: ${JSON.stringify(error.errors)}`);
|
|
177
|
+
}
|
|
178
|
+
if (isGitHubError(error)) {
|
|
179
|
+
throw new Error(formatGitHubError(error));
|
|
180
|
+
}
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
async function runServer() {
|
|
185
|
+
const transport = new StdioServerTransport();
|
|
186
|
+
await server.connect(transport);
|
|
187
|
+
console.error("GitHub MCP Server running on stdio");
|
|
188
|
+
if (DEFAULT_OWNER && DEFAULT_REPO) {
|
|
189
|
+
console.error(`Using default repository: ${DEFAULT_OWNER}/${DEFAULT_REPO}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
runServer().catch((error) => {
|
|
193
|
+
console.error("Fatal error in main():", error);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { githubRequest, buildUrl } from "../common/utils.js";
|
|
3
|
+
export const GetIssueSchema = z.object({
|
|
4
|
+
owner: z.string(),
|
|
5
|
+
repo: z.string(),
|
|
6
|
+
issue_number: z.number(),
|
|
7
|
+
});
|
|
8
|
+
export const IssueCommentSchema = z.object({
|
|
9
|
+
owner: z.string(),
|
|
10
|
+
repo: z.string(),
|
|
11
|
+
issue_number: z.number(),
|
|
12
|
+
body: z.string(),
|
|
13
|
+
});
|
|
14
|
+
export const CreateIssueOptionsSchema = z.object({
|
|
15
|
+
title: z.string(),
|
|
16
|
+
body: z.string().optional(),
|
|
17
|
+
assignees: z.array(z.string()).optional(),
|
|
18
|
+
milestone: z.number().optional(),
|
|
19
|
+
labels: z.array(z.string()).optional(),
|
|
20
|
+
});
|
|
21
|
+
export const CreateIssueSchema = z.object({
|
|
22
|
+
owner: z.string().optional(),
|
|
23
|
+
repo: z.string().optional(),
|
|
24
|
+
...CreateIssueOptionsSchema.shape,
|
|
25
|
+
});
|
|
26
|
+
export const ListIssuesOptionsSchema = z.object({
|
|
27
|
+
owner: z.string(),
|
|
28
|
+
repo: z.string(),
|
|
29
|
+
direction: z.enum(["asc", "desc"]).optional(),
|
|
30
|
+
labels: z.array(z.string()).optional(),
|
|
31
|
+
page: z.number().optional(),
|
|
32
|
+
per_page: z.number().optional(),
|
|
33
|
+
since: z.string().optional(),
|
|
34
|
+
sort: z.enum(["created", "updated", "comments"]).optional(),
|
|
35
|
+
state: z.enum(["open", "closed", "all"]).optional(),
|
|
36
|
+
});
|
|
37
|
+
export const UpdateIssueOptionsSchema = z.object({
|
|
38
|
+
owner: z.string(),
|
|
39
|
+
repo: z.string(),
|
|
40
|
+
issue_number: z.number(),
|
|
41
|
+
title: z.string().optional(),
|
|
42
|
+
body: z.string().optional(),
|
|
43
|
+
assignees: z.array(z.string()).optional(),
|
|
44
|
+
milestone: z.number().optional(),
|
|
45
|
+
labels: z.array(z.string()).optional(),
|
|
46
|
+
state: z.enum(["open", "closed"]).optional(),
|
|
47
|
+
});
|
|
48
|
+
export async function getIssue(owner, repo, issue_number) {
|
|
49
|
+
return githubRequest(`https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}`);
|
|
50
|
+
}
|
|
51
|
+
export async function addIssueComment(owner, repo, issue_number, body) {
|
|
52
|
+
return githubRequest(`https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}/comments`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
body: { body },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function createIssue(owner, repo, options) {
|
|
58
|
+
return githubRequest(`https://api.github.com/repos/${owner}/${repo}/issues`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
body: options,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export async function listIssues(owner, repo, options) {
|
|
64
|
+
const urlParams = {
|
|
65
|
+
direction: options.direction,
|
|
66
|
+
labels: options.labels?.join(","),
|
|
67
|
+
page: options.page?.toString(),
|
|
68
|
+
per_page: options.per_page?.toString(),
|
|
69
|
+
since: options.since,
|
|
70
|
+
sort: options.sort,
|
|
71
|
+
state: options.state
|
|
72
|
+
};
|
|
73
|
+
return githubRequest(buildUrl(`https://api.github.com/repos/${owner}/${repo}/issues`, urlParams));
|
|
74
|
+
}
|
|
75
|
+
export async function updateIssue(owner, repo, issue_number, options) {
|
|
76
|
+
return githubRequest(`https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}`, {
|
|
77
|
+
method: "PATCH",
|
|
78
|
+
body: options,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { githubRequest, buildUrl } from "../common/utils.js";
|
|
3
|
+
export const SearchOptions = z.object({
|
|
4
|
+
q: z.string(),
|
|
5
|
+
order: z.enum(["asc", "desc"]).optional(),
|
|
6
|
+
page: z.number().min(1).optional(),
|
|
7
|
+
per_page: z.number().min(1).max(100).optional(),
|
|
8
|
+
});
|
|
9
|
+
export const SearchUsersOptions = SearchOptions.extend({
|
|
10
|
+
sort: z.enum(["followers", "repositories", "joined"]).optional(),
|
|
11
|
+
});
|
|
12
|
+
export const SearchIssuesOptions = SearchOptions.extend({
|
|
13
|
+
sort: z.enum([
|
|
14
|
+
"comments",
|
|
15
|
+
"reactions",
|
|
16
|
+
"reactions-+1",
|
|
17
|
+
"reactions--1",
|
|
18
|
+
"reactions-smile",
|
|
19
|
+
"reactions-thinking_face",
|
|
20
|
+
"reactions-heart",
|
|
21
|
+
"reactions-tada",
|
|
22
|
+
"interactions",
|
|
23
|
+
"created",
|
|
24
|
+
"updated",
|
|
25
|
+
]).optional(),
|
|
26
|
+
});
|
|
27
|
+
export const SearchCodeSchema = SearchOptions;
|
|
28
|
+
export const SearchUsersSchema = SearchUsersOptions;
|
|
29
|
+
export const SearchIssuesSchema = SearchIssuesOptions;
|
|
30
|
+
export async function searchCode(params) {
|
|
31
|
+
return githubRequest(buildUrl("https://api.github.com/search/code", params));
|
|
32
|
+
}
|
|
33
|
+
export async function searchIssues(params) {
|
|
34
|
+
return githubRequest(buildUrl("https://api.github.com/search/issues", params));
|
|
35
|
+
}
|
|
36
|
+
export async function searchUsers(params) {
|
|
37
|
+
return githubRequest(buildUrl("https://api.github.com/search/users", params));
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@feedmob/github-issues",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "MCP server for using the GitHub API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "FeedMob",
|
|
7
|
+
"homepage": "https://github.com/feedmob/fm-mcp-servers",
|
|
8
|
+
"bugs": "https://github.com/feedmob/fm-mcp-servers/issues",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"bin": {
|
|
11
|
+
"mcp-server-github": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc && shx chmod +x dist/*.js",
|
|
18
|
+
"prepare": "npm run build",
|
|
19
|
+
"watch": "tsc --watch"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "1.0.1",
|
|
23
|
+
"@types/node": "^22",
|
|
24
|
+
"@types/node-fetch": "^2.6.12",
|
|
25
|
+
"node-fetch": "^3.3.2",
|
|
26
|
+
"universal-user-agent": "^7.0.2",
|
|
27
|
+
"zod": "^3.22.4",
|
|
28
|
+
"zod-to-json-schema": "^3.23.5"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"shx": "^0.3.4",
|
|
32
|
+
"typescript": "^5.6.2"
|
|
33
|
+
}
|
|
34
|
+
}
|