gitlab-mcp 0.1.4 → 1.0.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/.dockerignore +7 -0
- package/.editorconfig +9 -0
- package/.env.example +75 -0
- package/.github/workflows/nodejs.yml +31 -0
- package/.github/workflows/npm-publish.yml +31 -0
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/.prettierrc.json +6 -0
- package/Dockerfile +20 -0
- package/README.md +416 -251
- package/docker-compose.yml +10 -0
- package/docs/architecture.md +310 -0
- package/docs/authentication.md +299 -0
- package/docs/configuration.md +149 -0
- package/docs/deployment.md +336 -0
- package/docs/tools.md +294 -0
- package/eslint.config.js +23 -0
- package/package.json +70 -32
- package/scripts/get-oauth-token.example.sh +15 -0
- package/src/config/env.ts +171 -0
- package/src/http.ts +605 -0
- package/src/index.ts +77 -0
- package/src/lib/auth-context.ts +19 -0
- package/src/lib/gitlab-client.ts +1810 -0
- package/src/lib/logger.ts +17 -0
- package/src/lib/network.ts +45 -0
- package/src/lib/oauth.ts +287 -0
- package/src/lib/output.ts +51 -0
- package/src/lib/policy.ts +78 -0
- package/src/lib/request-runtime.ts +376 -0
- package/src/lib/sanitize.ts +25 -0
- package/src/server/build-server.ts +17 -0
- package/src/tools/gitlab.ts +3128 -0
- package/src/tools/health.ts +27 -0
- package/src/tools/mr-code-context.ts +473 -0
- package/src/types/context.ts +13 -0
- package/tests/auth-context.test.ts +102 -0
- package/tests/gitlab-client.test.ts +674 -0
- package/tests/graphql-guard.test.ts +121 -0
- package/tests/integration/agent-loop.integration.test.ts +552 -0
- package/tests/integration/server.integration.test.ts +543 -0
- package/tests/mr-code-context.test.ts +600 -0
- package/tests/oauth.test.ts +43 -0
- package/tests/output.test.ts +186 -0
- package/tests/policy.test.ts +324 -0
- package/tests/request-runtime.test.ts +252 -0
- package/tests/sanitize.test.ts +123 -0
- package/tests/upload-reference.test.ts +84 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +12 -0
- package/LICENSE +0 -21
- package/build/index.js +0 -1641
- package/build/schemas.js +0 -684
- package/build/test-note.js +0 -54
package/build/index.js
DELETED
|
@@ -1,1641 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
-
import fetch from "node-fetch";
|
|
7
|
-
import { z } from "zod";
|
|
8
|
-
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
9
|
-
import { fileURLToPath } from "url";
|
|
10
|
-
import { dirname } from "path";
|
|
11
|
-
import fs from "fs";
|
|
12
|
-
import path from "path";
|
|
13
|
-
import { config } from "dotenv";
|
|
14
|
-
import yargs from "yargs";
|
|
15
|
-
import { hideBin } from "yargs/helpers";
|
|
16
|
-
import express from "express";
|
|
17
|
-
import { GitLabForkSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabIssueSchema, GitLabMergeRequestSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabSearchResponseSchema, GitLabTreeSchema, GitLabCommitSchema, GitLabNamespaceSchema, GitLabNamespaceExistsResponseSchema, GitLabProjectSchema, CreateOrUpdateFileSchema, SearchRepositoriesSchema, CreateRepositorySchema, GetFileContentsSchema, PushFilesSchema, CreateIssueSchema, CreateMergeRequestSchema, ForkRepositorySchema, CreateBranchSchema, GitLabMergeRequestDiffSchema, GetMergeRequestSchema, GetMergeRequestDiffsSchema, UpdateMergeRequestSchema, ListIssuesSchema, GetIssueSchema, UpdateIssueSchema, DeleteIssueSchema, GitLabIssueLinkSchema, GitLabIssueWithLinkDetailsSchema, ListIssueLinksSchema, GetIssueLinkSchema, CreateIssueLinkSchema, DeleteIssueLinkSchema, ListNamespacesSchema, GetNamespaceSchema, VerifyNamespaceSchema, GetProjectSchema, ListProjectsSchema, ListLabelsSchema, GetLabelSchema, CreateLabelSchema, UpdateLabelSchema, DeleteLabelSchema, CreateNoteSchema, ListGroupProjectsSchema,
|
|
18
|
-
// Discussion Schemas
|
|
19
|
-
GitLabDiscussionNoteSchema, // Added
|
|
20
|
-
GitLabDiscussionSchema, UpdateMergeRequestNoteSchema, // Added
|
|
21
|
-
ListMergeRequestDiscussionsSchema, } from "./schemas.js";
|
|
22
|
-
// Load .env from the current working directory
|
|
23
|
-
config({ path: path.resolve(process.cwd(), ".env") });
|
|
24
|
-
const argv = yargs(hideBin(process.argv)).argv;
|
|
25
|
-
const isSSE = argv.mode === "sse";
|
|
26
|
-
const port = argv.port ?? 3044;
|
|
27
|
-
/**
|
|
28
|
-
* Read version from package.json
|
|
29
|
-
*/
|
|
30
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
-
const __dirname = dirname(__filename);
|
|
32
|
-
const packageJsonPath = path.resolve(__dirname, "../package.json");
|
|
33
|
-
let SERVER_VERSION = "unknown";
|
|
34
|
-
try {
|
|
35
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
36
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
37
|
-
SERVER_VERSION = packageJson.version || SERVER_VERSION;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
console.error("Warning: Could not read version from package.json:", error);
|
|
42
|
-
}
|
|
43
|
-
const server = new Server({
|
|
44
|
-
name: "better-gitlab-mcp-server",
|
|
45
|
-
version: SERVER_VERSION,
|
|
46
|
-
}, {
|
|
47
|
-
capabilities: {
|
|
48
|
-
tools: {},
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
|
|
52
|
-
const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true";
|
|
53
|
-
// Define all available tools
|
|
54
|
-
const allTools = [
|
|
55
|
-
{
|
|
56
|
-
name: "create_or_update_file",
|
|
57
|
-
description: "Create or update a single file in a GitLab project",
|
|
58
|
-
inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema),
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: "search_repositories",
|
|
62
|
-
description: "Search for GitLab projects",
|
|
63
|
-
inputSchema: zodToJsonSchema(SearchRepositoriesSchema),
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
name: "create_repository",
|
|
67
|
-
description: "Create a new GitLab project",
|
|
68
|
-
inputSchema: zodToJsonSchema(CreateRepositorySchema),
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: "get_file_contents",
|
|
72
|
-
description: "Get the contents of a file or directory from a GitLab project",
|
|
73
|
-
inputSchema: zodToJsonSchema(GetFileContentsSchema),
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: "push_files",
|
|
77
|
-
description: "Push multiple files to a GitLab project in a single commit",
|
|
78
|
-
inputSchema: zodToJsonSchema(PushFilesSchema),
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
name: "create_issue",
|
|
82
|
-
description: "Create a new issue in a GitLab project",
|
|
83
|
-
inputSchema: zodToJsonSchema(CreateIssueSchema),
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: "create_merge_request",
|
|
87
|
-
description: "Create a new merge request in a GitLab project",
|
|
88
|
-
inputSchema: zodToJsonSchema(CreateMergeRequestSchema),
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: "fork_repository",
|
|
92
|
-
description: "Fork a GitLab project to your account or specified namespace",
|
|
93
|
-
inputSchema: zodToJsonSchema(ForkRepositorySchema),
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
name: "create_branch",
|
|
97
|
-
description: "Create a new branch in a GitLab project",
|
|
98
|
-
inputSchema: zodToJsonSchema(CreateBranchSchema),
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
name: "get_merge_request",
|
|
102
|
-
description: "Get details of a merge request",
|
|
103
|
-
inputSchema: zodToJsonSchema(GetMergeRequestSchema),
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
name: "get_merge_request_diffs",
|
|
107
|
-
description: "Get the changes/diffs of a merge request",
|
|
108
|
-
inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema),
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
name: "update_merge_request",
|
|
112
|
-
description: "Update a merge request",
|
|
113
|
-
inputSchema: zodToJsonSchema(UpdateMergeRequestSchema),
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
name: "create_note",
|
|
117
|
-
description: "Create a new note (comment) to an issue or merge request",
|
|
118
|
-
inputSchema: zodToJsonSchema(CreateNoteSchema),
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: "list_merge_request_discussions",
|
|
122
|
-
description: "List discussion items for a merge request",
|
|
123
|
-
inputSchema: zodToJsonSchema(ListMergeRequestDiscussionsSchema),
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
name: "update_merge_request_note",
|
|
127
|
-
description: "Modify an existing merge request thread note",
|
|
128
|
-
inputSchema: zodToJsonSchema(UpdateMergeRequestNoteSchema),
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
name: "list_issues",
|
|
132
|
-
description: "List issues in a GitLab project with filtering options",
|
|
133
|
-
inputSchema: zodToJsonSchema(ListIssuesSchema),
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
name: "get_issue",
|
|
137
|
-
description: "Get details of a specific issue in a GitLab project",
|
|
138
|
-
inputSchema: zodToJsonSchema(GetIssueSchema),
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: "update_issue",
|
|
142
|
-
description: "Update an issue in a GitLab project",
|
|
143
|
-
inputSchema: zodToJsonSchema(UpdateIssueSchema),
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
name: "delete_issue",
|
|
147
|
-
description: "Delete an issue from a GitLab project",
|
|
148
|
-
inputSchema: zodToJsonSchema(DeleteIssueSchema),
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
name: "list_issue_links",
|
|
152
|
-
description: "List all issue links for a specific issue",
|
|
153
|
-
inputSchema: zodToJsonSchema(ListIssueLinksSchema),
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
name: "get_issue_link",
|
|
157
|
-
description: "Get a specific issue link",
|
|
158
|
-
inputSchema: zodToJsonSchema(GetIssueLinkSchema),
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
name: "create_issue_link",
|
|
162
|
-
description: "Create an issue link between two issues",
|
|
163
|
-
inputSchema: zodToJsonSchema(CreateIssueLinkSchema),
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
name: "delete_issue_link",
|
|
167
|
-
description: "Delete an issue link",
|
|
168
|
-
inputSchema: zodToJsonSchema(DeleteIssueLinkSchema),
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
name: "list_namespaces",
|
|
172
|
-
description: "List all namespaces available to the current user",
|
|
173
|
-
inputSchema: zodToJsonSchema(ListNamespacesSchema),
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
name: "get_namespace",
|
|
177
|
-
description: "Get details of a namespace by ID or path",
|
|
178
|
-
inputSchema: zodToJsonSchema(GetNamespaceSchema),
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
name: "verify_namespace",
|
|
182
|
-
description: "Verify if a namespace path exists",
|
|
183
|
-
inputSchema: zodToJsonSchema(VerifyNamespaceSchema),
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
name: "get_project",
|
|
187
|
-
description: "Get details of a specific project",
|
|
188
|
-
inputSchema: zodToJsonSchema(GetProjectSchema),
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
name: "list_projects",
|
|
192
|
-
description: "List projects accessible by the current user",
|
|
193
|
-
inputSchema: zodToJsonSchema(ListProjectsSchema),
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
name: "list_labels",
|
|
197
|
-
description: "List labels for a project",
|
|
198
|
-
inputSchema: zodToJsonSchema(ListLabelsSchema),
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
name: "get_label",
|
|
202
|
-
description: "Get a single label from a project",
|
|
203
|
-
inputSchema: zodToJsonSchema(GetLabelSchema),
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
name: "create_label",
|
|
207
|
-
description: "Create a new label in a project",
|
|
208
|
-
inputSchema: zodToJsonSchema(CreateLabelSchema),
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
name: "update_label",
|
|
212
|
-
description: "Update an existing label in a project",
|
|
213
|
-
inputSchema: zodToJsonSchema(UpdateLabelSchema),
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
name: "delete_label",
|
|
217
|
-
description: "Delete a label from a project",
|
|
218
|
-
inputSchema: zodToJsonSchema(DeleteLabelSchema),
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
name: "list_group_projects",
|
|
222
|
-
description: "List projects in a GitLab group with filtering options",
|
|
223
|
-
inputSchema: zodToJsonSchema(ListGroupProjectsSchema),
|
|
224
|
-
},
|
|
225
|
-
];
|
|
226
|
-
// Define which tools are read-only
|
|
227
|
-
const readOnlyTools = [
|
|
228
|
-
"search_repositories",
|
|
229
|
-
"get_file_contents",
|
|
230
|
-
"get_merge_request",
|
|
231
|
-
"get_merge_request_diffs",
|
|
232
|
-
"list_merge_request_discussions",
|
|
233
|
-
"list_issues",
|
|
234
|
-
"get_issue",
|
|
235
|
-
"list_issue_links",
|
|
236
|
-
"get_issue_link",
|
|
237
|
-
"list_namespaces",
|
|
238
|
-
"get_namespace",
|
|
239
|
-
"verify_namespace",
|
|
240
|
-
"get_project",
|
|
241
|
-
"list_projects",
|
|
242
|
-
"list_labels",
|
|
243
|
-
"get_label",
|
|
244
|
-
"list_group_projects",
|
|
245
|
-
];
|
|
246
|
-
/**
|
|
247
|
-
* Smart URL handling for GitLab API
|
|
248
|
-
*
|
|
249
|
-
* @param {string | undefined} url - Input GitLab API URL
|
|
250
|
-
* @returns {string} Normalized GitLab API URL with /api/v4 path
|
|
251
|
-
*/
|
|
252
|
-
function normalizeGitLabApiUrl(url) {
|
|
253
|
-
if (!url) {
|
|
254
|
-
return "https://gitlab.com/api/v4";
|
|
255
|
-
}
|
|
256
|
-
// Remove trailing slash if present
|
|
257
|
-
let normalizedUrl = url.endsWith("/") ? url.slice(0, -1) : url;
|
|
258
|
-
// Check if URL already has /api/v4
|
|
259
|
-
if (!normalizedUrl.endsWith("/api/v4") &&
|
|
260
|
-
!normalizedUrl.endsWith("/api/v4/")) {
|
|
261
|
-
// Append /api/v4 if not already present
|
|
262
|
-
normalizedUrl = `${normalizedUrl}/api/v4`;
|
|
263
|
-
}
|
|
264
|
-
return normalizedUrl;
|
|
265
|
-
}
|
|
266
|
-
// Use the normalizeGitLabApiUrl function to handle various URL formats
|
|
267
|
-
const GITLAB_API_URL = normalizeGitLabApiUrl(process.env.GITLAB_API_URL || "");
|
|
268
|
-
if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
|
|
269
|
-
console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
|
|
270
|
-
process.exit(1);
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Common headers for GitLab API requests
|
|
274
|
-
*/
|
|
275
|
-
const DEFAULT_HEADERS = {
|
|
276
|
-
Accept: "application/json",
|
|
277
|
-
"Content-Type": "application/json",
|
|
278
|
-
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
|
279
|
-
};
|
|
280
|
-
/**
|
|
281
|
-
* Utility function for handling GitLab API errors
|
|
282
|
-
*
|
|
283
|
-
* @param {import("node-fetch").Response} response - The response from GitLab API
|
|
284
|
-
* @throws {Error} Throws an error with response details if the request failed
|
|
285
|
-
*/
|
|
286
|
-
async function handleGitLabError(response) {
|
|
287
|
-
if (!response.ok) {
|
|
288
|
-
const errorBody = await response.text();
|
|
289
|
-
// Check specifically for Rate Limit error
|
|
290
|
-
if (response.status === 403 &&
|
|
291
|
-
errorBody.includes("User API Key Rate limit exceeded")) {
|
|
292
|
-
console.error("GitLab API Rate Limit Exceeded:", errorBody);
|
|
293
|
-
console.log("User API Key Rate limit exceeded. Please try again later.");
|
|
294
|
-
throw new Error(`GitLab API Rate Limit Exceeded: ${errorBody}`);
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
// Handle other API errors
|
|
298
|
-
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Create a fork of a GitLab project
|
|
304
|
-
*
|
|
305
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
306
|
-
* @param {string} [namespace] - The namespace to fork the project to
|
|
307
|
-
* @returns {Promise<GitLabFork>} The created fork
|
|
308
|
-
*/
|
|
309
|
-
async function forkProject(projectId, namespace) {
|
|
310
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork`);
|
|
311
|
-
if (namespace) {
|
|
312
|
-
url.searchParams.append("namespace", namespace);
|
|
313
|
-
}
|
|
314
|
-
const response = await fetch(url.toString(), {
|
|
315
|
-
method: "POST",
|
|
316
|
-
headers: DEFAULT_HEADERS,
|
|
317
|
-
});
|
|
318
|
-
if (response.status === 409) {
|
|
319
|
-
throw new Error("Project already exists in the target namespace");
|
|
320
|
-
}
|
|
321
|
-
await handleGitLabError(response);
|
|
322
|
-
const data = await response.json();
|
|
323
|
-
return GitLabForkSchema.parse(data);
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Create a new branch in a GitLab project
|
|
327
|
-
*
|
|
328
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
329
|
-
* @param {z.infer<typeof CreateBranchOptionsSchema>} options - Branch creation options
|
|
330
|
-
* @returns {Promise<GitLabReference>} The created branch reference
|
|
331
|
-
*/
|
|
332
|
-
async function createBranch(projectId, options) {
|
|
333
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/branches`);
|
|
334
|
-
const response = await fetch(url.toString(), {
|
|
335
|
-
method: "POST",
|
|
336
|
-
headers: DEFAULT_HEADERS,
|
|
337
|
-
body: JSON.stringify({
|
|
338
|
-
branch: options.name,
|
|
339
|
-
ref: options.ref,
|
|
340
|
-
}),
|
|
341
|
-
});
|
|
342
|
-
await handleGitLabError(response);
|
|
343
|
-
return GitLabReferenceSchema.parse(await response.json());
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Get the default branch for a GitLab project
|
|
347
|
-
*
|
|
348
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
349
|
-
* @returns {Promise<string>} The name of the default branch
|
|
350
|
-
*/
|
|
351
|
-
async function getDefaultBranchRef(projectId) {
|
|
352
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`);
|
|
353
|
-
const response = await fetch(url.toString(), {
|
|
354
|
-
headers: DEFAULT_HEADERS,
|
|
355
|
-
});
|
|
356
|
-
await handleGitLabError(response);
|
|
357
|
-
const project = GitLabRepositorySchema.parse(await response.json());
|
|
358
|
-
return project.default_branch ?? "main";
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* Get the contents of a file from a GitLab project
|
|
362
|
-
*
|
|
363
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
364
|
-
* @param {string} filePath - The path of the file to get
|
|
365
|
-
* @param {string} [ref] - The name of the branch, tag or commit
|
|
366
|
-
* @returns {Promise<GitLabContent>} The file content
|
|
367
|
-
*/
|
|
368
|
-
async function getFileContents(projectId, filePath, ref) {
|
|
369
|
-
const encodedPath = encodeURIComponent(filePath);
|
|
370
|
-
if (!ref) {
|
|
371
|
-
ref = await getDefaultBranchRef(projectId);
|
|
372
|
-
}
|
|
373
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
|
|
374
|
-
url.searchParams.append("ref", ref);
|
|
375
|
-
const response = await fetch(url.toString(), {
|
|
376
|
-
headers: DEFAULT_HEADERS,
|
|
377
|
-
});
|
|
378
|
-
if (response.status === 404) {
|
|
379
|
-
throw new Error(`File not found: ${filePath}`);
|
|
380
|
-
}
|
|
381
|
-
await handleGitLabError(response);
|
|
382
|
-
const data = await response.json();
|
|
383
|
-
const parsedData = GitLabContentSchema.parse(data);
|
|
384
|
-
if (!Array.isArray(parsedData) && parsedData.content) {
|
|
385
|
-
parsedData.content = Buffer.from(parsedData.content, "base64").toString("utf8");
|
|
386
|
-
parsedData.encoding = "utf8";
|
|
387
|
-
}
|
|
388
|
-
return parsedData;
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Create a new issue in a GitLab project
|
|
392
|
-
*
|
|
393
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
394
|
-
* @param {z.infer<typeof CreateIssueOptionsSchema>} options - Issue creation options
|
|
395
|
-
* @returns {Promise<GitLabIssue>} The created issue
|
|
396
|
-
*/
|
|
397
|
-
async function createIssue(projectId, options) {
|
|
398
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`);
|
|
399
|
-
const response = await fetch(url.toString(), {
|
|
400
|
-
method: "POST",
|
|
401
|
-
headers: DEFAULT_HEADERS,
|
|
402
|
-
body: JSON.stringify({
|
|
403
|
-
title: options.title,
|
|
404
|
-
description: options.description,
|
|
405
|
-
assignee_ids: options.assignee_ids,
|
|
406
|
-
milestone_id: options.milestone_id,
|
|
407
|
-
labels: options.labels?.join(","),
|
|
408
|
-
}),
|
|
409
|
-
});
|
|
410
|
-
if (response.status === 400) {
|
|
411
|
-
const errorBody = await response.text();
|
|
412
|
-
throw new Error(`Invalid request: ${errorBody}`);
|
|
413
|
-
}
|
|
414
|
-
await handleGitLabError(response);
|
|
415
|
-
const data = await response.json();
|
|
416
|
-
return GitLabIssueSchema.parse(data);
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* List issues in a GitLab project
|
|
420
|
-
*
|
|
421
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
422
|
-
* @param {Object} options - Options for listing issues
|
|
423
|
-
* @returns {Promise<GitLabIssue[]>} List of issues
|
|
424
|
-
*/
|
|
425
|
-
async function listIssues(projectId, options = {}) {
|
|
426
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`);
|
|
427
|
-
// Add all query parameters
|
|
428
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
429
|
-
if (value !== undefined) {
|
|
430
|
-
if (key === "label_name" && Array.isArray(value)) {
|
|
431
|
-
// Handle array of labels
|
|
432
|
-
url.searchParams.append(key, value.join(","));
|
|
433
|
-
}
|
|
434
|
-
else {
|
|
435
|
-
url.searchParams.append(key, value.toString());
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
const response = await fetch(url.toString(), {
|
|
440
|
-
headers: DEFAULT_HEADERS,
|
|
441
|
-
});
|
|
442
|
-
await handleGitLabError(response);
|
|
443
|
-
const data = await response.json();
|
|
444
|
-
return z.array(GitLabIssueSchema).parse(data);
|
|
445
|
-
}
|
|
446
|
-
/**
|
|
447
|
-
* Get a single issue from a GitLab project
|
|
448
|
-
*
|
|
449
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
450
|
-
* @param {number} issueIid - The internal ID of the project issue
|
|
451
|
-
* @returns {Promise<GitLabIssue>} The issue
|
|
452
|
-
*/
|
|
453
|
-
async function getIssue(projectId, issueIid) {
|
|
454
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`);
|
|
455
|
-
const response = await fetch(url.toString(), {
|
|
456
|
-
headers: DEFAULT_HEADERS,
|
|
457
|
-
});
|
|
458
|
-
await handleGitLabError(response);
|
|
459
|
-
const data = await response.json();
|
|
460
|
-
return GitLabIssueSchema.parse(data);
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Update an issue in a GitLab project
|
|
464
|
-
*
|
|
465
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
466
|
-
* @param {number} issueIid - The internal ID of the project issue
|
|
467
|
-
* @param {Object} options - Update options for the issue
|
|
468
|
-
* @returns {Promise<GitLabIssue>} The updated issue
|
|
469
|
-
*/
|
|
470
|
-
async function updateIssue(projectId, issueIid, options) {
|
|
471
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`);
|
|
472
|
-
// Convert labels array to comma-separated string if present
|
|
473
|
-
const body = { ...options };
|
|
474
|
-
if (body.labels && Array.isArray(body.labels)) {
|
|
475
|
-
body.labels = body.labels.join(",");
|
|
476
|
-
}
|
|
477
|
-
const response = await fetch(url.toString(), {
|
|
478
|
-
method: "PUT",
|
|
479
|
-
headers: DEFAULT_HEADERS,
|
|
480
|
-
body: JSON.stringify(body),
|
|
481
|
-
});
|
|
482
|
-
await handleGitLabError(response);
|
|
483
|
-
const data = await response.json();
|
|
484
|
-
return GitLabIssueSchema.parse(data);
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Delete an issue from a GitLab project
|
|
488
|
-
*
|
|
489
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
490
|
-
* @param {number} issueIid - The internal ID of the project issue
|
|
491
|
-
* @returns {Promise<void>}
|
|
492
|
-
*/
|
|
493
|
-
async function deleteIssue(projectId, issueIid) {
|
|
494
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`);
|
|
495
|
-
const response = await fetch(url.toString(), {
|
|
496
|
-
method: "DELETE",
|
|
497
|
-
headers: DEFAULT_HEADERS,
|
|
498
|
-
});
|
|
499
|
-
await handleGitLabError(response);
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* List all issue links for a specific issue
|
|
503
|
-
*
|
|
504
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
505
|
-
* @param {number} issueIid - The internal ID of the project issue
|
|
506
|
-
* @returns {Promise<GitLabIssueWithLinkDetails[]>} List of issues with link details
|
|
507
|
-
*/
|
|
508
|
-
async function listIssueLinks(projectId, issueIid) {
|
|
509
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links`);
|
|
510
|
-
const response = await fetch(url.toString(), {
|
|
511
|
-
headers: DEFAULT_HEADERS,
|
|
512
|
-
});
|
|
513
|
-
await handleGitLabError(response);
|
|
514
|
-
const data = await response.json();
|
|
515
|
-
return z.array(GitLabIssueWithLinkDetailsSchema).parse(data);
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Get a specific issue link
|
|
519
|
-
*
|
|
520
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
521
|
-
* @param {number} issueIid - The internal ID of the project issue
|
|
522
|
-
* @param {number} issueLinkId - The ID of the issue link
|
|
523
|
-
* @returns {Promise<GitLabIssueLink>} The issue link
|
|
524
|
-
*/
|
|
525
|
-
async function getIssueLink(projectId, issueIid, issueLinkId) {
|
|
526
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links/${issueLinkId}`);
|
|
527
|
-
const response = await fetch(url.toString(), {
|
|
528
|
-
headers: DEFAULT_HEADERS,
|
|
529
|
-
});
|
|
530
|
-
await handleGitLabError(response);
|
|
531
|
-
const data = await response.json();
|
|
532
|
-
return GitLabIssueLinkSchema.parse(data);
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Create an issue link between two issues
|
|
536
|
-
*
|
|
537
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
538
|
-
* @param {number} issueIid - The internal ID of the project issue
|
|
539
|
-
* @param {string} targetProjectId - The ID or URL-encoded path of the target project
|
|
540
|
-
* @param {number} targetIssueIid - The internal ID of the target project issue
|
|
541
|
-
* @param {string} linkType - The type of the relation (relates_to, blocks, is_blocked_by)
|
|
542
|
-
* @returns {Promise<GitLabIssueLink>} The created issue link
|
|
543
|
-
*/
|
|
544
|
-
async function createIssueLink(projectId, issueIid, targetProjectId, targetIssueIid, linkType = "relates_to") {
|
|
545
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links`);
|
|
546
|
-
const response = await fetch(url.toString(), {
|
|
547
|
-
method: "POST",
|
|
548
|
-
headers: DEFAULT_HEADERS,
|
|
549
|
-
body: JSON.stringify({
|
|
550
|
-
target_project_id: targetProjectId,
|
|
551
|
-
target_issue_iid: targetIssueIid,
|
|
552
|
-
link_type: linkType,
|
|
553
|
-
}),
|
|
554
|
-
});
|
|
555
|
-
await handleGitLabError(response);
|
|
556
|
-
const data = await response.json();
|
|
557
|
-
return GitLabIssueLinkSchema.parse(data);
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Delete an issue link
|
|
561
|
-
*
|
|
562
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
563
|
-
* @param {number} issueIid - The internal ID of the project issue
|
|
564
|
-
* @param {number} issueLinkId - The ID of the issue link
|
|
565
|
-
* @returns {Promise<void>}
|
|
566
|
-
*/
|
|
567
|
-
async function deleteIssueLink(projectId, issueIid, issueLinkId) {
|
|
568
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links/${issueLinkId}`);
|
|
569
|
-
const response = await fetch(url.toString(), {
|
|
570
|
-
method: "DELETE",
|
|
571
|
-
headers: DEFAULT_HEADERS,
|
|
572
|
-
});
|
|
573
|
-
await handleGitLabError(response);
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* Create a new merge request in a GitLab project
|
|
577
|
-
*
|
|
578
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
579
|
-
* @param {z.infer<typeof CreateMergeRequestOptionsSchema>} options - Merge request creation options
|
|
580
|
-
* @returns {Promise<GitLabMergeRequest>} The created merge request
|
|
581
|
-
*/
|
|
582
|
-
async function createMergeRequest(projectId, options) {
|
|
583
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`);
|
|
584
|
-
const response = await fetch(url.toString(), {
|
|
585
|
-
method: "POST",
|
|
586
|
-
headers: {
|
|
587
|
-
Accept: "application/json",
|
|
588
|
-
"Content-Type": "application/json",
|
|
589
|
-
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
|
590
|
-
},
|
|
591
|
-
body: JSON.stringify({
|
|
592
|
-
title: options.title,
|
|
593
|
-
description: options.description,
|
|
594
|
-
source_branch: options.source_branch,
|
|
595
|
-
target_branch: options.target_branch,
|
|
596
|
-
allow_collaboration: options.allow_collaboration,
|
|
597
|
-
draft: options.draft,
|
|
598
|
-
}),
|
|
599
|
-
});
|
|
600
|
-
if (response.status === 400) {
|
|
601
|
-
const errorBody = await response.text();
|
|
602
|
-
throw new Error(`Invalid request: ${errorBody}`);
|
|
603
|
-
}
|
|
604
|
-
if (!response.ok) {
|
|
605
|
-
const errorBody = await response.text();
|
|
606
|
-
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
607
|
-
}
|
|
608
|
-
const data = await response.json();
|
|
609
|
-
return GitLabMergeRequestSchema.parse(data);
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* List merge request discussion items
|
|
613
|
-
*
|
|
614
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
615
|
-
* @param {number} mergeRequestIid - The IID of a merge request
|
|
616
|
-
* @returns {Promise<GitLabDiscussion[]>} List of discussions
|
|
617
|
-
*/
|
|
618
|
-
async function listMergeRequestDiscussions(projectId, mergeRequestIid) {
|
|
619
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/discussions`);
|
|
620
|
-
const response = await fetch(url.toString(), {
|
|
621
|
-
headers: DEFAULT_HEADERS,
|
|
622
|
-
});
|
|
623
|
-
await handleGitLabError(response);
|
|
624
|
-
const data = await response.json();
|
|
625
|
-
// Ensure the response is parsed as an array of discussions
|
|
626
|
-
return z.array(GitLabDiscussionSchema).parse(data);
|
|
627
|
-
}
|
|
628
|
-
/**
|
|
629
|
-
* Modify an existing merge request thread note
|
|
630
|
-
*
|
|
631
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
632
|
-
* @param {number} mergeRequestIid - The IID of a merge request
|
|
633
|
-
* @param {string} discussionId - The ID of a thread
|
|
634
|
-
* @param {number} noteId - The ID of a thread note
|
|
635
|
-
* @param {string} body - The new content of the note
|
|
636
|
-
* @param {boolean} [resolved] - Resolve/unresolve state
|
|
637
|
-
* @returns {Promise<GitLabDiscussionNote>} The updated note
|
|
638
|
-
*/
|
|
639
|
-
async function updateMergeRequestNote(projectId, mergeRequestIid, discussionId, noteId, body, resolved) {
|
|
640
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`);
|
|
641
|
-
const payload = { body };
|
|
642
|
-
if (resolved !== undefined) {
|
|
643
|
-
payload.resolved = resolved;
|
|
644
|
-
}
|
|
645
|
-
const response = await fetch(url.toString(), {
|
|
646
|
-
method: "PUT",
|
|
647
|
-
headers: DEFAULT_HEADERS,
|
|
648
|
-
body: JSON.stringify(payload),
|
|
649
|
-
});
|
|
650
|
-
await handleGitLabError(response);
|
|
651
|
-
const data = await response.json();
|
|
652
|
-
return GitLabDiscussionNoteSchema.parse(data);
|
|
653
|
-
}
|
|
654
|
-
/**
|
|
655
|
-
* Create or update a file in a GitLab project
|
|
656
|
-
*
|
|
657
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
658
|
-
* @param {string} filePath - The path of the file to create or update
|
|
659
|
-
* @param {string} content - The content of the file
|
|
660
|
-
* @param {string} commitMessage - The commit message
|
|
661
|
-
* @param {string} branch - The branch name
|
|
662
|
-
* @param {string} [previousPath] - The previous path of the file in case of rename
|
|
663
|
-
* @returns {Promise<GitLabCreateUpdateFileResponse>} The file update response
|
|
664
|
-
*/
|
|
665
|
-
async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath, last_commit_id, commit_id) {
|
|
666
|
-
const encodedPath = encodeURIComponent(filePath);
|
|
667
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
|
|
668
|
-
const body = {
|
|
669
|
-
branch,
|
|
670
|
-
content,
|
|
671
|
-
commit_message: commitMessage,
|
|
672
|
-
encoding: "text",
|
|
673
|
-
...(previousPath ? { previous_path: previousPath } : {}),
|
|
674
|
-
};
|
|
675
|
-
// Check if file exists
|
|
676
|
-
let method = "POST";
|
|
677
|
-
try {
|
|
678
|
-
// Get file contents to check existence and retrieve commit IDs
|
|
679
|
-
const fileData = await getFileContents(projectId, filePath, branch);
|
|
680
|
-
method = "PUT";
|
|
681
|
-
// If fileData is not an array, it's a file content object with commit IDs
|
|
682
|
-
if (!Array.isArray(fileData)) {
|
|
683
|
-
// Use commit IDs from the file data if not provided in parameters
|
|
684
|
-
if (!commit_id && fileData.commit_id) {
|
|
685
|
-
body.commit_id = fileData.commit_id;
|
|
686
|
-
}
|
|
687
|
-
else if (commit_id) {
|
|
688
|
-
body.commit_id = commit_id;
|
|
689
|
-
}
|
|
690
|
-
if (!last_commit_id && fileData.last_commit_id) {
|
|
691
|
-
body.last_commit_id = fileData.last_commit_id;
|
|
692
|
-
}
|
|
693
|
-
else if (last_commit_id) {
|
|
694
|
-
body.last_commit_id = last_commit_id;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
catch (error) {
|
|
699
|
-
if (!(error instanceof Error && error.message.includes("File not found"))) {
|
|
700
|
-
throw error;
|
|
701
|
-
}
|
|
702
|
-
// File doesn't exist, use POST - no need for commit IDs for new files
|
|
703
|
-
// But still use any provided as parameters if they exist
|
|
704
|
-
if (commit_id) {
|
|
705
|
-
body.commit_id = commit_id;
|
|
706
|
-
}
|
|
707
|
-
if (last_commit_id) {
|
|
708
|
-
body.last_commit_id = last_commit_id;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
const response = await fetch(url.toString(), {
|
|
712
|
-
method,
|
|
713
|
-
headers: {
|
|
714
|
-
Accept: "application/json",
|
|
715
|
-
"Content-Type": "application/json",
|
|
716
|
-
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
|
717
|
-
},
|
|
718
|
-
body: JSON.stringify(body),
|
|
719
|
-
});
|
|
720
|
-
if (!response.ok) {
|
|
721
|
-
const errorBody = await response.text();
|
|
722
|
-
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
723
|
-
}
|
|
724
|
-
const data = await response.json();
|
|
725
|
-
return GitLabCreateUpdateFileResponseSchema.parse(data);
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Create a tree structure in a GitLab project repository
|
|
729
|
-
*
|
|
730
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
731
|
-
* @param {FileOperation[]} files - Array of file operations
|
|
732
|
-
* @param {string} [ref] - The name of the branch, tag or commit
|
|
733
|
-
* @returns {Promise<GitLabTree>} The created tree
|
|
734
|
-
*/
|
|
735
|
-
async function createTree(projectId, files, ref) {
|
|
736
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/tree`);
|
|
737
|
-
if (ref) {
|
|
738
|
-
url.searchParams.append("ref", ref);
|
|
739
|
-
}
|
|
740
|
-
const response = await fetch(url.toString(), {
|
|
741
|
-
method: "POST",
|
|
742
|
-
headers: {
|
|
743
|
-
Accept: "application/json",
|
|
744
|
-
"Content-Type": "application/json",
|
|
745
|
-
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
|
746
|
-
},
|
|
747
|
-
body: JSON.stringify({
|
|
748
|
-
files: files.map((file) => ({
|
|
749
|
-
file_path: file.path,
|
|
750
|
-
content: file.content,
|
|
751
|
-
encoding: "text",
|
|
752
|
-
})),
|
|
753
|
-
}),
|
|
754
|
-
});
|
|
755
|
-
if (response.status === 400) {
|
|
756
|
-
const errorBody = await response.text();
|
|
757
|
-
throw new Error(`Invalid request: ${errorBody}`);
|
|
758
|
-
}
|
|
759
|
-
if (!response.ok) {
|
|
760
|
-
const errorBody = await response.text();
|
|
761
|
-
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
762
|
-
}
|
|
763
|
-
const data = await response.json();
|
|
764
|
-
return GitLabTreeSchema.parse(data);
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Create a commit in a GitLab project repository
|
|
768
|
-
*
|
|
769
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
770
|
-
* @param {string} message - The commit message
|
|
771
|
-
* @param {string} branch - The branch name
|
|
772
|
-
* @param {FileOperation[]} actions - Array of file operations for the commit
|
|
773
|
-
* @returns {Promise<GitLabCommit>} The created commit
|
|
774
|
-
*/
|
|
775
|
-
async function createCommit(projectId, message, branch, actions) {
|
|
776
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits`);
|
|
777
|
-
const response = await fetch(url.toString(), {
|
|
778
|
-
method: "POST",
|
|
779
|
-
headers: {
|
|
780
|
-
Accept: "application/json",
|
|
781
|
-
"Content-Type": "application/json",
|
|
782
|
-
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
|
783
|
-
},
|
|
784
|
-
body: JSON.stringify({
|
|
785
|
-
branch,
|
|
786
|
-
commit_message: message,
|
|
787
|
-
actions: actions.map((action) => ({
|
|
788
|
-
action: "create",
|
|
789
|
-
file_path: action.path,
|
|
790
|
-
content: action.content,
|
|
791
|
-
encoding: "text",
|
|
792
|
-
})),
|
|
793
|
-
}),
|
|
794
|
-
});
|
|
795
|
-
if (response.status === 400) {
|
|
796
|
-
const errorBody = await response.text();
|
|
797
|
-
throw new Error(`Invalid request: ${errorBody}`);
|
|
798
|
-
}
|
|
799
|
-
if (!response.ok) {
|
|
800
|
-
const errorBody = await response.text();
|
|
801
|
-
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
802
|
-
}
|
|
803
|
-
const data = await response.json();
|
|
804
|
-
return GitLabCommitSchema.parse(data);
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* Search for GitLab projects
|
|
808
|
-
* 프로젝트 검색
|
|
809
|
-
*
|
|
810
|
-
* @param {string} query - The search query
|
|
811
|
-
* @param {number} [page=1] - The page number
|
|
812
|
-
* @param {number} [perPage=20] - Number of items per page
|
|
813
|
-
* @returns {Promise<GitLabSearchResponse>} The search results
|
|
814
|
-
*/
|
|
815
|
-
async function searchProjects(query, page = 1, perPage = 20) {
|
|
816
|
-
const url = new URL(`${GITLAB_API_URL}/projects`);
|
|
817
|
-
url.searchParams.append("search", query);
|
|
818
|
-
url.searchParams.append("page", page.toString());
|
|
819
|
-
url.searchParams.append("per_page", perPage.toString());
|
|
820
|
-
url.searchParams.append("order_by", "id");
|
|
821
|
-
url.searchParams.append("sort", "desc");
|
|
822
|
-
const response = await fetch(url.toString(), {
|
|
823
|
-
headers: {
|
|
824
|
-
Accept: "application/json",
|
|
825
|
-
"Content-Type": "application/json",
|
|
826
|
-
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
|
827
|
-
},
|
|
828
|
-
});
|
|
829
|
-
if (!response.ok) {
|
|
830
|
-
const errorBody = await response.text();
|
|
831
|
-
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
832
|
-
}
|
|
833
|
-
const projects = (await response.json());
|
|
834
|
-
const totalCount = response.headers.get("x-total");
|
|
835
|
-
const totalPages = response.headers.get("x-total-pages");
|
|
836
|
-
// GitLab API doesn't return these headers for results > 10,000
|
|
837
|
-
const count = totalCount ? parseInt(totalCount) : projects.length;
|
|
838
|
-
return GitLabSearchResponseSchema.parse({
|
|
839
|
-
count,
|
|
840
|
-
total_pages: totalPages ? parseInt(totalPages) : Math.ceil(count / perPage),
|
|
841
|
-
current_page: page,
|
|
842
|
-
items: projects,
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
|
-
/**
|
|
846
|
-
* Create a new GitLab repository
|
|
847
|
-
* 새 저장소 생성
|
|
848
|
-
*
|
|
849
|
-
* @param {z.infer<typeof CreateRepositoryOptionsSchema>} options - Repository creation options
|
|
850
|
-
* @returns {Promise<GitLabRepository>} The created repository
|
|
851
|
-
*/
|
|
852
|
-
async function createRepository(options) {
|
|
853
|
-
const response = await fetch(`${GITLAB_API_URL}/projects`, {
|
|
854
|
-
method: "POST",
|
|
855
|
-
headers: {
|
|
856
|
-
Accept: "application/json",
|
|
857
|
-
"Content-Type": "application/json",
|
|
858
|
-
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
|
859
|
-
},
|
|
860
|
-
body: JSON.stringify({
|
|
861
|
-
name: options.name,
|
|
862
|
-
description: options.description,
|
|
863
|
-
visibility: options.visibility,
|
|
864
|
-
initialize_with_readme: options.initialize_with_readme,
|
|
865
|
-
default_branch: "main",
|
|
866
|
-
path: options.name.toLowerCase().replace(/\s+/g, "-"),
|
|
867
|
-
}),
|
|
868
|
-
});
|
|
869
|
-
if (!response.ok) {
|
|
870
|
-
const errorBody = await response.text();
|
|
871
|
-
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
872
|
-
}
|
|
873
|
-
const data = await response.json();
|
|
874
|
-
return GitLabRepositorySchema.parse(data);
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Get merge request details
|
|
878
|
-
*
|
|
879
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
880
|
-
* @param {number} mergeRequestIid - The internal ID of the merge request
|
|
881
|
-
* @returns {Promise<GitLabMergeRequest>} The merge request details
|
|
882
|
-
*/
|
|
883
|
-
async function getMergeRequest(projectId, mergeRequestIid) {
|
|
884
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
|
|
885
|
-
const response = await fetch(url.toString(), {
|
|
886
|
-
headers: DEFAULT_HEADERS,
|
|
887
|
-
});
|
|
888
|
-
await handleGitLabError(response);
|
|
889
|
-
return GitLabMergeRequestSchema.parse(await response.json());
|
|
890
|
-
}
|
|
891
|
-
/**
|
|
892
|
-
* Get merge request changes/diffs
|
|
893
|
-
* MR 변경사항 조회 함수 (Function to retrieve merge request changes)
|
|
894
|
-
*
|
|
895
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
896
|
-
* @param {number} mergeRequestIid - The internal ID of the merge request
|
|
897
|
-
* @param {string} [view] - The view type for the diff (inline or parallel)
|
|
898
|
-
* @returns {Promise<GitLabMergeRequestDiff[]>} The merge request diffs
|
|
899
|
-
*/
|
|
900
|
-
async function getMergeRequestDiffs(projectId, mergeRequestIid, view) {
|
|
901
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/changes`);
|
|
902
|
-
if (view) {
|
|
903
|
-
url.searchParams.append("view", view);
|
|
904
|
-
}
|
|
905
|
-
const response = await fetch(url.toString(), {
|
|
906
|
-
headers: DEFAULT_HEADERS,
|
|
907
|
-
});
|
|
908
|
-
await handleGitLabError(response);
|
|
909
|
-
const data = (await response.json());
|
|
910
|
-
return z.array(GitLabMergeRequestDiffSchema).parse(data.changes);
|
|
911
|
-
}
|
|
912
|
-
/**
|
|
913
|
-
* Update a merge request
|
|
914
|
-
*
|
|
915
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
916
|
-
* @param {number} mergeRequestIid - The internal ID of the merge request
|
|
917
|
-
* @param {Object} options - The update options
|
|
918
|
-
* @returns {Promise<GitLabMergeRequest>} The updated merge request
|
|
919
|
-
*/
|
|
920
|
-
async function updateMergeRequest(projectId, mergeRequestIid, options) {
|
|
921
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
|
|
922
|
-
const response = await fetch(url.toString(), {
|
|
923
|
-
method: "PUT",
|
|
924
|
-
headers: DEFAULT_HEADERS,
|
|
925
|
-
body: JSON.stringify(options),
|
|
926
|
-
});
|
|
927
|
-
await handleGitLabError(response);
|
|
928
|
-
return GitLabMergeRequestSchema.parse(await response.json());
|
|
929
|
-
}
|
|
930
|
-
/**
|
|
931
|
-
* Create a new note (comment) on an issue or merge request
|
|
932
|
-
* (New function: createNote - Function to add a note (comment) to an issue or merge request)
|
|
933
|
-
*
|
|
934
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
935
|
-
* @param {"issue" | "merge_request"} noteableType - The type of the item to add a note to (issue or merge_request)
|
|
936
|
-
* @param {number} noteableIid - The internal ID of the issue or merge request
|
|
937
|
-
* @param {string} body - The content of the note
|
|
938
|
-
* @returns {Promise<any>} The created note
|
|
939
|
-
*/
|
|
940
|
-
async function createNote(projectId, noteableType, noteableIid, body) {
|
|
941
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/${noteableType}s/${noteableIid}/notes` // Using plural form (issues/merge_requests) as per GitLab API documentation
|
|
942
|
-
);
|
|
943
|
-
const response = await fetch(url.toString(), {
|
|
944
|
-
method: "POST",
|
|
945
|
-
headers: DEFAULT_HEADERS,
|
|
946
|
-
body: JSON.stringify({ body }),
|
|
947
|
-
});
|
|
948
|
-
if (!response.ok) {
|
|
949
|
-
const errorText = await response.text();
|
|
950
|
-
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
951
|
-
}
|
|
952
|
-
return await response.json();
|
|
953
|
-
}
|
|
954
|
-
/**
|
|
955
|
-
* List all namespaces
|
|
956
|
-
*
|
|
957
|
-
* @param {Object} options - Options for listing namespaces
|
|
958
|
-
* @param {string} [options.search] - Search query to filter namespaces
|
|
959
|
-
* @param {boolean} [options.owned_only] - Only return namespaces owned by the authenticated user
|
|
960
|
-
* @param {boolean} [options.top_level_only] - Only return top-level namespaces
|
|
961
|
-
* @returns {Promise<GitLabNamespace[]>} List of namespaces
|
|
962
|
-
*/
|
|
963
|
-
async function listNamespaces(options) {
|
|
964
|
-
const url = new URL(`${GITLAB_API_URL}/namespaces`);
|
|
965
|
-
if (options.search) {
|
|
966
|
-
url.searchParams.append("search", options.search);
|
|
967
|
-
}
|
|
968
|
-
if (options.owned_only) {
|
|
969
|
-
url.searchParams.append("owned_only", "true");
|
|
970
|
-
}
|
|
971
|
-
if (options.top_level_only) {
|
|
972
|
-
url.searchParams.append("top_level_only", "true");
|
|
973
|
-
}
|
|
974
|
-
const response = await fetch(url.toString(), {
|
|
975
|
-
headers: DEFAULT_HEADERS,
|
|
976
|
-
});
|
|
977
|
-
await handleGitLabError(response);
|
|
978
|
-
const data = await response.json();
|
|
979
|
-
return z.array(GitLabNamespaceSchema).parse(data);
|
|
980
|
-
}
|
|
981
|
-
/**
|
|
982
|
-
* Get details on a namespace
|
|
983
|
-
*
|
|
984
|
-
* @param {string} id - The ID or URL-encoded path of the namespace
|
|
985
|
-
* @returns {Promise<GitLabNamespace>} The namespace details
|
|
986
|
-
*/
|
|
987
|
-
async function getNamespace(id) {
|
|
988
|
-
const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(id)}`);
|
|
989
|
-
const response = await fetch(url.toString(), {
|
|
990
|
-
headers: DEFAULT_HEADERS,
|
|
991
|
-
});
|
|
992
|
-
await handleGitLabError(response);
|
|
993
|
-
const data = await response.json();
|
|
994
|
-
return GitLabNamespaceSchema.parse(data);
|
|
995
|
-
}
|
|
996
|
-
/**
|
|
997
|
-
* Verify if a namespace exists
|
|
998
|
-
*
|
|
999
|
-
* @param {string} namespacePath - The path of the namespace to check
|
|
1000
|
-
* @param {number} [parentId] - The ID of the parent namespace
|
|
1001
|
-
* @returns {Promise<GitLabNamespaceExistsResponse>} The verification result
|
|
1002
|
-
*/
|
|
1003
|
-
async function verifyNamespaceExistence(namespacePath, parentId) {
|
|
1004
|
-
const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(namespacePath)}/exists`);
|
|
1005
|
-
if (parentId) {
|
|
1006
|
-
url.searchParams.append("parent_id", parentId.toString());
|
|
1007
|
-
}
|
|
1008
|
-
const response = await fetch(url.toString(), {
|
|
1009
|
-
headers: DEFAULT_HEADERS,
|
|
1010
|
-
});
|
|
1011
|
-
await handleGitLabError(response);
|
|
1012
|
-
const data = await response.json();
|
|
1013
|
-
return GitLabNamespaceExistsResponseSchema.parse(data);
|
|
1014
|
-
}
|
|
1015
|
-
/**
|
|
1016
|
-
* Get a single project
|
|
1017
|
-
*
|
|
1018
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
1019
|
-
* @param {Object} options - Options for getting project details
|
|
1020
|
-
* @param {boolean} [options.license] - Include project license data
|
|
1021
|
-
* @param {boolean} [options.statistics] - Include project statistics
|
|
1022
|
-
* @param {boolean} [options.with_custom_attributes] - Include custom attributes in response
|
|
1023
|
-
* @returns {Promise<GitLabProject>} Project details
|
|
1024
|
-
*/
|
|
1025
|
-
async function getProject(projectId, options = {}) {
|
|
1026
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`);
|
|
1027
|
-
if (options.license) {
|
|
1028
|
-
url.searchParams.append("license", "true");
|
|
1029
|
-
}
|
|
1030
|
-
if (options.statistics) {
|
|
1031
|
-
url.searchParams.append("statistics", "true");
|
|
1032
|
-
}
|
|
1033
|
-
if (options.with_custom_attributes) {
|
|
1034
|
-
url.searchParams.append("with_custom_attributes", "true");
|
|
1035
|
-
}
|
|
1036
|
-
const response = await fetch(url.toString(), {
|
|
1037
|
-
headers: DEFAULT_HEADERS,
|
|
1038
|
-
});
|
|
1039
|
-
await handleGitLabError(response);
|
|
1040
|
-
const data = await response.json();
|
|
1041
|
-
return GitLabRepositorySchema.parse(data);
|
|
1042
|
-
}
|
|
1043
|
-
/**
|
|
1044
|
-
* List projects
|
|
1045
|
-
*
|
|
1046
|
-
* @param {Object} options - Options for listing projects
|
|
1047
|
-
* @returns {Promise<GitLabProject[]>} List of projects
|
|
1048
|
-
*/
|
|
1049
|
-
async function listProjects(options = {}) {
|
|
1050
|
-
// Construct the query parameters
|
|
1051
|
-
const params = new URLSearchParams();
|
|
1052
|
-
for (const [key, value] of Object.entries(options)) {
|
|
1053
|
-
if (value !== undefined && value !== null) {
|
|
1054
|
-
if (typeof value === "boolean") {
|
|
1055
|
-
params.append(key, value ? "true" : "false");
|
|
1056
|
-
}
|
|
1057
|
-
else {
|
|
1058
|
-
params.append(key, String(value));
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
// Make the API request
|
|
1063
|
-
const response = await fetch(`${GITLAB_API_URL}/projects?${params.toString()}`, {
|
|
1064
|
-
method: "GET",
|
|
1065
|
-
headers: DEFAULT_HEADERS,
|
|
1066
|
-
});
|
|
1067
|
-
// Handle errors
|
|
1068
|
-
await handleGitLabError(response);
|
|
1069
|
-
// Parse and return the data
|
|
1070
|
-
const data = await response.json();
|
|
1071
|
-
return z.array(GitLabProjectSchema).parse(data);
|
|
1072
|
-
}
|
|
1073
|
-
/**
|
|
1074
|
-
* List labels for a project
|
|
1075
|
-
*
|
|
1076
|
-
* @param projectId The ID or URL-encoded path of the project
|
|
1077
|
-
* @param options Optional parameters for listing labels
|
|
1078
|
-
* @returns Array of GitLab labels
|
|
1079
|
-
*/
|
|
1080
|
-
async function listLabels(projectId, options = {}) {
|
|
1081
|
-
// Construct the URL with project path
|
|
1082
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`);
|
|
1083
|
-
// Add query parameters
|
|
1084
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
1085
|
-
if (value !== undefined) {
|
|
1086
|
-
if (typeof value === "boolean") {
|
|
1087
|
-
url.searchParams.append(key, value ? "true" : "false");
|
|
1088
|
-
}
|
|
1089
|
-
else {
|
|
1090
|
-
url.searchParams.append(key, String(value));
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
});
|
|
1094
|
-
// Make the API request
|
|
1095
|
-
const response = await fetch(url.toString(), {
|
|
1096
|
-
headers: DEFAULT_HEADERS,
|
|
1097
|
-
});
|
|
1098
|
-
// Handle errors
|
|
1099
|
-
await handleGitLabError(response);
|
|
1100
|
-
// Parse and return the data
|
|
1101
|
-
const data = await response.json();
|
|
1102
|
-
return data;
|
|
1103
|
-
}
|
|
1104
|
-
/**
|
|
1105
|
-
* Get a single label from a project
|
|
1106
|
-
*
|
|
1107
|
-
* @param projectId The ID or URL-encoded path of the project
|
|
1108
|
-
* @param labelId The ID or name of the label
|
|
1109
|
-
* @param includeAncestorGroups Whether to include ancestor groups
|
|
1110
|
-
* @returns GitLab label
|
|
1111
|
-
*/
|
|
1112
|
-
async function getLabel(projectId, labelId, includeAncestorGroups) {
|
|
1113
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`);
|
|
1114
|
-
// Add query parameters
|
|
1115
|
-
if (includeAncestorGroups !== undefined) {
|
|
1116
|
-
url.searchParams.append("include_ancestor_groups", includeAncestorGroups ? "true" : "false");
|
|
1117
|
-
}
|
|
1118
|
-
// Make the API request
|
|
1119
|
-
const response = await fetch(url.toString(), {
|
|
1120
|
-
headers: DEFAULT_HEADERS,
|
|
1121
|
-
});
|
|
1122
|
-
// Handle errors
|
|
1123
|
-
await handleGitLabError(response);
|
|
1124
|
-
// Parse and return the data
|
|
1125
|
-
const data = await response.json();
|
|
1126
|
-
return data;
|
|
1127
|
-
}
|
|
1128
|
-
/**
|
|
1129
|
-
* Create a new label in a project
|
|
1130
|
-
*
|
|
1131
|
-
* @param projectId The ID or URL-encoded path of the project
|
|
1132
|
-
* @param options Options for creating the label
|
|
1133
|
-
* @returns Created GitLab label
|
|
1134
|
-
*/
|
|
1135
|
-
async function createLabel(projectId, options) {
|
|
1136
|
-
// Make the API request
|
|
1137
|
-
const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`, {
|
|
1138
|
-
method: "POST",
|
|
1139
|
-
headers: DEFAULT_HEADERS,
|
|
1140
|
-
body: JSON.stringify(options),
|
|
1141
|
-
});
|
|
1142
|
-
// Handle errors
|
|
1143
|
-
await handleGitLabError(response);
|
|
1144
|
-
// Parse and return the data
|
|
1145
|
-
const data = await response.json();
|
|
1146
|
-
return data;
|
|
1147
|
-
}
|
|
1148
|
-
/**
|
|
1149
|
-
* Update an existing label in a project
|
|
1150
|
-
*
|
|
1151
|
-
* @param projectId The ID or URL-encoded path of the project
|
|
1152
|
-
* @param labelId The ID or name of the label to update
|
|
1153
|
-
* @param options Options for updating the label
|
|
1154
|
-
* @returns Updated GitLab label
|
|
1155
|
-
*/
|
|
1156
|
-
async function updateLabel(projectId, labelId, options) {
|
|
1157
|
-
// Make the API request
|
|
1158
|
-
const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`, {
|
|
1159
|
-
method: "PUT",
|
|
1160
|
-
headers: DEFAULT_HEADERS,
|
|
1161
|
-
body: JSON.stringify(options),
|
|
1162
|
-
});
|
|
1163
|
-
// Handle errors
|
|
1164
|
-
await handleGitLabError(response);
|
|
1165
|
-
// Parse and return the data
|
|
1166
|
-
const data = await response.json();
|
|
1167
|
-
return data;
|
|
1168
|
-
}
|
|
1169
|
-
/**
|
|
1170
|
-
* Delete a label from a project
|
|
1171
|
-
*
|
|
1172
|
-
* @param projectId The ID or URL-encoded path of the project
|
|
1173
|
-
* @param labelId The ID or name of the label to delete
|
|
1174
|
-
*/
|
|
1175
|
-
async function deleteLabel(projectId, labelId) {
|
|
1176
|
-
// Make the API request
|
|
1177
|
-
const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`, {
|
|
1178
|
-
method: "DELETE",
|
|
1179
|
-
headers: DEFAULT_HEADERS,
|
|
1180
|
-
});
|
|
1181
|
-
// Handle errors
|
|
1182
|
-
await handleGitLabError(response);
|
|
1183
|
-
}
|
|
1184
|
-
/**
|
|
1185
|
-
* List all projects in a GitLab group
|
|
1186
|
-
*
|
|
1187
|
-
* @param {z.infer<typeof ListGroupProjectsSchema>} options - Options for listing group projects
|
|
1188
|
-
* @returns {Promise<GitLabProject[]>} Array of projects in the group
|
|
1189
|
-
*/
|
|
1190
|
-
async function listGroupProjects(options) {
|
|
1191
|
-
const url = new URL(`${GITLAB_API_URL}/groups/${encodeURIComponent(options.group_id)}/projects`);
|
|
1192
|
-
// Add optional parameters to URL
|
|
1193
|
-
if (options.include_subgroups)
|
|
1194
|
-
url.searchParams.append("include_subgroups", "true");
|
|
1195
|
-
if (options.search)
|
|
1196
|
-
url.searchParams.append("search", options.search);
|
|
1197
|
-
if (options.order_by)
|
|
1198
|
-
url.searchParams.append("order_by", options.order_by);
|
|
1199
|
-
if (options.sort)
|
|
1200
|
-
url.searchParams.append("sort", options.sort);
|
|
1201
|
-
if (options.page)
|
|
1202
|
-
url.searchParams.append("page", options.page.toString());
|
|
1203
|
-
if (options.per_page)
|
|
1204
|
-
url.searchParams.append("per_page", options.per_page.toString());
|
|
1205
|
-
if (options.archived !== undefined)
|
|
1206
|
-
url.searchParams.append("archived", options.archived.toString());
|
|
1207
|
-
if (options.visibility)
|
|
1208
|
-
url.searchParams.append("visibility", options.visibility);
|
|
1209
|
-
if (options.with_issues_enabled !== undefined)
|
|
1210
|
-
url.searchParams.append("with_issues_enabled", options.with_issues_enabled.toString());
|
|
1211
|
-
if (options.with_merge_requests_enabled !== undefined)
|
|
1212
|
-
url.searchParams.append("with_merge_requests_enabled", options.with_merge_requests_enabled.toString());
|
|
1213
|
-
if (options.min_access_level !== undefined)
|
|
1214
|
-
url.searchParams.append("min_access_level", options.min_access_level.toString());
|
|
1215
|
-
if (options.with_programming_language)
|
|
1216
|
-
url.searchParams.append("with_programming_language", options.with_programming_language);
|
|
1217
|
-
if (options.starred !== undefined)
|
|
1218
|
-
url.searchParams.append("starred", options.starred.toString());
|
|
1219
|
-
if (options.statistics !== undefined)
|
|
1220
|
-
url.searchParams.append("statistics", options.statistics.toString());
|
|
1221
|
-
if (options.with_custom_attributes !== undefined)
|
|
1222
|
-
url.searchParams.append("with_custom_attributes", options.with_custom_attributes.toString());
|
|
1223
|
-
if (options.with_security_reports !== undefined)
|
|
1224
|
-
url.searchParams.append("with_security_reports", options.with_security_reports.toString());
|
|
1225
|
-
const response = await fetch(url.toString(), {
|
|
1226
|
-
method: "GET",
|
|
1227
|
-
headers: DEFAULT_HEADERS,
|
|
1228
|
-
});
|
|
1229
|
-
await handleGitLabError(response);
|
|
1230
|
-
const projects = await response.json();
|
|
1231
|
-
return GitLabProjectSchema.array().parse(projects);
|
|
1232
|
-
}
|
|
1233
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1234
|
-
// If read-only mode is enabled, filter out write operations
|
|
1235
|
-
const tools = GITLAB_READ_ONLY_MODE
|
|
1236
|
-
? allTools.filter((tool) => readOnlyTools.includes(tool.name))
|
|
1237
|
-
: allTools;
|
|
1238
|
-
return {
|
|
1239
|
-
tools,
|
|
1240
|
-
};
|
|
1241
|
-
});
|
|
1242
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1243
|
-
try {
|
|
1244
|
-
if (!request.params.arguments) {
|
|
1245
|
-
throw new Error("Arguments are required");
|
|
1246
|
-
}
|
|
1247
|
-
switch (request.params.name) {
|
|
1248
|
-
case "fork_repository": {
|
|
1249
|
-
const forkArgs = ForkRepositorySchema.parse(request.params.arguments);
|
|
1250
|
-
try {
|
|
1251
|
-
const forkedProject = await forkProject(forkArgs.project_id, forkArgs.namespace);
|
|
1252
|
-
return {
|
|
1253
|
-
content: [
|
|
1254
|
-
{ type: "text", text: JSON.stringify(forkedProject, null, 2) },
|
|
1255
|
-
],
|
|
1256
|
-
};
|
|
1257
|
-
}
|
|
1258
|
-
catch (forkError) {
|
|
1259
|
-
console.error("Error forking repository:", forkError);
|
|
1260
|
-
let forkErrorMessage = "Failed to fork repository";
|
|
1261
|
-
if (forkError instanceof Error) {
|
|
1262
|
-
forkErrorMessage = `${forkErrorMessage}: ${forkError.message}`;
|
|
1263
|
-
}
|
|
1264
|
-
return {
|
|
1265
|
-
content: [
|
|
1266
|
-
{
|
|
1267
|
-
type: "text",
|
|
1268
|
-
text: JSON.stringify({ error: forkErrorMessage }, null, 2),
|
|
1269
|
-
},
|
|
1270
|
-
],
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
case "create_branch": {
|
|
1275
|
-
const args = CreateBranchSchema.parse(request.params.arguments);
|
|
1276
|
-
let ref = args.ref;
|
|
1277
|
-
if (!ref) {
|
|
1278
|
-
ref = await getDefaultBranchRef(args.project_id);
|
|
1279
|
-
}
|
|
1280
|
-
const branch = await createBranch(args.project_id, {
|
|
1281
|
-
name: args.branch,
|
|
1282
|
-
ref,
|
|
1283
|
-
});
|
|
1284
|
-
return {
|
|
1285
|
-
content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
case "search_repositories": {
|
|
1289
|
-
const args = SearchRepositoriesSchema.parse(request.params.arguments);
|
|
1290
|
-
const results = await searchProjects(args.search, args.page, args.per_page);
|
|
1291
|
-
return {
|
|
1292
|
-
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
1293
|
-
};
|
|
1294
|
-
}
|
|
1295
|
-
case "create_repository": {
|
|
1296
|
-
const args = CreateRepositorySchema.parse(request.params.arguments);
|
|
1297
|
-
const repository = await createRepository(args);
|
|
1298
|
-
return {
|
|
1299
|
-
content: [
|
|
1300
|
-
{ type: "text", text: JSON.stringify(repository, null, 2) },
|
|
1301
|
-
],
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
case "get_file_contents": {
|
|
1305
|
-
const args = GetFileContentsSchema.parse(request.params.arguments);
|
|
1306
|
-
const contents = await getFileContents(args.project_id, args.file_path, args.ref);
|
|
1307
|
-
return {
|
|
1308
|
-
content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
case "create_or_update_file": {
|
|
1312
|
-
const args = CreateOrUpdateFileSchema.parse(request.params.arguments);
|
|
1313
|
-
const result = await createOrUpdateFile(args.project_id, args.file_path, args.content, args.commit_message, args.branch, args.previous_path, args.last_commit_id, args.commit_id);
|
|
1314
|
-
return {
|
|
1315
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1316
|
-
};
|
|
1317
|
-
}
|
|
1318
|
-
case "push_files": {
|
|
1319
|
-
const args = PushFilesSchema.parse(request.params.arguments);
|
|
1320
|
-
const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map((f) => ({ path: f.file_path, content: f.content })));
|
|
1321
|
-
return {
|
|
1322
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
case "create_issue": {
|
|
1326
|
-
const args = CreateIssueSchema.parse(request.params.arguments);
|
|
1327
|
-
const { project_id, ...options } = args;
|
|
1328
|
-
const issue = await createIssue(project_id, options);
|
|
1329
|
-
return {
|
|
1330
|
-
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
case "create_merge_request": {
|
|
1334
|
-
const args = CreateMergeRequestSchema.parse(request.params.arguments);
|
|
1335
|
-
const { project_id, ...options } = args;
|
|
1336
|
-
const mergeRequest = await createMergeRequest(project_id, options);
|
|
1337
|
-
return {
|
|
1338
|
-
content: [
|
|
1339
|
-
{ type: "text", text: JSON.stringify(mergeRequest, null, 2) },
|
|
1340
|
-
],
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
case "update_merge_request_note": {
|
|
1344
|
-
const args = UpdateMergeRequestNoteSchema.parse(request.params.arguments);
|
|
1345
|
-
const note = await updateMergeRequestNote(args.project_id, args.merge_request_iid, args.discussion_id, args.note_id, args.body, args.resolved // Pass resolved if provided
|
|
1346
|
-
);
|
|
1347
|
-
return {
|
|
1348
|
-
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
1349
|
-
};
|
|
1350
|
-
}
|
|
1351
|
-
case "get_merge_request": {
|
|
1352
|
-
const args = GetMergeRequestSchema.parse(request.params.arguments);
|
|
1353
|
-
const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid);
|
|
1354
|
-
return {
|
|
1355
|
-
content: [
|
|
1356
|
-
{ type: "text", text: JSON.stringify(mergeRequest, null, 2) },
|
|
1357
|
-
],
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
case "get_merge_request_diffs": {
|
|
1361
|
-
const args = GetMergeRequestDiffsSchema.parse(request.params.arguments);
|
|
1362
|
-
const diffs = await getMergeRequestDiffs(args.project_id, args.merge_request_iid, args.view);
|
|
1363
|
-
return {
|
|
1364
|
-
content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }],
|
|
1365
|
-
};
|
|
1366
|
-
}
|
|
1367
|
-
case "update_merge_request": {
|
|
1368
|
-
const args = UpdateMergeRequestSchema.parse(request.params.arguments);
|
|
1369
|
-
const { project_id, merge_request_iid, ...options } = args;
|
|
1370
|
-
const mergeRequest = await updateMergeRequest(project_id, merge_request_iid, options);
|
|
1371
|
-
return {
|
|
1372
|
-
content: [
|
|
1373
|
-
{ type: "text", text: JSON.stringify(mergeRequest, null, 2) },
|
|
1374
|
-
],
|
|
1375
|
-
};
|
|
1376
|
-
}
|
|
1377
|
-
case "list_merge_request_discussions": {
|
|
1378
|
-
const args = ListMergeRequestDiscussionsSchema.parse(request.params.arguments);
|
|
1379
|
-
const discussions = await listMergeRequestDiscussions(args.project_id, args.merge_request_iid);
|
|
1380
|
-
return {
|
|
1381
|
-
content: [
|
|
1382
|
-
{ type: "text", text: JSON.stringify(discussions, null, 2) },
|
|
1383
|
-
],
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
case "list_namespaces": {
|
|
1387
|
-
const args = ListNamespacesSchema.parse(request.params.arguments);
|
|
1388
|
-
const url = new URL(`${GITLAB_API_URL}/namespaces`);
|
|
1389
|
-
if (args.search) {
|
|
1390
|
-
url.searchParams.append("search", args.search);
|
|
1391
|
-
}
|
|
1392
|
-
if (args.page) {
|
|
1393
|
-
url.searchParams.append("page", args.page.toString());
|
|
1394
|
-
}
|
|
1395
|
-
if (args.per_page) {
|
|
1396
|
-
url.searchParams.append("per_page", args.per_page.toString());
|
|
1397
|
-
}
|
|
1398
|
-
if (args.owned) {
|
|
1399
|
-
url.searchParams.append("owned", args.owned.toString());
|
|
1400
|
-
}
|
|
1401
|
-
const response = await fetch(url.toString(), {
|
|
1402
|
-
headers: DEFAULT_HEADERS,
|
|
1403
|
-
});
|
|
1404
|
-
await handleGitLabError(response);
|
|
1405
|
-
const data = await response.json();
|
|
1406
|
-
const namespaces = z.array(GitLabNamespaceSchema).parse(data);
|
|
1407
|
-
return {
|
|
1408
|
-
content: [
|
|
1409
|
-
{ type: "text", text: JSON.stringify(namespaces, null, 2) },
|
|
1410
|
-
],
|
|
1411
|
-
};
|
|
1412
|
-
}
|
|
1413
|
-
case "get_namespace": {
|
|
1414
|
-
const args = GetNamespaceSchema.parse(request.params.arguments);
|
|
1415
|
-
const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.namespace_id)}`);
|
|
1416
|
-
const response = await fetch(url.toString(), {
|
|
1417
|
-
headers: DEFAULT_HEADERS,
|
|
1418
|
-
});
|
|
1419
|
-
await handleGitLabError(response);
|
|
1420
|
-
const data = await response.json();
|
|
1421
|
-
const namespace = GitLabNamespaceSchema.parse(data);
|
|
1422
|
-
return {
|
|
1423
|
-
content: [{ type: "text", text: JSON.stringify(namespace, null, 2) }],
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
case "verify_namespace": {
|
|
1427
|
-
const args = VerifyNamespaceSchema.parse(request.params.arguments);
|
|
1428
|
-
const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.path)}/exists`);
|
|
1429
|
-
const response = await fetch(url.toString(), {
|
|
1430
|
-
headers: DEFAULT_HEADERS,
|
|
1431
|
-
});
|
|
1432
|
-
await handleGitLabError(response);
|
|
1433
|
-
const data = await response.json();
|
|
1434
|
-
const namespaceExists = GitLabNamespaceExistsResponseSchema.parse(data);
|
|
1435
|
-
return {
|
|
1436
|
-
content: [
|
|
1437
|
-
{ type: "text", text: JSON.stringify(namespaceExists, null, 2) },
|
|
1438
|
-
],
|
|
1439
|
-
};
|
|
1440
|
-
}
|
|
1441
|
-
case "get_project": {
|
|
1442
|
-
const args = GetProjectSchema.parse(request.params.arguments);
|
|
1443
|
-
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(args.project_id)}`);
|
|
1444
|
-
const response = await fetch(url.toString(), {
|
|
1445
|
-
headers: DEFAULT_HEADERS,
|
|
1446
|
-
});
|
|
1447
|
-
await handleGitLabError(response);
|
|
1448
|
-
const data = await response.json();
|
|
1449
|
-
const project = GitLabProjectSchema.parse(data);
|
|
1450
|
-
return {
|
|
1451
|
-
content: [{ type: "text", text: JSON.stringify(project, null, 2) }],
|
|
1452
|
-
};
|
|
1453
|
-
}
|
|
1454
|
-
case "list_projects": {
|
|
1455
|
-
const args = ListProjectsSchema.parse(request.params.arguments);
|
|
1456
|
-
const projects = await listProjects(args);
|
|
1457
|
-
return {
|
|
1458
|
-
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
|
|
1459
|
-
};
|
|
1460
|
-
}
|
|
1461
|
-
case "create_note": {
|
|
1462
|
-
const args = CreateNoteSchema.parse(request.params.arguments);
|
|
1463
|
-
const { project_id, noteable_type, noteable_iid, body } = args;
|
|
1464
|
-
const note = await createNote(project_id, noteable_type, noteable_iid, body);
|
|
1465
|
-
return {
|
|
1466
|
-
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
1467
|
-
};
|
|
1468
|
-
}
|
|
1469
|
-
case "list_issues": {
|
|
1470
|
-
const args = ListIssuesSchema.parse(request.params.arguments);
|
|
1471
|
-
const { project_id, ...options } = args;
|
|
1472
|
-
const issues = await listIssues(project_id, options);
|
|
1473
|
-
return {
|
|
1474
|
-
content: [{ type: "text", text: JSON.stringify(issues, null, 2) }],
|
|
1475
|
-
};
|
|
1476
|
-
}
|
|
1477
|
-
case "get_issue": {
|
|
1478
|
-
const args = GetIssueSchema.parse(request.params.arguments);
|
|
1479
|
-
const issue = await getIssue(args.project_id, args.issue_iid);
|
|
1480
|
-
return {
|
|
1481
|
-
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
|
|
1482
|
-
};
|
|
1483
|
-
}
|
|
1484
|
-
case "update_issue": {
|
|
1485
|
-
const args = UpdateIssueSchema.parse(request.params.arguments);
|
|
1486
|
-
const { project_id, issue_iid, ...options } = args;
|
|
1487
|
-
const issue = await updateIssue(project_id, issue_iid, options);
|
|
1488
|
-
return {
|
|
1489
|
-
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
|
|
1490
|
-
};
|
|
1491
|
-
}
|
|
1492
|
-
case "delete_issue": {
|
|
1493
|
-
const args = DeleteIssueSchema.parse(request.params.arguments);
|
|
1494
|
-
await deleteIssue(args.project_id, args.issue_iid);
|
|
1495
|
-
return {
|
|
1496
|
-
content: [
|
|
1497
|
-
{
|
|
1498
|
-
type: "text",
|
|
1499
|
-
text: JSON.stringify({ status: "success", message: "Issue deleted successfully" }, null, 2),
|
|
1500
|
-
},
|
|
1501
|
-
],
|
|
1502
|
-
};
|
|
1503
|
-
}
|
|
1504
|
-
case "list_issue_links": {
|
|
1505
|
-
const args = ListIssueLinksSchema.parse(request.params.arguments);
|
|
1506
|
-
const links = await listIssueLinks(args.project_id, args.issue_iid);
|
|
1507
|
-
return {
|
|
1508
|
-
content: [{ type: "text", text: JSON.stringify(links, null, 2) }],
|
|
1509
|
-
};
|
|
1510
|
-
}
|
|
1511
|
-
case "get_issue_link": {
|
|
1512
|
-
const args = GetIssueLinkSchema.parse(request.params.arguments);
|
|
1513
|
-
const link = await getIssueLink(args.project_id, args.issue_iid, args.issue_link_id);
|
|
1514
|
-
return {
|
|
1515
|
-
content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
|
|
1516
|
-
};
|
|
1517
|
-
}
|
|
1518
|
-
case "create_issue_link": {
|
|
1519
|
-
const args = CreateIssueLinkSchema.parse(request.params.arguments);
|
|
1520
|
-
const link = await createIssueLink(args.project_id, args.issue_iid, args.target_project_id, args.target_issue_iid, args.link_type);
|
|
1521
|
-
return {
|
|
1522
|
-
content: [{ type: "text", text: JSON.stringify(link, null, 2) }],
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1525
|
-
case "delete_issue_link": {
|
|
1526
|
-
const args = DeleteIssueLinkSchema.parse(request.params.arguments);
|
|
1527
|
-
await deleteIssueLink(args.project_id, args.issue_iid, args.issue_link_id);
|
|
1528
|
-
return {
|
|
1529
|
-
content: [
|
|
1530
|
-
{
|
|
1531
|
-
type: "text",
|
|
1532
|
-
text: JSON.stringify({
|
|
1533
|
-
status: "success",
|
|
1534
|
-
message: "Issue link deleted successfully",
|
|
1535
|
-
}, null, 2),
|
|
1536
|
-
},
|
|
1537
|
-
],
|
|
1538
|
-
};
|
|
1539
|
-
}
|
|
1540
|
-
case "list_labels": {
|
|
1541
|
-
const args = ListLabelsSchema.parse(request.params.arguments);
|
|
1542
|
-
const labels = await listLabels(args.project_id, args);
|
|
1543
|
-
return {
|
|
1544
|
-
content: [{ type: "text", text: JSON.stringify(labels, null, 2) }],
|
|
1545
|
-
};
|
|
1546
|
-
}
|
|
1547
|
-
case "get_label": {
|
|
1548
|
-
const args = GetLabelSchema.parse(request.params.arguments);
|
|
1549
|
-
const label = await getLabel(args.project_id, args.label_id, args.include_ancestor_groups);
|
|
1550
|
-
return {
|
|
1551
|
-
content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
|
|
1552
|
-
};
|
|
1553
|
-
}
|
|
1554
|
-
case "create_label": {
|
|
1555
|
-
const args = CreateLabelSchema.parse(request.params.arguments);
|
|
1556
|
-
const label = await createLabel(args.project_id, args);
|
|
1557
|
-
return {
|
|
1558
|
-
content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
|
|
1559
|
-
};
|
|
1560
|
-
}
|
|
1561
|
-
case "update_label": {
|
|
1562
|
-
const args = UpdateLabelSchema.parse(request.params.arguments);
|
|
1563
|
-
const { project_id, label_id, ...options } = args;
|
|
1564
|
-
const label = await updateLabel(project_id, label_id, options);
|
|
1565
|
-
return {
|
|
1566
|
-
content: [{ type: "text", text: JSON.stringify(label, null, 2) }],
|
|
1567
|
-
};
|
|
1568
|
-
}
|
|
1569
|
-
case "delete_label": {
|
|
1570
|
-
const args = DeleteLabelSchema.parse(request.params.arguments);
|
|
1571
|
-
await deleteLabel(args.project_id, args.label_id);
|
|
1572
|
-
return {
|
|
1573
|
-
content: [
|
|
1574
|
-
{
|
|
1575
|
-
type: "text",
|
|
1576
|
-
text: JSON.stringify({ status: "success", message: "Label deleted successfully" }, null, 2),
|
|
1577
|
-
},
|
|
1578
|
-
],
|
|
1579
|
-
};
|
|
1580
|
-
}
|
|
1581
|
-
case "list_group_projects": {
|
|
1582
|
-
const args = ListGroupProjectsSchema.parse(request.params.arguments);
|
|
1583
|
-
const projects = await listGroupProjects(args);
|
|
1584
|
-
return {
|
|
1585
|
-
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
|
|
1586
|
-
};
|
|
1587
|
-
}
|
|
1588
|
-
default:
|
|
1589
|
-
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
catch (error) {
|
|
1593
|
-
if (error instanceof z.ZodError) {
|
|
1594
|
-
throw new Error(`Invalid arguments: ${error.errors
|
|
1595
|
-
.map((e) => `${e.path.join(".")}: ${e.message}`)
|
|
1596
|
-
.join(", ")}`);
|
|
1597
|
-
}
|
|
1598
|
-
throw error;
|
|
1599
|
-
}
|
|
1600
|
-
});
|
|
1601
|
-
/**
|
|
1602
|
-
* Initialize and run the server
|
|
1603
|
-
*/
|
|
1604
|
-
async function runServer() {
|
|
1605
|
-
if (isSSE) {
|
|
1606
|
-
const app = express();
|
|
1607
|
-
let transport;
|
|
1608
|
-
app.get("/sse", async (req, res) => {
|
|
1609
|
-
console.log("Establishing new SSE connection");
|
|
1610
|
-
transport = new SSEServerTransport("/messages", res);
|
|
1611
|
-
await server.connect(transport);
|
|
1612
|
-
});
|
|
1613
|
-
app.post("/messages", async (req, res) => {
|
|
1614
|
-
await transport?.handlePostMessage(req, res);
|
|
1615
|
-
});
|
|
1616
|
-
app.listen(port, () => {
|
|
1617
|
-
console.log(`HTTP server listening on port ${port}`);
|
|
1618
|
-
console.log(`SSE endpoint available at http://localhost:${port}/sse`);
|
|
1619
|
-
console.log(`Message endpoint available at http://localhost:${port}/messages`);
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
else {
|
|
1623
|
-
try {
|
|
1624
|
-
console.error("========================");
|
|
1625
|
-
console.error(`GitLab MCP Server v${SERVER_VERSION}`);
|
|
1626
|
-
console.error(`API URL: ${GITLAB_API_URL}`);
|
|
1627
|
-
console.error("========================");
|
|
1628
|
-
const transport = new StdioServerTransport();
|
|
1629
|
-
await server.connect(transport);
|
|
1630
|
-
console.error("GitLab MCP Server running on stdio");
|
|
1631
|
-
}
|
|
1632
|
-
catch (error) {
|
|
1633
|
-
console.error("Error initializing server:", error);
|
|
1634
|
-
process.exit(1);
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
runServer().catch((error) => {
|
|
1639
|
-
console.error("Fatal error in main():", error);
|
|
1640
|
-
process.exit(1);
|
|
1641
|
-
});
|