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
package/src/lib/env.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
|
|
3
|
+
import { ToolValidationError } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
export function getGithubToken(): string {
|
|
6
|
+
const token = process.env.GITHUB_TOKEN;
|
|
7
|
+
if (!token) {
|
|
8
|
+
throw new ToolValidationError(
|
|
9
|
+
"GITHUB_TOKEN environment variable is required. Set it via env or .env file.",
|
|
10
|
+
500
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return token;
|
|
15
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ZodError } from "zod";
|
|
2
|
+
|
|
3
|
+
export class ToolValidationError extends Error {
|
|
4
|
+
public readonly status: number;
|
|
5
|
+
|
|
6
|
+
public constructor(message: string, status = 400) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "ToolValidationError";
|
|
9
|
+
this.status = status;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type ErrorWithMetadata = Error & {
|
|
14
|
+
status?: number;
|
|
15
|
+
response?: {
|
|
16
|
+
headers?: Record<string, string | number | undefined>;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function isErrorWithMetadata(error: unknown): error is ErrorWithMetadata {
|
|
22
|
+
if (!(error instanceof Error)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildErrorPayload(error: unknown, detail: string): Record<string, unknown> {
|
|
30
|
+
if (error instanceof ZodError) {
|
|
31
|
+
return {
|
|
32
|
+
error: "Invalid parameters",
|
|
33
|
+
status: 400,
|
|
34
|
+
detail,
|
|
35
|
+
validation_issues: error.issues,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isErrorWithMetadata(error)) {
|
|
40
|
+
const payload: Record<string, unknown> = {
|
|
41
|
+
error: error.message,
|
|
42
|
+
status: error.status,
|
|
43
|
+
detail,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const headers = error.response?.headers;
|
|
47
|
+
const rateLimitRemaining = headers?.["x-ratelimit-remaining"];
|
|
48
|
+
const rateLimitReset = headers?.["x-ratelimit-reset"];
|
|
49
|
+
|
|
50
|
+
if (rateLimitRemaining !== undefined || rateLimitReset !== undefined) {
|
|
51
|
+
payload.rate_limit = {
|
|
52
|
+
remaining: rateLimitRemaining,
|
|
53
|
+
reset: rateLimitReset,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return payload;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
error: "Unknown error",
|
|
62
|
+
detail,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Octokit } from "@octokit/rest";
|
|
2
|
+
|
|
3
|
+
import { getGithubToken } from "./env.js";
|
|
4
|
+
import { SERVER_VERSION } from "./version.js";
|
|
5
|
+
|
|
6
|
+
let octokitInstance: Octokit | null = null;
|
|
7
|
+
|
|
8
|
+
export function getOctokit(): Octokit {
|
|
9
|
+
if (!octokitInstance) {
|
|
10
|
+
octokitInstance = new Octokit({
|
|
11
|
+
auth: getGithubToken(),
|
|
12
|
+
userAgent: `issue-scribe-mcp/${SERVER_VERSION}`,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return octokitInstance;
|
|
17
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const PaginationSchema = z.object({
|
|
4
|
+
page: z.number().int().min(1).optional(),
|
|
5
|
+
per_page: z.number().int().min(1).max(100).optional(),
|
|
6
|
+
fetch_all: z.boolean().optional(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export interface PaginationOptions {
|
|
10
|
+
page: number;
|
|
11
|
+
perPage: number;
|
|
12
|
+
fetchAll: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PaginationResult<T> {
|
|
16
|
+
items: T[];
|
|
17
|
+
page: number;
|
|
18
|
+
per_page: number;
|
|
19
|
+
fetch_all: boolean;
|
|
20
|
+
pages_fetched: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolvePagination(
|
|
24
|
+
pagination: { page?: number; per_page?: number; fetch_all?: boolean },
|
|
25
|
+
defaults?: Partial<PaginationOptions>
|
|
26
|
+
): PaginationOptions {
|
|
27
|
+
return {
|
|
28
|
+
page: pagination.page ?? defaults?.page ?? 1,
|
|
29
|
+
perPage: pagination.per_page ?? defaults?.perPage ?? 30,
|
|
30
|
+
fetchAll: pagination.fetch_all ?? defaults?.fetchAll ?? false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function collectPaginated<T>(
|
|
35
|
+
options: PaginationOptions,
|
|
36
|
+
fetchPage: (page: number, perPage: number) => Promise<T[]>
|
|
37
|
+
): Promise<PaginationResult<T>> {
|
|
38
|
+
if (!options.fetchAll) {
|
|
39
|
+
const items = await fetchPage(options.page, options.perPage);
|
|
40
|
+
return {
|
|
41
|
+
items,
|
|
42
|
+
page: options.page,
|
|
43
|
+
per_page: options.perPage,
|
|
44
|
+
fetch_all: false,
|
|
45
|
+
pages_fetched: 1,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const allItems: T[] = [];
|
|
50
|
+
let currentPage = options.page;
|
|
51
|
+
let pagesFetched = 0;
|
|
52
|
+
|
|
53
|
+
while (true) {
|
|
54
|
+
const pageItems = await fetchPage(currentPage, options.perPage);
|
|
55
|
+
allItems.push(...pageItems);
|
|
56
|
+
pagesFetched += 1;
|
|
57
|
+
|
|
58
|
+
if (pageItems.length < options.perPage) {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
currentPage += 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
items: allItems,
|
|
67
|
+
page: options.page,
|
|
68
|
+
per_page: options.perPage,
|
|
69
|
+
fetch_all: true,
|
|
70
|
+
pages_fetched: pagesFetched,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { buildErrorPayload } from "./errors.js";
|
|
5
|
+
|
|
6
|
+
export function success(data: unknown): CallToolResult {
|
|
7
|
+
return {
|
|
8
|
+
content: [
|
|
9
|
+
{
|
|
10
|
+
type: "text",
|
|
11
|
+
text: JSON.stringify(data, null, 2),
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function failure(error: unknown, detail: string): CallToolResult {
|
|
18
|
+
return {
|
|
19
|
+
content: [
|
|
20
|
+
{
|
|
21
|
+
type: "text",
|
|
22
|
+
text: JSON.stringify(buildErrorPayload(error, detail), null, 2),
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
isError: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function executeTool<T>(
|
|
30
|
+
rawArgs: unknown,
|
|
31
|
+
schema: z.ZodType<T>,
|
|
32
|
+
detail: string,
|
|
33
|
+
run: (args: T) => Promise<unknown>
|
|
34
|
+
): Promise<CallToolResult> {
|
|
35
|
+
try {
|
|
36
|
+
const parsedArgs = schema.parse(rawArgs);
|
|
37
|
+
const payload = await run(parsedArgs);
|
|
38
|
+
return success(payload);
|
|
39
|
+
} catch (error: unknown) {
|
|
40
|
+
return failure(error, detail);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ToolValidationError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export const CONFIRM_TOKEN_VALUE = "CONFIRM";
|
|
4
|
+
|
|
5
|
+
export function assertConfirmation(confirmToken: string | undefined, actionLabel: string): void {
|
|
6
|
+
if (confirmToken !== CONFIRM_TOKEN_VALUE) {
|
|
7
|
+
throw new ToolValidationError(
|
|
8
|
+
`${actionLabel} requires confirm_token=\"${CONFIRM_TOKEN_VALUE}\". Use dry_run=true to preview without executing.`,
|
|
9
|
+
400
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function assertExpectedValue(
|
|
15
|
+
expectedValue: string | undefined,
|
|
16
|
+
actualValue: string,
|
|
17
|
+
valueName: string
|
|
18
|
+
): void {
|
|
19
|
+
if (expectedValue && expectedValue !== actualValue) {
|
|
20
|
+
throw new ToolValidationError(
|
|
21
|
+
`${valueName} mismatch. expected=${expectedValue}, actual=${actualValue}`,
|
|
22
|
+
409
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface RepositorySearchQueryOptions {
|
|
2
|
+
owner: string;
|
|
3
|
+
repo: string;
|
|
4
|
+
kind: "issue" | "pr";
|
|
5
|
+
state?: "open" | "closed" | "all";
|
|
6
|
+
query?: string;
|
|
7
|
+
labels?: string[];
|
|
8
|
+
qualifiers?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildRepositorySearchQuery(options: RepositorySearchQueryOptions): string {
|
|
12
|
+
const parts: string[] = [];
|
|
13
|
+
|
|
14
|
+
if (options.query && options.query.trim()) {
|
|
15
|
+
parts.push(options.query.trim());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
parts.push(`repo:${options.owner}/${options.repo}`);
|
|
19
|
+
parts.push(`is:${options.kind}`);
|
|
20
|
+
|
|
21
|
+
if (options.state && options.state !== "all") {
|
|
22
|
+
parts.push(`is:${options.state}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const label of options.labels ?? []) {
|
|
26
|
+
parts.push(`label:\"${label}\"`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const qualifier of options.qualifiers ?? []) {
|
|
30
|
+
if (qualifier.trim()) {
|
|
31
|
+
parts.push(qualifier.trim());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return parts.join(" ");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeSearchSort(sort: string | undefined): string | undefined {
|
|
39
|
+
if (!sort || sort === "best-match") {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (sort === "popularity") {
|
|
44
|
+
return "comments";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (sort === "long-running") {
|
|
48
|
+
return "updated";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return sort;
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const packageJsonPath = resolve(currentDir, "../../package.json");
|
|
7
|
+
|
|
8
|
+
let packageVersion = "0.0.0";
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
|
12
|
+
version?: string;
|
|
13
|
+
};
|
|
14
|
+
packageVersion = packageJson.version ?? packageVersion;
|
|
15
|
+
} catch {
|
|
16
|
+
packageVersion = "0.0.0";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const SERVER_VERSION = packageVersion;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { getOctokit } from "../lib/octokit.js";
|
|
4
|
+
import { collectPaginated, PaginationSchema, resolvePagination } from "../lib/pagination.js";
|
|
5
|
+
import { executeTool } from "../lib/response.js";
|
|
6
|
+
import { assertConfirmation, assertExpectedValue } from "../lib/safety.js";
|
|
7
|
+
import type { ToolRegistration } from "./types.js";
|
|
8
|
+
|
|
9
|
+
const ListBranchesSchema = z.object({
|
|
10
|
+
owner: z.string(),
|
|
11
|
+
repo: z.string(),
|
|
12
|
+
protected: z.boolean().optional(),
|
|
13
|
+
}).merge(PaginationSchema);
|
|
14
|
+
|
|
15
|
+
const CreateBranchSchema = z.object({
|
|
16
|
+
owner: z.string(),
|
|
17
|
+
repo: z.string(),
|
|
18
|
+
branch: z.string(),
|
|
19
|
+
ref: z.string(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const DeleteBranchSchema = z.object({
|
|
23
|
+
owner: z.string(),
|
|
24
|
+
repo: z.string(),
|
|
25
|
+
branch: z.string(),
|
|
26
|
+
dry_run: z.boolean().optional(),
|
|
27
|
+
confirm_token: z.string().optional(),
|
|
28
|
+
expected_sha: z.string().optional(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const CompareBranchesSchema = z.object({
|
|
32
|
+
owner: z.string(),
|
|
33
|
+
repo: z.string(),
|
|
34
|
+
base: z.string(),
|
|
35
|
+
head: z.string(),
|
|
36
|
+
max_commits: z.number().int().min(1).max(500).optional(),
|
|
37
|
+
max_files: z.number().int().min(1).max(500).optional(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const branchTools: ToolRegistration[] = [
|
|
41
|
+
{
|
|
42
|
+
definition: {
|
|
43
|
+
name: "github_list_branches",
|
|
44
|
+
description: "List repository branches with pagination",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
owner: { type: "string", description: "Repository owner" },
|
|
49
|
+
repo: { type: "string", description: "Repository name" },
|
|
50
|
+
protected: { type: "boolean", description: "Filter by protected status (optional)" },
|
|
51
|
+
page: { type: "number", description: "Page number (optional, default: 1)" },
|
|
52
|
+
per_page: { type: "number", description: "Results per page, max 100 (optional, default: 30)" },
|
|
53
|
+
fetch_all: { type: "boolean", description: "Fetch all pages (optional, default: false)" },
|
|
54
|
+
},
|
|
55
|
+
required: ["owner", "repo"],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
handler: async (rawArgs) => executeTool(
|
|
59
|
+
rawArgs,
|
|
60
|
+
ListBranchesSchema,
|
|
61
|
+
"Failed to list branches",
|
|
62
|
+
async (args) => {
|
|
63
|
+
const pagination = resolvePagination(args, {
|
|
64
|
+
page: 1,
|
|
65
|
+
perPage: 30,
|
|
66
|
+
fetchAll: false,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const branches = await collectPaginated(pagination, async (page, perPage) => {
|
|
70
|
+
const response = await getOctokit().rest.repos.listBranches({
|
|
71
|
+
owner: args.owner,
|
|
72
|
+
repo: args.repo,
|
|
73
|
+
protected: args.protected,
|
|
74
|
+
page,
|
|
75
|
+
per_page: perPage,
|
|
76
|
+
});
|
|
77
|
+
return response.data;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
success: true,
|
|
82
|
+
count: branches.items.length,
|
|
83
|
+
branches: branches.items.map((branch) => ({
|
|
84
|
+
name: branch.name,
|
|
85
|
+
commit: {
|
|
86
|
+
sha: branch.commit.sha,
|
|
87
|
+
url: branch.commit.url,
|
|
88
|
+
},
|
|
89
|
+
protected: branch.protected,
|
|
90
|
+
})),
|
|
91
|
+
pagination: {
|
|
92
|
+
page: branches.page,
|
|
93
|
+
per_page: branches.per_page,
|
|
94
|
+
fetch_all: branches.fetch_all,
|
|
95
|
+
pages_fetched: branches.pages_fetched,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
definition: {
|
|
103
|
+
name: "github_create_branch",
|
|
104
|
+
description: "Create a new branch from an existing branch or commit SHA",
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
owner: { type: "string", description: "Repository owner" },
|
|
109
|
+
repo: { type: "string", description: "Repository name" },
|
|
110
|
+
branch: { type: "string", description: "New branch name" },
|
|
111
|
+
ref: { type: "string", description: "Source branch name or commit SHA" },
|
|
112
|
+
},
|
|
113
|
+
required: ["owner", "repo", "branch", "ref"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
handler: async (rawArgs) => executeTool(
|
|
117
|
+
rawArgs,
|
|
118
|
+
CreateBranchSchema,
|
|
119
|
+
"Failed to create branch",
|
|
120
|
+
async (args) => {
|
|
121
|
+
let sha: string;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const branchRef = await getOctokit().rest.git.getRef({
|
|
125
|
+
owner: args.owner,
|
|
126
|
+
repo: args.repo,
|
|
127
|
+
ref: `heads/${args.ref}`,
|
|
128
|
+
});
|
|
129
|
+
sha = branchRef.data.object.sha;
|
|
130
|
+
} catch {
|
|
131
|
+
const commit = await getOctokit().rest.git.getCommit({
|
|
132
|
+
owner: args.owner,
|
|
133
|
+
repo: args.repo,
|
|
134
|
+
commit_sha: args.ref,
|
|
135
|
+
});
|
|
136
|
+
sha = commit.data.sha;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const newBranch = await getOctokit().rest.git.createRef({
|
|
140
|
+
owner: args.owner,
|
|
141
|
+
repo: args.repo,
|
|
142
|
+
ref: `refs/heads/${args.branch}`,
|
|
143
|
+
sha,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
branch: {
|
|
149
|
+
name: args.branch,
|
|
150
|
+
ref: newBranch.data.ref,
|
|
151
|
+
sha: newBranch.data.object.sha,
|
|
152
|
+
url: newBranch.data.url,
|
|
153
|
+
},
|
|
154
|
+
message: `Branch \"${args.branch}\" created successfully from \"${args.ref}\"`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
definition: {
|
|
161
|
+
name: "github_delete_branch",
|
|
162
|
+
description: "Delete a branch with dry-run, expected SHA check, and confirmation safeguards",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: {
|
|
166
|
+
owner: { type: "string", description: "Repository owner" },
|
|
167
|
+
repo: { type: "string", description: "Repository name" },
|
|
168
|
+
branch: { type: "string", description: "Branch name to delete" },
|
|
169
|
+
dry_run: { type: "boolean", description: "Preview deletion without executing (optional, default: false)" },
|
|
170
|
+
expected_sha: { type: "string", description: "Optional guard: branch HEAD SHA must match this value" },
|
|
171
|
+
confirm_token: { type: "string", description: "Must be \"CONFIRM\" to execute delete when dry_run=false" },
|
|
172
|
+
},
|
|
173
|
+
required: ["owner", "repo", "branch"],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
handler: async (rawArgs) => executeTool(
|
|
177
|
+
rawArgs,
|
|
178
|
+
DeleteBranchSchema,
|
|
179
|
+
"Failed to delete branch",
|
|
180
|
+
async (args) => {
|
|
181
|
+
const refData = await getOctokit().rest.git.getRef({
|
|
182
|
+
owner: args.owner,
|
|
183
|
+
repo: args.repo,
|
|
184
|
+
ref: `heads/${args.branch}`,
|
|
185
|
+
});
|
|
186
|
+
const currentSha = refData.data.object.sha;
|
|
187
|
+
|
|
188
|
+
assertExpectedValue(args.expected_sha, currentSha, "branch head sha");
|
|
189
|
+
|
|
190
|
+
if (args.dry_run ?? false) {
|
|
191
|
+
return {
|
|
192
|
+
success: true,
|
|
193
|
+
dry_run: true,
|
|
194
|
+
action: "delete_branch",
|
|
195
|
+
target: {
|
|
196
|
+
branch: args.branch,
|
|
197
|
+
current_sha: currentSha,
|
|
198
|
+
},
|
|
199
|
+
message: "Dry run only. No deletion executed.",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
assertConfirmation(args.confirm_token, "Branch deletion");
|
|
204
|
+
|
|
205
|
+
await getOctokit().rest.git.deleteRef({
|
|
206
|
+
owner: args.owner,
|
|
207
|
+
repo: args.repo,
|
|
208
|
+
ref: `heads/${args.branch}`,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
success: true,
|
|
213
|
+
dry_run: false,
|
|
214
|
+
message: `Branch \"${args.branch}\" deleted successfully from ${args.owner}/${args.repo}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
definition: {
|
|
221
|
+
name: "github_compare_branches",
|
|
222
|
+
description: "Compare two branches and return commit/file differences",
|
|
223
|
+
inputSchema: {
|
|
224
|
+
type: "object",
|
|
225
|
+
properties: {
|
|
226
|
+
owner: { type: "string", description: "Repository owner" },
|
|
227
|
+
repo: { type: "string", description: "Repository name" },
|
|
228
|
+
base: { type: "string", description: "Base branch" },
|
|
229
|
+
head: { type: "string", description: "Head branch to compare" },
|
|
230
|
+
max_commits: { type: "number", description: "Max commits to include in response (optional, default: 250)" },
|
|
231
|
+
max_files: { type: "number", description: "Max files to include in response (optional, default: 250)" },
|
|
232
|
+
},
|
|
233
|
+
required: ["owner", "repo", "base", "head"],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
handler: async (rawArgs) => executeTool(
|
|
237
|
+
rawArgs,
|
|
238
|
+
CompareBranchesSchema,
|
|
239
|
+
"Failed to compare branches",
|
|
240
|
+
async (args) => {
|
|
241
|
+
const comparison = await getOctokit().rest.repos.compareCommits({
|
|
242
|
+
owner: args.owner,
|
|
243
|
+
repo: args.repo,
|
|
244
|
+
base: args.base,
|
|
245
|
+
head: args.head,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const maxCommits = args.max_commits ?? 250;
|
|
249
|
+
const maxFiles = args.max_files ?? 250;
|
|
250
|
+
const commits = comparison.data.commits.slice(0, maxCommits);
|
|
251
|
+
const files = (comparison.data.files ?? []).slice(0, maxFiles);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
success: true,
|
|
255
|
+
comparison: {
|
|
256
|
+
status: comparison.data.status,
|
|
257
|
+
ahead_by: comparison.data.ahead_by,
|
|
258
|
+
behind_by: comparison.data.behind_by,
|
|
259
|
+
total_commits: comparison.data.total_commits,
|
|
260
|
+
base_commit: {
|
|
261
|
+
sha: comparison.data.base_commit.sha,
|
|
262
|
+
message: comparison.data.base_commit.commit.message,
|
|
263
|
+
},
|
|
264
|
+
commits: commits.map((commit) => ({
|
|
265
|
+
sha: commit.sha,
|
|
266
|
+
message: commit.commit.message,
|
|
267
|
+
author: commit.commit.author?.name,
|
|
268
|
+
date: commit.commit.author?.date,
|
|
269
|
+
})),
|
|
270
|
+
files: files.map((file) => ({
|
|
271
|
+
filename: file.filename,
|
|
272
|
+
status: file.status,
|
|
273
|
+
additions: file.additions,
|
|
274
|
+
deletions: file.deletions,
|
|
275
|
+
changes: file.changes,
|
|
276
|
+
})),
|
|
277
|
+
truncated: {
|
|
278
|
+
commits: comparison.data.commits.length > maxCommits,
|
|
279
|
+
files: (comparison.data.files?.length ?? 0) > maxFiles,
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
message: `Comparing ${args.base}...${args.head}: ${comparison.data.ahead_by} commits ahead, ${comparison.data.behind_by} commits behind`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
),
|
|
286
|
+
},
|
|
287
|
+
];
|