issue-scribe-mcp 1.2.0 → 1.3.0
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/.github/workflows/ci.yml +29 -0
- package/README.md +110 -5
- package/README_EN.md +108 -3
- package/dist/index.js +7 -1689
- package/dist/lib/env.js +9 -0
- package/dist/lib/errors.js +46 -0
- package/dist/lib/octokit.js +13 -0
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/response.js +32 -0
- package/dist/lib/safety.js +12 -0
- package/dist/lib/search.js +32 -0
- package/dist/lib/version.js +14 -0
- package/dist/tools/branches.js +249 -0
- package/dist/tools/comments.js +233 -0
- package/dist/tools/index.js +29 -0
- package/dist/tools/issues.js +393 -0
- package/dist/tools/labels.js +204 -0
- package/dist/tools/pull-requests.js +724 -0
- package/dist/tools/types.js +1 -0
- package/package.json +3 -3
- package/src/index.ts +7 -1869
- package/src/lib/env.ts +15 -0
- package/src/lib/errors.ts +64 -0
- package/src/lib/octokit.ts +17 -0
- package/src/lib/pagination.ts +72 -0
- package/src/lib/response.ts +42 -0
- package/src/lib/safety.ts +25 -0
- package/src/lib/search.ts +52 -0
- package/src/lib/version.ts +19 -0
- package/src/tools/branches.ts +287 -0
- package/src/tools/comments.ts +272 -0
- package/src/tools/index.ts +39 -0
- package/src/tools/issues.ts +452 -0
- package/src/tools/labels.ts +239 -0
- package/src/tools/pull-requests.ts +838 -0
- package/src/tools/types.ts +18 -0
- package/test/safety.test.mjs +28 -0
- package/test/search.test.mjs +28 -0
- package/test/version-and-metadata.test.mjs +32 -0
- package/test-local.sh +1 -1
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { getOctokit } from "../lib/octokit.js";
|
|
4
|
+
import { executeTool } from "../lib/response.js";
|
|
5
|
+
import { assertConfirmation } from "../lib/safety.js";
|
|
6
|
+
import type { ToolRegistration } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const REACTION_MAP: Record<string, string> = {
|
|
9
|
+
thumbs_up: "+1",
|
|
10
|
+
thumbs_down: "-1",
|
|
11
|
+
laugh: "laugh",
|
|
12
|
+
confused: "confused",
|
|
13
|
+
heart: "heart",
|
|
14
|
+
hooray: "hooray",
|
|
15
|
+
rocket: "rocket",
|
|
16
|
+
eyes: "eyes",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const AddCommentSchema = z.object({
|
|
20
|
+
owner: z.string(),
|
|
21
|
+
repo: z.string(),
|
|
22
|
+
issue_number: z.number(),
|
|
23
|
+
body: z.string(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const UpdateCommentSchema = z.object({
|
|
27
|
+
owner: z.string(),
|
|
28
|
+
repo: z.string(),
|
|
29
|
+
comment_id: z.number(),
|
|
30
|
+
body: z.string(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const DeleteCommentSchema = z.object({
|
|
34
|
+
owner: z.string(),
|
|
35
|
+
repo: z.string(),
|
|
36
|
+
comment_id: z.number(),
|
|
37
|
+
dry_run: z.boolean().optional(),
|
|
38
|
+
confirm_token: z.string().optional(),
|
|
39
|
+
expected_body_substring: z.string().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const AddReactionSchema = z.object({
|
|
43
|
+
owner: z.string(),
|
|
44
|
+
repo: z.string(),
|
|
45
|
+
comment_id: z.number().optional(),
|
|
46
|
+
issue_number: z.number().optional(),
|
|
47
|
+
reaction: z.enum(["thumbs_up", "thumbs_down", "laugh", "confused", "heart", "hooray", "rocket", "eyes"]),
|
|
48
|
+
}).refine((data) => Boolean(data.comment_id) !== Boolean(data.issue_number), {
|
|
49
|
+
message: "Provide exactly one of comment_id or issue_number",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const commentTools: ToolRegistration[] = [
|
|
53
|
+
{
|
|
54
|
+
definition: {
|
|
55
|
+
name: "github_add_comment",
|
|
56
|
+
description: "Add a comment to a GitHub issue or pull request",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
owner: { type: "string", description: "Repository owner" },
|
|
61
|
+
repo: { type: "string", description: "Repository name" },
|
|
62
|
+
issue_number: { type: "number", description: "Issue or PR number" },
|
|
63
|
+
body: { type: "string", description: "Comment body" },
|
|
64
|
+
},
|
|
65
|
+
required: ["owner", "repo", "issue_number", "body"],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
handler: async (rawArgs) => executeTool(
|
|
69
|
+
rawArgs,
|
|
70
|
+
AddCommentSchema,
|
|
71
|
+
"Failed to add comment",
|
|
72
|
+
async (args) => {
|
|
73
|
+
const comment = await getOctokit().rest.issues.createComment({
|
|
74
|
+
owner: args.owner,
|
|
75
|
+
repo: args.repo,
|
|
76
|
+
issue_number: args.issue_number,
|
|
77
|
+
body: args.body,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
success: true,
|
|
82
|
+
comment: {
|
|
83
|
+
id: comment.data.id,
|
|
84
|
+
body: comment.data.body,
|
|
85
|
+
user: comment.data.user?.login,
|
|
86
|
+
html_url: comment.data.html_url,
|
|
87
|
+
created_at: comment.data.created_at,
|
|
88
|
+
},
|
|
89
|
+
message: `Comment added successfully to issue/PR #${args.issue_number}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
definition: {
|
|
96
|
+
name: "github_update_comment",
|
|
97
|
+
description: "Update an existing GitHub issue/PR comment",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
owner: { type: "string", description: "Repository owner" },
|
|
102
|
+
repo: { type: "string", description: "Repository name" },
|
|
103
|
+
comment_id: { type: "number", description: "Comment ID" },
|
|
104
|
+
body: { type: "string", description: "Updated comment body" },
|
|
105
|
+
},
|
|
106
|
+
required: ["owner", "repo", "comment_id", "body"],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
handler: async (rawArgs) => executeTool(
|
|
110
|
+
rawArgs,
|
|
111
|
+
UpdateCommentSchema,
|
|
112
|
+
"Failed to update comment",
|
|
113
|
+
async (args) => {
|
|
114
|
+
const comment = await getOctokit().rest.issues.updateComment({
|
|
115
|
+
owner: args.owner,
|
|
116
|
+
repo: args.repo,
|
|
117
|
+
comment_id: args.comment_id,
|
|
118
|
+
body: args.body,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
comment: {
|
|
124
|
+
id: comment.data.id,
|
|
125
|
+
body: comment.data.body,
|
|
126
|
+
user: comment.data.user?.login,
|
|
127
|
+
html_url: comment.data.html_url,
|
|
128
|
+
updated_at: comment.data.updated_at,
|
|
129
|
+
},
|
|
130
|
+
message: `Comment #${args.comment_id} updated successfully`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
definition: {
|
|
137
|
+
name: "github_delete_comment",
|
|
138
|
+
description: "Delete a comment with dry-run and confirmation safeguards",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
owner: { type: "string", description: "Repository owner" },
|
|
143
|
+
repo: { type: "string", description: "Repository name" },
|
|
144
|
+
comment_id: { type: "number", description: "Comment ID to delete" },
|
|
145
|
+
dry_run: { type: "boolean", description: "Preview deletion without executing (optional, default: false)" },
|
|
146
|
+
confirm_token: { type: "string", description: "Must be \"CONFIRM\" to execute delete when dry_run=false" },
|
|
147
|
+
expected_body_substring: { type: "string", description: "Optional guard: comment must include this substring" },
|
|
148
|
+
},
|
|
149
|
+
required: ["owner", "repo", "comment_id"],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
handler: async (rawArgs) => executeTool(
|
|
153
|
+
rawArgs,
|
|
154
|
+
DeleteCommentSchema,
|
|
155
|
+
"Failed to delete comment",
|
|
156
|
+
async (args) => {
|
|
157
|
+
const comment = await getOctokit().rest.issues.getComment({
|
|
158
|
+
owner: args.owner,
|
|
159
|
+
repo: args.repo,
|
|
160
|
+
comment_id: args.comment_id,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (args.expected_body_substring && !comment.data.body?.includes(args.expected_body_substring)) {
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
dry_run: true,
|
|
167
|
+
message: "Comment body guard check failed. Deletion skipped.",
|
|
168
|
+
expected_body_substring: args.expected_body_substring,
|
|
169
|
+
current_comment_preview: comment.data.body?.slice(0, 200) ?? "",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (args.dry_run ?? false) {
|
|
174
|
+
return {
|
|
175
|
+
success: true,
|
|
176
|
+
dry_run: true,
|
|
177
|
+
action: "delete_comment",
|
|
178
|
+
target: {
|
|
179
|
+
comment_id: args.comment_id,
|
|
180
|
+
html_url: comment.data.html_url,
|
|
181
|
+
body_preview: comment.data.body?.slice(0, 200) ?? "",
|
|
182
|
+
},
|
|
183
|
+
message: "Dry run only. No deletion executed.",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
assertConfirmation(args.confirm_token, "Comment deletion");
|
|
188
|
+
|
|
189
|
+
await getOctokit().rest.issues.deleteComment({
|
|
190
|
+
owner: args.owner,
|
|
191
|
+
repo: args.repo,
|
|
192
|
+
comment_id: args.comment_id,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
dry_run: false,
|
|
198
|
+
message: `Comment #${args.comment_id} deleted successfully`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
),
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
definition: {
|
|
205
|
+
name: "github_add_reaction",
|
|
206
|
+
description: "Add a reaction to an issue/PR or to a specific comment",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
owner: { type: "string", description: "Repository owner" },
|
|
211
|
+
repo: { type: "string", description: "Repository name" },
|
|
212
|
+
comment_id: { type: "number", description: "Comment ID target (optional)" },
|
|
213
|
+
issue_number: { type: "number", description: "Issue/PR number target (optional)" },
|
|
214
|
+
reaction: {
|
|
215
|
+
type: "string",
|
|
216
|
+
enum: ["thumbs_up", "thumbs_down", "laugh", "confused", "heart", "hooray", "rocket", "eyes"],
|
|
217
|
+
description: "Reaction type",
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
required: ["owner", "repo", "reaction"],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
handler: async (rawArgs) => executeTool(
|
|
224
|
+
rawArgs,
|
|
225
|
+
AddReactionSchema,
|
|
226
|
+
"Failed to add reaction",
|
|
227
|
+
async (args) => {
|
|
228
|
+
const reactionContent = REACTION_MAP[args.reaction] ?? args.reaction;
|
|
229
|
+
|
|
230
|
+
if (args.comment_id) {
|
|
231
|
+
const reaction = await getOctokit().rest.reactions.createForIssueComment({
|
|
232
|
+
owner: args.owner,
|
|
233
|
+
repo: args.repo,
|
|
234
|
+
comment_id: args.comment_id,
|
|
235
|
+
content: reactionContent as "+1" | "-1" | "laugh" | "confused" | "heart" | "hooray" | "rocket" | "eyes",
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
success: true,
|
|
240
|
+
target: `comment #${args.comment_id}`,
|
|
241
|
+
reaction: {
|
|
242
|
+
id: reaction.data.id,
|
|
243
|
+
content: reaction.data.content,
|
|
244
|
+
user: reaction.data.user?.login,
|
|
245
|
+
created_at: reaction.data.created_at,
|
|
246
|
+
},
|
|
247
|
+
message: `Reaction \"${args.reaction}\" added to comment #${args.comment_id}`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const reaction = await getOctokit().rest.reactions.createForIssue({
|
|
252
|
+
owner: args.owner,
|
|
253
|
+
repo: args.repo,
|
|
254
|
+
issue_number: args.issue_number!,
|
|
255
|
+
content: reactionContent as "+1" | "-1" | "laugh" | "confused" | "heart" | "hooray" | "rocket" | "eyes",
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
success: true,
|
|
260
|
+
target: `issue/PR #${args.issue_number}`,
|
|
261
|
+
reaction: {
|
|
262
|
+
id: reaction.data.id,
|
|
263
|
+
content: reaction.data.content,
|
|
264
|
+
user: reaction.data.user?.login,
|
|
265
|
+
created_at: reaction.data.created_at,
|
|
266
|
+
},
|
|
267
|
+
message: `Reaction \"${args.reaction}\" added to issue/PR #${args.issue_number}`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
),
|
|
271
|
+
},
|
|
272
|
+
];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
|
|
3
|
+
import { ToolValidationError } from "../lib/errors.js";
|
|
4
|
+
import { failure } from "../lib/response.js";
|
|
5
|
+
import { branchTools } from "./branches.js";
|
|
6
|
+
import { commentTools } from "./comments.js";
|
|
7
|
+
import { issueTools } from "./issues.js";
|
|
8
|
+
import { labelTools } from "./labels.js";
|
|
9
|
+
import { pullRequestTools } from "./pull-requests.js";
|
|
10
|
+
import type { ToolHandler, ToolRegistration } from "./types.js";
|
|
11
|
+
|
|
12
|
+
const toolRegistry: ToolRegistration[] = [
|
|
13
|
+
...issueTools,
|
|
14
|
+
...pullRequestTools,
|
|
15
|
+
...commentTools,
|
|
16
|
+
...labelTools,
|
|
17
|
+
...branchTools,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const toolMap = new Map<string, ToolHandler>(
|
|
21
|
+
toolRegistry.map((tool) => [tool.definition.name, tool.handler])
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const toolDefinitions = toolRegistry.map((tool) => tool.definition);
|
|
25
|
+
export const toolCount = toolDefinitions.length;
|
|
26
|
+
|
|
27
|
+
export async function dispatchTool(name: string, args: unknown): Promise<CallToolResult> {
|
|
28
|
+
const handler = toolMap.get(name);
|
|
29
|
+
|
|
30
|
+
if (!handler) {
|
|
31
|
+
return failure(new ToolValidationError(`Unknown tool: ${name}`, 404), `Unknown tool requested: ${name}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return await handler(args);
|
|
36
|
+
} catch (error: unknown) {
|
|
37
|
+
return failure(error, `Unexpected error while executing tool: ${name}`);
|
|
38
|
+
}
|
|
39
|
+
}
|