@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 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
+ }
@@ -0,0 +1,3 @@
1
+ // If the format of this file changes, so it doesn't simply export a VERSION constant,
2
+ // this will break .github/workflows/version-check.yml.
3
+ export const VERSION = "0.0.2";
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
+ }