@inkeep/agents-work-apps 0.0.0-dev-20260203033642
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/LICENSE.md +49 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +3 -0
- package/dist/db/runDbClient.d.ts +6 -0
- package/dist/db/runDbClient.js +9 -0
- package/dist/env.d.ts +47 -0
- package/dist/env.js +48 -0
- package/dist/github/config.d.ts +22 -0
- package/dist/github/config.js +79 -0
- package/dist/github/index.d.ts +13 -0
- package/dist/github/index.js +23 -0
- package/dist/github/installation.d.ts +66 -0
- package/dist/github/installation.js +293 -0
- package/dist/github/jwks.d.ts +20 -0
- package/dist/github/jwks.js +85 -0
- package/dist/github/mcp/auth.d.ts +10 -0
- package/dist/github/mcp/auth.js +43 -0
- package/dist/github/mcp/index.d.ts +11 -0
- package/dist/github/mcp/index.js +670 -0
- package/dist/github/mcp/schemas.d.ts +87 -0
- package/dist/github/mcp/schemas.js +69 -0
- package/dist/github/mcp/utils.d.ts +228 -0
- package/dist/github/mcp/utils.js +464 -0
- package/dist/github/oidcToken.d.ts +22 -0
- package/dist/github/oidcToken.js +140 -0
- package/dist/github/routes/setup.d.ts +7 -0
- package/dist/github/routes/setup.js +217 -0
- package/dist/github/routes/tokenExchange.d.ts +7 -0
- package/dist/github/routes/tokenExchange.js +233 -0
- package/dist/github/routes/webhooks.d.ts +12 -0
- package/dist/github/routes/webhooks.js +278 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +3 -0
- package/package.json +65 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import runDbClient_default from "../../db/runDbClient.js";
|
|
2
|
+
import { githubMcpAuth } from "./auth.js";
|
|
3
|
+
import { commitFileChanges, commitNewFile, fetchPrFileDiffs, fetchPrFiles, fetchPrInfo, formatFileDiff, generatePrMarkdown, getGitHubClientFromRepo, visualizeUpdateOperations } from "./utils.js";
|
|
4
|
+
import { getMcpToolRepositoryAccessWithDetails } from "@inkeep/agents-core";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
8
|
+
import { toFetchResponse, toReqRes } from "fetch-to-node";
|
|
9
|
+
import { z } from "zod/v3";
|
|
10
|
+
|
|
11
|
+
//#region src/github/mcp/index.ts
|
|
12
|
+
const updateOperationSchema = z.object({
|
|
13
|
+
operation: z.enum([
|
|
14
|
+
"replace_lines",
|
|
15
|
+
"insert_after",
|
|
16
|
+
"insert_before",
|
|
17
|
+
"delete_lines"
|
|
18
|
+
]).describe("Operation type"),
|
|
19
|
+
lineStart: z.number().min(1).describe("Starting line number (1-indexed)"),
|
|
20
|
+
lineEnd: z.number().min(1).optional().describe("Ending line number (1-indexed, inclusive). Required for 'replace_lines' and 'delete_lines'"),
|
|
21
|
+
content: z.string().optional().describe("New content to insert/replace"),
|
|
22
|
+
reason: z.string().describe("Explanation of why this change is needed")
|
|
23
|
+
}).describe("Update operation to apply to a file");
|
|
24
|
+
const updateOperationsSchema = z.array(updateOperationSchema).describe("List of update operations to apply to a file");
|
|
25
|
+
const getAvailableRepositoryString = (repositoryAccess) => {
|
|
26
|
+
if (repositoryAccess.length === 0) return "No repositories available";
|
|
27
|
+
return `Available repositories: ${repositoryAccess.map((r) => `"${r.repositoryFullName}"`).join(", ")}`;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Creates and configures an MCP server for the given context
|
|
31
|
+
*/
|
|
32
|
+
const getServer = async (toolId) => {
|
|
33
|
+
const repositoryAccess = await getMcpToolRepositoryAccessWithDetails(runDbClient_default)(toolId);
|
|
34
|
+
const installationIdMap = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const repo of repositoryAccess) installationIdMap.set(repo.repositoryFullName, repo.installationId);
|
|
36
|
+
if (repositoryAccess.length === 0) throw new Error("No repository access found for tool");
|
|
37
|
+
const server = new McpServer({
|
|
38
|
+
name: "inkeep-github-mcp-server",
|
|
39
|
+
version: "1.0.0",
|
|
40
|
+
description: "A GitHub MCP server with access to the following repositories:\n" + repositoryAccess.map((r) => `• ${r.repositoryFullName}`).join("\n")
|
|
41
|
+
}, { capabilities: { logging: {} } });
|
|
42
|
+
server.tool("search-files-in-repo", `Find files in a repository using GitHub's search API. ${getAvailableRepositoryString(repositoryAccess)}`, {
|
|
43
|
+
owner: z.string().describe("The owner of the repository"),
|
|
44
|
+
repo: z.string().describe("The name of the repository"),
|
|
45
|
+
query: z.string().describe("The query to search for"),
|
|
46
|
+
path: z.string().optional().describe("The path to the files to search in relative to the root of the repository"),
|
|
47
|
+
limit: z.number().default(30).describe("The maximum number of files to return")
|
|
48
|
+
}, async ({ owner, repo, query, path, limit }) => {
|
|
49
|
+
try {
|
|
50
|
+
const repoFullName = `${owner}/${repo}`;
|
|
51
|
+
let githubClient;
|
|
52
|
+
try {
|
|
53
|
+
githubClient = getGitHubClientFromRepo(owner, repo, installationIdMap);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return {
|
|
56
|
+
content: [{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: `Error accessing GitHub: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
59
|
+
}],
|
|
60
|
+
isError: true
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
let searchQuery = `${query} repo:${repoFullName}`;
|
|
64
|
+
if (path) searchQuery += ` path:${path}`;
|
|
65
|
+
const files = (await githubClient.rest.search.code({
|
|
66
|
+
q: searchQuery,
|
|
67
|
+
per_page: Math.min(limit, 100)
|
|
68
|
+
})).data.items.map((item) => ({
|
|
69
|
+
name: item.name,
|
|
70
|
+
path: item.path,
|
|
71
|
+
url: item.html_url,
|
|
72
|
+
repository: repoFullName,
|
|
73
|
+
sha: item.sha,
|
|
74
|
+
score: item.score
|
|
75
|
+
}));
|
|
76
|
+
return { content: [{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `Found ${files.length} files matching "${query}" in ${repoFullName}:\n\n${files.map((file) => `• ${file.name} (${file.path})\n URL: ${file.url}\n Score: ${file.score}`).join("\n\n")}`
|
|
79
|
+
}] };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
content: [{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: `Error searching files: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
85
|
+
}],
|
|
86
|
+
isError: true
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
server.tool("get-file-content", `Get the content of a specific file from a repository. Returns a mapping of line number to line content. ${getAvailableRepositoryString(repositoryAccess)}`, {
|
|
91
|
+
owner: z.string().describe("Repository owner name"),
|
|
92
|
+
repo: z.string().describe("Repository name"),
|
|
93
|
+
file_path: z.string().describe("Path to the file. the path is relative to the root of the repository")
|
|
94
|
+
}, async ({ owner, repo, file_path }) => {
|
|
95
|
+
try {
|
|
96
|
+
let githubClient;
|
|
97
|
+
try {
|
|
98
|
+
githubClient = getGitHubClientFromRepo(owner, repo, installationIdMap);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
content: [{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: `Error accessing GitHub: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
104
|
+
}],
|
|
105
|
+
isError: true
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const response = await githubClient.rest.repos.getContent({
|
|
109
|
+
owner,
|
|
110
|
+
repo,
|
|
111
|
+
path: file_path
|
|
112
|
+
});
|
|
113
|
+
if ("content" in response.data && !Array.isArray(response.data)) {
|
|
114
|
+
const fileData = response.data;
|
|
115
|
+
const output_mapping = Buffer.from(fileData.content, "base64").toString("utf-8").split("\n").map((line, index) => ({
|
|
116
|
+
line: index + 1,
|
|
117
|
+
content: line
|
|
118
|
+
}));
|
|
119
|
+
return { content: [{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: JSON.stringify(output_mapping)
|
|
122
|
+
}] };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
content: [{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: `The path "${file_path}" is not a file or could not be retrieved as file content.`
|
|
128
|
+
}],
|
|
129
|
+
isError: true
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return {
|
|
133
|
+
content: [{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: `Error getting file content: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
136
|
+
}],
|
|
137
|
+
isError: true
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
server.tool("get-pull-request-details", `Get the details of a pull request from a repository including the pull request details, the commits, and the files that were changed. ${getAvailableRepositoryString(repositoryAccess)}`, {
|
|
142
|
+
owner: z.string().describe("Repository owner name"),
|
|
143
|
+
repo: z.string().describe("Repository name"),
|
|
144
|
+
pull_request_number: z.number().describe("Pull request number")
|
|
145
|
+
}, async ({ owner, repo, pull_request_number }) => {
|
|
146
|
+
try {
|
|
147
|
+
let githubClient;
|
|
148
|
+
try {
|
|
149
|
+
githubClient = getGitHubClientFromRepo(owner, repo, installationIdMap);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
content: [{
|
|
153
|
+
type: "text",
|
|
154
|
+
text: `Error accessing GitHub: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
155
|
+
}],
|
|
156
|
+
isError: true
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return { content: [{
|
|
160
|
+
type: "text",
|
|
161
|
+
text: generatePrMarkdown(await fetchPrInfo(githubClient, owner, repo, pull_request_number), await fetchPrFileDiffs(githubClient, owner, repo, pull_request_number), owner, repo)
|
|
162
|
+
}] };
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error instanceof Error && "status" in error) {
|
|
165
|
+
const apiError = error;
|
|
166
|
+
if (apiError.status === 404) return {
|
|
167
|
+
content: [{
|
|
168
|
+
type: "text",
|
|
169
|
+
text: `Pull request #${pull_request_number} not found in ${owner}/${repo}. Please check the repository exists and the pull request number is correct.`
|
|
170
|
+
}],
|
|
171
|
+
isError: true
|
|
172
|
+
};
|
|
173
|
+
if (apiError.status === 403) return {
|
|
174
|
+
content: [{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: `Access denied to pull request #${pull_request_number} in ${owner}/${repo}. Your GitHub App may not have sufficient permissions.`
|
|
177
|
+
}],
|
|
178
|
+
isError: true
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
content: [{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: `Error getting pull request: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
185
|
+
}],
|
|
186
|
+
isError: true
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
server.tool("get-file-patches", `Get the patch/diff text for specific files in a pull request. Use this to fetch detailed changes for one or more files without retrieving the entire PR. ${getAvailableRepositoryString(repositoryAccess)}`, {
|
|
191
|
+
owner: z.string().describe("Repository owner name"),
|
|
192
|
+
repo: z.string().describe("Repository name"),
|
|
193
|
+
pull_request_number: z.number().describe("Pull request number"),
|
|
194
|
+
file_paths: z.array(z.string()).min(1).describe("List of file paths to get patches for (exact paths or glob patterns)"),
|
|
195
|
+
include_contents: z.boolean().default(false).describe("Whether to include full file contents in addition to patches")
|
|
196
|
+
}, async ({ owner, repo, pull_request_number, file_paths, include_contents }) => {
|
|
197
|
+
try {
|
|
198
|
+
let githubClient;
|
|
199
|
+
try {
|
|
200
|
+
githubClient = getGitHubClientFromRepo(owner, repo, installationIdMap);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
content: [{
|
|
204
|
+
type: "text",
|
|
205
|
+
text: `Error accessing GitHub: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
206
|
+
}],
|
|
207
|
+
isError: true
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const results = await fetchPrFiles(githubClient, owner, repo, pull_request_number, file_paths, include_contents, true);
|
|
211
|
+
if (results.length === 0) return { content: [{
|
|
212
|
+
type: "text",
|
|
213
|
+
text: `No files found matching the specified paths in PR #${pull_request_number}.\n\nRequested paths: ${file_paths.join(", ")}`
|
|
214
|
+
}] };
|
|
215
|
+
return { content: [{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: await formatFileDiff(pull_request_number, results, include_contents)
|
|
218
|
+
}] };
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (error instanceof Error && "status" in error) {
|
|
221
|
+
if (error.status === 404) return {
|
|
222
|
+
content: [{
|
|
223
|
+
type: "text",
|
|
224
|
+
text: `Pull request #${pull_request_number} not found in ${owner}/${repo}.`
|
|
225
|
+
}],
|
|
226
|
+
isError: true
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
content: [{
|
|
231
|
+
type: "text",
|
|
232
|
+
text: `Error getting file patches: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
233
|
+
}],
|
|
234
|
+
isError: true
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
server.tool("create-branch", `Create a new branch in a repository. ${getAvailableRepositoryString(repositoryAccess)}`, {
|
|
239
|
+
owner: z.string().describe("Repository owner name"),
|
|
240
|
+
repo: z.string().describe("Repository name"),
|
|
241
|
+
from_branch: z.string().optional().describe("Branch to create from (defaults to default branch)")
|
|
242
|
+
}, async ({ owner, repo, from_branch }) => {
|
|
243
|
+
const branch_name = `docs-writer-ai-update-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
244
|
+
try {
|
|
245
|
+
const githubClient = getGitHubClientFromRepo(owner, repo, installationIdMap);
|
|
246
|
+
const repoInfo = await githubClient.rest.repos.get({
|
|
247
|
+
owner,
|
|
248
|
+
repo
|
|
249
|
+
});
|
|
250
|
+
const sourceBranch = from_branch || repoInfo.data.default_branch;
|
|
251
|
+
const sourceSha = (await githubClient.rest.git.getRef({
|
|
252
|
+
owner,
|
|
253
|
+
repo,
|
|
254
|
+
ref: `heads/${sourceBranch}`
|
|
255
|
+
})).data.object.sha;
|
|
256
|
+
await githubClient.rest.git.createRef({
|
|
257
|
+
owner,
|
|
258
|
+
repo,
|
|
259
|
+
ref: `refs/heads/${branch_name}`,
|
|
260
|
+
sha: sourceSha
|
|
261
|
+
});
|
|
262
|
+
return { content: [{
|
|
263
|
+
type: "text",
|
|
264
|
+
text: `Successfully created branch "${branch_name}" in ${owner}/${repo}`
|
|
265
|
+
}] };
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error instanceof Error && "status" in error) {
|
|
268
|
+
const apiError = error;
|
|
269
|
+
if (apiError.status === 422) return {
|
|
270
|
+
content: [{
|
|
271
|
+
type: "text",
|
|
272
|
+
text: `Branch "${branch_name}" already exists in ${owner}/${repo}`
|
|
273
|
+
}],
|
|
274
|
+
isError: true
|
|
275
|
+
};
|
|
276
|
+
if (apiError.status === 404) return {
|
|
277
|
+
content: [{
|
|
278
|
+
type: "text",
|
|
279
|
+
text: `Repository ${owner}/${repo} not found or source branch "${from_branch || "default"}" does not exist`
|
|
280
|
+
}],
|
|
281
|
+
isError: true
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
content: [{
|
|
286
|
+
type: "text",
|
|
287
|
+
text: `Error creating branch: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
288
|
+
}],
|
|
289
|
+
isError: true
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
server.tool("commit-file-changes", `Commit changes to a files in a repository. ${getAvailableRepositoryString(repositoryAccess)}`, {
|
|
294
|
+
owner: z.string().describe("Repository owner name"),
|
|
295
|
+
repo: z.string().describe("Repository name"),
|
|
296
|
+
branch_name: z.string().describe("Branch to commit to"),
|
|
297
|
+
file_path: z.string().describe("Path to the file to commit"),
|
|
298
|
+
update_operations: updateOperationsSchema,
|
|
299
|
+
commit_message: z.string().describe("Commit message")
|
|
300
|
+
}, async ({ owner, repo, branch_name, file_path, update_operations, commit_message }) => {
|
|
301
|
+
try {
|
|
302
|
+
let githubClient;
|
|
303
|
+
try {
|
|
304
|
+
githubClient = getGitHubClientFromRepo(owner, repo, installationIdMap);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
return {
|
|
307
|
+
content: [{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: `Error accessing GitHub: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
310
|
+
}],
|
|
311
|
+
isError: true
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const fileResponse = await githubClient.rest.repos.getContent({
|
|
315
|
+
owner,
|
|
316
|
+
repo,
|
|
317
|
+
path: file_path
|
|
318
|
+
});
|
|
319
|
+
if (!("content" in fileResponse.data) || Array.isArray(fileResponse.data)) throw new Error(`File ${file_path} not found or is not a file`);
|
|
320
|
+
const currentFileContent = Buffer.from(fileResponse.data.content, "base64").toString("utf-8");
|
|
321
|
+
const updateOperations = update_operations.map((op) => ({
|
|
322
|
+
operation: op.operation,
|
|
323
|
+
lineStart: op.lineStart,
|
|
324
|
+
lineEnd: op.lineEnd,
|
|
325
|
+
content: op.content,
|
|
326
|
+
reason: op.reason
|
|
327
|
+
}));
|
|
328
|
+
await commitFileChanges({
|
|
329
|
+
githubClient,
|
|
330
|
+
owner,
|
|
331
|
+
repo,
|
|
332
|
+
fileContent: currentFileContent,
|
|
333
|
+
filePath: file_path,
|
|
334
|
+
branchName: branch_name,
|
|
335
|
+
operations: updateOperations,
|
|
336
|
+
commitMessage: commit_message
|
|
337
|
+
});
|
|
338
|
+
return { content: [{
|
|
339
|
+
type: "text",
|
|
340
|
+
text: `Successfully committed changes to ${owner}/${repo} on branch "${branch_name}"`
|
|
341
|
+
}] };
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (error instanceof Error && "status" in error) {
|
|
344
|
+
const apiError = error;
|
|
345
|
+
if (apiError.status === 404) return {
|
|
346
|
+
content: [{
|
|
347
|
+
type: "text",
|
|
348
|
+
text: `Repository ${owner}/${repo} or branch "${branch_name}" not found`
|
|
349
|
+
}],
|
|
350
|
+
isError: true
|
|
351
|
+
};
|
|
352
|
+
if (apiError.status === 422) return {
|
|
353
|
+
content: [{
|
|
354
|
+
type: "text",
|
|
355
|
+
text: "Invalid commit data. Please check file path and update operations format."
|
|
356
|
+
}],
|
|
357
|
+
isError: true
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
content: [{
|
|
362
|
+
type: "text",
|
|
363
|
+
text: `Error creating commit: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
364
|
+
}],
|
|
365
|
+
isError: true
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
server.tool("commit-new-file", `Create and commit a new file in a repository. ${getAvailableRepositoryString(repositoryAccess)}`, {
|
|
370
|
+
owner: z.string().describe("Repository owner name"),
|
|
371
|
+
repo: z.string().describe("Repository name"),
|
|
372
|
+
branch_name: z.string().describe("Branch to commit to"),
|
|
373
|
+
file_path: z.string().describe("Path for the new file (relative to repository root)"),
|
|
374
|
+
content: z.string().describe("Content for the new file"),
|
|
375
|
+
commit_message: z.string().describe("Commit message")
|
|
376
|
+
}, async ({ owner, repo, branch_name, file_path, content, commit_message }) => {
|
|
377
|
+
try {
|
|
378
|
+
let githubClient;
|
|
379
|
+
try {
|
|
380
|
+
githubClient = getGitHubClientFromRepo(owner, repo, installationIdMap);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
return {
|
|
383
|
+
content: [{
|
|
384
|
+
type: "text",
|
|
385
|
+
text: `Error accessing GitHub: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
386
|
+
}],
|
|
387
|
+
isError: true
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return { content: [{
|
|
391
|
+
type: "text",
|
|
392
|
+
text: `Successfully created and committed new file "${file_path}" to ${owner}/${repo} on branch "${branch_name}"\n\nCommit SHA: ${await commitNewFile({
|
|
393
|
+
githubClient,
|
|
394
|
+
owner,
|
|
395
|
+
repo,
|
|
396
|
+
filePath: file_path,
|
|
397
|
+
branchName: branch_name,
|
|
398
|
+
content,
|
|
399
|
+
commitMessage: commit_message
|
|
400
|
+
})}`
|
|
401
|
+
}] };
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error instanceof Error && "status" in error) {
|
|
404
|
+
const apiError = error;
|
|
405
|
+
if (apiError.status === 404) return {
|
|
406
|
+
content: [{
|
|
407
|
+
type: "text",
|
|
408
|
+
text: `Repository ${owner}/${repo} or branch "${branch_name}" not found`
|
|
409
|
+
}],
|
|
410
|
+
isError: true
|
|
411
|
+
};
|
|
412
|
+
if (apiError.status === 422) return {
|
|
413
|
+
content: [{
|
|
414
|
+
type: "text",
|
|
415
|
+
text: `File "${file_path}" may already exist or the path is invalid.`
|
|
416
|
+
}],
|
|
417
|
+
isError: true
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
content: [{
|
|
422
|
+
type: "text",
|
|
423
|
+
text: `Error creating file: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
424
|
+
}],
|
|
425
|
+
isError: true
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
server.tool("create-pull-request", `Create a pull request in a repository. ${getAvailableRepositoryString(repositoryAccess)}`, {
|
|
430
|
+
owner: z.string().describe("Repository owner name"),
|
|
431
|
+
repo: z.string().describe("Repository name"),
|
|
432
|
+
title: z.string().describe("Pull request title"),
|
|
433
|
+
body: z.string().describe("Pull request description"),
|
|
434
|
+
head_branch: z.string().describe("Branch containing the changes")
|
|
435
|
+
}, async ({ owner, repo, title, body, head_branch }) => {
|
|
436
|
+
try {
|
|
437
|
+
let githubClient;
|
|
438
|
+
try {
|
|
439
|
+
githubClient = getGitHubClientFromRepo(owner, repo, installationIdMap);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
return {
|
|
442
|
+
content: [{
|
|
443
|
+
type: "text",
|
|
444
|
+
text: `Error accessing GitHub: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
445
|
+
}],
|
|
446
|
+
isError: true
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const targetBaseBranch = (await githubClient.rest.repos.get({
|
|
450
|
+
owner,
|
|
451
|
+
repo
|
|
452
|
+
})).data.default_branch;
|
|
453
|
+
const pullRequestResponse = await githubClient.rest.pulls.create({
|
|
454
|
+
owner,
|
|
455
|
+
repo,
|
|
456
|
+
title,
|
|
457
|
+
body,
|
|
458
|
+
head: head_branch,
|
|
459
|
+
base: targetBaseBranch,
|
|
460
|
+
draft: false
|
|
461
|
+
});
|
|
462
|
+
return { content: [{
|
|
463
|
+
type: "text",
|
|
464
|
+
text: `Successfully created pull request in ${owner}/${repo}\n\nPull Request details:\n• Number: #${pullRequestResponse.data.number}\n• Title: ${title}\n• Head: ${head_branch}\n• Base: ${targetBaseBranch}\n• Status: Open\n• URL: ${pullRequestResponse.data.html_url}\n\nDescription:\n${body}`
|
|
465
|
+
}] };
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (error instanceof Error && "status" in error) {
|
|
468
|
+
const apiError = error;
|
|
469
|
+
if (apiError.status === 404) return {
|
|
470
|
+
content: [{
|
|
471
|
+
type: "text",
|
|
472
|
+
text: `Repository ${owner}/${repo} not found or branch "${head_branch}" does not exist`
|
|
473
|
+
}],
|
|
474
|
+
isError: true
|
|
475
|
+
};
|
|
476
|
+
if (apiError.status === 422) return {
|
|
477
|
+
content: [{
|
|
478
|
+
type: "text",
|
|
479
|
+
text: "Pull request validation failed. This could be due to: no commits between branches, pull request already exists, or invalid branch names."
|
|
480
|
+
}],
|
|
481
|
+
isError: true
|
|
482
|
+
};
|
|
483
|
+
if (apiError.status === 403) return {
|
|
484
|
+
content: [{
|
|
485
|
+
type: "text",
|
|
486
|
+
text: "Permission denied. Your GitHub App may not have sufficient permissions to create pull requests in this repository."
|
|
487
|
+
}],
|
|
488
|
+
isError: true
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
content: [{
|
|
493
|
+
type: "text",
|
|
494
|
+
text: `Error creating pull request: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
495
|
+
}],
|
|
496
|
+
isError: true
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
server.tool("leave-comment-on-pull-request", `Leave a comment on a pull request. This creates a general comment on the PR, not a line-specific review comment. ${getAvailableRepositoryString(repositoryAccess)}`, {
|
|
501
|
+
owner: z.string().describe("Repository owner name"),
|
|
502
|
+
repo: z.string().describe("Repository name"),
|
|
503
|
+
pull_request_number: z.number().describe("Pull request number"),
|
|
504
|
+
body: z.string().describe("The comment body text (supports GitHub markdown)")
|
|
505
|
+
}, async ({ owner, repo, pull_request_number, body }) => {
|
|
506
|
+
try {
|
|
507
|
+
let githubClient;
|
|
508
|
+
try {
|
|
509
|
+
githubClient = getGitHubClientFromRepo(owner, repo, installationIdMap);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
return {
|
|
512
|
+
content: [{
|
|
513
|
+
type: "text",
|
|
514
|
+
text: `Error accessing GitHub: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
515
|
+
}],
|
|
516
|
+
isError: true
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const commentResponse = await githubClient.rest.issues.createComment({
|
|
520
|
+
owner,
|
|
521
|
+
repo,
|
|
522
|
+
issue_number: pull_request_number,
|
|
523
|
+
body
|
|
524
|
+
});
|
|
525
|
+
return { content: [{
|
|
526
|
+
type: "text",
|
|
527
|
+
text: `Successfully posted comment on PR #${pull_request_number} in ${owner}/${repo}\n\nComment details:\n• ID: ${commentResponse.data.id}\n• URL: ${commentResponse.data.html_url}\n• Created at: ${commentResponse.data.created_at}`
|
|
528
|
+
}] };
|
|
529
|
+
} catch (error) {
|
|
530
|
+
if (error instanceof Error && "status" in error) {
|
|
531
|
+
const apiError = error;
|
|
532
|
+
if (apiError.status === 404) return {
|
|
533
|
+
content: [{
|
|
534
|
+
type: "text",
|
|
535
|
+
text: `Pull request #${pull_request_number} not found in ${owner}/${repo}. Please check the repository exists and the pull request number is correct.`
|
|
536
|
+
}],
|
|
537
|
+
isError: true
|
|
538
|
+
};
|
|
539
|
+
if (apiError.status === 403) return {
|
|
540
|
+
content: [{
|
|
541
|
+
type: "text",
|
|
542
|
+
text: `Access denied to PR #${pull_request_number} in ${owner}/${repo}. Your GitHub App may not have sufficient permissions to comment on pull requests.`
|
|
543
|
+
}],
|
|
544
|
+
isError: true
|
|
545
|
+
};
|
|
546
|
+
if (apiError.status === 422) return {
|
|
547
|
+
content: [{
|
|
548
|
+
type: "text",
|
|
549
|
+
text: "Invalid comment data. Please check the comment body is not empty and is valid."
|
|
550
|
+
}],
|
|
551
|
+
isError: true
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
content: [{
|
|
556
|
+
type: "text",
|
|
557
|
+
text: `Error posting comment: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
558
|
+
}],
|
|
559
|
+
isError: true
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
server.tool("visualize-update-operations", `Apply a list of operations to a piece of documentation and return a mapping of line number to line content. ${getAvailableRepositoryString(repositoryAccess)}`, {
|
|
564
|
+
owner: z.string().describe("Repository owner name"),
|
|
565
|
+
repo: z.string().describe("Repository name"),
|
|
566
|
+
file_path: z.string().describe("The path of the file to visualize the update operations for"),
|
|
567
|
+
operations: updateOperationsSchema
|
|
568
|
+
}, async ({ owner, repo, file_path, operations }) => {
|
|
569
|
+
try {
|
|
570
|
+
let githubClient;
|
|
571
|
+
try {
|
|
572
|
+
githubClient = getGitHubClientFromRepo(owner, repo, installationIdMap);
|
|
573
|
+
} catch (error) {
|
|
574
|
+
return {
|
|
575
|
+
content: [{
|
|
576
|
+
type: "text",
|
|
577
|
+
text: `Error accessing GitHub: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
578
|
+
}],
|
|
579
|
+
isError: true
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
const response = await githubClient.rest.repos.getContent({
|
|
583
|
+
owner,
|
|
584
|
+
repo,
|
|
585
|
+
path: file_path
|
|
586
|
+
});
|
|
587
|
+
if ("content" in response.data && !Array.isArray(response.data)) {
|
|
588
|
+
const fileData = response.data;
|
|
589
|
+
const result = visualizeUpdateOperations(Buffer.from(fileData.content, "base64").toString("utf-8"), operations.map((op) => ({
|
|
590
|
+
operation: op.operation,
|
|
591
|
+
lineStart: op.lineStart,
|
|
592
|
+
lineEnd: op.lineEnd,
|
|
593
|
+
content: op.content,
|
|
594
|
+
reason: op.reason
|
|
595
|
+
})));
|
|
596
|
+
if (typeof result === "string") return {
|
|
597
|
+
content: [{
|
|
598
|
+
type: "text",
|
|
599
|
+
text: result
|
|
600
|
+
}],
|
|
601
|
+
isError: true
|
|
602
|
+
};
|
|
603
|
+
return { content: [{
|
|
604
|
+
type: "text",
|
|
605
|
+
text: `${Object.entries(result).sort(([a], [b]) => Number(a) - Number(b)).map(([lineNum, content]) => `${lineNum.padStart(3, " ")}| ${content}`).join("\n")}`
|
|
606
|
+
}] };
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
content: [{
|
|
610
|
+
type: "text",
|
|
611
|
+
text: `The path "${file_path}" is not a file or could not be retrieved as file content.`
|
|
612
|
+
}],
|
|
613
|
+
isError: true
|
|
614
|
+
};
|
|
615
|
+
} catch (error) {
|
|
616
|
+
return {
|
|
617
|
+
content: [{
|
|
618
|
+
type: "text",
|
|
619
|
+
text: `Error visualizing update operations: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
620
|
+
}],
|
|
621
|
+
isError: true
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
return server;
|
|
626
|
+
};
|
|
627
|
+
const app = new Hono();
|
|
628
|
+
app.use("/", githubMcpAuth());
|
|
629
|
+
app.post("/", async (c) => {
|
|
630
|
+
if (!process.env.GITHUB_APP_ID || !process.env.GITHUB_APP_PRIVATE_KEY) return c.json({ error: "GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY must be set" }, 500);
|
|
631
|
+
const toolId = c.get("toolId");
|
|
632
|
+
const body = await c.req.json();
|
|
633
|
+
const server = await getServer(toolId);
|
|
634
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
635
|
+
server.connect(transport);
|
|
636
|
+
const { req, res } = toReqRes(c.req.raw);
|
|
637
|
+
await transport.handleRequest(req, res, body);
|
|
638
|
+
return toFetchResponse(res);
|
|
639
|
+
});
|
|
640
|
+
app.delete("/", async (c) => {
|
|
641
|
+
return c.json({
|
|
642
|
+
jsonrpc: "2.0",
|
|
643
|
+
error: {
|
|
644
|
+
code: -32001,
|
|
645
|
+
message: "Method Not Allowed"
|
|
646
|
+
},
|
|
647
|
+
id: null
|
|
648
|
+
}, { status: 405 });
|
|
649
|
+
});
|
|
650
|
+
app.get("/", async (c) => {
|
|
651
|
+
return c.json({
|
|
652
|
+
jsonrpc: "2.0",
|
|
653
|
+
error: {
|
|
654
|
+
code: -32e3,
|
|
655
|
+
message: "Method not allowed."
|
|
656
|
+
},
|
|
657
|
+
id: null
|
|
658
|
+
}, { status: 405 });
|
|
659
|
+
});
|
|
660
|
+
app.get("/health", async (c) => {
|
|
661
|
+
return c.json({
|
|
662
|
+
status: "healthy",
|
|
663
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
664
|
+
service: "GitHub MCP Server"
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
var mcp_default = app;
|
|
668
|
+
|
|
669
|
+
//#endregion
|
|
670
|
+
export { mcp_default as default };
|