@templmf/temp-solf-lmf 0.0.40 → 0.0.41

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/test.txt +648 -0
  3. package/files.7z +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@templmf/temp-solf-lmf",
3
- "version": "0.0.40",
3
+ "version": "0.0.41",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/test.txt ADDED
@@ -0,0 +1,648 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import axios, { AxiosInstance } from "axios";
6
+ import { z } from "zod";
7
+
8
+ // ─── Config ───────────────────────────────────────────────────────────────────
9
+ const GITLAB_URL = process.env.GITLAB_API_URL || "http://localhost/api/v3";
10
+ const TOKEN = process.env.GITLAB_PRIVATE_TOKEN || process.env.GITLAB_PERSONAL_ACCESS_TOKEN || "";
11
+
12
+ if (!TOKEN) {
13
+ process.stderr.write(
14
+ "Error: GITLAB_PRIVATE_TOKEN or GITLAB_PERSONAL_ACCESS_TOKEN environment variable is required\n"
15
+ );
16
+ process.exit(1);
17
+ }
18
+
19
+ // ─── API Client ───────────────────────────────────────────────────────────────
20
+ const api: AxiosInstance = axios.create({
21
+ baseURL: GITLAB_URL,
22
+ headers: { "PRIVATE-TOKEN": TOKEN },
23
+ timeout: 30000,
24
+ });
25
+
26
+ // Ignore self-signed certs for internal GitLab
27
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") {
28
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
29
+ }
30
+
31
+ async function gitlabGet<T>(path: string, params?: Record<string, unknown>): Promise<T> {
32
+ try {
33
+ const res = await api.get<T>(path, { params });
34
+ return res.data;
35
+ } catch (err: unknown) {
36
+ if (axios.isAxiosError(err)) {
37
+ const status = err.response?.status;
38
+ const msg = (err.response?.data as { message?: string })?.message || err.message;
39
+ throw new Error(`GitLab API error ${status}: ${msg}`);
40
+ }
41
+ throw err;
42
+ }
43
+ }
44
+
45
+ async function gitlabPost<T>(path: string, data?: Record<string, unknown>): Promise<T> {
46
+ try {
47
+ const res = await api.post<T>(path, data);
48
+ return res.data;
49
+ } catch (err: unknown) {
50
+ if (axios.isAxiosError(err)) {
51
+ const status = err.response?.status;
52
+ const msg = (err.response?.data as { message?: string })?.message || err.message;
53
+ throw new Error(`GitLab API error ${status}: ${msg}`);
54
+ }
55
+ throw err;
56
+ }
57
+ }
58
+
59
+ async function gitlabPut<T>(path: string, data?: Record<string, unknown>): Promise<T> {
60
+ try {
61
+ const res = await api.put<T>(path, data);
62
+ return res.data;
63
+ } catch (err: unknown) {
64
+ if (axios.isAxiosError(err)) {
65
+ const status = err.response?.status;
66
+ const msg = (err.response?.data as { message?: string })?.message || err.message;
67
+ throw new Error(`GitLab API error ${status}: ${msg}`);
68
+ }
69
+ throw err;
70
+ }
71
+ }
72
+
73
+ function text(content: unknown): { content: Array<{ type: "text"; text: string }> } {
74
+ return {
75
+ content: [{ type: "text", text: JSON.stringify(content, null, 2) }],
76
+ };
77
+ }
78
+
79
+ // ─── MCP Server ───────────────────────────────────────────────────────────────
80
+ const server = new McpServer({
81
+ name: "gitlab-v3-mcp-server",
82
+ version: "1.0.0",
83
+ });
84
+
85
+ // ── Projects ──────────────────────────────────────────────────────────────────
86
+
87
+ server.registerTool(
88
+ "gitlab_list_projects",
89
+ {
90
+ title: "List Projects",
91
+ description: "List GitLab projects accessible to the current user.",
92
+ inputSchema: {
93
+ search: z.string().optional().describe("Search keyword"),
94
+ page: z.number().int().min(1).default(1).describe("Page number"),
95
+ per_page: z.number().int().min(1).max(100).default(20).describe("Results per page"),
96
+ },
97
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
98
+ },
99
+ async ({ search, page, per_page }) => {
100
+ const data = await gitlabGet("/projects", { search, page, per_page });
101
+ return text(data);
102
+ }
103
+ );
104
+
105
+ server.registerTool(
106
+ "gitlab_get_project",
107
+ {
108
+ title: "Get Project",
109
+ description: "Get details of a specific project by ID or namespace/name (e.g. 'group/repo').",
110
+ inputSchema: {
111
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
112
+ },
113
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
114
+ },
115
+ async ({ project_id }) => {
116
+ const id = encodeURIComponent(String(project_id));
117
+ const data = await gitlabGet(`/projects/${id}`);
118
+ return text(data);
119
+ }
120
+ );
121
+
122
+ // ── Repository ────────────────────────────────────────────────────────────────
123
+
124
+ server.registerTool(
125
+ "gitlab_list_branches",
126
+ {
127
+ title: "List Branches",
128
+ description: "List all branches of a project.",
129
+ inputSchema: {
130
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
131
+ },
132
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
133
+ },
134
+ async ({ project_id }) => {
135
+ const id = encodeURIComponent(String(project_id));
136
+ const data = await gitlabGet(`/projects/${id}/repository/branches`);
137
+ return text(data);
138
+ }
139
+ );
140
+
141
+ server.registerTool(
142
+ "gitlab_get_file",
143
+ {
144
+ title: "Get File Content",
145
+ description: "Get the content of a file from a project repository.",
146
+ inputSchema: {
147
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
148
+ file_path: z.string().describe("Path to file, e.g. 'src/index.js'"),
149
+ ref: z.string().default("master").describe("Branch, tag or commit SHA"),
150
+ },
151
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
152
+ },
153
+ async ({ project_id, file_path, ref }) => {
154
+ const id = encodeURIComponent(String(project_id));
155
+ const fp = encodeURIComponent(file_path);
156
+ const data = await gitlabGet(`/projects/${id}/repository/files`, { file_path: fp, ref });
157
+ // Decode base64 content
158
+ type FileData = { content?: string; encoding?: string; [key: string]: unknown };
159
+ const fd = data as FileData;
160
+ if (fd.content && fd.encoding === "base64") {
161
+ fd.content = Buffer.from(fd.content, "base64").toString("utf-8");
162
+ fd.encoding = "utf-8 (decoded)";
163
+ }
164
+ return text(fd);
165
+ }
166
+ );
167
+
168
+ server.registerTool(
169
+ "gitlab_list_commits",
170
+ {
171
+ title: "List Commits",
172
+ description: "List commits in a project repository.",
173
+ inputSchema: {
174
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
175
+ ref_name: z.string().optional().describe("Branch or tag name"),
176
+ page: z.number().int().min(1).default(1).describe("Page number"),
177
+ per_page: z.number().int().min(1).max(100).default(20).describe("Results per page"),
178
+ },
179
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
180
+ },
181
+ async ({ project_id, ref_name, page, per_page }) => {
182
+ const id = encodeURIComponent(String(project_id));
183
+ const data = await gitlabGet(`/projects/${id}/repository/commits`, { ref_name, page, per_page });
184
+ return text(data);
185
+ }
186
+ );
187
+
188
+ server.registerTool(
189
+ "gitlab_list_tree",
190
+ {
191
+ title: "List Repository Tree",
192
+ description: "List files and directories in a project repository path.",
193
+ inputSchema: {
194
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
195
+ path: z.string().optional().describe("Directory path, empty for root"),
196
+ ref_name: z.string().optional().describe("Branch or tag name"),
197
+ },
198
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
199
+ },
200
+ async ({ project_id, path, ref_name }) => {
201
+ const id = encodeURIComponent(String(project_id));
202
+ const data = await gitlabGet(`/projects/${id}/repository/tree`, { path, ref_name });
203
+ return text(data);
204
+ }
205
+ );
206
+
207
+ // ── Issues ────────────────────────────────────────────────────────────────────
208
+
209
+ server.registerTool(
210
+ "gitlab_list_issues",
211
+ {
212
+ title: "List Issues",
213
+ description: "List issues in a project.",
214
+ inputSchema: {
215
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
216
+ state: z.enum(["opened", "closed"]).optional().describe("Filter by state"),
217
+ labels: z.string().optional().describe("Comma-separated list of labels"),
218
+ page: z.number().int().min(1).default(1).describe("Page number"),
219
+ per_page: z.number().int().min(1).max(100).default(20).describe("Results per page"),
220
+ },
221
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
222
+ },
223
+ async ({ project_id, state, labels, page, per_page }) => {
224
+ const id = encodeURIComponent(String(project_id));
225
+ const data = await gitlabGet(`/projects/${id}/issues`, { state, labels, page, per_page });
226
+ return text(data);
227
+ }
228
+ );
229
+
230
+ server.registerTool(
231
+ "gitlab_get_issue",
232
+ {
233
+ title: "Get Issue",
234
+ description: "Get details of a specific issue.",
235
+ inputSchema: {
236
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
237
+ issue_id: z.number().int().describe("Issue IID (project-level issue number)"),
238
+ },
239
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
240
+ },
241
+ async ({ project_id, issue_id }) => {
242
+ const id = encodeURIComponent(String(project_id));
243
+ const data = await gitlabGet(`/projects/${id}/issues/${issue_id}`);
244
+ return text(data);
245
+ }
246
+ );
247
+
248
+ server.registerTool(
249
+ "gitlab_create_issue",
250
+ {
251
+ title: "Create Issue",
252
+ description: "Create a new issue in a project.",
253
+ inputSchema: {
254
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
255
+ title: z.string().min(1).describe("Issue title"),
256
+ description: z.string().optional().describe("Issue description (Markdown supported)"),
257
+ labels: z.string().optional().describe("Comma-separated labels"),
258
+ assignee_id: z.number().int().optional().describe("User ID to assign"),
259
+ },
260
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
261
+ },
262
+ async ({ project_id, title, description, labels, assignee_id }) => {
263
+ const id = encodeURIComponent(String(project_id));
264
+ const data = await gitlabPost(`/projects/${id}/issues`, {
265
+ title,
266
+ description,
267
+ labels,
268
+ assignee_id,
269
+ });
270
+ return text(data);
271
+ }
272
+ );
273
+
274
+ server.registerTool(
275
+ "gitlab_add_issue_comment",
276
+ {
277
+ title: "Add Issue Comment",
278
+ description: "Add a comment (note) to an issue.",
279
+ inputSchema: {
280
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
281
+ issue_id: z.number().int().describe("Issue IID"),
282
+ body: z.string().min(1).describe("Comment text (Markdown supported)"),
283
+ },
284
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
285
+ },
286
+ async ({ project_id, issue_id, body }) => {
287
+ const id = encodeURIComponent(String(project_id));
288
+ const data = await gitlabPost(`/projects/${id}/issues/${issue_id}/notes`, { body });
289
+ return text(data);
290
+ }
291
+ );
292
+
293
+ // ── Merge Requests ────────────────────────────────────────────────────────────
294
+
295
+ server.registerTool(
296
+ "gitlab_list_merge_requests",
297
+ {
298
+ title: "List Merge Requests",
299
+ description: "List merge requests in a project.",
300
+ inputSchema: {
301
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
302
+ state: z.enum(["opened", "closed", "merged"]).optional().describe("Filter by state"),
303
+ page: z.number().int().min(1).default(1).describe("Page number"),
304
+ per_page: z.number().int().min(1).max(100).default(20).describe("Results per page"),
305
+ },
306
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
307
+ },
308
+ async ({ project_id, state, page, per_page }) => {
309
+ const id = encodeURIComponent(String(project_id));
310
+ const data = await gitlabGet(`/projects/${id}/merge_requests`, { state, page, per_page });
311
+ return text(data);
312
+ }
313
+ );
314
+
315
+ server.registerTool(
316
+ "gitlab_get_merge_request",
317
+ {
318
+ title: "Get Merge Request",
319
+ description: "Get details of a specific merge request.",
320
+ inputSchema: {
321
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
322
+ mr_id: z.number().int().describe("Merge request IID"),
323
+ },
324
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
325
+ },
326
+ async ({ project_id, mr_id }) => {
327
+ const id = encodeURIComponent(String(project_id));
328
+ const data = await gitlabGet(`/projects/${id}/merge_requests`, { iid: mr_id });
329
+ return text(data);
330
+ }
331
+ );
332
+
333
+ server.registerTool(
334
+ "gitlab_get_merge_request_changes",
335
+ {
336
+ title: "Get Merge Request Changes",
337
+ description: "Get file changes (diffs) for a merge request by its IID (e.g. #26). Internally resolves the real mr id via GET /projects/:id/merge_requests?iid=:merge_iid, then fetches changes via GET /projects/:id/merge_requests/:mr_id/changes.",
338
+ inputSchema: {
339
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
340
+ merge_iid: z.number().int().describe("The merge request IID shown in the UI (e.g. 26 for !26)"),
341
+ raw_diff: z.boolean().optional().default(false).describe("If true, returns only file path info + unified diff per file, omitting MR metadata"),
342
+ },
343
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
344
+ },
345
+ async ({ project_id, merge_iid, raw_diff }) => {
346
+ const id = encodeURIComponent(String(project_id));
347
+
348
+ type MRItem = {
349
+ id: number;
350
+ iid: number;
351
+ title: string;
352
+ [key: string]: unknown;
353
+ };
354
+
355
+ type MRChange = {
356
+ old_path: string;
357
+ new_path: string;
358
+ new_file: boolean;
359
+ renamed_file: boolean;
360
+ deleted_file: boolean;
361
+ diff: string;
362
+ };
363
+
364
+ type MRChangesResponse = {
365
+ id: number;
366
+ iid: number;
367
+ title: string;
368
+ changes: MRChange[];
369
+ [key: string]: unknown;
370
+ };
371
+
372
+ // Step 1: resolve iid → real id
373
+ const mrList = await gitlabGet<MRItem[]>(`/projects/${id}/merge_requests`, { iid: merge_iid });
374
+ if (!mrList || mrList.length === 0) {
375
+ return text({ error: `No merge request found for iid=${merge_iid}` });
376
+ }
377
+ const mrRequestId = mrList[0].id;
378
+
379
+ // Step 2: fetch changes using the real id
380
+ const data = await gitlabGet<MRChangesResponse>(
381
+ `/projects/${id}/merge_requests/${mrRequestId}/changes`
382
+ );
383
+
384
+ if (raw_diff) {
385
+ return text(
386
+ (data.changes ?? []).map((c: MRChange) => ({
387
+ old_path: c.old_path,
388
+ new_path: c.new_path,
389
+ new_file: c.new_file,
390
+ renamed_file: c.renamed_file,
391
+ deleted_file: c.deleted_file,
392
+ diff: c.diff,
393
+ }))
394
+ );
395
+ }
396
+
397
+ return text(data);
398
+ }
399
+ );
400
+
401
+ server.registerTool(
402
+ "gitlab_create_merge_request",
403
+ {
404
+ title: "Create Merge Request",
405
+ description: "Create a new merge request.",
406
+ inputSchema: {
407
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
408
+ title: z.string().min(1).describe("MR title"),
409
+ source_branch: z.string().describe("Source branch name"),
410
+ target_branch: z.string().describe("Target branch name"),
411
+ description: z.string().optional().describe("MR description"),
412
+ assignee_id: z.number().int().optional().describe("User ID to assign"),
413
+ },
414
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
415
+ },
416
+ async ({ project_id, title, source_branch, target_branch, description, assignee_id }) => {
417
+ const id = encodeURIComponent(String(project_id));
418
+ const data = await gitlabPost(`/projects/${id}/merge_requests`, {
419
+ title,
420
+ source_branch,
421
+ target_branch,
422
+ description,
423
+ assignee_id,
424
+ });
425
+ return text(data);
426
+ }
427
+ );
428
+
429
+ server.registerTool(
430
+ "gitlab_update_merge_request",
431
+ {
432
+ title: "Update Merge Request",
433
+ description: "Update an existing merge request. Can change target branch, title, assignee, description, labels, milestone, or close the MR. Internally resolves the iid to the real merge_request_id via GET /projects/:id/merge_requests?iid=:merge_iid, then calls PUT /projects/:id/merge_requests/:merge_request_id.",
434
+ inputSchema: {
435
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
436
+ merge_iid: z.number().int().describe("The merge request IID shown in the UI (e.g. 26 for !26)"),
437
+ title: z.string().min(1).optional().describe("Title of MR"),
438
+ source_branch: z.string().optional().describe("The source branch"),
439
+ target_branch: z.string().optional().describe("The target branch"),
440
+ assignee_id: z.number().int().optional().describe("Assignee user ID"),
441
+ description: z.string().optional().describe("Description of MR"),
442
+ target_project_id: z.number().int().optional().describe("The target project numeric ID"),
443
+ labels: z.string().optional().describe("Labels for MR as a comma-separated list"),
444
+ milestone_id: z.number().int().optional().describe("The ID of a milestone"),
445
+ remove_source_branch: z.boolean().optional().describe("Flag indicating if a merge request should remove the source branch when merging"),
446
+ },
447
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
448
+ },
449
+ async ({ project_id, merge_iid, title, source_branch, target_branch, assignee_id, description, target_project_id, labels, milestone_id, remove_source_branch }) => {
450
+ const id = encodeURIComponent(String(project_id));
451
+
452
+ type MRItem = { id: number; iid: number; [key: string]: unknown };
453
+
454
+ // Step 1: resolve iid → real merge_request_id
455
+ const mrList = await gitlabGet<MRItem[]>(`/projects/${id}/merge_requests`, { iid: merge_iid });
456
+ if (!mrList || mrList.length === 0) {
457
+ return text({ error: `No merge request found for iid=${merge_iid}` });
458
+ }
459
+ const mergeRequestId = mrList[0].id;
460
+
461
+ // Step 2: update the MR
462
+ const data = await gitlabPut(`/projects/${id}/merge_requests/${mergeRequestId}`, {
463
+ title,
464
+ source_branch,
465
+ target_branch,
466
+ assignee_id,
467
+ description,
468
+ target_project_id,
469
+ labels,
470
+ milestone_id,
471
+ remove_source_branch,
472
+ });
473
+ return text(data);
474
+ }
475
+ );
476
+
477
+ server.registerTool(
478
+ "gitlab_accept_merge_request",
479
+ {
480
+ title: "Accept Merge Request",
481
+ description: "Accept (merge) a merge request by its IID. Internally resolves the iid to the real merge_request_id via GET /projects/:id/merge_requests?iid=:merge_iid, then calls PUT /projects/:id/merge_requests/:merge_request_id/merge.",
482
+ inputSchema: {
483
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
484
+ merge_iid: z.number().int().describe("The merge request IID shown in the UI (e.g. 26 for !26)"),
485
+ merge_commit_message: z.string().optional().describe("Custom merge commit message"),
486
+ should_remove_source_branch: z.boolean().optional().describe("If true, removes the source branch after merge"),
487
+ merge_when_build_succeeds: z.boolean().optional().describe("If true, merges only when the build succeeds"),
488
+ sha: z.string().optional().describe("If present, must match HEAD of the source branch or the merge will fail"),
489
+ },
490
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
491
+ },
492
+ async ({ project_id, merge_iid, merge_commit_message, should_remove_source_branch, merge_when_build_succeeds, sha }) => {
493
+ const id = encodeURIComponent(String(project_id));
494
+
495
+ type MRItem = { id: number; iid: number; [key: string]: unknown };
496
+
497
+ // Step 1: resolve iid → real merge_request_id
498
+ const mrList = await gitlabGet<MRItem[]>(`/projects/${id}/merge_requests`, { iid: merge_iid });
499
+ if (!mrList || mrList.length === 0) {
500
+ return text({ error: `No merge request found for iid=${merge_iid}` });
501
+ }
502
+ const mergeRequestId = mrList[0].id;
503
+
504
+ // Step 2: accept the MR
505
+ const data = await gitlabPut(`/projects/${id}/merge_requests/${mergeRequestId}/merge`, {
506
+ merge_commit_message,
507
+ should_remove_source_branch,
508
+ merge_when_build_succeeds,
509
+ sha,
510
+ });
511
+ return text(data);
512
+ }
513
+ );
514
+
515
+ server.registerTool(
516
+ "gitlab_add_merge_request_comment",
517
+ {
518
+ title: "Add Merge Request Comment",
519
+ description: "Add a comment (note) to a merge request by its IID. Internally resolves the iid to the real merge_request_id via GET /projects/:id/merge_requests?iid=:merge_iid, then calls POST /projects/:id/merge_requests/:merge_request_id/notes.",
520
+ inputSchema: {
521
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
522
+ merge_iid: z.number().int().describe("The merge request IID shown in the UI (e.g. 26 for !26)"),
523
+ body: z.string().min(1).describe("Comment text (Markdown supported)"),
524
+ },
525
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
526
+ },
527
+ async ({ project_id, merge_iid, body }) => {
528
+ const id = encodeURIComponent(String(project_id));
529
+
530
+ type MRItem = { id: number; iid: number; [key: string]: unknown };
531
+
532
+ // Step 1: resolve iid → real merge_request_id
533
+ const mrList = await gitlabGet<MRItem[]>(`/projects/${id}/merge_requests`, { iid: merge_iid });
534
+ if (!mrList || mrList.length === 0) {
535
+ return text({ error: `No merge request found for iid=${merge_iid}` });
536
+ }
537
+ const mergeRequestId = mrList[0].id;
538
+
539
+ // Step 2: post the comment
540
+ const data = await gitlabPost(`/projects/${id}/merge_requests/${mergeRequestId}/notes`, { body });
541
+ return text(data);
542
+ }
543
+ );
544
+
545
+ // ── Users ─────────────────────────────────────────────────────────────────────
546
+
547
+ server.registerTool(
548
+ "gitlab_get_current_user",
549
+ {
550
+ title: "Get Current User",
551
+ description: "Get details of the currently authenticated user.",
552
+ inputSchema: {},
553
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
554
+ },
555
+ async () => {
556
+ const data = await gitlabGet("/user");
557
+ return text(data);
558
+ }
559
+ );
560
+
561
+ server.registerTool(
562
+ "gitlab_list_project_members",
563
+ {
564
+ title: "List Project Members",
565
+ description: "List members of a project.",
566
+ inputSchema: {
567
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
568
+ },
569
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
570
+ },
571
+ async ({ project_id }) => {
572
+ const id = encodeURIComponent(String(project_id));
573
+ const data = await gitlabGet(`/projects/${id}/members`);
574
+ return text(data);
575
+ }
576
+ );
577
+
578
+ // ── Groups ────────────────────────────────────────────────────────────────────
579
+
580
+ server.registerTool(
581
+ "gitlab_list_groups",
582
+ {
583
+ title: "List Groups",
584
+ description: "List all groups accessible to the current user.",
585
+ inputSchema: {
586
+ search: z.string().optional().describe("Search keyword"),
587
+ page: z.number().int().min(1).default(1).describe("Page number"),
588
+ per_page: z.number().int().min(1).max(100).default(20).describe("Results per page"),
589
+ },
590
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
591
+ },
592
+ async ({ search, page, per_page }) => {
593
+ const data = await gitlabGet("/groups", { search, page, per_page });
594
+ return text(data);
595
+ }
596
+ );
597
+
598
+ server.registerTool(
599
+ "gitlab_list_group_projects",
600
+ {
601
+ title: "List Group Projects",
602
+ description: "List all projects in a group.",
603
+ inputSchema: {
604
+ group_id: z.union([z.number(), z.string()]).describe("Group ID or group path"),
605
+ page: z.number().int().min(1).default(1).describe("Page number"),
606
+ per_page: z.number().int().min(1).max(100).default(20).describe("Results per page"),
607
+ },
608
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
609
+ },
610
+ async ({ group_id, page, per_page }) => {
611
+ const id = encodeURIComponent(String(group_id));
612
+ const data = await gitlabGet(`/groups/${id}/projects`, { page, per_page });
613
+ return text(data);
614
+ }
615
+ );
616
+
617
+ // ── Search ────────────────────────────────────────────────────────────────────
618
+
619
+ server.registerTool(
620
+ "gitlab_search_code",
621
+ {
622
+ title: "Search Code in Project",
623
+ description: "Search for code within a project repository.",
624
+ inputSchema: {
625
+ project_id: z.union([z.number(), z.string()]).describe("Project ID or 'namespace/project_name'"),
626
+ query: z.string().min(1).describe("Search query string"),
627
+ },
628
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
629
+ },
630
+ async ({ project_id, query }) => {
631
+ const id = encodeURIComponent(String(project_id));
632
+ // GitLab v3 search endpoint
633
+ const data = await gitlabGet(`/projects/${id}/search`, { scope: "blobs", search: query });
634
+ return text(data);
635
+ }
636
+ );
637
+
638
+ // ─── Start Server ─────────────────────────────────────────────────────────────
639
+ async function main(): Promise<void> {
640
+ const transport = new StdioServerTransport();
641
+ await server.connect(transport);
642
+ process.stderr.write(`GitLab v3 MCP Server started. Connected to: ${GITLAB_URL}\n`);
643
+ }
644
+
645
+ main().catch((err: unknown) => {
646
+ process.stderr.write(`Fatal error: ${err}\n`);
647
+ process.exit(1);
648
+ });
package/files.7z DELETED
Binary file