github-pro-mcp 0.0.1

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,133 @@
1
+ # GitHub Pro MCP
2
+
3
+ [![npm version](https://img.shields.io/npm/v/github-pro-mcp)](https://www.npmjs.com/package/github-pro-mcp)
4
+ [![Docker image](https://img.shields.io/docker/v/anantanandgupta/github-pro-mcp)](https://hub.docker.com/r/anantanandgupta/github-pro-mcp)
5
+
6
+ An MCP (Model Context Protocol) server providing comprehensive GitHub API access via hybrid REST + GraphQL — **32 tools across 7 domains**.
7
+
8
+ ## Features
9
+
10
+ - **32 Tools** — issues, pull requests, repositories, projects v2, labels, organizations, file operations
11
+ - **Hybrid API** — REST for CRUD/search, GraphQL for cursor pagination and composite queries
12
+ - **Resilient by Default** — automatic retry with exponential backoff + jitter, circuit breaker for API outages, proactive rate limit management
13
+ - **Structured Logging** — multi-level (error/warn/info/debug) stderr output
14
+ - **Owner/Repo Pinning** — lock the server to a specific owner or repository as a safety guardrail
15
+
16
+ ## Installation
17
+
18
+ ### npm
19
+
20
+ ```bash
21
+ # Install globally
22
+ npm install -g github-pro-mcp
23
+
24
+ # Or run directly without installing
25
+ npx github-pro-mcp
26
+ ```
27
+
28
+ ### Docker
29
+
30
+ Multi-arch images for `linux/amd64` and `linux/arm64`:
31
+
32
+ ```bash
33
+ docker pull anantanandgupta/github-pro-mcp
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ Set your GitHub token and run:
39
+
40
+ ```bash
41
+ export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_your_token_here
42
+ github-pro-mcp
43
+ ```
44
+
45
+ Or with Docker:
46
+
47
+ ```bash
48
+ docker run -i --rm \
49
+ -e GITHUB_PERSONAL_ACCESS_TOKEN=ghp_your_token_here \
50
+ anantanandgupta/github-pro-mcp
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ ### Required
56
+
57
+ | Variable | Description |
58
+ |----------|-------------|
59
+ | `GITHUB_PERSONAL_ACCESS_TOKEN` | GitHub PAT with appropriate scopes |
60
+
61
+ ### Optional
62
+
63
+ | Variable | Default | Description |
64
+ |----------|---------|-------------|
65
+ | `GITHUB_HOST` | — | For GHES: `https://{hostname}/api/v3` |
66
+ | `GITHUB_OWNER` | — | Pin server to a single owner (rejects mismatched calls) |
67
+ | `GITHUB_REPO` | — | Pin server to a single repository (rejects mismatched calls) |
68
+ | `GITHUB_RETRY_MAX_ATTEMPTS` | `3` | Max retries for transient errors |
69
+ | `GITHUB_RETRY_BACKOFF_MS` | `1000` | Initial retry backoff (ms) |
70
+ | `GITHUB_CIRCUIT_BREAKER_THRESHOLD` | `5` | Failures before circuit opens |
71
+ | `GITHUB_CIRCUIT_BREAKER_RESET_MS` | `30000` | Circuit recovery timeout (ms) |
72
+ | `GITHUB_LOG_LEVEL` | `error` | Log level: `error`, `warn`, `info`, `debug` |
73
+
74
+ ## Usage with opencode
75
+
76
+ Add to your `opencode.json`:
77
+
78
+ ```json
79
+ {
80
+ "mcp": {
81
+ "github_pro": {
82
+ "type": "local",
83
+ "command": ["node", "path/to/github-pro-mcp/dist/index.js"],
84
+ "environment": {
85
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "{env:GITHUB_TOKEN}"
86
+ }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ For Docker-based usage:
93
+
94
+ ```json
95
+ {
96
+ "mcp": {
97
+ "github_pro": {
98
+ "type": "local",
99
+ "command": ["docker", "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "anantanandgupta/github-pro-mcp"],
100
+ "environment": {
101
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "{env:GITHUB_TOKEN}"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ## Tool Reference
109
+
110
+ | Domain | Tools | Count |
111
+ |--------|-------|-------|
112
+ | **User & Repos** | `auth_get_current_login`, `file_get_content`, `profile_get`, `owner_list_repository`, `file_create_update`, `code_search`, `repository_search` | 7 |
113
+ | **Issues** | `issue_read`, `issue_list`, `issue_search`, `issue_add_comment`, `sub_issue_write`, `issue_create`, `issue_close`, `issue_reopen`, `issue_update`, `issue_add_label`, `issue_remove_label` | 11 |
114
+ | **Issue Fields** | `owner_list_issue_field`, `issue_get_field_value`, `issue_set_field_value` | 3 |
115
+ | **Pull Requests** | `pull_request_read`, `pull_request_create`, `pull_request_merge` | 3 |
116
+ | **Organizations** | `organization_search`, `owner_list_issue_type`, `owner_list_organization` | 3 |
117
+ | **Projects v2** | `owner_list_project`, `projects_get`, `projects_write` | 3 |
118
+ | **Labels** | `label_list`, `label_write` | 2 |
119
+ | **Total** | | **32** |
120
+
121
+ ## FAQ / Troubleshooting
122
+
123
+ | Error | Likely Cause |
124
+ |-------|-------------|
125
+ | `401` | Token expired or invalid |
126
+ | `403` | Missing OAuth scopes or SSO |
127
+ | `404` | Resource doesn't exist or no access |
128
+ | `429` | Rate limited — wait and retry |
129
+ | Circuit breaker open | GitHub API returning repeated errors — auto-recovers in 30s |
130
+
131
+ ## License
132
+
133
+ ISC
@@ -0,0 +1,27 @@
1
+ import { Octokit } from "@octokit/core";
2
+ import type { Api } from "@octokit/plugin-rest-endpoint-methods";
3
+ export type OctokitWithRest = Octokit & {
4
+ rest: Api["rest"];
5
+ };
6
+ /**
7
+ * Creates an authenticated Octokit client with all plugins configured.
8
+ *
9
+ * The returned client wraps all REST requests through:
10
+ * circuit breaker → retry → throttling → GitHub API
11
+ *
12
+ * Rate limit headers from responses are automatically tracked
13
+ * via `updateRateLimits()`.
14
+ */
15
+ export declare function createClient(): OctokitWithRest;
16
+ /**
17
+ * Execute a raw GraphQL query against the GitHub API.
18
+ * The query is wrapped with circuit breaker and retry protection.
19
+ */
20
+ export declare function gql<T>(query: string, variables?: Record<string, unknown>): Promise<T>;
21
+ /**
22
+ * Derive the web UI base URL from the GITHUB_HOST env var.
23
+ * For api.github.com, returns https://github.com.
24
+ * For GHES, derives from the hostname.
25
+ */
26
+ export declare function getWebBaseUrl(): string;
27
+ export declare function gqlPaginate<T>(query: string, variables?: Record<string, unknown>): Promise<T[]>;
package/dist/client.js ADDED
@@ -0,0 +1,80 @@
1
+ import { Octokit } from "@octokit/core";
2
+ import { restEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
3
+ import { paginateGraphQL } from "@octokit/plugin-paginate-graphql";
4
+ import { throttling } from "@octokit/plugin-throttling";
5
+ import { withRetry } from "./utils/retry.js";
6
+ import { withCircuitBreaker } from "./utils/circuitBreaker.js";
7
+ import { createLogger } from "./utils/logger.js";
8
+ import { updateRateLimits } from "./utils/rateLimit.js";
9
+ const log = createLogger("client");
10
+ const MyOctokit = Octokit
11
+ .plugin(restEndpointMethods)
12
+ .plugin(paginateGraphQL)
13
+ .plugin(throttling);
14
+ /**
15
+ * Creates an authenticated Octokit client with all plugins configured.
16
+ *
17
+ * The returned client wraps all REST requests through:
18
+ * circuit breaker → retry → throttling → GitHub API
19
+ *
20
+ * Rate limit headers from responses are automatically tracked
21
+ * via `updateRateLimits()`.
22
+ */
23
+ export function createClient() {
24
+ const token = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
25
+ if (!token) {
26
+ throw new Error("GITHUB_PERSONAL_ACCESS_TOKEN environment variable is not set. " +
27
+ "Create a token at https://github.com/settings/tokens " +
28
+ "and set it as GITHUB_PERSONAL_ACCESS_TOKEN in your environment.");
29
+ }
30
+ const onRateLimitStrategy = process.env.GITHUB_THROTTLE_ON_RATE_LIMIT !== "queue";
31
+ const octokit = new MyOctokit({
32
+ auth: token,
33
+ baseUrl: process.env.GITHUB_HOST ?? "https://api.github.com",
34
+ throttle: {
35
+ onRateLimit: (retryAfter, options) => {
36
+ log.warn(`Primary rate limit hit. Retrying after ${retryAfter}s. ` +
37
+ `Request: ${options.method} ${options.url}`);
38
+ return onRateLimitStrategy;
39
+ },
40
+ onSecondaryRateLimit: (retryAfter, options) => {
41
+ log.warn(`Secondary rate limit hit. Retrying after ${retryAfter}s. ` +
42
+ `Request: ${options.method} ${options.url}`);
43
+ return process.env.GITHUB_THROTTLE_ON_SECONDARY_RATE_LIMIT !== "queue";
44
+ },
45
+ },
46
+ });
47
+ octokit.hook.after("request", (response) => {
48
+ if (response?.headers) {
49
+ updateRateLimits(response.headers);
50
+ }
51
+ });
52
+ octokit.hook.wrap("request", async (request, options) => {
53
+ const label = `REST ${options.method} ${options.url}`;
54
+ return withCircuitBreaker(() => withRetry(() => Promise.resolve(request(options)), label), "github-rest");
55
+ });
56
+ return octokit;
57
+ }
58
+ /**
59
+ * Execute a raw GraphQL query against the GitHub API.
60
+ * The query is wrapped with circuit breaker and retry protection.
61
+ */
62
+ export async function gql(query, variables) {
63
+ const octokit = createClient();
64
+ return withCircuitBreaker(() => withRetry(() => octokit.graphql(query, variables), `GraphQL ${query.trim().split("\n")[0].slice(0, 60)}`), "github-graphql");
65
+ }
66
+ /**
67
+ * Derive the web UI base URL from the GITHUB_HOST env var.
68
+ * For api.github.com, returns https://github.com.
69
+ * For GHES, derives from the hostname.
70
+ */
71
+ export function getWebBaseUrl() {
72
+ const apiUrl = new URL(process.env.GITHUB_HOST ?? "https://api.github.com");
73
+ if (apiUrl.hostname === "api.github.com")
74
+ return "https://github.com";
75
+ return `https://${apiUrl.hostname}`;
76
+ }
77
+ export async function gqlPaginate(query, variables) {
78
+ const octokit = createClient();
79
+ return withCircuitBreaker(() => withRetry(() => octokit.graphql.paginate(query, variables), `GraphQL paginate ${query.trim().split("\n")[0].slice(0, 60)}`), "github-graphql");
80
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerAuthGetCurrentLogin, registerFileGetContent, registerProfileGet, registerOwnerListRepository, registerFileCreateUpdate, registerCodeSearch, registerRepositorySearch, } from "./tools/repos.js";
5
+ import { registerOrganizationSearch, registerOwnerListIssueType, registerOwnerListOrganization, } from "./tools/organizations.js";
6
+ import { registerPullRequestRead, registerPullRequestCreate, registerPullRequestMerge, } from "./tools/pull_requests.js";
7
+ import { registerIssueRead, registerIssueList, registerIssueSearch, registerIssueAddComment, registerSubIssueWrite, registerIssueCreate, registerIssueClose, registerIssueReopen, registerIssueUpdate, registerIssueAddLabel, registerIssueRemoveLabel, } from "./tools/issues.js";
8
+ import { registerOwnerListIssueField, registerIssueGetFieldValue, registerIssueSetFieldValue, } from "./tools/issue_fields.js";
9
+ import { registerLabelList, registerLabelWrite, } from "./tools/labels.js";
10
+ import { registerOwnerListProject, registerProjectsGet, registerProjectsWrite, } from "./tools/projects.js";
11
+ const server = new McpServer({ name: "github-pro-mcp", version: "2.0.1" }, { capabilities: { tools: {} } });
12
+ registerAuthGetCurrentLogin(server);
13
+ registerFileGetContent(server);
14
+ registerProfileGet(server);
15
+ registerOwnerListRepository(server);
16
+ registerFileCreateUpdate(server);
17
+ registerCodeSearch(server);
18
+ registerRepositorySearch(server);
19
+ registerOrganizationSearch(server);
20
+ registerOwnerListIssueType(server);
21
+ registerOwnerListOrganization(server);
22
+ registerPullRequestRead(server);
23
+ registerPullRequestCreate(server);
24
+ registerPullRequestMerge(server);
25
+ registerIssueRead(server);
26
+ registerIssueList(server);
27
+ registerIssueSearch(server);
28
+ registerIssueAddComment(server);
29
+ registerSubIssueWrite(server);
30
+ registerIssueCreate(server);
31
+ registerIssueClose(server);
32
+ registerIssueReopen(server);
33
+ registerIssueUpdate(server);
34
+ registerIssueAddLabel(server);
35
+ registerIssueRemoveLabel(server);
36
+ registerOwnerListIssueField(server);
37
+ registerIssueGetFieldValue(server);
38
+ registerIssueSetFieldValue(server);
39
+ registerLabelList(server);
40
+ registerLabelWrite(server);
41
+ registerOwnerListProject(server);
42
+ registerProjectsGet(server);
43
+ registerProjectsWrite(server);
44
+ async function main() {
45
+ const transport = new StdioServerTransport();
46
+ await server.connect(transport);
47
+ console.error("github-pro-mcp running on stdio");
48
+ }
49
+ main().catch((err) => {
50
+ console.error("Fatal error:", err);
51
+ process.exit(1);
52
+ });
@@ -0,0 +1,4 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerOwnerListIssueField(server: McpServer): void;
3
+ export declare function registerIssueGetFieldValue(server: McpServer): void;
4
+ export declare function registerIssueSetFieldValue(server: McpServer): void;
@@ -0,0 +1,104 @@
1
+ import * as z from "zod/v4";
2
+ import { createClient } from "../client.js";
3
+ import { handleGitHubError } from "../utils/errors.js";
4
+ import { requireOrg, validatePinnedOwnerRepo } from "../utils/validation.js";
5
+ import { OwnerRepoSchema, IssueNumberSchema } from "../utils/types.js";
6
+ export function registerOwnerListIssueField(server) {
7
+ server.registerTool("owner_list_issue_field", {
8
+ description: "List all issue field definitions for an organization (e.g., Priority, Effort, Start date, Target date). Owner must be an organization.",
9
+ inputSchema: z.object({
10
+ owner: z.string().min(1).describe("Organization owner"),
11
+ }),
12
+ }, async ({ owner }) => {
13
+ try {
14
+ const pinErr = validatePinnedOwnerRepo(owner);
15
+ if (pinErr)
16
+ return pinErr;
17
+ const octokit = createClient();
18
+ const orgErr = await requireOrg(owner);
19
+ if (orgErr)
20
+ return orgErr;
21
+ const { data } = await octokit.request('GET /orgs/{owner}/issue-fields', { owner });
22
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
23
+ }
24
+ catch (err) {
25
+ return handleGitHubError(err);
26
+ }
27
+ });
28
+ }
29
+ export function registerIssueGetFieldValue(server) {
30
+ server.registerTool("issue_get_field_value", {
31
+ description: "Get the current field values (Priority, Effort, dates, etc.) assigned to an issue.",
32
+ inputSchema: OwnerRepoSchema.extend({
33
+ issue_number: IssueNumberSchema,
34
+ }),
35
+ }, async ({ owner, repo, issue_number }) => {
36
+ try {
37
+ const pinErr = validatePinnedOwnerRepo(owner, repo);
38
+ if (pinErr)
39
+ return pinErr;
40
+ const octokit = createClient();
41
+ const { data } = await octokit.rest.issues.get({ owner, repo, issue_number });
42
+ const issueData = data;
43
+ return { content: [{ type: "text", text: JSON.stringify(issueData.issue_field_values ?? [], null, 2) }] };
44
+ }
45
+ catch (err) {
46
+ return handleGitHubError(err);
47
+ }
48
+ });
49
+ }
50
+ export function registerIssueSetFieldValue(server) {
51
+ server.registerTool("issue_set_field_value", {
52
+ description: "Set field values (Priority, Effort, dates, etc.) on an issue. Accepts either field_id (numeric ID) or field_name (e.g. 'Priority'). For single-select fields, value can be the option name (e.g. 'Medium') or option ID. For dates, use ISO 8601 (e.g. '2026-06-15'). Pass null to clear a field.",
53
+ inputSchema: OwnerRepoSchema.extend({
54
+ issue_number: IssueNumberSchema,
55
+ fields: z.array(z.object({
56
+ field_id: z.number().int().positive().optional().describe("Field ID from owner_list_issue_field"),
57
+ field_name: z.string().optional().describe("Field name (e.g. 'Priority') as alternative to field_id"),
58
+ value: z.union([z.string(), z.number(), z.null()]).describe("Field value: option name/ID for single-select, ISO date for dates, number for numeric fields, or null to clear"),
59
+ })).min(1).describe("Array of field updates"),
60
+ }),
61
+ }, async ({ owner, repo, issue_number, fields }) => {
62
+ try {
63
+ const pinErr = validatePinnedOwnerRepo(owner, repo);
64
+ if (pinErr)
65
+ return pinErr;
66
+ const octokit = createClient();
67
+ const resolvedFields = [];
68
+ for (const f of fields) {
69
+ let fieldId = f.field_id;
70
+ if (!fieldId && f.field_name) {
71
+ const fieldName = f.field_name;
72
+ const { data: fieldDefs } = await octokit.request('GET /orgs/{owner}/issue-fields', { owner });
73
+ const matching = fieldDefs.find((fd) => fd.name.toLowerCase() === fieldName.toLowerCase());
74
+ if (!matching) {
75
+ return {
76
+ content: [{ type: "text", text: `Field "${f.field_name}" not found. Available fields: ${fieldDefs.map((fd) => fd.name).join(", ")}` }],
77
+ isError: true,
78
+ };
79
+ }
80
+ fieldId = matching.id;
81
+ }
82
+ if (!fieldId) {
83
+ return {
84
+ content: [{ type: "text", text: "Either field_id or field_name is required for each field update." }],
85
+ isError: true,
86
+ };
87
+ }
88
+ resolvedFields.push({ field_id: fieldId, value: f.value });
89
+ }
90
+ const response = await octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', {
91
+ owner, repo, issue_number,
92
+ issue_field_values: resolvedFields,
93
+ });
94
+ const data = response.data;
95
+ return { content: [{ type: "text", text: JSON.stringify({
96
+ success: true,
97
+ issue_field_values: data.issue_field_values ?? [],
98
+ }, null, 2) }] };
99
+ }
100
+ catch (err) {
101
+ return handleGitHubError(err);
102
+ }
103
+ });
104
+ }
@@ -0,0 +1,12 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerIssueRead(server: McpServer): void;
3
+ export declare function registerIssueList(server: McpServer): void;
4
+ export declare function registerIssueSearch(server: McpServer): void;
5
+ export declare function registerIssueAddComment(server: McpServer): void;
6
+ export declare function registerSubIssueWrite(server: McpServer): void;
7
+ export declare function registerIssueCreate(server: McpServer): void;
8
+ export declare function registerIssueClose(server: McpServer): void;
9
+ export declare function registerIssueReopen(server: McpServer): void;
10
+ export declare function registerIssueUpdate(server: McpServer): void;
11
+ export declare function registerIssueAddLabel(server: McpServer): void;
12
+ export declare function registerIssueRemoveLabel(server: McpServer): void;