github-pro-mcp 0.0.1 → 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 +9 -4
- package/dist/client.d.ts +2 -1
- package/dist/client.js +6 -1
- package/dist/index.js +20 -2
- package/dist/tools/issue_fields.js +7 -3
- package/dist/tools/issues.js +60 -33
- package/dist/tools/organizations.js +1 -1
- package/dist/tools/projects.js +141 -175
- package/dist/tools/pull_requests.js +1 -1
- package/dist/tools/repos.d.ts +1 -0
- package/dist/tools/repos.js +38 -4
- package/dist/tools/server.d.ts +14 -0
- package/dist/tools/server.js +37 -0
- package/dist/utils/validation.d.ts +1 -0
- package/dist/utils/validation.js +13 -0
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/github-pro-mcp)
|
|
4
4
|
[](https://hub.docker.com/r/anantanandgupta/github-pro-mcp)
|
|
5
5
|
|
|
6
|
-
An MCP (Model Context Protocol) server providing comprehensive GitHub API access via hybrid REST + GraphQL — **
|
|
6
|
+
An MCP (Model Context Protocol) server providing comprehensive GitHub API access via hybrid REST + GraphQL — **34 tools across 8 domains**.
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
|
-
- **
|
|
10
|
+
- **34 Tools** — issues, pull requests, repositories, projects v2, labels, organizations, file operations, server info
|
|
11
11
|
- **Hybrid API** — REST for CRUD/search, GraphQL for cursor pagination and composite queries
|
|
12
12
|
- **Resilient by Default** — automatic retry with exponential backoff + jitter, circuit breaker for API outages, proactive rate limit management
|
|
13
13
|
- **Structured Logging** — multi-level (error/warn/info/debug) stderr output
|
|
@@ -69,6 +69,10 @@ docker run -i --rm \
|
|
|
69
69
|
| `GITHUB_RETRY_BACKOFF_MS` | `1000` | Initial retry backoff (ms) |
|
|
70
70
|
| `GITHUB_CIRCUIT_BREAKER_THRESHOLD` | `5` | Failures before circuit opens |
|
|
71
71
|
| `GITHUB_CIRCUIT_BREAKER_RESET_MS` | `30000` | Circuit recovery timeout (ms) |
|
|
72
|
+
| `GITHUB_RETRY_BACKOFF_FACTOR` | `2` | Exponential backoff multiplier |
|
|
73
|
+
| `GITHUB_OWNER_TYPE_CACHE_MS` | `300000` | Cache TTL for owner type lookups (5 min) |
|
|
74
|
+
| `GITHUB_THROTTLE_ON_RATE_LIMIT` | `retry` | Primary rate limit strategy: `retry` or `queue` |
|
|
75
|
+
| `GITHUB_THROTTLE_ON_SECONDARY_RATE_LIMIT` | `retry` | Secondary rate limit strategy: `retry` or `queue` |
|
|
72
76
|
| `GITHUB_LOG_LEVEL` | `error` | Log level: `error`, `warn`, `info`, `debug` |
|
|
73
77
|
|
|
74
78
|
## Usage with opencode
|
|
@@ -109,14 +113,15 @@ For Docker-based usage:
|
|
|
109
113
|
|
|
110
114
|
| Domain | Tools | Count |
|
|
111
115
|
|--------|-------|-------|
|
|
112
|
-
| **User & Repos** | `auth_get_current_login`, `file_get_content`, `profile_get`, `owner_list_repository`, `file_create_update`, `code_search`, `repository_search` |
|
|
116
|
+
| **User & Repos** | `auth_get_current_login`, `file_get_content`, `profile_get`, `owner_list_repository`, `file_create_update`, `file_delete`, `code_search`, `repository_search` | 8 |
|
|
113
117
|
| **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
118
|
| **Issue Fields** | `owner_list_issue_field`, `issue_get_field_value`, `issue_set_field_value` | 3 |
|
|
115
119
|
| **Pull Requests** | `pull_request_read`, `pull_request_create`, `pull_request_merge` | 3 |
|
|
116
120
|
| **Organizations** | `organization_search`, `owner_list_issue_type`, `owner_list_organization` | 3 |
|
|
117
121
|
| **Projects v2** | `owner_list_project`, `projects_get`, `projects_write` | 3 |
|
|
118
122
|
| **Labels** | `label_list`, `label_write` | 2 |
|
|
119
|
-
| **
|
|
123
|
+
| **Server Info** | `server_info` | 1 |
|
|
124
|
+
| **Total** | | **34** |
|
|
120
125
|
|
|
121
126
|
## FAQ / Troubleshooting
|
|
122
127
|
|
package/dist/client.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ export type OctokitWithRest = Octokit & {
|
|
|
4
4
|
rest: Api["rest"];
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
|
-
* Creates
|
|
7
|
+
* Creates a lazy singleton Octokit client with all plugins configured.
|
|
8
8
|
*
|
|
9
|
+
* The singleton preserves throttling queue state across calls.
|
|
9
10
|
* The returned client wraps all REST requests through:
|
|
10
11
|
* circuit breaker → retry → throttling → GitHub API
|
|
11
12
|
*
|
package/dist/client.js
CHANGED
|
@@ -11,9 +11,11 @@ const MyOctokit = Octokit
|
|
|
11
11
|
.plugin(restEndpointMethods)
|
|
12
12
|
.plugin(paginateGraphQL)
|
|
13
13
|
.plugin(throttling);
|
|
14
|
+
let cachedClient = null;
|
|
14
15
|
/**
|
|
15
|
-
* Creates
|
|
16
|
+
* Creates a lazy singleton Octokit client with all plugins configured.
|
|
16
17
|
*
|
|
18
|
+
* The singleton preserves throttling queue state across calls.
|
|
17
19
|
* The returned client wraps all REST requests through:
|
|
18
20
|
* circuit breaker → retry → throttling → GitHub API
|
|
19
21
|
*
|
|
@@ -21,6 +23,8 @@ const MyOctokit = Octokit
|
|
|
21
23
|
* via `updateRateLimits()`.
|
|
22
24
|
*/
|
|
23
25
|
export function createClient() {
|
|
26
|
+
if (cachedClient)
|
|
27
|
+
return cachedClient;
|
|
24
28
|
const token = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
|
|
25
29
|
if (!token) {
|
|
26
30
|
throw new Error("GITHUB_PERSONAL_ACCESS_TOKEN environment variable is not set. " +
|
|
@@ -53,6 +57,7 @@ export function createClient() {
|
|
|
53
57
|
const label = `REST ${options.method} ${options.url}`;
|
|
54
58
|
return withCircuitBreaker(() => withRetry(() => Promise.resolve(request(options)), label), "github-rest");
|
|
55
59
|
});
|
|
60
|
+
cachedClient = octokit;
|
|
56
61
|
return octokit;
|
|
57
62
|
}
|
|
58
63
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const pkg = require("../package.json");
|
|
7
|
+
import { registerAuthGetCurrentLogin, registerFileGetContent, registerProfileGet, registerOwnerListRepository, registerFileCreateUpdate, registerFileDelete, registerCodeSearch, registerRepositorySearch, } from "./tools/repos.js";
|
|
5
8
|
import { registerOrganizationSearch, registerOwnerListIssueType, registerOwnerListOrganization, } from "./tools/organizations.js";
|
|
6
9
|
import { registerPullRequestRead, registerPullRequestCreate, registerPullRequestMerge, } from "./tools/pull_requests.js";
|
|
7
10
|
import { registerIssueRead, registerIssueList, registerIssueSearch, registerIssueAddComment, registerSubIssueWrite, registerIssueCreate, registerIssueClose, registerIssueReopen, registerIssueUpdate, registerIssueAddLabel, registerIssueRemoveLabel, } from "./tools/issues.js";
|
|
8
11
|
import { registerOwnerListIssueField, registerIssueGetFieldValue, registerIssueSetFieldValue, } from "./tools/issue_fields.js";
|
|
9
12
|
import { registerLabelList, registerLabelWrite, } from "./tools/labels.js";
|
|
10
13
|
import { registerOwnerListProject, registerProjectsGet, registerProjectsWrite, } from "./tools/projects.js";
|
|
11
|
-
|
|
14
|
+
import { registerServerInfo } from "./tools/server.js";
|
|
15
|
+
const server = new McpServer({ name: pkg.name, version: pkg.version }, { capabilities: { tools: {} } });
|
|
16
|
+
const serverMeta = {
|
|
17
|
+
name: pkg.name,
|
|
18
|
+
version: pkg.version,
|
|
19
|
+
description: pkg.description,
|
|
20
|
+
author: pkg.author,
|
|
21
|
+
license: pkg.license,
|
|
22
|
+
repository: pkg.repository?.url ?? "",
|
|
23
|
+
homepage: pkg.homepage ?? "",
|
|
24
|
+
bug_tracker: pkg.bugs?.url ?? "",
|
|
25
|
+
bin: pkg.bin ?? {},
|
|
26
|
+
tool_count: 34,
|
|
27
|
+
};
|
|
12
28
|
registerAuthGetCurrentLogin(server);
|
|
13
29
|
registerFileGetContent(server);
|
|
30
|
+
registerFileDelete(server);
|
|
14
31
|
registerProfileGet(server);
|
|
15
32
|
registerOwnerListRepository(server);
|
|
16
33
|
registerFileCreateUpdate(server);
|
|
@@ -41,6 +58,7 @@ registerLabelWrite(server);
|
|
|
41
58
|
registerOwnerListProject(server);
|
|
42
59
|
registerProjectsGet(server);
|
|
43
60
|
registerProjectsWrite(server);
|
|
61
|
+
registerServerInfo(server, serverMeta);
|
|
44
62
|
async function main() {
|
|
45
63
|
const transport = new StdioServerTransport();
|
|
46
64
|
await server.connect(transport);
|
|
@@ -64,13 +64,17 @@ export function registerIssueSetFieldValue(server) {
|
|
|
64
64
|
if (pinErr)
|
|
65
65
|
return pinErr;
|
|
66
66
|
const octokit = createClient();
|
|
67
|
+
const nameFields = fields.filter((f) => !f.field_id && f.field_name);
|
|
68
|
+
let fieldDefs = null;
|
|
69
|
+
if (nameFields.length > 0) {
|
|
70
|
+
const { data } = await octokit.request('GET /orgs/{owner}/issue-fields', { owner });
|
|
71
|
+
fieldDefs = data;
|
|
72
|
+
}
|
|
67
73
|
const resolvedFields = [];
|
|
68
74
|
for (const f of fields) {
|
|
69
75
|
let fieldId = f.field_id;
|
|
70
76
|
if (!fieldId && f.field_name) {
|
|
71
|
-
const
|
|
72
|
-
const { data: fieldDefs } = await octokit.request('GET /orgs/{owner}/issue-fields', { owner });
|
|
73
|
-
const matching = fieldDefs.find((fd) => fd.name.toLowerCase() === fieldName.toLowerCase());
|
|
77
|
+
const matching = fieldDefs.find((fd) => fd.name.toLowerCase() === f.field_name.toLowerCase());
|
|
74
78
|
if (!matching) {
|
|
75
79
|
return {
|
|
76
80
|
content: [{ type: "text", text: `Field "${f.field_name}" not found. Available fields: ${fieldDefs.map((fd) => fd.name).join(", ")}` }],
|
package/dist/tools/issues.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as z from "zod/v4";
|
|
2
2
|
import { createClient, gql } from "../client.js";
|
|
3
3
|
import { handleGitHubError } from "../utils/errors.js";
|
|
4
|
-
import { validatePinnedOwnerRepo } from "../utils/validation.js";
|
|
4
|
+
import { validatePinnedOwnerRepo, requireIssueOpen } from "../utils/validation.js";
|
|
5
5
|
import { OwnerRepoSchema, PaginationSchema, IssueNumberSchema, IncludeFlagsSchema } from "../utils/types.js";
|
|
6
6
|
export function registerIssueRead(server) {
|
|
7
7
|
server.registerTool("issue_read", {
|
|
@@ -53,17 +53,17 @@ export function registerIssueRead(server) {
|
|
|
53
53
|
if (include_labels) {
|
|
54
54
|
const labelLimit = max_labels ?? 20;
|
|
55
55
|
const query = `
|
|
56
|
-
query($owner: String!, $repo: String!, $number: Int!) {
|
|
56
|
+
query($owner: String!, $repo: String!, $number: Int!, $labelLimit: Int!) {
|
|
57
57
|
repository(owner: $owner, name: $repo) {
|
|
58
58
|
issue(number: $number) {
|
|
59
|
-
labels(first: $
|
|
59
|
+
labels(first: $labelLimit) {
|
|
60
60
|
nodes { name color description }
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
`;
|
|
66
|
-
const labelData = await gql(query, { owner, repo, number: issue_number });
|
|
66
|
+
const labelData = await gql(query, { owner, repo, number: issue_number, labelLimit });
|
|
67
67
|
result.labels_graphql = labelData.repository?.issue?.labels?.nodes ?? [];
|
|
68
68
|
}
|
|
69
69
|
if (include_fields) {
|
|
@@ -72,17 +72,17 @@ export function registerIssueRead(server) {
|
|
|
72
72
|
if (include_sub_issues) {
|
|
73
73
|
const subLimit = max_sub_issues ?? 20;
|
|
74
74
|
const query = `
|
|
75
|
-
query($owner: String!, $repo: String!, $number: Int!) {
|
|
75
|
+
query($owner: String!, $repo: String!, $number: Int!, $subLimit: Int!) {
|
|
76
76
|
repository(owner: $owner, name: $repo) {
|
|
77
77
|
issue(number: $number) {
|
|
78
|
-
subIssues(first: $
|
|
78
|
+
subIssues(first: $subLimit) {
|
|
79
79
|
nodes { number title state }
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
`;
|
|
85
|
-
const subData = await gql(query, { owner, repo, number: issue_number });
|
|
85
|
+
const subData = await gql(query, { owner, repo, number: issue_number, subLimit });
|
|
86
86
|
result.sub_issues = subData.repository?.issue?.subIssues?.nodes ?? [];
|
|
87
87
|
}
|
|
88
88
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -96,7 +96,7 @@ export function registerIssueList(server) {
|
|
|
96
96
|
server.registerTool("issue_list", {
|
|
97
97
|
description: "List issues in a repository. Optionally include org-level field values (Priority, Effort) and/or project board field values (Status, Application). Supports field-based filtering with filter_fields (AND logic).",
|
|
98
98
|
inputSchema: OwnerRepoSchema.extend({
|
|
99
|
-
state: z.enum(["open", "closed"]).optional().describe("Filter by issue state"),
|
|
99
|
+
state: z.enum(["open", "closed", "all"]).optional().describe("Filter by issue state"),
|
|
100
100
|
labels: z.string().optional().describe("Comma-separated label names to filter by"),
|
|
101
101
|
per_page: PaginationSchema.shape.per_page,
|
|
102
102
|
page: PaginationSchema.shape.page,
|
|
@@ -129,18 +129,18 @@ export function registerIssueList(server) {
|
|
|
129
129
|
fieldIdToName = null;
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
-
const restParams = { owner, repo, per_page, page };
|
|
132
|
+
const restParams = { owner, repo, per_page, page, sort: "updated", direction: "desc" };
|
|
133
133
|
if (state)
|
|
134
134
|
restParams.state = state;
|
|
135
135
|
if (labels)
|
|
136
136
|
restParams.labels = labels;
|
|
137
137
|
const { data: restIssues } = await octokit.rest.issues.listForRepo(restParams);
|
|
138
138
|
if (!restIssues.length) {
|
|
139
|
-
return { content: [{ type: "text", text: JSON.stringify({ total: 0, issues: [] }, null, 2) }] };
|
|
139
|
+
return { content: [{ type: "text", text: JSON.stringify({ total: 0, page, per_page, issues: [] }, null, 2) }] };
|
|
140
140
|
}
|
|
141
141
|
let projectMap = null;
|
|
142
142
|
if (needProjectFields) {
|
|
143
|
-
const gqlStates = state ? [state.toUpperCase()] : null;
|
|
143
|
+
const gqlStates = state && state !== "all" ? [state.toUpperCase()] : null;
|
|
144
144
|
const gqlLabels = labels ? labels.split(",").map((l) => l.trim()) : null;
|
|
145
145
|
const gqlQuery = `
|
|
146
146
|
query($owner: String!, $repo: String!, $first: Int!, $states: [IssueState!], $labels: [String!]) {
|
|
@@ -169,10 +169,11 @@ export function registerIssueList(server) {
|
|
|
169
169
|
}
|
|
170
170
|
`;
|
|
171
171
|
const gqlResult = await gql(gqlQuery, {
|
|
172
|
-
owner, repo, first: per_page, states: gqlStates, labels: gqlLabels,
|
|
172
|
+
owner, repo, first: page * per_page, states: gqlStates, labels: gqlLabels,
|
|
173
173
|
});
|
|
174
174
|
projectMap = new Map();
|
|
175
|
-
|
|
175
|
+
const skipItems = (page - 1) * per_page;
|
|
176
|
+
for (const node of (gqlResult.repository?.issues?.nodes ?? []).slice(skipItems)) {
|
|
176
177
|
projectMap.set(node.number, (node.projectItems?.nodes ?? []));
|
|
177
178
|
}
|
|
178
179
|
}
|
|
@@ -301,28 +302,32 @@ export function registerIssueSearch(server) {
|
|
|
301
302
|
server.registerTool("issue_search", {
|
|
302
303
|
description: "Search for issues in GitHub repositories using GitHub's search syntax.",
|
|
303
304
|
inputSchema: z.object({
|
|
304
|
-
query: z.string().min(1).describe("Search query using GitHub issues search syntax, e.g., 'label:bug repo:owner/repo'"),
|
|
305
|
+
query: z.string().min(1).max(1000).describe("Search query using GitHub issues search syntax, e.g., 'label:bug repo:owner/repo'"),
|
|
305
306
|
page: PaginationSchema.shape.page,
|
|
306
307
|
per_page: PaginationSchema.shape.per_page,
|
|
307
308
|
}),
|
|
308
309
|
}, async ({ query, page, per_page }) => {
|
|
309
310
|
try {
|
|
310
311
|
const octokit = createClient();
|
|
311
|
-
const
|
|
312
|
+
const searchQuery = query.includes("is:issue") ? query : `${query} is:issue`;
|
|
313
|
+
const { data } = await octokit.rest.search.issuesAndPullRequests({ q: searchQuery, page, per_page });
|
|
314
|
+
const items = data.items
|
|
315
|
+
.filter((i) => !i.pull_request)
|
|
316
|
+
.map((i) => ({
|
|
317
|
+
number: i.number,
|
|
318
|
+
title: i.title,
|
|
319
|
+
state: i.state,
|
|
320
|
+
html_url: i.html_url,
|
|
321
|
+
repository: i.repository?.full_name ?? null,
|
|
322
|
+
created_at: i.created_at,
|
|
323
|
+
updated_at: i.updated_at,
|
|
324
|
+
user: i.user?.login,
|
|
325
|
+
labels: i.labels?.map((l) => (typeof l === "string" ? l : l.name)) ?? [],
|
|
326
|
+
}));
|
|
312
327
|
return {
|
|
313
328
|
content: [{ type: "text", text: JSON.stringify({
|
|
314
329
|
total_count: data.total_count,
|
|
315
|
-
items
|
|
316
|
-
number: i.number,
|
|
317
|
-
title: i.title,
|
|
318
|
-
state: i.state,
|
|
319
|
-
html_url: i.html_url,
|
|
320
|
-
repository: i.repository?.full_name ?? null,
|
|
321
|
-
created_at: i.created_at,
|
|
322
|
-
updated_at: i.updated_at,
|
|
323
|
-
user: i.user?.login,
|
|
324
|
-
labels: i.labels?.map((l) => (typeof l === "string" ? l : l.name)) ?? [],
|
|
325
|
-
})),
|
|
330
|
+
items,
|
|
326
331
|
}, null, 2) }],
|
|
327
332
|
};
|
|
328
333
|
}
|
|
@@ -474,9 +479,15 @@ export function registerIssueClose(server) {
|
|
|
474
479
|
const octokit = createClient();
|
|
475
480
|
const { data } = await octokit.rest.issues.update({ owner, repo, issue_number, state: "closed" });
|
|
476
481
|
let commentPosted = false;
|
|
482
|
+
let commentError;
|
|
477
483
|
if (comment) {
|
|
478
|
-
|
|
479
|
-
|
|
484
|
+
try {
|
|
485
|
+
await octokit.rest.issues.createComment({ owner, repo, issue_number, body: comment });
|
|
486
|
+
commentPosted = true;
|
|
487
|
+
}
|
|
488
|
+
catch (e) {
|
|
489
|
+
commentError = e instanceof Error ? e.message : String(e);
|
|
490
|
+
}
|
|
480
491
|
}
|
|
481
492
|
return {
|
|
482
493
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -484,6 +495,7 @@ export function registerIssueClose(server) {
|
|
|
484
495
|
state: data.state,
|
|
485
496
|
closed_at: data.closed_at,
|
|
486
497
|
comment_posted: commentPosted,
|
|
498
|
+
comment_error: commentError,
|
|
487
499
|
}, null, 2) }],
|
|
488
500
|
};
|
|
489
501
|
}
|
|
@@ -525,12 +537,19 @@ export function registerIssueUpdate(server) {
|
|
|
525
537
|
title: z.string().optional().describe("New title for the issue"),
|
|
526
538
|
body: z.string().optional().describe("New body content for the issue"),
|
|
527
539
|
state: z.enum(["open", "closed"]).optional().describe("New state for the issue"),
|
|
540
|
+
}).refine((data) => data.title !== undefined || data.body !== undefined || data.state !== undefined, {
|
|
541
|
+
message: "At least one of title, body, or state must be provided.",
|
|
528
542
|
}),
|
|
529
543
|
}, async ({ owner, repo, issue_number, title, body, state }) => {
|
|
530
544
|
try {
|
|
531
545
|
const pinErr = validatePinnedOwnerRepo(owner, repo);
|
|
532
546
|
if (pinErr)
|
|
533
547
|
return pinErr;
|
|
548
|
+
if (title !== undefined || body !== undefined) {
|
|
549
|
+
const closedErr = await requireIssueOpen(owner, repo, issue_number);
|
|
550
|
+
if (closedErr)
|
|
551
|
+
return closedErr;
|
|
552
|
+
}
|
|
534
553
|
const octokit = createClient();
|
|
535
554
|
const { data } = await octokit.rest.issues.update({ owner, repo, issue_number, title, body, state });
|
|
536
555
|
return {
|
|
@@ -588,15 +607,23 @@ export function registerIssueRemoveLabel(server) {
|
|
|
588
607
|
if (pinErr)
|
|
589
608
|
return pinErr;
|
|
590
609
|
const octokit = createClient();
|
|
591
|
-
const
|
|
610
|
+
const removed = [];
|
|
611
|
+
const failed = [];
|
|
592
612
|
for (const label of labels) {
|
|
593
|
-
|
|
594
|
-
|
|
613
|
+
try {
|
|
614
|
+
await octokit.rest.issues.removeLabel({ owner, repo, issue_number, name: label });
|
|
615
|
+
removed.push(label);
|
|
616
|
+
}
|
|
617
|
+
catch (e) {
|
|
618
|
+
failed.push({ label, error: e instanceof Error ? e.message : String(e) });
|
|
619
|
+
}
|
|
595
620
|
}
|
|
596
621
|
return {
|
|
597
622
|
content: [{ type: "text", text: JSON.stringify({
|
|
598
|
-
|
|
599
|
-
|
|
623
|
+
partial: removed.length > 0 && failed.length > 0,
|
|
624
|
+
success: removed.length > 0,
|
|
625
|
+
labels_removed: removed,
|
|
626
|
+
labels_failed: failed.length > 0 ? failed : undefined,
|
|
600
627
|
}, null, 2) }],
|
|
601
628
|
};
|
|
602
629
|
}
|
|
@@ -7,7 +7,7 @@ export function registerOrganizationSearch(server) {
|
|
|
7
7
|
server.registerTool("organization_search", {
|
|
8
8
|
description: "Search for organizations on GitHub by query. Supports qualifiers like 'location:...', 'repos:>...'.",
|
|
9
9
|
inputSchema: z.object({
|
|
10
|
-
query: z.string().min(1).describe("Search query, e.g., 'microsoft' or 'location:seattle repos:>5'"),
|
|
10
|
+
query: z.string().min(1).max(1000).describe("Search query, e.g., 'microsoft' or 'location:seattle repos:>5'"),
|
|
11
11
|
page: PaginationSchema.shape.page,
|
|
12
12
|
per_page: PaginationSchema.shape.per_page,
|
|
13
13
|
}),
|
package/dist/tools/projects.js
CHANGED
|
@@ -17,114 +17,62 @@ export function registerOwnerListProject(server) {
|
|
|
17
17
|
const pinErr = validatePinnedOwnerRepo(owner);
|
|
18
18
|
if (pinErr)
|
|
19
19
|
return pinErr;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
pageInfo {
|
|
36
|
-
hasNextPage
|
|
37
|
-
endCursor
|
|
38
|
-
}
|
|
20
|
+
const rootType = owner_type === "org" ? "organization" : "user";
|
|
21
|
+
const gqlQuery = `
|
|
22
|
+
query($owner: String!, $first: Int!, $after: String, $searchStr: String) {
|
|
23
|
+
${rootType}(login: $owner) {
|
|
24
|
+
projectsV2(first: $first, after: $after, query: $searchStr) {
|
|
25
|
+
nodes {
|
|
26
|
+
id
|
|
27
|
+
number
|
|
28
|
+
title
|
|
29
|
+
shortDescription
|
|
30
|
+
url
|
|
31
|
+
closed
|
|
32
|
+
createdAt
|
|
33
|
+
updatedAt
|
|
39
34
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const data = await gql(gqlQuery, {
|
|
44
|
-
owner, first, after: after ?? null, searchStr: state ? `is:${state}` : null,
|
|
45
|
-
});
|
|
46
|
-
const org = data.organization;
|
|
47
|
-
if (!org) {
|
|
48
|
-
return {
|
|
49
|
-
content: [{ type: "text", text: `Organization "${owner}" not found.` }],
|
|
50
|
-
isError: true,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
const projects = org.projectsV2?.nodes ?? [];
|
|
54
|
-
const pageInfo = org.projectsV2?.pageInfo;
|
|
55
|
-
return {
|
|
56
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
57
|
-
projects: projects.map((p) => ({
|
|
58
|
-
id: p.id,
|
|
59
|
-
number: p.number,
|
|
60
|
-
title: p.title,
|
|
61
|
-
description: p.shortDescription,
|
|
62
|
-
url: p.url,
|
|
63
|
-
closed: p.closed,
|
|
64
|
-
created_at: p.createdAt,
|
|
65
|
-
updated_at: p.updatedAt,
|
|
66
|
-
})),
|
|
67
|
-
page_info: {
|
|
68
|
-
has_next_page: pageInfo?.hasNextPage ?? false,
|
|
69
|
-
end_cursor: pageInfo?.endCursor ?? null,
|
|
70
|
-
},
|
|
71
|
-
}, null, 2) }],
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
const gqlQuery = `
|
|
76
|
-
query($owner: String!, $first: Int!, $after: String, $searchStr: String) {
|
|
77
|
-
user(login: $owner) {
|
|
78
|
-
projectsV2(first: $first, after: $after, query: $searchStr) {
|
|
79
|
-
nodes {
|
|
80
|
-
id
|
|
81
|
-
number
|
|
82
|
-
title
|
|
83
|
-
shortDescription
|
|
84
|
-
url
|
|
85
|
-
closed
|
|
86
|
-
createdAt
|
|
87
|
-
updatedAt
|
|
88
|
-
}
|
|
89
|
-
pageInfo {
|
|
90
|
-
hasNextPage
|
|
91
|
-
endCursor
|
|
92
|
-
}
|
|
35
|
+
pageInfo {
|
|
36
|
+
hasNextPage
|
|
37
|
+
endCursor
|
|
93
38
|
}
|
|
94
39
|
}
|
|
95
40
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
const projects = user.projectsV2?.nodes ?? [];
|
|
108
|
-
const pageInfo = user.projectsV2?.pageInfo;
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
const data = await gql(gqlQuery, {
|
|
44
|
+
owner, first, after: after ?? null, searchStr: state ? `is:${state}` : null,
|
|
45
|
+
});
|
|
46
|
+
const root = owner_type === "org"
|
|
47
|
+
? data.organization
|
|
48
|
+
: data.user;
|
|
49
|
+
if (!root) {
|
|
50
|
+
const label = owner_type === "org" ? "Organization" : "User";
|
|
109
51
|
return {
|
|
110
|
-
content: [{ type: "text", text:
|
|
111
|
-
|
|
112
|
-
id: p.id,
|
|
113
|
-
number: p.number,
|
|
114
|
-
title: p.title,
|
|
115
|
-
description: p.shortDescription,
|
|
116
|
-
url: p.url,
|
|
117
|
-
closed: p.closed,
|
|
118
|
-
created_at: p.createdAt,
|
|
119
|
-
updated_at: p.updatedAt,
|
|
120
|
-
})),
|
|
121
|
-
page_info: {
|
|
122
|
-
has_next_page: pageInfo?.hasNextPage ?? false,
|
|
123
|
-
end_cursor: pageInfo?.endCursor ?? null,
|
|
124
|
-
},
|
|
125
|
-
}, null, 2) }],
|
|
52
|
+
content: [{ type: "text", text: `${label} "${owner}" not found.` }],
|
|
53
|
+
isError: true,
|
|
126
54
|
};
|
|
127
55
|
}
|
|
56
|
+
const projects = root.projectsV2?.nodes ?? [];
|
|
57
|
+
const pageInfo = root.projectsV2?.pageInfo;
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
60
|
+
projects: projects.map((p) => ({
|
|
61
|
+
id: p.id,
|
|
62
|
+
number: p.number,
|
|
63
|
+
title: p.title,
|
|
64
|
+
description: p.shortDescription,
|
|
65
|
+
url: p.url,
|
|
66
|
+
closed: p.closed,
|
|
67
|
+
created_at: p.createdAt,
|
|
68
|
+
updated_at: p.updatedAt,
|
|
69
|
+
})),
|
|
70
|
+
page_info: {
|
|
71
|
+
has_next_page: pageInfo?.hasNextPage ?? false,
|
|
72
|
+
end_cursor: pageInfo?.endCursor ?? null,
|
|
73
|
+
},
|
|
74
|
+
}, null, 2) }],
|
|
75
|
+
};
|
|
128
76
|
}
|
|
129
77
|
catch (err) {
|
|
130
78
|
return handleGitHubError(err);
|
|
@@ -177,69 +125,17 @@ export function registerProjectsGet(server) {
|
|
|
177
125
|
const itemsLimit = max_items ?? 50;
|
|
178
126
|
const statusUpdatesLimit = max_status_updates ?? 20;
|
|
179
127
|
const fieldValueLimit = max_field_values ?? 20;
|
|
180
|
-
const fieldsFragment = include_fields ? `
|
|
181
|
-
fields(first: ${fieldsLimit}) {
|
|
182
|
-
nodes {
|
|
183
|
-
... on ProjectV2Field {
|
|
184
|
-
id name dataType
|
|
185
|
-
}
|
|
186
|
-
... on ProjectV2SingleSelectField {
|
|
187
|
-
id name dataType
|
|
188
|
-
options { id name color }
|
|
189
|
-
}
|
|
190
|
-
... on ProjectV2IterationField {
|
|
191
|
-
id name dataType
|
|
192
|
-
configuration { iterations { id title startDate duration } }
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
` : "";
|
|
197
|
-
const itemsFragment = include_items ? `
|
|
198
|
-
items(first: ${itemsLimit}) {
|
|
199
|
-
nodes {
|
|
200
|
-
id
|
|
201
|
-
type
|
|
202
|
-
fieldValues(first: ${fieldValueLimit}) {
|
|
203
|
-
nodes {
|
|
204
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
205
|
-
name optionId field { ... on ProjectV2SingleSelectField { id name } }
|
|
206
|
-
}
|
|
207
|
-
... on ProjectV2ItemFieldTextValue {
|
|
208
|
-
text field { ... on ProjectV2Field { id name } }
|
|
209
|
-
}
|
|
210
|
-
... on ProjectV2ItemFieldNumberValue {
|
|
211
|
-
number field { ... on ProjectV2Field { id name } }
|
|
212
|
-
}
|
|
213
|
-
... on ProjectV2ItemFieldDateValue {
|
|
214
|
-
date field { ... on ProjectV2Field { id name } }
|
|
215
|
-
}
|
|
216
|
-
... on ProjectV2ItemFieldIterationValue {
|
|
217
|
-
title field { ... on ProjectV2IterationField { id name } }
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
content {
|
|
222
|
-
... on Issue { number title state url assignees(first: 5) { nodes { login } } }
|
|
223
|
-
... on PullRequest { number title state url }
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
` : "";
|
|
228
|
-
const statusUpdatesFragment = include_status_updates ? `
|
|
229
|
-
statusUpdates(first: ${statusUpdatesLimit}) {
|
|
230
|
-
nodes {
|
|
231
|
-
id
|
|
232
|
-
body
|
|
233
|
-
status
|
|
234
|
-
createdAt
|
|
235
|
-
updatedAt
|
|
236
|
-
startDate
|
|
237
|
-
targetDate
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
` : "";
|
|
241
128
|
const query = `
|
|
242
|
-
query(
|
|
129
|
+
query(
|
|
130
|
+
$id: ID!,
|
|
131
|
+
$includeFields: Boolean!,
|
|
132
|
+
$includeItems: Boolean!,
|
|
133
|
+
$includeStatusUpdates: Boolean!,
|
|
134
|
+
$fieldsLimit: Int!,
|
|
135
|
+
$itemsLimit: Int!,
|
|
136
|
+
$statusUpdatesLimit: Int!,
|
|
137
|
+
$fieldValueLimit: Int!
|
|
138
|
+
) {
|
|
243
139
|
node(id: $id) {
|
|
244
140
|
... on ProjectV2 {
|
|
245
141
|
id
|
|
@@ -251,14 +147,75 @@ export function registerProjectsGet(server) {
|
|
|
251
147
|
closedAt
|
|
252
148
|
createdAt
|
|
253
149
|
updatedAt
|
|
254
|
-
${
|
|
255
|
-
|
|
256
|
-
|
|
150
|
+
fields(first: $fieldsLimit) @include(if: $includeFields) {
|
|
151
|
+
nodes {
|
|
152
|
+
... on ProjectV2Field {
|
|
153
|
+
id name dataType
|
|
154
|
+
}
|
|
155
|
+
... on ProjectV2SingleSelectField {
|
|
156
|
+
id name dataType
|
|
157
|
+
options { id name color }
|
|
158
|
+
}
|
|
159
|
+
... on ProjectV2IterationField {
|
|
160
|
+
id name dataType
|
|
161
|
+
configuration { iterations { id title startDate duration } }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
items(first: $itemsLimit) @include(if: $includeItems) {
|
|
166
|
+
nodes {
|
|
167
|
+
id
|
|
168
|
+
type
|
|
169
|
+
fieldValues(first: $fieldValueLimit) {
|
|
170
|
+
nodes {
|
|
171
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
172
|
+
name optionId field { ... on ProjectV2SingleSelectField { id name } }
|
|
173
|
+
}
|
|
174
|
+
... on ProjectV2ItemFieldTextValue {
|
|
175
|
+
text field { ... on ProjectV2Field { id name } }
|
|
176
|
+
}
|
|
177
|
+
... on ProjectV2ItemFieldNumberValue {
|
|
178
|
+
number field { ... on ProjectV2Field { id name } }
|
|
179
|
+
}
|
|
180
|
+
... on ProjectV2ItemFieldDateValue {
|
|
181
|
+
date field { ... on ProjectV2Field { id name } }
|
|
182
|
+
}
|
|
183
|
+
... on ProjectV2ItemFieldIterationValue {
|
|
184
|
+
title field { ... on ProjectV2IterationField { id name } }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
content {
|
|
189
|
+
... on Issue { number title state url assignees(first: 5) { nodes { login } } }
|
|
190
|
+
... on PullRequest { number title state url }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
statusUpdates(first: $statusUpdatesLimit) @include(if: $includeStatusUpdates) {
|
|
195
|
+
nodes {
|
|
196
|
+
id
|
|
197
|
+
body
|
|
198
|
+
status
|
|
199
|
+
createdAt
|
|
200
|
+
updatedAt
|
|
201
|
+
startDate
|
|
202
|
+
targetDate
|
|
203
|
+
}
|
|
204
|
+
}
|
|
257
205
|
}
|
|
258
206
|
}
|
|
259
207
|
}
|
|
260
208
|
`;
|
|
261
|
-
const data = await gql(query, {
|
|
209
|
+
const data = await gql(query, {
|
|
210
|
+
id: project_id,
|
|
211
|
+
includeFields: !!include_fields,
|
|
212
|
+
includeItems: !!include_items,
|
|
213
|
+
includeStatusUpdates: !!include_status_updates,
|
|
214
|
+
fieldsLimit,
|
|
215
|
+
itemsLimit,
|
|
216
|
+
statusUpdatesLimit,
|
|
217
|
+
fieldValueLimit,
|
|
218
|
+
});
|
|
262
219
|
const project = data.node;
|
|
263
220
|
if (!project) {
|
|
264
221
|
return {
|
|
@@ -307,10 +264,10 @@ export function registerProjectsWrite(server) {
|
|
|
307
264
|
field_values: z.array(z.object({
|
|
308
265
|
field_id: z.string().describe("GraphQL node ID of the project field"),
|
|
309
266
|
value: z.union([z.string(), z.number(), z.null()]).describe("Field value"),
|
|
310
|
-
value_type: z.enum(["text", "number", "date", "
|
|
267
|
+
value_type: z.enum(["single_select", "text", "number", "date", "iteration", "title"]).describe("Type of the field value — use the field's dataType from projects_get / owner_list_project to determine the correct value_type"),
|
|
311
268
|
})).optional().describe("Field values to set (update_item)"),
|
|
312
269
|
body: z.string().optional().describe("Status update body text (create_status_update)"),
|
|
313
|
-
status: z.
|
|
270
|
+
status: z.string().optional().describe("Status update status (create_status_update) — pass any value GitHub accepts"),
|
|
314
271
|
start_date: z.string().optional().describe("Start date in ISO 8601 format (create_status_update)"),
|
|
315
272
|
target_date: z.string().optional().describe("Target date in ISO 8601 format (create_status_update)"),
|
|
316
273
|
owner: z.string().optional().describe("Required when using issue_number. Must match the server's configured owner if pinned."),
|
|
@@ -388,22 +345,31 @@ export function registerProjectsWrite(server) {
|
|
|
388
345
|
let value;
|
|
389
346
|
switch (fv.value_type) {
|
|
390
347
|
case "single_select":
|
|
391
|
-
value = { singleSelectOptionId: String(fv.value) };
|
|
348
|
+
value = fv.value === null ? { singleSelectOptionId: null } : { singleSelectOptionId: String(fv.value) };
|
|
392
349
|
break;
|
|
393
350
|
case "date":
|
|
394
|
-
value = { date: fv.value };
|
|
351
|
+
value = fv.value === null ? { date: null } : { date: fv.value };
|
|
395
352
|
break;
|
|
396
353
|
case "text":
|
|
397
|
-
value = { text: String(fv.value) };
|
|
354
|
+
value = fv.value === null ? { text: null } : { text: String(fv.value) };
|
|
398
355
|
break;
|
|
399
356
|
case "number":
|
|
400
|
-
value = { number: Number(fv.value) };
|
|
357
|
+
value = fv.value === null ? { number: null } : { number: Number(fv.value) };
|
|
401
358
|
break;
|
|
402
359
|
case "iteration":
|
|
403
|
-
value = { iterationId: String(fv.value) };
|
|
360
|
+
value = fv.value === null ? { iterationId: null } : { iterationId: String(fv.value) };
|
|
361
|
+
break;
|
|
362
|
+
case "title":
|
|
363
|
+
value = fv.value === null ? { title: null } : { title: String(fv.value) };
|
|
404
364
|
break;
|
|
405
365
|
default:
|
|
406
|
-
|
|
366
|
+
if (fv.value === null) {
|
|
367
|
+
return {
|
|
368
|
+
content: [{ type: "text", text: `Cannot clear field — unknown value_type "${fv.value_type}". Provide a valid value_type or use a known type (single_select, text, number, date, iteration, title).` }],
|
|
369
|
+
isError: true,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
value = { text: String(fv.value) };
|
|
407
373
|
}
|
|
408
374
|
const result = await gql(mutation, {
|
|
409
375
|
projectId: project_id, itemId: item_id, fieldId: fv.field_id, value,
|
|
@@ -101,7 +101,7 @@ export function registerPullRequestMerge(server) {
|
|
|
101
101
|
description: "Merge a pull request in a GitHub repository.",
|
|
102
102
|
inputSchema: OwnerRepoSchema.extend({
|
|
103
103
|
pull_number: PullNumberSchema,
|
|
104
|
-
merge_method: z.enum(["merge", "squash", "rebase"]).optional().default("merge").describe("Merge method"),
|
|
104
|
+
merge_method: z.enum(["merge", "squash", "rebase"]).optional().default("merge").describe("Merge method — GitHub accepts: merge, squash, rebase"),
|
|
105
105
|
commit_title: z.string().optional().describe("Title for merge commit"),
|
|
106
106
|
commit_message: z.string().optional().describe("Extra detail for merge commit"),
|
|
107
107
|
}),
|
package/dist/tools/repos.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ export declare function registerProfileGet(server: McpServer): void;
|
|
|
5
5
|
export declare function registerOwnerListRepository(server: McpServer): void;
|
|
6
6
|
export declare function registerFileCreateUpdate(server: McpServer): void;
|
|
7
7
|
export declare function registerCodeSearch(server: McpServer): void;
|
|
8
|
+
export declare function registerFileDelete(server: McpServer): void;
|
|
8
9
|
export declare function registerRepositorySearch(server: McpServer): void;
|
package/dist/tools/repos.js
CHANGED
|
@@ -100,7 +100,7 @@ export function registerOwnerListRepository(server) {
|
|
|
100
100
|
inputSchema: z.object({
|
|
101
101
|
owner: z.string().min(1).describe("Repository owner (user or organization)"),
|
|
102
102
|
type: z.enum(["all", "owner", "member"]).optional().default("all").describe("Type of repositories to list (all, owner, member)"),
|
|
103
|
-
sort: z.enum(["created", "updated", "pushed", "full_name"]).optional().default("created").describe("Sort field"),
|
|
103
|
+
sort: z.enum(["created", "updated", "pushed", "full_name", "name"]).optional().default("created").describe("Sort field"),
|
|
104
104
|
direction: z.enum(["asc", "desc"]).optional().describe("Sort direction (asc or desc)"),
|
|
105
105
|
page: PaginationSchema.shape.page,
|
|
106
106
|
per_page: PaginationSchema.shape.per_page,
|
|
@@ -111,7 +111,7 @@ export function registerOwnerListRepository(server) {
|
|
|
111
111
|
if (pinErr)
|
|
112
112
|
return pinErr;
|
|
113
113
|
const octokit = createClient();
|
|
114
|
-
const { data } = await octokit.rest.repos.listForUser({ username: owner, type, sort, direction, page, per_page });
|
|
114
|
+
const { data } = await octokit.rest.repos.listForUser({ username: owner, type, sort: sort, direction, page, per_page });
|
|
115
115
|
return {
|
|
116
116
|
content: [{ type: "text", text: JSON.stringify(data.map((r) => formatRepo(r)), null, 2) }],
|
|
117
117
|
};
|
|
@@ -163,7 +163,7 @@ export function registerCodeSearch(server) {
|
|
|
163
163
|
server.registerTool("code_search", {
|
|
164
164
|
description: "Search for code across GitHub repositories. Supports GitHub search qualifiers like 'repo:...', 'language:...', 'path:...'.",
|
|
165
165
|
inputSchema: z.object({
|
|
166
|
-
query: z.string().min(1).describe("Search query with GitHub qualifiers, e.g., 'function repo:owner/repo language:python'"),
|
|
166
|
+
query: z.string().min(1).max(1000).describe("Search query with GitHub qualifiers, e.g., 'function repo:owner/repo language:python'"),
|
|
167
167
|
page: PaginationSchema.shape.page,
|
|
168
168
|
per_page: PaginationSchema.shape.per_page,
|
|
169
169
|
}),
|
|
@@ -188,11 +188,45 @@ export function registerCodeSearch(server) {
|
|
|
188
188
|
}
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
|
+
export function registerFileDelete(server) {
|
|
192
|
+
server.registerTool("file_delete", {
|
|
193
|
+
description: "Delete a file from a GitHub repository. Requires the SHA of the file (obtain from file_get_content).",
|
|
194
|
+
inputSchema: OwnerRepoSchema.extend({
|
|
195
|
+
path: z.string().min(1).describe("Path to the file to delete"),
|
|
196
|
+
message: z.string().min(1).describe("Commit message"),
|
|
197
|
+
branch: z.string().min(1).describe("Branch to delete the file from"),
|
|
198
|
+
sha: z.string().min(1).describe("SHA of the file to delete (obtain from file_get_content or file_create_update)"),
|
|
199
|
+
}),
|
|
200
|
+
}, async ({ owner, repo, path, message, branch, sha }) => {
|
|
201
|
+
try {
|
|
202
|
+
const pinErr = validatePinnedOwnerRepo(owner, repo);
|
|
203
|
+
if (pinErr)
|
|
204
|
+
return pinErr;
|
|
205
|
+
const octokit = createClient();
|
|
206
|
+
const response = await octokit.rest.repos.deleteFile({
|
|
207
|
+
owner, repo, path, message, branch, sha,
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
211
|
+
commit: {
|
|
212
|
+
sha: response.data.commit.sha,
|
|
213
|
+
message: response.data.commit.message,
|
|
214
|
+
html_url: response.data.commit.html_url,
|
|
215
|
+
},
|
|
216
|
+
content: null,
|
|
217
|
+
}, null, 2) }],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
return handleGitHubError(err);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
191
225
|
export function registerRepositorySearch(server) {
|
|
192
226
|
server.registerTool("repository_search", {
|
|
193
227
|
description: "Search for GitHub repositories by query. Supports GitHub search qualifiers like 'org:...', 'language:...', 'stars:>100'.",
|
|
194
228
|
inputSchema: z.object({
|
|
195
|
-
query: z.string().min(1).describe("Search query with GitHub qualifiers, e.g., 'org:microsoft language:python'"),
|
|
229
|
+
query: z.string().min(1).max(1000).describe("Search query with GitHub qualifiers, e.g., 'org:microsoft language:python'"),
|
|
196
230
|
page: PaginationSchema.shape.page,
|
|
197
231
|
per_page: PaginationSchema.shape.per_page,
|
|
198
232
|
}),
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export interface ServerMetadata {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
description: string;
|
|
6
|
+
author: string;
|
|
7
|
+
license: string;
|
|
8
|
+
repository: string;
|
|
9
|
+
homepage: string;
|
|
10
|
+
bug_tracker: string;
|
|
11
|
+
bin: Record<string, string>;
|
|
12
|
+
tool_count: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function registerServerInfo(server: McpServer, meta: ServerMetadata): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
export function registerServerInfo(server, meta) {
|
|
3
|
+
server.registerTool("server_info", {
|
|
4
|
+
description: "Get information about this MCP server, including version, support links, and runtime configuration.",
|
|
5
|
+
inputSchema: z.object({}),
|
|
6
|
+
}, async () => {
|
|
7
|
+
const runtime = {
|
|
8
|
+
node_version: process.version,
|
|
9
|
+
platform: process.platform,
|
|
10
|
+
pinned_owner: process.env.GITHUB_OWNER ?? null,
|
|
11
|
+
pinned_repo: process.env.GITHUB_REPO ?? null,
|
|
12
|
+
log_level: process.env.GITHUB_LOG_LEVEL ?? "error",
|
|
13
|
+
retry_max_attempts: parseInt(process.env.GITHUB_RETRY_MAX_ATTEMPTS ?? "3", 10),
|
|
14
|
+
circuit_breaker_threshold: parseInt(process.env.GITHUB_CIRCUIT_BREAKER_THRESHOLD ?? "5", 10),
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
content: [{
|
|
18
|
+
type: "text",
|
|
19
|
+
text: JSON.stringify({
|
|
20
|
+
name: meta.name,
|
|
21
|
+
version: meta.version,
|
|
22
|
+
description: meta.description,
|
|
23
|
+
author: meta.author,
|
|
24
|
+
license: meta.license,
|
|
25
|
+
repository: meta.repository,
|
|
26
|
+
homepage: meta.homepage,
|
|
27
|
+
bug_tracker: meta.bug_tracker,
|
|
28
|
+
npm_package: "https://www.npmjs.com/package/github-pro-mcp",
|
|
29
|
+
docker_image: "https://hub.docker.com/r/anantanandgupta/github-pro-mcp",
|
|
30
|
+
bin: meta.bin,
|
|
31
|
+
tools_registered: meta.tool_count,
|
|
32
|
+
runtime,
|
|
33
|
+
}, null, 2),
|
|
34
|
+
}],
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -9,4 +9,5 @@ interface ErrorResponse {
|
|
|
9
9
|
export declare function validatePinnedOwnerRepo(owner?: string, repo?: string): ErrorResponse | null;
|
|
10
10
|
export declare function requireOrg(owner: string): Promise<ErrorResponse | null>;
|
|
11
11
|
export declare function requireUser(owner: string): Promise<ErrorResponse | null>;
|
|
12
|
+
export declare function requireIssueOpen(owner: string, repo: string, issue_number: number): Promise<ErrorResponse | null>;
|
|
12
13
|
export {};
|
package/dist/utils/validation.js
CHANGED
|
@@ -73,3 +73,16 @@ export async function requireUser(owner) {
|
|
|
73
73
|
}
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
|
+
export async function requireIssueOpen(owner, repo, issue_number) {
|
|
77
|
+
const octokit = createClient();
|
|
78
|
+
const { data } = await octokit.rest.issues.get({ owner, repo, issue_number });
|
|
79
|
+
if (data.state === "closed") {
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: "text", text: `Issue #${issue_number} in ${owner}/${repo} is closed. ` +
|
|
82
|
+
`Reopen it first before modifying the title or body.`
|
|
83
|
+
}],
|
|
84
|
+
isError: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-pro-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "MCP server providing GitHub API access with hybrid REST + GraphQL support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,15 +12,26 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"start": "node dist/index.js",
|
|
15
|
-
"test": "node --test 'src/__tests__/**/*.test.mjs'",
|
|
15
|
+
"test": "node --test --test-global-setup='src/__tests__/setup.mjs' --test-concurrency=1 'src/__tests__/**/*.test.mjs'",
|
|
16
|
+
"test:unit": "node --test 'src/__tests__/{retry,circuit_breaker,rate_limit}.test.mjs'",
|
|
17
|
+
"test:repos": "node --test --test-global-setup='src/__tests__/setup.mjs' 'src/__tests__/repos.test.mjs'",
|
|
18
|
+
"test:issues": "node --test --test-global-setup='src/__tests__/setup.mjs' 'src/__tests__/issues.test.mjs'",
|
|
19
|
+
"test:issue_fields": "node --test --test-global-setup='src/__tests__/setup.mjs' 'src/__tests__/issue_fields.test.mjs'",
|
|
20
|
+
"test:projects": "node --test --test-global-setup='src/__tests__/setup.mjs' 'src/__tests__/projects.test.mjs'",
|
|
21
|
+
"test:orgs": "node --test --test-global-setup='src/__tests__/setup.mjs' 'src/__tests__/organizations.test.mjs'",
|
|
22
|
+
"test:labels": "node --test --test-global-setup='src/__tests__/setup.mjs' 'src/__tests__/labels.test.mjs'",
|
|
23
|
+
"test:prs": "node --test --test-global-setup='src/__tests__/setup.mjs' 'src/__tests__/pull_requests.test.mjs'",
|
|
24
|
+
"test:write": "node --test --test-global-setup='src/__tests__/setup.mjs' 'src/__tests__/write.test.mjs'",
|
|
25
|
+
"test:consistency": "node --test --test-global-setup='src/__tests__/setup.mjs' 'src/__tests__/consistency.test.mjs'",
|
|
16
26
|
"test:docker": "node --test 'test/docker/**/*.test.mjs'",
|
|
17
27
|
"test:npm": "node --test 'test/integration/**/*.test.mjs'",
|
|
18
28
|
"test:all": "npm test && npm run test:docker && npm run test:npm",
|
|
19
|
-
"test:coverage": "node --experimental-test-coverage --test 'src/__tests__/**/*.test.mjs'",
|
|
29
|
+
"test:coverage": "node --experimental-test-coverage --test --test-global-setup='src/__tests__/setup.mjs' 'src/__tests__/**/*.test.mjs'",
|
|
20
30
|
"test:inspector": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
21
31
|
"dev": "tsc --watch",
|
|
22
32
|
"prepack": "npm run build",
|
|
23
33
|
"docker:build": "make docker-build",
|
|
34
|
+
"docker:build:local": "make docker-build-local",
|
|
24
35
|
"docker:push": "make docker-push",
|
|
25
36
|
"publish:npm": "npm run build && npm test && npm publish",
|
|
26
37
|
"publish:all": "npm run publish:npm && npm run docker:push"
|