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 +133 -0
- package/dist/client.d.ts +27 -0
- package/dist/client.js +80 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +52 -0
- package/dist/tools/issue_fields.d.ts +4 -0
- package/dist/tools/issue_fields.js +104 -0
- package/dist/tools/issues.d.ts +12 -0
- package/dist/tools/issues.js +607 -0
- package/dist/tools/labels.d.ts +3 -0
- package/dist/tools/labels.js +87 -0
- package/dist/tools/organizations.d.ts +4 -0
- package/dist/tools/organizations.js +89 -0
- package/dist/tools/projects.d.ts +4 -0
- package/dist/tools/projects.js +474 -0
- package/dist/tools/pull_requests.d.ts +4 -0
- package/dist/tools/pull_requests.js +127 -0
- package/dist/tools/repos.d.ts +8 -0
- package/dist/tools/repos.js +217 -0
- package/dist/types/github-api.d.ts +63 -0
- package/dist/types/github-api.js +1 -0
- package/dist/utils/circuitBreaker.d.ts +11 -0
- package/dist/utils/circuitBreaker.js +125 -0
- package/dist/utils/errors.d.ts +17 -0
- package/dist/utils/errors.js +107 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.js +40 -0
- package/dist/utils/metrics.d.ts +11 -0
- package/dist/utils/metrics.js +70 -0
- package/dist/utils/rateLimit.d.ts +21 -0
- package/dist/utils/rateLimit.js +40 -0
- package/dist/utils/retry.d.ts +9 -0
- package/dist/utils/retry.js +76 -0
- package/dist/utils/types.d.ts +33 -0
- package/dist/utils/types.js +33 -0
- package/dist/utils/validation.d.ts +12 -0
- package/dist/utils/validation.js +75 -0
- package/package.json +61 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { createClient } from "../client.js";
|
|
3
|
+
import { handleGitHubError } from "../utils/errors.js";
|
|
4
|
+
import { validatePinnedOwnerRepo } from "../utils/validation.js";
|
|
5
|
+
import { OwnerRepoSchema, PaginationSchema } from "../utils/types.js";
|
|
6
|
+
export function registerLabelList(server) {
|
|
7
|
+
server.registerTool("label_list", {
|
|
8
|
+
description: "List labels in a GitHub repository.",
|
|
9
|
+
inputSchema: OwnerRepoSchema.extend({
|
|
10
|
+
page: PaginationSchema.shape.page,
|
|
11
|
+
per_page: PaginationSchema.shape.per_page,
|
|
12
|
+
}),
|
|
13
|
+
}, async ({ owner, repo, page, per_page }) => {
|
|
14
|
+
try {
|
|
15
|
+
const pinErr = validatePinnedOwnerRepo(owner, repo);
|
|
16
|
+
if (pinErr)
|
|
17
|
+
return pinErr;
|
|
18
|
+
const octokit = createClient();
|
|
19
|
+
const { data } = await octokit.rest.issues.listLabelsForRepo({ owner, repo, page, per_page });
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text", text: JSON.stringify(data.map((l) => ({
|
|
22
|
+
name: l.name,
|
|
23
|
+
color: l.color,
|
|
24
|
+
description: l.description,
|
|
25
|
+
default: l.default,
|
|
26
|
+
})), null, 2) }],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
return handleGitHubError(err);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function registerLabelWrite(server) {
|
|
35
|
+
server.registerTool("label_write", {
|
|
36
|
+
description: "Create, update, or delete a label in a GitHub repository. Method 'create' requires name and color. Method 'update' requires name (current name) and optional color/description/new_name. Method 'delete' requires name only.",
|
|
37
|
+
inputSchema: OwnerRepoSchema.extend({
|
|
38
|
+
method: z.enum(["create", "update", "delete"]).describe("Action to perform on the label"),
|
|
39
|
+
name: z.string().min(1).describe("Label name (for update: current name)"),
|
|
40
|
+
color: z.string().regex(/^[0-9a-fA-F]{6}$/).optional().describe("6-character hex color without # (required for create)"),
|
|
41
|
+
description: z.string().optional().describe("Label description"),
|
|
42
|
+
new_name: z.string().optional().describe("New name for rename (update only)"),
|
|
43
|
+
}),
|
|
44
|
+
}, async ({ owner, repo, method, name, color, description, new_name }) => {
|
|
45
|
+
try {
|
|
46
|
+
const pinErr = validatePinnedOwnerRepo(owner, repo);
|
|
47
|
+
if (pinErr)
|
|
48
|
+
return pinErr;
|
|
49
|
+
const octokit = createClient();
|
|
50
|
+
if (method === "create") {
|
|
51
|
+
if (!color) {
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: "color is required for creating a label (6-char hex without #)" }],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const { data } = await octokit.rest.issues.createLabel({ owner, repo, name, color, description });
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
60
|
+
name: data.name,
|
|
61
|
+
color: data.color,
|
|
62
|
+
description: data.description,
|
|
63
|
+
}, null, 2) }],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
else if (method === "update") {
|
|
67
|
+
const { data } = await octokit.rest.issues.updateLabel({ owner, repo, name, color, description, new_name });
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
70
|
+
name: data.name,
|
|
71
|
+
color: data.color,
|
|
72
|
+
description: data.description,
|
|
73
|
+
}, null, 2) }],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
await octokit.rest.issues.deleteLabel({ owner, repo, name });
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, deleted: name }, null, 2) }],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
return handleGitHubError(err);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export declare function registerOrganizationSearch(server: McpServer): void;
|
|
3
|
+
export declare function registerOwnerListIssueType(server: McpServer): void;
|
|
4
|
+
export declare function registerOwnerListOrganization(server: McpServer): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { createClient, getWebBaseUrl } from "../client.js";
|
|
3
|
+
import { handleGitHubError } from "../utils/errors.js";
|
|
4
|
+
import { PaginationSchema } from "../utils/types.js";
|
|
5
|
+
import { requireOrg, requireUser, validatePinnedOwnerRepo } from "../utils/validation.js";
|
|
6
|
+
export function registerOrganizationSearch(server) {
|
|
7
|
+
server.registerTool("organization_search", {
|
|
8
|
+
description: "Search for organizations on GitHub by query. Supports qualifiers like 'location:...', 'repos:>...'.",
|
|
9
|
+
inputSchema: z.object({
|
|
10
|
+
query: z.string().min(1).describe("Search query, e.g., 'microsoft' or 'location:seattle repos:>5'"),
|
|
11
|
+
page: PaginationSchema.shape.page,
|
|
12
|
+
per_page: PaginationSchema.shape.per_page,
|
|
13
|
+
}),
|
|
14
|
+
}, async ({ query, page, per_page }) => {
|
|
15
|
+
try {
|
|
16
|
+
const octokit = createClient();
|
|
17
|
+
const { data } = await octokit.rest.search.users({ q: `${query}+type:org`, page, per_page });
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
20
|
+
total_count: data.total_count,
|
|
21
|
+
items: data.items.map((u) => {
|
|
22
|
+
const item = u;
|
|
23
|
+
return {
|
|
24
|
+
login: item.login,
|
|
25
|
+
description: item.description ?? null,
|
|
26
|
+
html_url: item.html_url,
|
|
27
|
+
};
|
|
28
|
+
}),
|
|
29
|
+
}, null, 2) }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
return handleGitHubError(err);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function registerOwnerListIssueType(server) {
|
|
38
|
+
server.registerTool("owner_list_issue_type", {
|
|
39
|
+
description: "List issue types for an organization (e.g., Bug, Feature, Enhancement). Owner must be an organization.",
|
|
40
|
+
inputSchema: z.object({
|
|
41
|
+
owner: z.string().min(1).describe("Organization owner"),
|
|
42
|
+
}),
|
|
43
|
+
}, async ({ owner }) => {
|
|
44
|
+
try {
|
|
45
|
+
const pinErr = validatePinnedOwnerRepo(owner);
|
|
46
|
+
if (pinErr)
|
|
47
|
+
return pinErr;
|
|
48
|
+
const octokit = createClient();
|
|
49
|
+
const orgErr = await requireOrg(owner);
|
|
50
|
+
if (orgErr)
|
|
51
|
+
return orgErr;
|
|
52
|
+
const { data } = await octokit.request('GET /orgs/{owner}/issue-types', { owner });
|
|
53
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return handleGitHubError(err);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function registerOwnerListOrganization(server) {
|
|
61
|
+
server.registerTool("owner_list_organization", {
|
|
62
|
+
description: "List all organizations that a user account belongs to. Provide a username, not an organization name.",
|
|
63
|
+
inputSchema: z.object({
|
|
64
|
+
owner: z.string().min(1).describe("Username to list organizations for"),
|
|
65
|
+
}),
|
|
66
|
+
}, async ({ owner }) => {
|
|
67
|
+
try {
|
|
68
|
+
const pinErr = validatePinnedOwnerRepo(owner);
|
|
69
|
+
if (pinErr)
|
|
70
|
+
return pinErr;
|
|
71
|
+
const octokit = createClient();
|
|
72
|
+
const userErr = await requireUser(owner);
|
|
73
|
+
if (userErr)
|
|
74
|
+
return userErr;
|
|
75
|
+
const { data } = await octokit.rest.orgs.listForUser({ username: owner });
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: JSON.stringify(data.map((o) => ({
|
|
78
|
+
login: o.login,
|
|
79
|
+
description: o.description,
|
|
80
|
+
avatar_url: o.avatar_url,
|
|
81
|
+
html_url: `${getWebBaseUrl()}/${o.login}`,
|
|
82
|
+
})), null, 2) }],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
return handleGitHubError(err);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export declare function registerOwnerListProject(server: McpServer): void;
|
|
3
|
+
export declare function registerProjectsGet(server: McpServer): void;
|
|
4
|
+
export declare function registerProjectsWrite(server: McpServer): void;
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { gql } from "../client.js";
|
|
3
|
+
import { handleGitHubError } from "../utils/errors.js";
|
|
4
|
+
import { validatePinnedOwnerRepo } from "../utils/validation.js";
|
|
5
|
+
export function registerOwnerListProject(server) {
|
|
6
|
+
server.registerTool("owner_list_project", {
|
|
7
|
+
description: "List GitHub Projects (v2) for an owner. Works for both organizations and individual users.",
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
owner: z.string().min(1).describe("Organization or user login"),
|
|
10
|
+
owner_type: z.enum(["org", "user"]).describe("Type of owner: 'org' for organization, 'user' for individual user"),
|
|
11
|
+
state: z.enum(["open", "closed"]).optional().describe("Filter by project state"),
|
|
12
|
+
first: z.number().int().min(1).max(100).optional().default(30).describe("Results per page (max 100)"),
|
|
13
|
+
after: z.string().optional().describe("Cursor for pagination (endCursor from previous response)"),
|
|
14
|
+
}),
|
|
15
|
+
}, async ({ owner, owner_type, state, first, after }) => {
|
|
16
|
+
try {
|
|
17
|
+
const pinErr = validatePinnedOwnerRepo(owner);
|
|
18
|
+
if (pinErr)
|
|
19
|
+
return pinErr;
|
|
20
|
+
if (owner_type === "org") {
|
|
21
|
+
const gqlQuery = `
|
|
22
|
+
query($owner: String!, $first: Int!, $after: String, $searchStr: String) {
|
|
23
|
+
organization(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
|
|
34
|
+
}
|
|
35
|
+
pageInfo {
|
|
36
|
+
hasNextPage
|
|
37
|
+
endCursor
|
|
38
|
+
}
|
|
39
|
+
}
|
|
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
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
const data = await gql(gqlQuery, {
|
|
98
|
+
owner, first, after: after ?? null, searchStr: state ? `is:${state}` : null,
|
|
99
|
+
});
|
|
100
|
+
const user = data.user;
|
|
101
|
+
if (!user) {
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: `User "${owner}" not found.` }],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const projects = user.projectsV2?.nodes ?? [];
|
|
108
|
+
const pageInfo = user.projectsV2?.pageInfo;
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
111
|
+
projects: projects.map((p) => ({
|
|
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) }],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
return handleGitHubError(err);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function formatFieldValue(fv) {
|
|
135
|
+
if (fv.name !== undefined)
|
|
136
|
+
return { field: fv.field?.name, value: fv.name };
|
|
137
|
+
if (fv.text !== undefined)
|
|
138
|
+
return { field: fv.field?.name, value: fv.text };
|
|
139
|
+
if (fv.number !== undefined)
|
|
140
|
+
return { field: fv.field?.name, value: fv.number };
|
|
141
|
+
if (fv.date !== undefined)
|
|
142
|
+
return { field: fv.field?.name, value: fv.date };
|
|
143
|
+
if (fv.title !== undefined)
|
|
144
|
+
return { field: fv.field?.name, value: fv.title };
|
|
145
|
+
return { value: null };
|
|
146
|
+
}
|
|
147
|
+
function formatProjectItem(item) {
|
|
148
|
+
return {
|
|
149
|
+
id: item.id,
|
|
150
|
+
type: item.type,
|
|
151
|
+
content: item.content ? {
|
|
152
|
+
number: item.content.number,
|
|
153
|
+
title: item.content.title,
|
|
154
|
+
state: item.content.state,
|
|
155
|
+
url: item.content.url,
|
|
156
|
+
assignees: item.content.assignees?.nodes?.map((a) => a.login) ?? [],
|
|
157
|
+
} : null,
|
|
158
|
+
field_values: item.fieldValues?.nodes?.map(formatFieldValue) ?? [],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export function registerProjectsGet(server) {
|
|
162
|
+
server.registerTool("projects_get", {
|
|
163
|
+
description: "Get a single GitHub Project (v2) by its GraphQL node ID with optional fields, items, and status updates. Each include_* flag makes additional API calls.",
|
|
164
|
+
inputSchema: z.object({
|
|
165
|
+
project_id: z.string().min(1).describe("GraphQL node ID of the project (obtain from owner_list_project)"),
|
|
166
|
+
include_fields: z.boolean().optional().default(false).describe("Include project field definitions"),
|
|
167
|
+
include_items: z.boolean().optional().default(false).describe("Include project items (issues/PRs)"),
|
|
168
|
+
include_status_updates: z.boolean().optional().default(false).describe("Include status updates"),
|
|
169
|
+
max_fields: z.number().int().positive().optional().default(20).describe("Maximum number of project field definitions to return (default: 20)"),
|
|
170
|
+
max_items: z.number().int().positive().optional().default(50).describe("Maximum number of project items to return (default: 50)"),
|
|
171
|
+
max_status_updates: z.number().int().positive().optional().default(20).describe("Maximum number of status updates to return (default: 20)"),
|
|
172
|
+
max_field_values: z.number().int().positive().optional().default(20).describe("Maximum number of field values per item (default: 20)"),
|
|
173
|
+
}),
|
|
174
|
+
}, async ({ project_id, include_fields, include_items, include_status_updates, max_fields, max_items, max_status_updates, max_field_values }) => {
|
|
175
|
+
try {
|
|
176
|
+
const fieldsLimit = max_fields ?? 20;
|
|
177
|
+
const itemsLimit = max_items ?? 50;
|
|
178
|
+
const statusUpdatesLimit = max_status_updates ?? 20;
|
|
179
|
+
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
|
+
const query = `
|
|
242
|
+
query($id: ID!) {
|
|
243
|
+
node(id: $id) {
|
|
244
|
+
... on ProjectV2 {
|
|
245
|
+
id
|
|
246
|
+
number
|
|
247
|
+
title
|
|
248
|
+
shortDescription
|
|
249
|
+
url
|
|
250
|
+
closed
|
|
251
|
+
closedAt
|
|
252
|
+
createdAt
|
|
253
|
+
updatedAt
|
|
254
|
+
${fieldsFragment}
|
|
255
|
+
${itemsFragment}
|
|
256
|
+
${statusUpdatesFragment}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
`;
|
|
261
|
+
const data = await gql(query, { id: project_id });
|
|
262
|
+
const project = data.node;
|
|
263
|
+
if (!project) {
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: "text", text: `Project not found. Check the project_id is a valid GraphQL node ID.` }],
|
|
266
|
+
isError: true,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const result = {
|
|
270
|
+
id: project.id,
|
|
271
|
+
number: project.number,
|
|
272
|
+
title: project.title,
|
|
273
|
+
description: project.shortDescription,
|
|
274
|
+
url: project.url,
|
|
275
|
+
closed: project.closed,
|
|
276
|
+
closed_at: project.closedAt,
|
|
277
|
+
created_at: project.createdAt,
|
|
278
|
+
updated_at: project.updatedAt,
|
|
279
|
+
};
|
|
280
|
+
if (include_fields) {
|
|
281
|
+
result.fields = project.fields?.nodes ?? [];
|
|
282
|
+
}
|
|
283
|
+
if (include_items) {
|
|
284
|
+
result.items = (project.items?.nodes ?? []).map(formatProjectItem);
|
|
285
|
+
}
|
|
286
|
+
if (include_status_updates) {
|
|
287
|
+
result.status_updates = project.statusUpdates?.nodes ?? [];
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
return handleGitHubError(err);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
export function registerProjectsWrite(server) {
|
|
299
|
+
server.registerTool("projects_write", {
|
|
300
|
+
description: "Manage items in a GitHub Project (v2): add, update field values, delete items, or create status updates. Methods: add_item (requires content_id), update_item (requires item_id + field_values; field_values each need field_id, value, and value_type), delete_item (requires item_id), create_status_update (requires body; optional status/start_date/target_date). Note: update_item sends field values one at a time — if one fails, earlier updates are already committed. Check partial: true in the response.",
|
|
301
|
+
inputSchema: z.object({
|
|
302
|
+
project_id: z.string().min(1).describe("GraphQL node ID of the project"),
|
|
303
|
+
method: z.enum(["add_item", "update_item", "delete_item", "create_status_update"]).describe("Action to perform"),
|
|
304
|
+
content_id: z.string().optional().describe("GraphQL node ID of issue/PR to add (required for add_item)"),
|
|
305
|
+
issue_number: z.number().int().positive().optional().describe("Issue number (alternative to content_id — auto-resolves to node ID)"),
|
|
306
|
+
item_id: z.string().optional().describe("GraphQL node ID of the project item (required for update_item, delete_item)"),
|
|
307
|
+
field_values: z.array(z.object({
|
|
308
|
+
field_id: z.string().describe("GraphQL node ID of the project field"),
|
|
309
|
+
value: z.union([z.string(), z.number(), z.null()]).describe("Field value"),
|
|
310
|
+
value_type: z.enum(["text", "number", "date", "single_select", "iteration"]).describe("Type of the field value: single_select (option ID as string), date (ISO 8601), text, number, or iteration (iteration ID)"),
|
|
311
|
+
})).optional().describe("Field values to set (update_item)"),
|
|
312
|
+
body: z.string().optional().describe("Status update body text (create_status_update)"),
|
|
313
|
+
status: z.enum(["IN_PROGRESS", "ON_TRACK", "BLOCKED", "AT_RISK", "OFF_TRACK", "COMPLETE"]).optional().describe("Status update status (create_status_update)"),
|
|
314
|
+
start_date: z.string().optional().describe("Start date in ISO 8601 format (create_status_update)"),
|
|
315
|
+
target_date: z.string().optional().describe("Target date in ISO 8601 format (create_status_update)"),
|
|
316
|
+
owner: z.string().optional().describe("Required when using issue_number. Must match the server's configured owner if pinned."),
|
|
317
|
+
repo: z.string().optional().describe("Required when using issue_number. Must match the server's configured repository if pinned."),
|
|
318
|
+
}),
|
|
319
|
+
}, async ({ project_id, method, content_id, issue_number, item_id, field_values, body, status, start_date, target_date, owner, repo }) => {
|
|
320
|
+
try {
|
|
321
|
+
if (method === "add_item") {
|
|
322
|
+
let resolvedContentId = content_id;
|
|
323
|
+
if (!resolvedContentId && issue_number) {
|
|
324
|
+
if (!owner || !repo) {
|
|
325
|
+
return {
|
|
326
|
+
content: [{ type: "text", text: "owner and repo are required when using issue_number." }],
|
|
327
|
+
isError: true,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const pinErr = validatePinnedOwnerRepo(owner, repo);
|
|
331
|
+
if (pinErr)
|
|
332
|
+
return pinErr;
|
|
333
|
+
const idQuery = `
|
|
334
|
+
query($owner: String!, $repo: String!, $number: Int!) {
|
|
335
|
+
repository(owner: $owner, name: $repo) {
|
|
336
|
+
issue(number: $number) { id }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
`;
|
|
340
|
+
const idData = await gql(idQuery, { owner, repo, number: issue_number });
|
|
341
|
+
resolvedContentId = idData.repository?.issue?.id;
|
|
342
|
+
if (!resolvedContentId) {
|
|
343
|
+
return {
|
|
344
|
+
content: [{ type: "text", text: `Could not resolve issue #${issue_number} in ${owner}/${repo}.` }],
|
|
345
|
+
isError: true,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!resolvedContentId) {
|
|
350
|
+
return {
|
|
351
|
+
content: [{ type: "text", text: "content_id or issue_number is required for add_item." }],
|
|
352
|
+
isError: true,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const mutation = `
|
|
356
|
+
mutation($projectId: ID!, $contentId: ID!) {
|
|
357
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
|
|
358
|
+
item { id }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
`;
|
|
362
|
+
const result = await gql(mutation, { projectId: project_id, contentId: resolvedContentId });
|
|
363
|
+
return {
|
|
364
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
365
|
+
success: true,
|
|
366
|
+
item_id: result.addProjectV2ItemById?.item?.id,
|
|
367
|
+
}, null, 2) }],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
else if (method === "update_item") {
|
|
371
|
+
if (!item_id || !field_values || field_values.length === 0) {
|
|
372
|
+
return {
|
|
373
|
+
content: [{ type: "text", text: "item_id and field_values are required for update_item." }],
|
|
374
|
+
isError: true,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const results = [];
|
|
378
|
+
for (const fv of field_values) {
|
|
379
|
+
const mutation = `
|
|
380
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) {
|
|
381
|
+
updateProjectV2ItemFieldValue(input: {
|
|
382
|
+
projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: $value
|
|
383
|
+
}) {
|
|
384
|
+
projectV2Item { id }
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
`;
|
|
388
|
+
let value;
|
|
389
|
+
switch (fv.value_type) {
|
|
390
|
+
case "single_select":
|
|
391
|
+
value = { singleSelectOptionId: String(fv.value) };
|
|
392
|
+
break;
|
|
393
|
+
case "date":
|
|
394
|
+
value = { date: fv.value };
|
|
395
|
+
break;
|
|
396
|
+
case "text":
|
|
397
|
+
value = { text: String(fv.value) };
|
|
398
|
+
break;
|
|
399
|
+
case "number":
|
|
400
|
+
value = { number: Number(fv.value) };
|
|
401
|
+
break;
|
|
402
|
+
case "iteration":
|
|
403
|
+
value = { iterationId: String(fv.value) };
|
|
404
|
+
break;
|
|
405
|
+
default:
|
|
406
|
+
value = fv.value === null ? { value: null } : { text: String(fv.value) };
|
|
407
|
+
}
|
|
408
|
+
const result = await gql(mutation, {
|
|
409
|
+
projectId: project_id, itemId: item_id, fieldId: fv.field_id, value,
|
|
410
|
+
});
|
|
411
|
+
results.push({
|
|
412
|
+
field_id: fv.field_id,
|
|
413
|
+
result: result.updateProjectV2ItemFieldValue?.projectV2Item?.id ? "updated" : "failed",
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
418
|
+
success: results.every((r) => r.result === "updated"),
|
|
419
|
+
updates: results,
|
|
420
|
+
}, null, 2) }],
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
else if (method === "delete_item") {
|
|
424
|
+
if (!item_id) {
|
|
425
|
+
return {
|
|
426
|
+
content: [{ type: "text", text: "item_id is required for delete_item." }],
|
|
427
|
+
isError: true,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const mutation = `
|
|
431
|
+
mutation($projectId: ID!, $itemId: ID!) {
|
|
432
|
+
deleteProjectV2Item(input: { projectId: $projectId, itemId: $itemId }) {
|
|
433
|
+
deletedItemId
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
`;
|
|
437
|
+
const result = await gql(mutation, { projectId: project_id, itemId: item_id });
|
|
438
|
+
return {
|
|
439
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
440
|
+
success: true,
|
|
441
|
+
deleted_item_id: result.deleteProjectV2Item?.deletedItemId,
|
|
442
|
+
}, null, 2) }],
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
else if (method === "create_status_update") {
|
|
446
|
+
const mutation = `
|
|
447
|
+
mutation($projectId: ID!, $body: String!, $status: ProjectV2StatusUpdateStatus, $startDate: Date, $targetDate: Date) {
|
|
448
|
+
createProjectV2StatusUpdate(input: {
|
|
449
|
+
projectId: $projectId, body: $body, status: $status, startDate: $startDate, targetDate: $targetDate
|
|
450
|
+
}) {
|
|
451
|
+
statusUpdate { id body status createdAt }
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
`;
|
|
455
|
+
const result = await gql(mutation, {
|
|
456
|
+
projectId: project_id, body: body ?? "", status: status ?? null, startDate: start_date ?? null, targetDate: target_date ?? null,
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
460
|
+
success: true,
|
|
461
|
+
status_update: result.createProjectV2StatusUpdate?.statusUpdate,
|
|
462
|
+
}, null, 2) }],
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
content: [{ type: "text", text: `Unknown method: ${method}` }],
|
|
467
|
+
isError: true,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
catch (err) {
|
|
471
|
+
return handleGitHubError(err);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export declare function registerPullRequestRead(server: McpServer): void;
|
|
3
|
+
export declare function registerPullRequestCreate(server: McpServer): void;
|
|
4
|
+
export declare function registerPullRequestMerge(server: McpServer): void;
|