gitlab-mcp 1.1.0 → 1.2.1

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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -1
  3. package/dist/config/dotenv.d.ts +2 -0
  4. package/dist/config/dotenv.js +40 -0
  5. package/dist/config/dotenv.js.map +1 -0
  6. package/dist/config/env.d.ts +55 -0
  7. package/dist/config/env.js +164 -0
  8. package/dist/config/env.js.map +1 -0
  9. package/dist/http-app.d.ts +45 -0
  10. package/dist/http-app.js +550 -0
  11. package/dist/http-app.js.map +1 -0
  12. package/dist/http.d.ts +2 -0
  13. package/dist/http.js +65 -0
  14. package/dist/http.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +65 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lib/auth-context.d.ts +9 -0
  19. package/dist/lib/auth-context.js +9 -0
  20. package/dist/lib/auth-context.js.map +1 -0
  21. package/dist/lib/gitlab-client.d.ts +331 -0
  22. package/dist/lib/gitlab-client.js +1025 -0
  23. package/dist/lib/gitlab-client.js.map +1 -0
  24. package/dist/lib/logger.d.ts +2 -0
  25. package/dist/lib/logger.js +13 -0
  26. package/dist/lib/logger.js.map +1 -0
  27. package/dist/lib/network.d.ts +3 -0
  28. package/dist/lib/network.js +38 -0
  29. package/dist/lib/network.js.map +1 -0
  30. package/dist/lib/oauth.d.ts +29 -0
  31. package/dist/lib/oauth.js +220 -0
  32. package/dist/lib/oauth.js.map +1 -0
  33. package/dist/lib/output.d.ts +14 -0
  34. package/dist/lib/output.js +38 -0
  35. package/dist/lib/output.js.map +1 -0
  36. package/dist/lib/policy.d.ts +25 -0
  37. package/dist/lib/policy.js +48 -0
  38. package/dist/lib/policy.js.map +1 -0
  39. package/dist/lib/request-runtime.d.ts +26 -0
  40. package/dist/lib/request-runtime.js +323 -0
  41. package/dist/lib/request-runtime.js.map +1 -0
  42. package/dist/lib/sanitize.d.ts +1 -0
  43. package/dist/lib/sanitize.js +21 -0
  44. package/dist/lib/sanitize.js.map +1 -0
  45. package/dist/lib/session-capacity.d.ts +8 -0
  46. package/dist/lib/session-capacity.js +7 -0
  47. package/dist/lib/session-capacity.js.map +1 -0
  48. package/dist/server/build-server.d.ts +3 -0
  49. package/dist/server/build-server.js +13 -0
  50. package/dist/server/build-server.js.map +1 -0
  51. package/dist/tools/gitlab.d.ts +9 -0
  52. package/dist/tools/gitlab.js +2576 -0
  53. package/dist/tools/gitlab.js.map +1 -0
  54. package/dist/tools/health.d.ts +2 -0
  55. package/dist/tools/health.js +21 -0
  56. package/dist/tools/health.js.map +1 -0
  57. package/dist/tools/mr-code-context.d.ts +38 -0
  58. package/dist/tools/mr-code-context.js +330 -0
  59. package/dist/tools/mr-code-context.js.map +1 -0
  60. package/{src/types/context.ts → dist/types/context.d.ts} +5 -6
  61. package/dist/types/context.js +2 -0
  62. package/dist/types/context.js.map +1 -0
  63. package/docs/architecture.md +10 -10
  64. package/docs/configuration.md +12 -7
  65. package/docs/mcp-integration-testing-best-practices.md +981 -0
  66. package/package.json +13 -1
  67. package/.dockerignore +0 -7
  68. package/.editorconfig +0 -9
  69. package/.env.example +0 -75
  70. package/.github/workflows/nodejs.yml +0 -31
  71. package/.github/workflows/npm-publish.yml +0 -31
  72. package/.husky/pre-commit +0 -1
  73. package/.nvmrc +0 -1
  74. package/.prettierrc.json +0 -6
  75. package/Dockerfile +0 -20
  76. package/docker-compose.yml +0 -10
  77. package/eslint.config.js +0 -23
  78. package/scripts/get-oauth-token.example.sh +0 -15
  79. package/src/config/env.ts +0 -171
  80. package/src/http.ts +0 -620
  81. package/src/index.ts +0 -77
  82. package/src/lib/auth-context.ts +0 -19
  83. package/src/lib/gitlab-client.ts +0 -1810
  84. package/src/lib/logger.ts +0 -17
  85. package/src/lib/network.ts +0 -45
  86. package/src/lib/oauth.ts +0 -287
  87. package/src/lib/output.ts +0 -51
  88. package/src/lib/policy.ts +0 -78
  89. package/src/lib/request-runtime.ts +0 -376
  90. package/src/lib/sanitize.ts +0 -25
  91. package/src/lib/session-capacity.ts +0 -14
  92. package/src/server/build-server.ts +0 -17
  93. package/src/tools/gitlab.ts +0 -3135
  94. package/src/tools/health.ts +0 -27
  95. package/src/tools/mr-code-context.ts +0 -473
  96. package/tests/auth-context.test.ts +0 -102
  97. package/tests/gitlab-client.test.ts +0 -672
  98. package/tests/graphql-guard.test.ts +0 -121
  99. package/tests/integration/agent-loop.integration.test.ts +0 -558
  100. package/tests/integration/server.integration.test.ts +0 -543
  101. package/tests/mr-code-context.test.ts +0 -600
  102. package/tests/oauth.test.ts +0 -43
  103. package/tests/output.test.ts +0 -186
  104. package/tests/policy.test.ts +0 -324
  105. package/tests/request-runtime.test.ts +0 -252
  106. package/tests/sanitize.test.ts +0 -123
  107. package/tests/session-capacity.test.ts +0 -49
  108. package/tests/upload-reference.test.ts +0 -88
  109. package/tsconfig.build.json +0 -11
  110. package/tsconfig.json +0 -21
  111. package/vitest.config.ts +0 -12
@@ -0,0 +1,2576 @@
1
+ import { Kind, parse } from "graphql";
2
+ import { z } from "zod";
3
+ import { GitLabApiError } from "../lib/gitlab-client.js";
4
+ import { getSessionAuth } from "../lib/auth-context.js";
5
+ import { stripNullsDeep } from "../lib/sanitize.js";
6
+ import { getMergeRequestCodeContext, mergeRequestCodeContextSchema } from "./mr-code-context.js";
7
+ const optionalString = z.preprocess((value) => (value === null ? undefined : value), z.string().optional());
8
+ const optionalNumber = z.preprocess((value) => (value === null ? undefined : value), z.number().optional());
9
+ const optionalBoolean = z.preprocess((value) => (value === null ? undefined : value), z.boolean().optional());
10
+ const optionalStringArray = z.preprocess((value) => (value === null ? undefined : value), z.array(z.string()).optional());
11
+ const optionalNumberArray = z.preprocess((value) => (value === null ? undefined : value), z.array(z.number()).optional());
12
+ const optionalStringOrNumber = z.preprocess((value) => (value === null ? undefined : value), z.union([z.string(), z.number()]).optional());
13
+ const optionalStringOrStringArray = z.preprocess((value) => (value === null ? undefined : value), z.union([z.string(), z.array(z.string())]).optional());
14
+ const optionalRecord = z.preprocess((value) => (value === null ? undefined : value), z.record(z.string(), z.unknown()).optional());
15
+ const paginationShape = {
16
+ page: optionalNumber,
17
+ per_page: optionalNumber
18
+ };
19
+ export function registerGitLabTools(server, context) {
20
+ const definitions = getGitLabToolDefinitions();
21
+ const disableGraphqlTools = shouldDisableGraphqlTools(context.env.GITLAB_ALLOWED_PROJECT_IDS, context.env.GITLAB_ALLOW_GRAPHQL_WITH_PROJECT_SCOPE);
22
+ const filtered = context.policy.filterTools(definitions.map((item) => ({
23
+ name: item.name,
24
+ mutating: item.mutating,
25
+ requiresFeature: item.requiresFeature
26
+ })));
27
+ const enabledNames = new Set(filtered.map((item) => item.name));
28
+ for (const definition of definitions) {
29
+ if (!enabledNames.has(definition.name)) {
30
+ continue;
31
+ }
32
+ if (disableGraphqlTools && isGraphqlToolName(definition.name)) {
33
+ continue;
34
+ }
35
+ server.registerTool(definition.name, {
36
+ title: definition.title,
37
+ description: definition.description,
38
+ inputSchema: definition.inputSchema ?? {}
39
+ }, async (rawArgs) => {
40
+ try {
41
+ context.policy.assertCanExecute({
42
+ name: definition.name,
43
+ mutating: definition.mutating,
44
+ requiresFeature: definition.requiresFeature
45
+ });
46
+ if (definition.requiresAuth ?? true) {
47
+ assertAuthReady(context);
48
+ }
49
+ const args = stripNullsDeep((rawArgs ?? {}));
50
+ const result = await definition.handler(args, context);
51
+ const formatted = context.formatter.format(result);
52
+ const structuredResult = formatted.truncated
53
+ ? { truncated: true }
54
+ : toStructuredContent(result);
55
+ return {
56
+ content: [
57
+ {
58
+ type: "text",
59
+ text: formatted.text
60
+ }
61
+ ],
62
+ structuredContent: {
63
+ result: structuredResult,
64
+ meta: {
65
+ truncated: formatted.truncated,
66
+ bytes: formatted.bytes
67
+ }
68
+ }
69
+ };
70
+ }
71
+ catch (error) {
72
+ return toToolError(error, context);
73
+ }
74
+ });
75
+ }
76
+ }
77
+ function getGitLabToolDefinitions() {
78
+ return [
79
+ {
80
+ name: "gitlab_get_project",
81
+ title: "Get Project",
82
+ description: "Get project details by ID or path.",
83
+ mutating: false,
84
+ inputSchema: {
85
+ project_id: z.string().optional()
86
+ },
87
+ handler: async (args, context) => {
88
+ const projectId = resolveProjectId(args, context, true);
89
+ return context.gitlab.getProject(projectId);
90
+ }
91
+ },
92
+ {
93
+ name: "gitlab_list_projects",
94
+ title: "List Projects",
95
+ description: "List projects available to the current user.",
96
+ mutating: false,
97
+ inputSchema: {
98
+ search: optionalString,
99
+ search_namespaces: optionalBoolean,
100
+ membership: optionalBoolean,
101
+ owned: optionalBoolean,
102
+ simple: optionalBoolean,
103
+ archived: optionalBoolean,
104
+ visibility: z.enum(["public", "internal", "private"]).optional(),
105
+ order_by: z
106
+ .enum(["id", "name", "path", "created_at", "updated_at", "last_activity_at"])
107
+ .optional(),
108
+ sort: z.enum(["asc", "desc"]).optional(),
109
+ with_issues_enabled: optionalBoolean,
110
+ with_merge_requests_enabled: optionalBoolean,
111
+ min_access_level: optionalNumber,
112
+ ...paginationShape
113
+ },
114
+ handler: async (args, context) => context.gitlab.listProjects({ query: toQuery(args) })
115
+ },
116
+ {
117
+ name: "gitlab_create_repository",
118
+ title: "Create Repository",
119
+ description: "Create a new GitLab project/repository.",
120
+ mutating: true,
121
+ inputSchema: {
122
+ name: optionalString,
123
+ description: optionalString,
124
+ visibility: z.enum(["private", "internal", "public"]).optional(),
125
+ initialize_with_readme: optionalBoolean,
126
+ path: optionalString,
127
+ namespace_id: optionalString,
128
+ default_branch: optionalString
129
+ },
130
+ handler: async (args, context) => context.gitlab.createRepository({
131
+ name: getString(args, "name"),
132
+ description: getOptionalString(args, "description"),
133
+ visibility: getOptionalString(args, "visibility"),
134
+ initialize_with_readme: getOptionalBoolean(args, "initialize_with_readme"),
135
+ path: getOptionalString(args, "path"),
136
+ namespace_id: getOptionalString(args, "namespace_id"),
137
+ default_branch: getOptionalString(args, "default_branch")
138
+ })
139
+ },
140
+ {
141
+ name: "gitlab_list_project_members",
142
+ title: "List Project Members",
143
+ description: "List members of a project.",
144
+ mutating: false,
145
+ inputSchema: {
146
+ project_id: z.string().optional(),
147
+ query: optionalString,
148
+ user_ids: optionalNumberArray,
149
+ skip_users: optionalNumberArray,
150
+ include_inheritance: optionalBoolean,
151
+ ...paginationShape
152
+ },
153
+ handler: async (args, context) => {
154
+ const projectId = resolveProjectId(args, context, true);
155
+ return context.gitlab.listProjectMembers(projectId, {
156
+ query: toQuery(omit(args, ["project_id"]))
157
+ });
158
+ }
159
+ },
160
+ {
161
+ name: "gitlab_list_group_projects",
162
+ title: "List Group Projects",
163
+ description: "List projects under a group.",
164
+ mutating: false,
165
+ inputSchema: {
166
+ group_id: z.string(),
167
+ include_subgroups: optionalBoolean,
168
+ search: optionalString,
169
+ order_by: z
170
+ .enum(["name", "path", "created_at", "updated_at", "last_activity_at"])
171
+ .optional(),
172
+ sort: z.enum(["asc", "desc"]).optional(),
173
+ archived: optionalBoolean,
174
+ visibility: z.enum(["public", "internal", "private"]).optional(),
175
+ with_issues_enabled: optionalBoolean,
176
+ with_merge_requests_enabled: optionalBoolean,
177
+ min_access_level: optionalNumber,
178
+ with_programming_language: optionalString,
179
+ starred: optionalBoolean,
180
+ statistics: optionalBoolean,
181
+ with_custom_attributes: optionalBoolean,
182
+ with_security_reports: optionalBoolean,
183
+ ...paginationShape
184
+ },
185
+ handler: async (args, context) => {
186
+ return context.gitlab.listGroupProjects(getString(args, "group_id"), {
187
+ query: toQuery(omit(args, ["group_id"]))
188
+ });
189
+ }
190
+ },
191
+ {
192
+ name: "gitlab_list_group_iterations",
193
+ title: "List Group Iterations",
194
+ description: "List iterations for a group.",
195
+ mutating: false,
196
+ inputSchema: {
197
+ group_id: z.string().min(1),
198
+ state: optionalString,
199
+ search: optionalString,
200
+ search_in: optionalStringArray,
201
+ include_ancestors: optionalBoolean,
202
+ include_descendants: optionalBoolean,
203
+ updated_before: optionalString,
204
+ updated_after: optionalString,
205
+ ...paginationShape
206
+ },
207
+ handler: async (args, context) => {
208
+ const query = toQuery(omit(args, ["group_id"]));
209
+ const searchIn = getOptionalStringArray(args, "search_in");
210
+ if (searchIn && searchIn.length > 0) {
211
+ query.in = searchIn.join(",");
212
+ delete query.search_in;
213
+ }
214
+ return context.gitlab.listGroupIterations(getString(args, "group_id"), { query });
215
+ }
216
+ },
217
+ {
218
+ name: "gitlab_search_repositories",
219
+ title: "Search Repositories",
220
+ description: "Search repositories by keyword.",
221
+ mutating: false,
222
+ inputSchema: {
223
+ search: z.string().min(1),
224
+ ...paginationShape
225
+ },
226
+ handler: async (args, context) => context.gitlab.searchRepositories(getString(args, "search"), {
227
+ query: toQuery(omit(args, ["search"]))
228
+ })
229
+ },
230
+ {
231
+ name: "gitlab_search_code_blobs",
232
+ title: "Search Code Blobs",
233
+ description: "Search repository code blobs in a specific project.",
234
+ mutating: false,
235
+ inputSchema: {
236
+ project_id: z.string().optional(),
237
+ search: z.string().min(1),
238
+ ref: optionalString,
239
+ ...paginationShape
240
+ },
241
+ handler: async (args, context) => context.gitlab.searchCodeBlobs(resolveProjectId(args, context, true), getString(args, "search"), { query: toQuery(omit(args, ["project_id", "search"])) })
242
+ },
243
+ {
244
+ name: "gitlab_get_repository_tree",
245
+ title: "Get Repository Tree",
246
+ description: "List files and directories in a repository tree.",
247
+ mutating: false,
248
+ inputSchema: {
249
+ project_id: z.string().optional(),
250
+ path: optionalString,
251
+ ref: optionalString,
252
+ recursive: optionalBoolean,
253
+ ...paginationShape
254
+ },
255
+ handler: async (args, context) => {
256
+ const projectId = resolveProjectId(args, context, true);
257
+ return context.gitlab.getRepositoryTree(projectId, {
258
+ query: toQuery(omit(args, ["project_id"]))
259
+ });
260
+ }
261
+ },
262
+ {
263
+ name: "gitlab_get_file_contents",
264
+ title: "Get File Contents",
265
+ description: "Get a file in repository by path and ref.",
266
+ mutating: false,
267
+ inputSchema: {
268
+ project_id: z.string().optional(),
269
+ file_path: z.string().min(1),
270
+ ref: optionalString
271
+ },
272
+ handler: async (args, context) => {
273
+ const projectId = resolveProjectId(args, context, true);
274
+ let ref = getOptionalString(args, "ref");
275
+ if (!ref) {
276
+ const project = (await context.gitlab.getProject(projectId));
277
+ ref = typeof project.default_branch === "string" ? project.default_branch : "main";
278
+ }
279
+ return context.gitlab.getFileContents(projectId, getString(args, "file_path"), ref);
280
+ }
281
+ },
282
+ {
283
+ name: "gitlab_create_or_update_file",
284
+ title: "Create Or Update File",
285
+ description: "Create or update one file in repository.",
286
+ mutating: true,
287
+ inputSchema: {
288
+ project_id: z.string().optional(),
289
+ file_path: z.string().min(1),
290
+ branch: z.string().min(1),
291
+ content: z.string(),
292
+ commit_message: z.string().min(1),
293
+ previous_path: optionalString,
294
+ author_email: optionalString,
295
+ author_name: optionalString,
296
+ encoding: optionalString,
297
+ execute_filemode: optionalBoolean,
298
+ start_branch: optionalString,
299
+ last_commit_id: optionalString,
300
+ commit_id: optionalString
301
+ },
302
+ handler: async (args, context) => {
303
+ const projectId = resolveProjectId(args, context, true);
304
+ return context.gitlab.createOrUpdateFile(projectId, getString(args, "file_path"), {
305
+ branch: getString(args, "branch"),
306
+ content: getString(args, "content"),
307
+ commit_message: getString(args, "commit_message"),
308
+ author_email: getOptionalString(args, "author_email"),
309
+ author_name: getOptionalString(args, "author_name"),
310
+ encoding: getOptionalString(args, "encoding"),
311
+ execute_filemode: getOptionalBoolean(args, "execute_filemode"),
312
+ start_branch: getOptionalString(args, "start_branch"),
313
+ last_commit_id: getOptionalString(args, "last_commit_id") ?? getOptionalString(args, "commit_id")
314
+ });
315
+ }
316
+ },
317
+ {
318
+ name: "gitlab_push_files",
319
+ title: "Push Files",
320
+ description: "Create a commit with multiple file actions.",
321
+ mutating: true,
322
+ inputSchema: {
323
+ project_id: z.string().optional(),
324
+ branch: z.string().min(1),
325
+ commit_message: z.string().min(1),
326
+ actions: z
327
+ .array(z.object({
328
+ action: z.enum(["create", "delete", "move", "update", "chmod"]),
329
+ file_path: z.string(),
330
+ previous_path: optionalString,
331
+ content: optionalString,
332
+ encoding: optionalString,
333
+ execute_filemode: optionalBoolean,
334
+ last_commit_id: optionalString
335
+ }))
336
+ .optional(),
337
+ files: z
338
+ .array(z.object({
339
+ file_path: z.string(),
340
+ content: z.string()
341
+ }))
342
+ .optional(),
343
+ start_branch: optionalString,
344
+ author_name: optionalString,
345
+ author_email: optionalString,
346
+ force: optionalBoolean
347
+ },
348
+ handler: async (args, context) => {
349
+ const projectId = resolveProjectId(args, context, true);
350
+ const actionsInput = args.actions;
351
+ const filesInput = args.files;
352
+ let actions = [];
353
+ if (Array.isArray(actionsInput) && actionsInput.length > 0) {
354
+ actions = actionsInput;
355
+ }
356
+ else if (Array.isArray(filesInput) && filesInput.length > 0) {
357
+ actions = filesInput.map((item) => {
358
+ const record = item;
359
+ return {
360
+ action: "create",
361
+ file_path: record.file_path,
362
+ content: record.content
363
+ };
364
+ });
365
+ }
366
+ if (actions.length === 0) {
367
+ throw new Error("Either actions or files must contain at least one item");
368
+ }
369
+ return context.gitlab.pushFiles(projectId, {
370
+ branch: getString(args, "branch"),
371
+ commit_message: getString(args, "commit_message"),
372
+ actions,
373
+ start_branch: getOptionalString(args, "start_branch"),
374
+ author_name: getOptionalString(args, "author_name"),
375
+ author_email: getOptionalString(args, "author_email"),
376
+ force: getOptionalBoolean(args, "force")
377
+ });
378
+ }
379
+ },
380
+ {
381
+ name: "gitlab_create_branch",
382
+ title: "Create Branch",
383
+ description: "Create a new branch from an existing ref.",
384
+ mutating: true,
385
+ inputSchema: {
386
+ project_id: z.string().optional(),
387
+ branch: z.string().min(1),
388
+ ref: optionalString
389
+ },
390
+ handler: async (args, context) => {
391
+ const projectId = resolveProjectId(args, context, true);
392
+ let ref = getOptionalString(args, "ref");
393
+ if (!ref) {
394
+ const project = (await context.gitlab.getProject(projectId));
395
+ ref = typeof project.default_branch === "string" ? project.default_branch : "main";
396
+ }
397
+ return context.gitlab.createBranch(projectId, {
398
+ branch: getString(args, "branch"),
399
+ ref
400
+ });
401
+ }
402
+ },
403
+ {
404
+ name: "gitlab_get_branch_diffs",
405
+ title: "Get Branch Diffs",
406
+ description: "Compare two branches/refs and return diffs.",
407
+ mutating: false,
408
+ inputSchema: {
409
+ project_id: z.string().optional(),
410
+ from: z.string().min(1),
411
+ to: z.string().min(1),
412
+ straight: optionalBoolean,
413
+ excluded_file_patterns: optionalStringArray
414
+ },
415
+ handler: async (args, context) => {
416
+ const projectId = resolveProjectId(args, context, true);
417
+ const query = toQuery({ excluded_file_patterns: args.excluded_file_patterns });
418
+ return context.gitlab.getBranchDiffs(projectId, {
419
+ from: getString(args, "from"),
420
+ to: getString(args, "to"),
421
+ straight: getOptionalBoolean(args, "straight")
422
+ }, {
423
+ query
424
+ });
425
+ }
426
+ },
427
+ {
428
+ name: "gitlab_list_commits",
429
+ title: "List Commits",
430
+ description: "List commits in a project.",
431
+ mutating: false,
432
+ inputSchema: {
433
+ project_id: z.string().optional(),
434
+ ref_name: optionalString,
435
+ since: optionalString,
436
+ until: optionalString,
437
+ path: optionalString,
438
+ author: optionalString,
439
+ all: optionalBoolean,
440
+ with_stats: optionalBoolean,
441
+ first_parent: optionalBoolean,
442
+ order: z.enum(["default", "topo"]).optional(),
443
+ trailers: optionalBoolean,
444
+ ...paginationShape
445
+ },
446
+ handler: async (args, context) => {
447
+ const projectId = resolveProjectId(args, context, true);
448
+ return context.gitlab.listCommits(projectId, {
449
+ query: toQuery(omit(args, ["project_id"]))
450
+ });
451
+ }
452
+ },
453
+ {
454
+ name: "gitlab_get_commit",
455
+ title: "Get Commit",
456
+ description: "Get one commit by SHA.",
457
+ mutating: false,
458
+ inputSchema: {
459
+ project_id: z.string().optional(),
460
+ sha: z.string().min(1),
461
+ stats: optionalBoolean
462
+ },
463
+ handler: async (args, context) => {
464
+ const projectId = resolveProjectId(args, context, true);
465
+ return context.gitlab.getCommit(projectId, getString(args, "sha"), {
466
+ query: toQuery(omit(args, ["project_id", "sha"]))
467
+ });
468
+ }
469
+ },
470
+ {
471
+ name: "gitlab_get_commit_diff",
472
+ title: "Get Commit Diff",
473
+ description: "Get diff for one commit.",
474
+ mutating: false,
475
+ inputSchema: {
476
+ project_id: z.string().optional(),
477
+ sha: z.string().min(1),
478
+ full_diff: optionalBoolean,
479
+ ...paginationShape
480
+ },
481
+ handler: async (args, context) => {
482
+ const projectId = resolveProjectId(args, context, true);
483
+ return context.gitlab.getCommitDiff(projectId, getString(args, "sha"), {
484
+ query: toQuery(omit(args, ["project_id", "sha"]))
485
+ });
486
+ }
487
+ },
488
+ {
489
+ name: "gitlab_list_merge_requests",
490
+ title: "List Merge Requests",
491
+ description: "List merge requests for a project.",
492
+ mutating: false,
493
+ inputSchema: {
494
+ project_id: z.string().optional(),
495
+ assignee_id: optionalStringOrNumber,
496
+ assignee_username: optionalString,
497
+ author_id: optionalStringOrNumber,
498
+ author_username: optionalString,
499
+ reviewer_id: optionalStringOrNumber,
500
+ reviewer_username: optionalString,
501
+ created_after: optionalString,
502
+ created_before: optionalString,
503
+ updated_after: optionalString,
504
+ updated_before: optionalString,
505
+ labels: optionalStringOrStringArray,
506
+ milestone: optionalString,
507
+ state: optionalString,
508
+ scope: optionalString,
509
+ order_by: z
510
+ .enum([
511
+ "created_at",
512
+ "updated_at",
513
+ "priority",
514
+ "label_priority",
515
+ "milestone_due",
516
+ "popularity"
517
+ ])
518
+ .optional(),
519
+ sort: z.enum(["asc", "desc"]).optional(),
520
+ source_branch: optionalString,
521
+ target_branch: optionalString,
522
+ search: optionalString,
523
+ wip: z.enum(["yes", "no"]).optional(),
524
+ with_labels_details: optionalBoolean,
525
+ ...paginationShape
526
+ },
527
+ handler: async (args, context) => {
528
+ const projectId = resolveProjectId(args, context, false);
529
+ const query = toQuery(omit(args, ["project_id"]));
530
+ if (projectId) {
531
+ return context.gitlab.listMergeRequests(projectId, { query });
532
+ }
533
+ return context.gitlab.listGlobalMergeRequests({ query });
534
+ }
535
+ },
536
+ {
537
+ name: "gitlab_get_merge_request",
538
+ title: "Get Merge Request",
539
+ description: "Get one merge request.",
540
+ mutating: false,
541
+ inputSchema: {
542
+ project_id: z.string().optional(),
543
+ merge_request_iid: optionalString,
544
+ source_branch: optionalString
545
+ },
546
+ handler: async (args, context) => {
547
+ const projectId = resolveProjectId(args, context, true);
548
+ const mergeRequestIid = getOptionalString(args, "merge_request_iid");
549
+ if (mergeRequestIid) {
550
+ return context.gitlab.getMergeRequest(projectId, mergeRequestIid);
551
+ }
552
+ const sourceBranch = getOptionalString(args, "source_branch");
553
+ if (!sourceBranch) {
554
+ throw new Error("Either merge_request_iid or source_branch must be provided");
555
+ }
556
+ const candidates = await context.gitlab.listMergeRequests(projectId, {
557
+ query: {
558
+ source_branch: sourceBranch,
559
+ per_page: 100,
560
+ page: 1
561
+ }
562
+ });
563
+ const match = pickMergeRequestForSourceBranch(candidates, sourceBranch, {
564
+ requireOpened: false
565
+ });
566
+ return match;
567
+ }
568
+ },
569
+ {
570
+ name: "gitlab_create_merge_request",
571
+ title: "Create Merge Request",
572
+ description: "Create a merge request.",
573
+ mutating: true,
574
+ inputSchema: {
575
+ project_id: z.string().optional(),
576
+ source_branch: z.string().min(1),
577
+ target_branch: z.string().min(1),
578
+ title: z.string().min(1),
579
+ description: optionalString,
580
+ target_project_id: optionalString,
581
+ assignee_ids: optionalNumberArray,
582
+ reviewer_ids: optionalNumberArray,
583
+ labels: optionalStringOrStringArray,
584
+ allow_collaboration: optionalBoolean,
585
+ remove_source_branch: optionalBoolean,
586
+ squash: optionalBoolean,
587
+ draft: optionalBoolean
588
+ },
589
+ handler: async (args, context) => {
590
+ const projectId = resolveProjectId(args, context, true);
591
+ return context.gitlab.createMergeRequest(projectId, {
592
+ source_branch: getString(args, "source_branch"),
593
+ target_branch: getString(args, "target_branch"),
594
+ title: getString(args, "title"),
595
+ description: getOptionalString(args, "description"),
596
+ target_project_id: getOptionalString(args, "target_project_id"),
597
+ assignee_ids: getOptionalNumberArray(args, "assignee_ids"),
598
+ reviewer_ids: getOptionalNumberArray(args, "reviewer_ids"),
599
+ labels: toCsvValue(args.labels),
600
+ allow_collaboration: getOptionalBoolean(args, "allow_collaboration"),
601
+ remove_source_branch: getOptionalBoolean(args, "remove_source_branch"),
602
+ squash: getOptionalBoolean(args, "squash"),
603
+ draft: getOptionalBoolean(args, "draft")
604
+ });
605
+ }
606
+ },
607
+ {
608
+ name: "gitlab_fork_repository",
609
+ title: "Fork Repository",
610
+ description: "Fork an existing project to another namespace.",
611
+ mutating: true,
612
+ inputSchema: {
613
+ project_id: z.string().optional(),
614
+ namespace: optionalString,
615
+ namespace_id: optionalString,
616
+ path: optionalString,
617
+ name: optionalString,
618
+ description: optionalString,
619
+ visibility: z.enum(["private", "internal", "public"]).optional(),
620
+ default_branch: optionalString
621
+ },
622
+ handler: async (args, context) => context.gitlab.forkRepository(resolveProjectId(args, context, true), {
623
+ namespace: getOptionalString(args, "namespace"),
624
+ namespace_id: getOptionalString(args, "namespace_id"),
625
+ path: getOptionalString(args, "path"),
626
+ name: getOptionalString(args, "name"),
627
+ description: getOptionalString(args, "description"),
628
+ visibility: getOptionalString(args, "visibility"),
629
+ default_branch: getOptionalString(args, "default_branch")
630
+ })
631
+ },
632
+ {
633
+ name: "gitlab_update_merge_request",
634
+ title: "Update Merge Request",
635
+ description: "Update merge request fields.",
636
+ mutating: true,
637
+ inputSchema: {
638
+ project_id: z.string().optional(),
639
+ merge_request_iid: z.string().min(1),
640
+ source_branch: optionalString,
641
+ title: optionalString,
642
+ description: optionalString,
643
+ target_branch: optionalString,
644
+ assignee_ids: optionalNumberArray,
645
+ reviewer_ids: optionalNumberArray,
646
+ reviewers: optionalStringArray,
647
+ labels: optionalStringOrStringArray,
648
+ state_event: optionalString,
649
+ squash: optionalBoolean,
650
+ draft: optionalBoolean,
651
+ remove_source_branch: optionalBoolean
652
+ },
653
+ handler: async (args, context) => {
654
+ const projectId = resolveProjectId(args, context, true);
655
+ const payload = toQuery(omit(args, ["project_id", "merge_request_iid"]));
656
+ if (payload.labels === undefined) {
657
+ payload.labels = toCsvValue(args.labels);
658
+ }
659
+ if (Array.isArray(args.assignee_ids)) {
660
+ payload.assignee_ids = args.assignee_ids;
661
+ }
662
+ if (Array.isArray(args.reviewer_ids)) {
663
+ payload.reviewer_ids = args.reviewer_ids;
664
+ }
665
+ if (payload.reviewer_ids === undefined && Array.isArray(args.reviewers)) {
666
+ payload.reviewer_ids = args.reviewers.join(",");
667
+ }
668
+ return context.gitlab.updateMergeRequest(projectId, getString(args, "merge_request_iid"), payload);
669
+ }
670
+ },
671
+ {
672
+ name: "gitlab_merge_merge_request",
673
+ title: "Merge Merge Request",
674
+ description: "Merge an existing merge request.",
675
+ mutating: true,
676
+ inputSchema: {
677
+ project_id: z.string().optional(),
678
+ merge_request_iid: optionalString,
679
+ source_branch: optionalString,
680
+ auto_merge: optionalBoolean,
681
+ merge_when_pipeline_succeeds: optionalBoolean,
682
+ merge_commit_message: optionalString,
683
+ squash_commit_message: optionalString,
684
+ should_remove_source_branch: optionalBoolean,
685
+ squash: optionalBoolean,
686
+ sha: optionalString
687
+ },
688
+ handler: async (args, context) => {
689
+ const projectId = resolveProjectId(args, context, true);
690
+ let mergeRequestIid = getOptionalString(args, "merge_request_iid");
691
+ if (!mergeRequestIid) {
692
+ const sourceBranch = getOptionalString(args, "source_branch");
693
+ if (!sourceBranch) {
694
+ throw new Error("Either merge_request_iid or source_branch must be provided");
695
+ }
696
+ const candidates = await context.gitlab.listMergeRequests(projectId, {
697
+ query: {
698
+ source_branch: sourceBranch,
699
+ per_page: 100,
700
+ page: 1
701
+ }
702
+ });
703
+ const match = pickMergeRequestForSourceBranch(candidates, sourceBranch, {
704
+ requireOpened: true
705
+ });
706
+ const iid = getMergeRequestIid(match);
707
+ mergeRequestIid = String(iid);
708
+ }
709
+ return context.gitlab.mergeMergeRequest(projectId, mergeRequestIid, toQuery(omit(args, ["project_id", "merge_request_iid", "source_branch"])));
710
+ }
711
+ },
712
+ {
713
+ name: "gitlab_get_merge_request_diffs",
714
+ title: "Get Merge Request Diffs",
715
+ description: "Get MR diffs with changed files.",
716
+ mutating: false,
717
+ inputSchema: {
718
+ project_id: z.string().optional(),
719
+ merge_request_iid: z.string().min(1),
720
+ view: z.enum(["inline", "parallel"]).optional(),
721
+ excluded_file_patterns: optionalStringArray
722
+ },
723
+ handler: async (args, context) => context.gitlab.getMergeRequestDiffs(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
724
+ },
725
+ {
726
+ name: "gitlab_list_merge_request_diffs",
727
+ title: "List Merge Request Diffs",
728
+ description: "List detailed MR diffs (versions/changes view).",
729
+ mutating: false,
730
+ inputSchema: {
731
+ project_id: z.string().optional(),
732
+ merge_request_iid: z.string().min(1),
733
+ page: optionalNumber,
734
+ per_page: optionalNumber,
735
+ unidiff: optionalBoolean
736
+ },
737
+ handler: async (args, context) => context.gitlab.listMergeRequestDiffs(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
738
+ },
739
+ {
740
+ name: "gitlab_get_merge_request_code_context",
741
+ title: "Get Merge Request Code Context",
742
+ description: "High-signal MR code context with include/exclude filters, sorting, and token-budgeted output.",
743
+ mutating: false,
744
+ inputSchema: mergeRequestCodeContextSchema,
745
+ handler: async (args, context) => getMergeRequestCodeContext({
746
+ projectId: resolveProjectId(args, context, true),
747
+ mergeRequestIid: getString(args, "merge_request_iid"),
748
+ includePaths: getOptionalStringArray(args, "include_paths"),
749
+ excludePaths: getOptionalStringArray(args, "exclude_paths"),
750
+ extensions: getOptionalStringArray(args, "extensions"),
751
+ languages: getOptionalStringArray(args, "languages"),
752
+ maxFiles: getOptionalNumber(args, "max_files") ?? 30,
753
+ maxTotalChars: getOptionalNumber(args, "max_total_chars") ?? 120_000,
754
+ contextLines: getOptionalNumber(args, "context_lines") ?? 20,
755
+ mode: getOptionalString(args, "mode") ?? "patch",
756
+ sort: getOptionalString(args, "sort") ?? "changed_lines",
757
+ listOnly: getOptionalBoolean(args, "list_only") ?? false
758
+ }, context)
759
+ },
760
+ {
761
+ name: "gitlab_list_merge_request_versions",
762
+ title: "List Merge Request Versions",
763
+ description: "List MR diff versions.",
764
+ mutating: false,
765
+ inputSchema: {
766
+ project_id: z.string().optional(),
767
+ merge_request_iid: z.string().min(1)
768
+ },
769
+ handler: async (args, context) => context.gitlab.listMergeRequestVersions(resolveProjectId(args, context, true), getString(args, "merge_request_iid"))
770
+ },
771
+ {
772
+ name: "gitlab_get_merge_request_version",
773
+ title: "Get Merge Request Version",
774
+ description: "Get one MR diff version.",
775
+ mutating: false,
776
+ inputSchema: {
777
+ project_id: z.string().optional(),
778
+ merge_request_iid: z.string().min(1),
779
+ version_id: z.string().min(1),
780
+ unidiff: optionalBoolean
781
+ },
782
+ handler: async (args, context) => context.gitlab.getMergeRequestVersion(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "version_id"), { query: toQuery(omit(args, ["project_id", "merge_request_iid", "version_id"])) })
783
+ },
784
+ {
785
+ name: "gitlab_approve_merge_request",
786
+ title: "Approve Merge Request",
787
+ description: "Approve a merge request.",
788
+ mutating: true,
789
+ inputSchema: {
790
+ project_id: z.string().optional(),
791
+ merge_request_iid: z.string().min(1),
792
+ sha: optionalString,
793
+ approval_password: optionalString
794
+ },
795
+ handler: async (args, context) => context.gitlab.approveMergeRequest(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), toQuery(omit(args, ["project_id", "merge_request_iid"])))
796
+ },
797
+ {
798
+ name: "gitlab_unapprove_merge_request",
799
+ title: "Unapprove Merge Request",
800
+ description: "Remove current user approval from MR.",
801
+ mutating: true,
802
+ inputSchema: {
803
+ project_id: z.string().optional(),
804
+ merge_request_iid: z.string().min(1)
805
+ },
806
+ handler: async (args, context) => context.gitlab.unapproveMergeRequest(resolveProjectId(args, context, true), getString(args, "merge_request_iid"))
807
+ },
808
+ {
809
+ name: "gitlab_get_merge_request_approval_state",
810
+ title: "Get Merge Request Approval State",
811
+ description: "Get approval state for MR.",
812
+ mutating: false,
813
+ inputSchema: {
814
+ project_id: z.string().optional(),
815
+ merge_request_iid: z.string().min(1)
816
+ },
817
+ handler: async (args, context) => context.gitlab.getMergeRequestApprovalState(resolveProjectId(args, context, true), getString(args, "merge_request_iid"))
818
+ },
819
+ {
820
+ name: "gitlab_list_merge_request_discussions",
821
+ title: "List Merge Request Discussions",
822
+ description: "List MR discussions.",
823
+ mutating: false,
824
+ inputSchema: {
825
+ project_id: z.string().optional(),
826
+ merge_request_iid: z.string().min(1),
827
+ ...paginationShape
828
+ },
829
+ handler: async (args, context) => context.gitlab.listMergeRequestDiscussions(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
830
+ },
831
+ {
832
+ name: "gitlab_create_merge_request_thread",
833
+ title: "Create Merge Request Thread",
834
+ description: "Create a new MR discussion thread (supports diff positions).",
835
+ mutating: true,
836
+ inputSchema: {
837
+ project_id: z.string().optional(),
838
+ merge_request_iid: z.string().min(1),
839
+ body: z.string().min(1),
840
+ position: optionalRecord,
841
+ created_at: optionalString
842
+ },
843
+ handler: async (args, context) => context.gitlab.createMergeRequestThread(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), {
844
+ body: getString(args, "body"),
845
+ position: getOptionalRecord(args, "position"),
846
+ created_at: getOptionalString(args, "created_at")
847
+ })
848
+ },
849
+ {
850
+ name: "gitlab_mr_discussions",
851
+ title: "Merge Request Discussions (Alias)",
852
+ description: "Backward-compatible alias of gitlab_list_merge_request_discussions.",
853
+ mutating: false,
854
+ inputSchema: {
855
+ project_id: z.string().optional(),
856
+ merge_request_iid: z.string().min(1),
857
+ ...paginationShape
858
+ },
859
+ handler: async (args, context) => context.gitlab.listMergeRequestDiscussions(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
860
+ },
861
+ {
862
+ name: "gitlab_create_merge_request_discussion_note",
863
+ title: "Create MR Discussion Note",
864
+ description: "Add note to existing MR discussion thread.",
865
+ mutating: true,
866
+ inputSchema: {
867
+ project_id: z.string().optional(),
868
+ merge_request_iid: z.string().min(1),
869
+ discussion_id: z.string().min(1),
870
+ body: z.string().min(1),
871
+ created_at: optionalString
872
+ },
873
+ handler: async (args, context) => context.gitlab.createMergeRequestDiscussionNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "discussion_id"), {
874
+ body: getString(args, "body"),
875
+ created_at: getOptionalString(args, "created_at")
876
+ })
877
+ },
878
+ {
879
+ name: "gitlab_update_merge_request_discussion_note",
880
+ title: "Update MR Discussion Note",
881
+ description: "Update note body/resolved state in MR discussion.",
882
+ mutating: true,
883
+ inputSchema: {
884
+ project_id: z.string().optional(),
885
+ merge_request_iid: z.string().min(1),
886
+ discussion_id: z.string().min(1),
887
+ note_id: z.string().min(1),
888
+ body: optionalString,
889
+ resolved: optionalBoolean
890
+ },
891
+ handler: async (args, context) => {
892
+ const body = getOptionalString(args, "body");
893
+ const resolved = getOptionalBoolean(args, "resolved");
894
+ if (body === undefined && resolved === undefined) {
895
+ throw new Error("Either body or resolved must be provided");
896
+ }
897
+ if (body !== undefined && resolved !== undefined) {
898
+ throw new Error("Provide either body or resolved, not both");
899
+ }
900
+ return context.gitlab.updateMergeRequestDiscussionNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "discussion_id"), getString(args, "note_id"), {
901
+ body,
902
+ resolved
903
+ });
904
+ }
905
+ },
906
+ {
907
+ name: "gitlab_delete_merge_request_discussion_note",
908
+ title: "Delete MR Discussion Note",
909
+ description: "Delete note from MR discussion thread.",
910
+ mutating: true,
911
+ inputSchema: {
912
+ project_id: z.string().optional(),
913
+ merge_request_iid: z.string().min(1),
914
+ discussion_id: z.string().min(1),
915
+ note_id: z.string().min(1)
916
+ },
917
+ handler: async (args, context) => context.gitlab.deleteMergeRequestDiscussionNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "discussion_id"), getString(args, "note_id"))
918
+ },
919
+ {
920
+ name: "gitlab_resolve_merge_request_thread",
921
+ title: "Resolve Merge Request Thread",
922
+ description: "Resolve/unresolve an MR discussion note.",
923
+ mutating: true,
924
+ inputSchema: {
925
+ project_id: z.string().optional(),
926
+ merge_request_iid: z.string().min(1),
927
+ discussion_id: z.string().min(1),
928
+ note_id: z.string().min(1),
929
+ resolved: z.boolean().default(true)
930
+ },
931
+ handler: async (args, context) => context.gitlab.resolveMergeRequestThread(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "discussion_id"), getString(args, "note_id"), getBoolean(args, "resolved"))
932
+ },
933
+ {
934
+ name: "gitlab_list_merge_request_notes",
935
+ title: "List Merge Request Notes",
936
+ description: "List top-level notes for an MR.",
937
+ mutating: false,
938
+ inputSchema: {
939
+ project_id: z.string().optional(),
940
+ merge_request_iid: z.string().min(1),
941
+ sort: optionalString,
942
+ order_by: optionalString,
943
+ ...paginationShape
944
+ },
945
+ handler: async (args, context) => context.gitlab.listMergeRequestNotes(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
946
+ },
947
+ {
948
+ name: "gitlab_get_merge_request_notes",
949
+ title: "Get Merge Request Notes (Alias)",
950
+ description: "Backward-compatible alias of gitlab_list_merge_request_notes.",
951
+ mutating: false,
952
+ inputSchema: {
953
+ project_id: z.string().optional(),
954
+ merge_request_iid: z.string().min(1),
955
+ sort: optionalString,
956
+ order_by: optionalString,
957
+ ...paginationShape
958
+ },
959
+ handler: async (args, context) => context.gitlab.listMergeRequestNotes(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
960
+ },
961
+ {
962
+ name: "gitlab_get_draft_note",
963
+ title: "Get Draft Note",
964
+ description: "Get a single merge-request draft note.",
965
+ mutating: false,
966
+ inputSchema: {
967
+ project_id: z.string().optional(),
968
+ merge_request_iid: z.string().min(1),
969
+ draft_note_id: z.string().min(1)
970
+ },
971
+ handler: async (args, context) => context.gitlab.getDraftNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "draft_note_id"))
972
+ },
973
+ {
974
+ name: "gitlab_list_draft_notes",
975
+ title: "List Draft Notes",
976
+ description: "List draft notes on a merge request.",
977
+ mutating: false,
978
+ inputSchema: {
979
+ project_id: z.string().optional(),
980
+ merge_request_iid: z.string().min(1)
981
+ },
982
+ handler: async (args, context) => context.gitlab.listDraftNotes(resolveProjectId(args, context, true), getString(args, "merge_request_iid"))
983
+ },
984
+ {
985
+ name: "gitlab_create_draft_note",
986
+ title: "Create Draft Note",
987
+ description: "Create a merge-request draft note.",
988
+ mutating: true,
989
+ inputSchema: {
990
+ project_id: z.string().optional(),
991
+ merge_request_iid: z.string().min(1),
992
+ body: z.string().min(1),
993
+ position: optionalRecord,
994
+ resolve_discussion: optionalBoolean
995
+ },
996
+ handler: async (args, context) => context.gitlab.createDraftNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), {
997
+ body: getString(args, "body"),
998
+ position: getOptionalRecord(args, "position"),
999
+ resolve_discussion: getOptionalBoolean(args, "resolve_discussion")
1000
+ })
1001
+ },
1002
+ {
1003
+ name: "gitlab_update_draft_note",
1004
+ title: "Update Draft Note",
1005
+ description: "Update a merge-request draft note.",
1006
+ mutating: true,
1007
+ inputSchema: {
1008
+ project_id: z.string().optional(),
1009
+ merge_request_iid: z.string().min(1),
1010
+ draft_note_id: z.string().min(1),
1011
+ body: optionalString,
1012
+ position: optionalRecord,
1013
+ resolve_discussion: optionalBoolean
1014
+ },
1015
+ handler: async (args, context) => {
1016
+ if (getOptionalString(args, "body") === undefined &&
1017
+ getOptionalRecord(args, "position") === undefined &&
1018
+ getOptionalBoolean(args, "resolve_discussion") === undefined) {
1019
+ throw new Error("At least one of body, position, or resolve_discussion is required");
1020
+ }
1021
+ return context.gitlab.updateDraftNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "draft_note_id"), {
1022
+ body: getOptionalString(args, "body"),
1023
+ position: getOptionalRecord(args, "position"),
1024
+ resolve_discussion: getOptionalBoolean(args, "resolve_discussion")
1025
+ });
1026
+ }
1027
+ },
1028
+ {
1029
+ name: "gitlab_delete_draft_note",
1030
+ title: "Delete Draft Note",
1031
+ description: "Delete a merge-request draft note.",
1032
+ mutating: true,
1033
+ inputSchema: {
1034
+ project_id: z.string().optional(),
1035
+ merge_request_iid: z.string().min(1),
1036
+ draft_note_id: z.string().min(1)
1037
+ },
1038
+ handler: async (args, context) => context.gitlab.deleteDraftNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "draft_note_id"))
1039
+ },
1040
+ {
1041
+ name: "gitlab_publish_draft_note",
1042
+ title: "Publish Draft Note",
1043
+ description: "Publish one merge-request draft note.",
1044
+ mutating: true,
1045
+ inputSchema: {
1046
+ project_id: z.string().optional(),
1047
+ merge_request_iid: z.string().min(1),
1048
+ draft_note_id: z.string().min(1)
1049
+ },
1050
+ handler: async (args, context) => context.gitlab.publishDraftNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "draft_note_id"))
1051
+ },
1052
+ {
1053
+ name: "gitlab_bulk_publish_draft_notes",
1054
+ title: "Bulk Publish Draft Notes",
1055
+ description: "Publish all merge-request draft notes.",
1056
+ mutating: true,
1057
+ inputSchema: {
1058
+ project_id: z.string().optional(),
1059
+ merge_request_iid: z.string().min(1)
1060
+ },
1061
+ handler: async (args, context) => context.gitlab.bulkPublishDraftNotes(resolveProjectId(args, context, true), getString(args, "merge_request_iid"))
1062
+ },
1063
+ {
1064
+ name: "gitlab_get_merge_request_note",
1065
+ title: "Get Merge Request Note",
1066
+ description: "Get a single MR note.",
1067
+ mutating: false,
1068
+ inputSchema: {
1069
+ project_id: z.string().optional(),
1070
+ merge_request_iid: z.string().min(1),
1071
+ note_id: z.string().min(1)
1072
+ },
1073
+ handler: async (args, context) => context.gitlab.getMergeRequestNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "note_id"))
1074
+ },
1075
+ {
1076
+ name: "gitlab_create_merge_request_note",
1077
+ title: "Create Merge Request Note",
1078
+ description: "Create a top-level MR note.",
1079
+ mutating: true,
1080
+ inputSchema: {
1081
+ project_id: z.string().optional(),
1082
+ merge_request_iid: z.string().min(1),
1083
+ body: z.string().min(1)
1084
+ },
1085
+ handler: async (args, context) => context.gitlab.createMergeRequestNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "body"))
1086
+ },
1087
+ {
1088
+ name: "gitlab_create_note",
1089
+ title: "Create Note",
1090
+ description: "Create a note on an issue or merge request.",
1091
+ mutating: true,
1092
+ inputSchema: {
1093
+ project_id: z.string().optional(),
1094
+ noteable_type: z.enum(["issue", "merge_request"]),
1095
+ noteable_iid: z.string().min(1),
1096
+ body: z.string().min(1)
1097
+ },
1098
+ handler: async (args, context) => context.gitlab.createNote(resolveProjectId(args, context, true), getString(args, "noteable_type"), getString(args, "noteable_iid"), getString(args, "body"))
1099
+ },
1100
+ {
1101
+ name: "gitlab_update_merge_request_note",
1102
+ title: "Update Merge Request Note",
1103
+ description: "Update MR note body.",
1104
+ mutating: true,
1105
+ inputSchema: {
1106
+ project_id: z.string().optional(),
1107
+ merge_request_iid: z.string().min(1),
1108
+ note_id: z.string().min(1),
1109
+ body: z.string().min(1)
1110
+ },
1111
+ handler: async (args, context) => context.gitlab.updateMergeRequestNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "note_id"), getString(args, "body"))
1112
+ },
1113
+ {
1114
+ name: "gitlab_delete_merge_request_note",
1115
+ title: "Delete Merge Request Note",
1116
+ description: "Delete an MR note.",
1117
+ mutating: true,
1118
+ inputSchema: {
1119
+ project_id: z.string().optional(),
1120
+ merge_request_iid: z.string().min(1),
1121
+ note_id: z.string().min(1)
1122
+ },
1123
+ handler: async (args, context) => context.gitlab.deleteMergeRequestNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "note_id"))
1124
+ },
1125
+ {
1126
+ name: "gitlab_list_issues",
1127
+ title: "List Issues",
1128
+ description: "List issues in project.",
1129
+ mutating: false,
1130
+ inputSchema: {
1131
+ project_id: z.string().optional(),
1132
+ assignee_id: optionalStringOrNumber,
1133
+ assignee_username: optionalStringArray,
1134
+ author_id: optionalStringOrNumber,
1135
+ author_username: optionalString,
1136
+ confidential: optionalBoolean,
1137
+ created_after: optionalString,
1138
+ created_before: optionalString,
1139
+ due_date: optionalString,
1140
+ labels: optionalStringOrStringArray,
1141
+ milestone: optionalString,
1142
+ issue_type: z.enum(["issue", "incident", "test_case", "task"]).optional(),
1143
+ iteration_id: optionalStringOrNumber,
1144
+ scope: z.enum(["created_by_me", "assigned_to_me", "all"]).optional(),
1145
+ state: optionalString,
1146
+ search: optionalString,
1147
+ updated_after: optionalString,
1148
+ updated_before: optionalString,
1149
+ with_labels_details: optionalBoolean,
1150
+ ...paginationShape
1151
+ },
1152
+ handler: async (args, context) => {
1153
+ const projectId = resolveProjectId(args, context, false);
1154
+ const query = toQuery(omit(args, ["project_id"]));
1155
+ if (projectId) {
1156
+ return context.gitlab.listIssues(projectId, { query });
1157
+ }
1158
+ return context.gitlab.listGlobalIssues({ query });
1159
+ }
1160
+ },
1161
+ {
1162
+ name: "gitlab_my_issues",
1163
+ title: "My Issues",
1164
+ description: "List issues assigned to the current authenticated user.",
1165
+ mutating: false,
1166
+ inputSchema: {
1167
+ project_id: z.string().optional(),
1168
+ state: z.enum(["opened", "closed", "all"]).optional(),
1169
+ labels: optionalStringOrStringArray,
1170
+ milestone: optionalString,
1171
+ search: optionalString,
1172
+ created_after: optionalString,
1173
+ created_before: optionalString,
1174
+ updated_after: optionalString,
1175
+ updated_before: optionalString,
1176
+ ...paginationShape
1177
+ },
1178
+ handler: async (args, context) => {
1179
+ const projectId = resolveProjectId(args, context, false);
1180
+ return context.gitlab.myIssues({
1181
+ project_id: projectId || undefined,
1182
+ ...toQuery(omit(args, ["project_id"]))
1183
+ });
1184
+ }
1185
+ },
1186
+ {
1187
+ name: "gitlab_get_issue",
1188
+ title: "Get Issue",
1189
+ description: "Get issue by IID.",
1190
+ mutating: false,
1191
+ inputSchema: {
1192
+ project_id: z.string().optional(),
1193
+ issue_iid: z.string().min(1)
1194
+ },
1195
+ handler: async (args, context) => context.gitlab.getIssue(resolveProjectId(args, context, true), getString(args, "issue_iid"))
1196
+ },
1197
+ {
1198
+ name: "gitlab_create_issue",
1199
+ title: "Create Issue",
1200
+ description: "Create a new issue.",
1201
+ mutating: true,
1202
+ inputSchema: {
1203
+ project_id: z.string().optional(),
1204
+ title: z.string().min(1),
1205
+ description: optionalString,
1206
+ labels: optionalStringOrStringArray,
1207
+ milestone_id: optionalNumber,
1208
+ due_date: optionalString,
1209
+ confidential: optionalBoolean,
1210
+ issue_type: optionalString,
1211
+ assignee_ids: optionalNumberArray
1212
+ },
1213
+ handler: async (args, context) => context.gitlab.createIssue(resolveProjectId(args, context, true), {
1214
+ title: getString(args, "title"),
1215
+ description: getOptionalString(args, "description"),
1216
+ labels: toCsvValue(args.labels),
1217
+ milestone_id: getOptionalNumber(args, "milestone_id"),
1218
+ due_date: getOptionalString(args, "due_date"),
1219
+ confidential: getOptionalBoolean(args, "confidential"),
1220
+ issue_type: getOptionalString(args, "issue_type"),
1221
+ assignee_ids: getOptionalNumberArray(args, "assignee_ids")
1222
+ })
1223
+ },
1224
+ {
1225
+ name: "gitlab_update_issue",
1226
+ title: "Update Issue",
1227
+ description: "Update issue fields.",
1228
+ mutating: true,
1229
+ inputSchema: {
1230
+ project_id: z.string().optional(),
1231
+ issue_iid: z.string().min(1),
1232
+ title: optionalString,
1233
+ description: optionalString,
1234
+ state_event: optionalString,
1235
+ labels: optionalStringOrStringArray,
1236
+ milestone_id: optionalNumber,
1237
+ due_date: optionalString,
1238
+ confidential: optionalBoolean,
1239
+ assignee_ids: optionalNumberArray,
1240
+ discussion_locked: optionalBoolean,
1241
+ weight: optionalNumber,
1242
+ issue_type: z.enum(["issue", "incident", "test_case", "task"]).optional()
1243
+ },
1244
+ handler: async (args, context) => {
1245
+ const payload = toQuery(omit(args, ["project_id", "issue_iid"]));
1246
+ if (payload.labels === undefined) {
1247
+ payload.labels = toCsvValue(args.labels);
1248
+ }
1249
+ if (Array.isArray(args.assignee_ids)) {
1250
+ payload.assignee_ids = args.assignee_ids;
1251
+ }
1252
+ return context.gitlab.updateIssue(resolveProjectId(args, context, true), getString(args, "issue_iid"), payload);
1253
+ }
1254
+ },
1255
+ {
1256
+ name: "gitlab_delete_issue",
1257
+ title: "Delete Issue",
1258
+ description: "Delete an issue.",
1259
+ mutating: true,
1260
+ inputSchema: {
1261
+ project_id: z.string().optional(),
1262
+ issue_iid: z.string().min(1)
1263
+ },
1264
+ handler: async (args, context) => context.gitlab.deleteIssue(resolveProjectId(args, context, true), getString(args, "issue_iid"))
1265
+ },
1266
+ {
1267
+ name: "gitlab_list_issue_discussions",
1268
+ title: "List Issue Discussions",
1269
+ description: "List issue discussions.",
1270
+ mutating: false,
1271
+ inputSchema: {
1272
+ project_id: z.string().optional(),
1273
+ issue_iid: z.string().min(1),
1274
+ ...paginationShape
1275
+ },
1276
+ handler: async (args, context) => context.gitlab.listIssueDiscussions(resolveProjectId(args, context, true), getString(args, "issue_iid"), { query: toQuery(omit(args, ["project_id", "issue_iid"])) })
1277
+ },
1278
+ {
1279
+ name: "gitlab_create_issue_note",
1280
+ title: "Create Issue Note",
1281
+ description: "Create issue comment (top-level or discussion note).",
1282
+ mutating: true,
1283
+ inputSchema: {
1284
+ project_id: z.string().optional(),
1285
+ issue_iid: z.string().min(1),
1286
+ discussion_id: optionalString,
1287
+ body: z.string().min(1),
1288
+ created_at: optionalString
1289
+ },
1290
+ handler: async (args, context) => context.gitlab.createIssueNote(resolveProjectId(args, context, true), getString(args, "issue_iid"), {
1291
+ body: getString(args, "body"),
1292
+ discussion_id: getOptionalString(args, "discussion_id"),
1293
+ created_at: getOptionalString(args, "created_at")
1294
+ })
1295
+ },
1296
+ {
1297
+ name: "gitlab_update_issue_note",
1298
+ title: "Update Issue Note",
1299
+ description: "Update an issue discussion note body or resolved state.",
1300
+ mutating: true,
1301
+ inputSchema: {
1302
+ project_id: z.string().optional(),
1303
+ issue_iid: z.string().min(1),
1304
+ discussion_id: z.string().min(1),
1305
+ note_id: z.string().min(1),
1306
+ body: optionalString,
1307
+ resolved: optionalBoolean
1308
+ },
1309
+ handler: async (args, context) => {
1310
+ const body = getOptionalString(args, "body");
1311
+ const resolved = getOptionalBoolean(args, "resolved");
1312
+ if (body === undefined && resolved === undefined) {
1313
+ throw new Error("Either body or resolved must be provided");
1314
+ }
1315
+ if (body !== undefined && resolved !== undefined) {
1316
+ throw new Error("Provide either body or resolved, not both");
1317
+ }
1318
+ return context.gitlab.updateIssueNote(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "discussion_id"), getString(args, "note_id"), { body, resolved });
1319
+ }
1320
+ },
1321
+ {
1322
+ name: "gitlab_list_issue_links",
1323
+ title: "List Issue Links",
1324
+ description: "List related issue links for an issue.",
1325
+ mutating: false,
1326
+ inputSchema: {
1327
+ project_id: z.string().optional(),
1328
+ issue_iid: z.string().min(1)
1329
+ },
1330
+ handler: async (args, context) => context.gitlab.listIssueLinks(resolveProjectId(args, context, true), getString(args, "issue_iid"))
1331
+ },
1332
+ {
1333
+ name: "gitlab_get_issue_link",
1334
+ title: "Get Issue Link",
1335
+ description: "Get a single issue link by ID.",
1336
+ mutating: false,
1337
+ inputSchema: {
1338
+ project_id: z.string().optional(),
1339
+ issue_iid: z.string().min(1),
1340
+ issue_link_id: z.string().min(1)
1341
+ },
1342
+ handler: async (args, context) => context.gitlab.getIssueLink(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "issue_link_id"))
1343
+ },
1344
+ {
1345
+ name: "gitlab_create_issue_link",
1346
+ title: "Create Issue Link",
1347
+ description: "Create a relation between two issues.",
1348
+ mutating: true,
1349
+ inputSchema: {
1350
+ project_id: z.string().optional(),
1351
+ issue_iid: z.string().min(1),
1352
+ target_project_id: z.string().min(1),
1353
+ target_issue_iid: z.string().min(1),
1354
+ link_type: z.enum(["relates_to", "blocks", "is_blocked_by"]).optional()
1355
+ },
1356
+ handler: async (args, context) => context.gitlab.createIssueLink(resolveProjectId(args, context, true), getString(args, "issue_iid"), {
1357
+ target_project_id: getString(args, "target_project_id"),
1358
+ target_issue_iid: getString(args, "target_issue_iid"),
1359
+ link_type: getOptionalString(args, "link_type")
1360
+ })
1361
+ },
1362
+ {
1363
+ name: "gitlab_delete_issue_link",
1364
+ title: "Delete Issue Link",
1365
+ description: "Delete a relation between issues.",
1366
+ mutating: true,
1367
+ inputSchema: {
1368
+ project_id: z.string().optional(),
1369
+ issue_iid: z.string().min(1),
1370
+ issue_link_id: z.string().min(1)
1371
+ },
1372
+ handler: async (args, context) => context.gitlab.deleteIssueLink(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "issue_link_id"))
1373
+ },
1374
+ {
1375
+ name: "gitlab_list_wiki_pages",
1376
+ title: "List Wiki Pages",
1377
+ description: "List wiki pages in a project.",
1378
+ mutating: false,
1379
+ requiresFeature: "wiki",
1380
+ inputSchema: {
1381
+ project_id: z.string().optional(),
1382
+ with_content: optionalBoolean,
1383
+ ...paginationShape
1384
+ },
1385
+ handler: async (args, context) => context.gitlab.listWikiPages(resolveProjectId(args, context, true), {
1386
+ query: toQuery(omit(args, ["project_id"]))
1387
+ })
1388
+ },
1389
+ {
1390
+ name: "gitlab_get_wiki_page",
1391
+ title: "Get Wiki Page",
1392
+ description: "Get wiki page by slug.",
1393
+ mutating: false,
1394
+ requiresFeature: "wiki",
1395
+ inputSchema: {
1396
+ project_id: z.string().optional(),
1397
+ slug: z.string().min(1),
1398
+ version: optionalString
1399
+ },
1400
+ handler: async (args, context) => context.gitlab.getWikiPage(resolveProjectId(args, context, true), getString(args, "slug"), {
1401
+ query: toQuery(omit(args, ["project_id", "slug"]))
1402
+ })
1403
+ },
1404
+ {
1405
+ name: "gitlab_create_wiki_page",
1406
+ title: "Create Wiki Page",
1407
+ description: "Create a wiki page.",
1408
+ mutating: true,
1409
+ requiresFeature: "wiki",
1410
+ inputSchema: {
1411
+ project_id: z.string().optional(),
1412
+ title: z.string().min(1),
1413
+ content: z.string().min(1),
1414
+ format: optionalString
1415
+ },
1416
+ handler: async (args, context) => context.gitlab.createWikiPage(resolveProjectId(args, context, true), {
1417
+ title: getString(args, "title"),
1418
+ content: getString(args, "content"),
1419
+ format: getOptionalString(args, "format")
1420
+ })
1421
+ },
1422
+ {
1423
+ name: "gitlab_update_wiki_page",
1424
+ title: "Update Wiki Page",
1425
+ description: "Update wiki page by slug.",
1426
+ mutating: true,
1427
+ requiresFeature: "wiki",
1428
+ inputSchema: {
1429
+ project_id: z.string().optional(),
1430
+ slug: z.string().min(1),
1431
+ content: z.string().min(1),
1432
+ title: optionalString,
1433
+ format: optionalString
1434
+ },
1435
+ handler: async (args, context) => context.gitlab.updateWikiPage(resolveProjectId(args, context, true), getString(args, "slug"), {
1436
+ content: getString(args, "content"),
1437
+ title: getOptionalString(args, "title"),
1438
+ format: getOptionalString(args, "format")
1439
+ })
1440
+ },
1441
+ {
1442
+ name: "gitlab_delete_wiki_page",
1443
+ title: "Delete Wiki Page",
1444
+ description: "Delete wiki page by slug.",
1445
+ mutating: true,
1446
+ requiresFeature: "wiki",
1447
+ inputSchema: {
1448
+ project_id: z.string().optional(),
1449
+ slug: z.string().min(1)
1450
+ },
1451
+ handler: async (args, context) => context.gitlab.deleteWikiPage(resolveProjectId(args, context, true), getString(args, "slug"))
1452
+ },
1453
+ {
1454
+ name: "gitlab_list_pipelines",
1455
+ title: "List Pipelines",
1456
+ description: "List pipelines for a project.",
1457
+ mutating: false,
1458
+ requiresFeature: "pipeline",
1459
+ inputSchema: {
1460
+ project_id: z.string().optional(),
1461
+ scope: z.enum(["running", "pending", "finished", "branches", "tags"]).optional(),
1462
+ status: z
1463
+ .enum([
1464
+ "created",
1465
+ "waiting_for_resource",
1466
+ "preparing",
1467
+ "pending",
1468
+ "running",
1469
+ "success",
1470
+ "failed",
1471
+ "canceled",
1472
+ "skipped",
1473
+ "manual",
1474
+ "scheduled"
1475
+ ])
1476
+ .optional(),
1477
+ ref: optionalString,
1478
+ sha: optionalString,
1479
+ yaml_errors: optionalBoolean,
1480
+ username: optionalString,
1481
+ updated_after: optionalString,
1482
+ updated_before: optionalString,
1483
+ order_by: z.enum(["id", "status", "ref", "updated_at", "user_id"]).optional(),
1484
+ sort: z.enum(["asc", "desc"]).optional(),
1485
+ source: optionalString,
1486
+ ...paginationShape
1487
+ },
1488
+ handler: async (args, context) => context.gitlab.listPipelines(resolveProjectId(args, context, true), {
1489
+ query: toQuery(omit(args, ["project_id"]))
1490
+ })
1491
+ },
1492
+ {
1493
+ name: "gitlab_get_pipeline",
1494
+ title: "Get Pipeline",
1495
+ description: "Get one pipeline.",
1496
+ mutating: false,
1497
+ requiresFeature: "pipeline",
1498
+ inputSchema: {
1499
+ project_id: z.string().optional(),
1500
+ pipeline_id: z.string().min(1)
1501
+ },
1502
+ handler: async (args, context) => context.gitlab.getPipeline(resolveProjectId(args, context, true), getString(args, "pipeline_id"))
1503
+ },
1504
+ {
1505
+ name: "gitlab_list_pipeline_jobs",
1506
+ title: "List Pipeline Jobs",
1507
+ description: "List jobs in a pipeline.",
1508
+ mutating: false,
1509
+ requiresFeature: "pipeline",
1510
+ inputSchema: {
1511
+ project_id: z.string().optional(),
1512
+ pipeline_id: z.string().min(1),
1513
+ scope: z
1514
+ .enum([
1515
+ "created",
1516
+ "pending",
1517
+ "running",
1518
+ "failed",
1519
+ "success",
1520
+ "canceled",
1521
+ "skipped",
1522
+ "manual"
1523
+ ])
1524
+ .optional(),
1525
+ include_retried: optionalBoolean,
1526
+ ...paginationShape
1527
+ },
1528
+ handler: async (args, context) => context.gitlab.listPipelineJobs(resolveProjectId(args, context, true), getString(args, "pipeline_id"), { query: toQuery(omit(args, ["project_id", "pipeline_id"])) })
1529
+ },
1530
+ {
1531
+ name: "gitlab_list_pipeline_trigger_jobs",
1532
+ title: "List Pipeline Trigger Jobs",
1533
+ description: "List downstream/bridge trigger jobs in a pipeline.",
1534
+ mutating: false,
1535
+ requiresFeature: "pipeline",
1536
+ inputSchema: {
1537
+ project_id: z.string().optional(),
1538
+ pipeline_id: z.string().min(1),
1539
+ scope: z
1540
+ .enum([
1541
+ "canceled",
1542
+ "canceling",
1543
+ "created",
1544
+ "failed",
1545
+ "manual",
1546
+ "pending",
1547
+ "preparing",
1548
+ "running",
1549
+ "scheduled",
1550
+ "skipped",
1551
+ "success",
1552
+ "waiting_for_resource"
1553
+ ])
1554
+ .optional(),
1555
+ ...paginationShape
1556
+ },
1557
+ handler: async (args, context) => context.gitlab.listPipelineTriggerJobs(resolveProjectId(args, context, true), getString(args, "pipeline_id"), { query: toQuery(omit(args, ["project_id", "pipeline_id"])) })
1558
+ },
1559
+ {
1560
+ name: "gitlab_get_pipeline_job",
1561
+ title: "Get Pipeline Job",
1562
+ description: "Get one job by job ID.",
1563
+ mutating: false,
1564
+ requiresFeature: "pipeline",
1565
+ inputSchema: {
1566
+ project_id: z.string().optional(),
1567
+ job_id: z.string().min(1)
1568
+ },
1569
+ handler: async (args, context) => context.gitlab.getPipelineJob(resolveProjectId(args, context, true), getString(args, "job_id"))
1570
+ },
1571
+ {
1572
+ name: "gitlab_get_pipeline_job_output",
1573
+ title: "Get Pipeline Job Output",
1574
+ description: "Get raw job trace output.",
1575
+ mutating: false,
1576
+ requiresFeature: "pipeline",
1577
+ inputSchema: {
1578
+ project_id: z.string().optional(),
1579
+ job_id: z.string().min(1)
1580
+ },
1581
+ handler: async (args, context) => context.gitlab.getPipelineJobOutput(resolveProjectId(args, context, true), getString(args, "job_id"))
1582
+ },
1583
+ {
1584
+ name: "gitlab_create_pipeline",
1585
+ title: "Create Pipeline",
1586
+ description: "Trigger a new pipeline.",
1587
+ mutating: true,
1588
+ requiresFeature: "pipeline",
1589
+ inputSchema: {
1590
+ project_id: z.string().optional(),
1591
+ ref: z.string().min(1),
1592
+ variables: z
1593
+ .array(z.object({
1594
+ key: z.string(),
1595
+ value: z.string(),
1596
+ variable_type: optionalString
1597
+ }))
1598
+ .optional()
1599
+ },
1600
+ handler: async (args, context) => context.gitlab.createPipeline(resolveProjectId(args, context, true), {
1601
+ ref: getString(args, "ref"),
1602
+ variables: getArray(args, "variables")
1603
+ })
1604
+ },
1605
+ {
1606
+ name: "gitlab_retry_pipeline",
1607
+ title: "Retry Pipeline",
1608
+ description: "Retry failed jobs in pipeline.",
1609
+ mutating: true,
1610
+ requiresFeature: "pipeline",
1611
+ inputSchema: {
1612
+ project_id: z.string().optional(),
1613
+ pipeline_id: z.string().min(1)
1614
+ },
1615
+ handler: async (args, context) => context.gitlab.retryPipeline(resolveProjectId(args, context, true), getString(args, "pipeline_id"))
1616
+ },
1617
+ {
1618
+ name: "gitlab_cancel_pipeline",
1619
+ title: "Cancel Pipeline",
1620
+ description: "Cancel a running pipeline.",
1621
+ mutating: true,
1622
+ requiresFeature: "pipeline",
1623
+ inputSchema: {
1624
+ project_id: z.string().optional(),
1625
+ pipeline_id: z.string().min(1)
1626
+ },
1627
+ handler: async (args, context) => context.gitlab.cancelPipeline(resolveProjectId(args, context, true), getString(args, "pipeline_id"))
1628
+ },
1629
+ {
1630
+ name: "gitlab_retry_pipeline_job",
1631
+ title: "Retry Pipeline Job",
1632
+ description: "Retry one failed job.",
1633
+ mutating: true,
1634
+ requiresFeature: "pipeline",
1635
+ inputSchema: {
1636
+ project_id: z.string().optional(),
1637
+ job_id: z.string().min(1)
1638
+ },
1639
+ handler: async (args, context) => context.gitlab.retryPipelineJob(resolveProjectId(args, context, true), getString(args, "job_id"))
1640
+ },
1641
+ {
1642
+ name: "gitlab_cancel_pipeline_job",
1643
+ title: "Cancel Pipeline Job",
1644
+ description: "Cancel one running job.",
1645
+ mutating: true,
1646
+ requiresFeature: "pipeline",
1647
+ inputSchema: {
1648
+ project_id: z.string().optional(),
1649
+ job_id: z.string().min(1)
1650
+ },
1651
+ handler: async (args, context) => context.gitlab.cancelPipelineJob(resolveProjectId(args, context, true), getString(args, "job_id"))
1652
+ },
1653
+ {
1654
+ name: "gitlab_play_pipeline_job",
1655
+ title: "Play Pipeline Job",
1656
+ description: "Play a manual job.",
1657
+ mutating: true,
1658
+ requiresFeature: "pipeline",
1659
+ inputSchema: {
1660
+ project_id: z.string().optional(),
1661
+ job_id: z.string().min(1)
1662
+ },
1663
+ handler: async (args, context) => context.gitlab.playPipelineJob(resolveProjectId(args, context, true), getString(args, "job_id"))
1664
+ },
1665
+ {
1666
+ name: "gitlab_list_milestones",
1667
+ title: "List Milestones",
1668
+ description: "List project milestones.",
1669
+ mutating: false,
1670
+ requiresFeature: "milestone",
1671
+ inputSchema: {
1672
+ project_id: z.string().optional(),
1673
+ iids: optionalNumberArray,
1674
+ state: optionalString,
1675
+ title: optionalString,
1676
+ search: optionalString,
1677
+ include_ancestors: optionalBoolean,
1678
+ updated_before: optionalString,
1679
+ updated_after: optionalString,
1680
+ ...paginationShape
1681
+ },
1682
+ handler: async (args, context) => context.gitlab.listMilestones(resolveProjectId(args, context, true), {
1683
+ query: toQuery(omit(args, ["project_id"]))
1684
+ })
1685
+ },
1686
+ {
1687
+ name: "gitlab_get_milestone",
1688
+ title: "Get Milestone",
1689
+ description: "Get a milestone by ID.",
1690
+ mutating: false,
1691
+ requiresFeature: "milestone",
1692
+ inputSchema: {
1693
+ project_id: z.string().optional(),
1694
+ milestone_id: z.string().min(1)
1695
+ },
1696
+ handler: async (args, context) => context.gitlab.getMilestone(resolveProjectId(args, context, true), getString(args, "milestone_id"))
1697
+ },
1698
+ {
1699
+ name: "gitlab_create_milestone",
1700
+ title: "Create Milestone",
1701
+ description: "Create a milestone.",
1702
+ mutating: true,
1703
+ requiresFeature: "milestone",
1704
+ inputSchema: {
1705
+ project_id: z.string().optional(),
1706
+ title: z.string().min(1),
1707
+ description: optionalString,
1708
+ due_date: optionalString,
1709
+ start_date: optionalString
1710
+ },
1711
+ handler: async (args, context) => context.gitlab.createMilestone(resolveProjectId(args, context, true), {
1712
+ title: getString(args, "title"),
1713
+ description: getOptionalString(args, "description"),
1714
+ due_date: getOptionalString(args, "due_date"),
1715
+ start_date: getOptionalString(args, "start_date")
1716
+ })
1717
+ },
1718
+ {
1719
+ name: "gitlab_update_milestone",
1720
+ title: "Update Milestone",
1721
+ description: "Update milestone fields.",
1722
+ mutating: true,
1723
+ requiresFeature: "milestone",
1724
+ inputSchema: {
1725
+ project_id: z.string().optional(),
1726
+ milestone_id: z.string().min(1),
1727
+ title: optionalString,
1728
+ description: optionalString,
1729
+ due_date: optionalString,
1730
+ start_date: optionalString,
1731
+ state_event: optionalString
1732
+ },
1733
+ handler: async (args, context) => context.gitlab.updateMilestone(resolveProjectId(args, context, true), getString(args, "milestone_id"), toQuery(omit(args, ["project_id", "milestone_id"])))
1734
+ },
1735
+ {
1736
+ name: "gitlab_edit_milestone",
1737
+ title: "Edit Milestone (Alias)",
1738
+ description: "Backward-compatible alias of gitlab_update_milestone.",
1739
+ mutating: true,
1740
+ requiresFeature: "milestone",
1741
+ inputSchema: {
1742
+ project_id: z.string().optional(),
1743
+ milestone_id: z.string().min(1),
1744
+ title: optionalString,
1745
+ description: optionalString,
1746
+ due_date: optionalString,
1747
+ start_date: optionalString,
1748
+ state_event: optionalString
1749
+ },
1750
+ handler: async (args, context) => context.gitlab.updateMilestone(resolveProjectId(args, context, true), getString(args, "milestone_id"), toQuery(omit(args, ["project_id", "milestone_id"])))
1751
+ },
1752
+ {
1753
+ name: "gitlab_delete_milestone",
1754
+ title: "Delete Milestone",
1755
+ description: "Delete a milestone.",
1756
+ mutating: true,
1757
+ requiresFeature: "milestone",
1758
+ inputSchema: {
1759
+ project_id: z.string().optional(),
1760
+ milestone_id: z.string().min(1)
1761
+ },
1762
+ handler: async (args, context) => context.gitlab.deleteMilestone(resolveProjectId(args, context, true), getString(args, "milestone_id"))
1763
+ },
1764
+ {
1765
+ name: "gitlab_get_milestone_issue",
1766
+ title: "Get Milestone Issues",
1767
+ description: "List issues assigned to a milestone.",
1768
+ mutating: false,
1769
+ requiresFeature: "milestone",
1770
+ inputSchema: {
1771
+ project_id: z.string().optional(),
1772
+ milestone_id: z.string().min(1)
1773
+ },
1774
+ handler: async (args, context) => context.gitlab.getMilestoneIssues(resolveProjectId(args, context, true), getString(args, "milestone_id"))
1775
+ },
1776
+ {
1777
+ name: "gitlab_get_milestone_merge_requests",
1778
+ title: "Get Milestone Merge Requests",
1779
+ description: "List merge requests assigned to a milestone.",
1780
+ mutating: false,
1781
+ requiresFeature: "milestone",
1782
+ inputSchema: {
1783
+ project_id: z.string().optional(),
1784
+ milestone_id: z.string().min(1),
1785
+ ...paginationShape
1786
+ },
1787
+ handler: async (args, context) => context.gitlab.getMilestoneMergeRequests(resolveProjectId(args, context, true), getString(args, "milestone_id"), { query: toQuery(omit(args, ["project_id", "milestone_id"])) })
1788
+ },
1789
+ {
1790
+ name: "gitlab_promote_milestone",
1791
+ title: "Promote Milestone",
1792
+ description: "Promote a project milestone to a group milestone.",
1793
+ mutating: true,
1794
+ requiresFeature: "milestone",
1795
+ inputSchema: {
1796
+ project_id: z.string().optional(),
1797
+ milestone_id: z.string().min(1)
1798
+ },
1799
+ handler: async (args, context) => context.gitlab.promoteMilestone(resolveProjectId(args, context, true), getString(args, "milestone_id"))
1800
+ },
1801
+ {
1802
+ name: "gitlab_get_milestone_burndown_events",
1803
+ title: "Get Milestone Burndown Events",
1804
+ description: "List burndown events for a milestone.",
1805
+ mutating: false,
1806
+ requiresFeature: "milestone",
1807
+ inputSchema: {
1808
+ project_id: z.string().optional(),
1809
+ milestone_id: z.string().min(1),
1810
+ ...paginationShape
1811
+ },
1812
+ handler: async (args, context) => context.gitlab.getMilestoneBurndownEvents(resolveProjectId(args, context, true), getString(args, "milestone_id"), { query: toQuery(omit(args, ["project_id", "milestone_id"])) })
1813
+ },
1814
+ {
1815
+ name: "gitlab_list_releases",
1816
+ title: "List Releases",
1817
+ description: "List project releases.",
1818
+ mutating: false,
1819
+ requiresFeature: "release",
1820
+ inputSchema: {
1821
+ project_id: z.string().optional(),
1822
+ order_by: z.enum(["released_at", "created_at"]).optional(),
1823
+ sort: z.enum(["asc", "desc"]).optional(),
1824
+ include_html_description: optionalBoolean,
1825
+ ...paginationShape
1826
+ },
1827
+ handler: async (args, context) => context.gitlab.listReleases(resolveProjectId(args, context, true), {
1828
+ query: toQuery(omit(args, ["project_id"]))
1829
+ })
1830
+ },
1831
+ {
1832
+ name: "gitlab_get_release",
1833
+ title: "Get Release",
1834
+ description: "Get one release by tag name.",
1835
+ mutating: false,
1836
+ requiresFeature: "release",
1837
+ inputSchema: {
1838
+ project_id: z.string().optional(),
1839
+ tag_name: z.string().min(1),
1840
+ include_html_description: optionalBoolean
1841
+ },
1842
+ handler: async (args, context) => context.gitlab.getRelease(resolveProjectId(args, context, true), getString(args, "tag_name"), { query: toQuery(omit(args, ["project_id", "tag_name"])) })
1843
+ },
1844
+ {
1845
+ name: "gitlab_create_release",
1846
+ title: "Create Release",
1847
+ description: "Create a release.",
1848
+ mutating: true,
1849
+ requiresFeature: "release",
1850
+ inputSchema: {
1851
+ project_id: z.string().optional(),
1852
+ name: optionalString,
1853
+ tag_name: z.string().min(1),
1854
+ tag_message: optionalString,
1855
+ description: optionalString,
1856
+ ref: optionalString,
1857
+ released_at: optionalString,
1858
+ milestones: optionalStringArray,
1859
+ assets: optionalRecord
1860
+ },
1861
+ handler: async (args, context) => context.gitlab.createRelease(resolveProjectId(args, context, true), toQuery(omit(args, ["project_id"])))
1862
+ },
1863
+ {
1864
+ name: "gitlab_update_release",
1865
+ title: "Update Release",
1866
+ description: "Update existing release.",
1867
+ mutating: true,
1868
+ requiresFeature: "release",
1869
+ inputSchema: {
1870
+ project_id: z.string().optional(),
1871
+ tag_name: z.string().min(1),
1872
+ name: optionalString,
1873
+ description: optionalString,
1874
+ released_at: optionalString,
1875
+ milestones: optionalStringArray,
1876
+ assets: optionalRecord
1877
+ },
1878
+ handler: async (args, context) => context.gitlab.updateRelease(resolveProjectId(args, context, true), getString(args, "tag_name"), toQuery(omit(args, ["project_id", "tag_name"])))
1879
+ },
1880
+ {
1881
+ name: "gitlab_delete_release",
1882
+ title: "Delete Release",
1883
+ description: "Delete a release by tag.",
1884
+ mutating: true,
1885
+ requiresFeature: "release",
1886
+ inputSchema: {
1887
+ project_id: z.string().optional(),
1888
+ tag_name: z.string().min(1)
1889
+ },
1890
+ handler: async (args, context) => context.gitlab.deleteRelease(resolveProjectId(args, context, true), getString(args, "tag_name"))
1891
+ },
1892
+ {
1893
+ name: "gitlab_create_release_evidence",
1894
+ title: "Create Release Evidence",
1895
+ description: "Create evidence for an existing release.",
1896
+ mutating: true,
1897
+ requiresFeature: "release",
1898
+ inputSchema: {
1899
+ project_id: z.string().optional(),
1900
+ tag_name: z.string().min(1)
1901
+ },
1902
+ handler: async (args, context) => context.gitlab.createReleaseEvidence(resolveProjectId(args, context, true), getString(args, "tag_name"))
1903
+ },
1904
+ {
1905
+ name: "gitlab_download_release_asset",
1906
+ title: "Download Release Asset",
1907
+ description: "Download a release asset using its direct asset path.",
1908
+ mutating: false,
1909
+ requiresFeature: "release",
1910
+ inputSchema: {
1911
+ project_id: z.string().optional(),
1912
+ tag_name: z.string().min(1),
1913
+ direct_asset_path: z.string().min(1)
1914
+ },
1915
+ handler: async (args, context) => context.gitlab.downloadReleaseAsset(resolveProjectId(args, context, true), getString(args, "tag_name"), getString(args, "direct_asset_path"))
1916
+ },
1917
+ {
1918
+ name: "gitlab_list_labels",
1919
+ title: "List Labels",
1920
+ description: "List project labels.",
1921
+ mutating: false,
1922
+ inputSchema: {
1923
+ project_id: z.string().optional(),
1924
+ with_counts: optionalBoolean,
1925
+ include_ancestor_groups: optionalBoolean,
1926
+ search: optionalString,
1927
+ ...paginationShape
1928
+ },
1929
+ handler: async (args, context) => context.gitlab.listLabels(resolveProjectId(args, context, true), {
1930
+ query: toQuery(omit(args, ["project_id"]))
1931
+ })
1932
+ },
1933
+ {
1934
+ name: "gitlab_get_label",
1935
+ title: "Get Label",
1936
+ description: "Get one label by ID.",
1937
+ mutating: false,
1938
+ inputSchema: {
1939
+ project_id: z.string().optional(),
1940
+ label_id: z.string().min(1),
1941
+ include_ancestor_groups: optionalBoolean
1942
+ },
1943
+ handler: async (args, context) => context.gitlab.getLabel(resolveProjectId(args, context, true), getString(args, "label_id"), {
1944
+ query: toQuery(omit(args, ["project_id", "label_id"]))
1945
+ })
1946
+ },
1947
+ {
1948
+ name: "gitlab_create_label",
1949
+ title: "Create Label",
1950
+ description: "Create a label.",
1951
+ mutating: true,
1952
+ inputSchema: {
1953
+ project_id: z.string().optional(),
1954
+ name: z.string().min(1),
1955
+ color: z.string().min(1),
1956
+ description: optionalString,
1957
+ priority: optionalNumber
1958
+ },
1959
+ handler: async (args, context) => context.gitlab.createLabel(resolveProjectId(args, context, true), toQuery(omit(args, ["project_id"])))
1960
+ },
1961
+ {
1962
+ name: "gitlab_update_label",
1963
+ title: "Update Label",
1964
+ description: "Update a label.",
1965
+ mutating: true,
1966
+ inputSchema: {
1967
+ project_id: z.string().optional(),
1968
+ name: optionalString,
1969
+ label_id: optionalString,
1970
+ new_name: optionalString,
1971
+ color: optionalString,
1972
+ description: optionalString,
1973
+ priority: optionalNumber
1974
+ },
1975
+ handler: async (args, context) => {
1976
+ const payload = toQuery(omit(args, ["project_id"]));
1977
+ if (payload.name === undefined) {
1978
+ payload.name = getOptionalString(args, "label_id");
1979
+ }
1980
+ if (payload.name === undefined) {
1981
+ throw new Error("Either name or label_id must be provided");
1982
+ }
1983
+ return context.gitlab.updateLabel(resolveProjectId(args, context, true), payload);
1984
+ }
1985
+ },
1986
+ {
1987
+ name: "gitlab_delete_label",
1988
+ title: "Delete Label",
1989
+ description: "Delete a label by name.",
1990
+ mutating: true,
1991
+ inputSchema: {
1992
+ project_id: z.string().optional(),
1993
+ name: optionalString,
1994
+ label_id: optionalString
1995
+ },
1996
+ handler: async (args, context) => {
1997
+ const labelName = getOptionalString(args, "name") ?? getOptionalString(args, "label_id");
1998
+ if (!labelName) {
1999
+ throw new Error("Either name or label_id must be provided");
2000
+ }
2001
+ return context.gitlab.deleteLabel(resolveProjectId(args, context, true), labelName);
2002
+ }
2003
+ },
2004
+ {
2005
+ name: "gitlab_list_namespaces",
2006
+ title: "List Namespaces",
2007
+ description: "List namespaces visible to user.",
2008
+ mutating: false,
2009
+ inputSchema: {
2010
+ search: optionalString,
2011
+ owned: optionalBoolean,
2012
+ ...paginationShape
2013
+ },
2014
+ handler: async (args, context) => context.gitlab.listNamespaces({ query: toQuery(args) })
2015
+ },
2016
+ {
2017
+ name: "gitlab_get_namespace",
2018
+ title: "Get Namespace",
2019
+ description: "Get namespace by ID or path.",
2020
+ mutating: false,
2021
+ inputSchema: {
2022
+ namespace_id_or_path: optionalString,
2023
+ namespace_id: optionalString
2024
+ },
2025
+ handler: async (args, context) => {
2026
+ const namespaceId = getOptionalString(args, "namespace_id_or_path") ??
2027
+ getOptionalString(args, "namespace_id");
2028
+ if (!namespaceId) {
2029
+ throw new Error("Either namespace_id_or_path or namespace_id must be provided");
2030
+ }
2031
+ return context.gitlab.getNamespace(namespaceId);
2032
+ }
2033
+ },
2034
+ {
2035
+ name: "gitlab_verify_namespace",
2036
+ title: "Verify Namespace",
2037
+ description: "Verify if namespace path exists.",
2038
+ mutating: false,
2039
+ inputSchema: {
2040
+ path: z.string().min(1)
2041
+ },
2042
+ handler: async (args, context) => context.gitlab.verifyNamespace(getString(args, "path"))
2043
+ },
2044
+ {
2045
+ name: "gitlab_get_users",
2046
+ title: "Get Users",
2047
+ description: "Search users.",
2048
+ mutating: false,
2049
+ inputSchema: {
2050
+ username: optionalString,
2051
+ search: optionalString,
2052
+ active: optionalBoolean,
2053
+ extern_uid: optionalString,
2054
+ provider: optionalString,
2055
+ ...paginationShape
2056
+ },
2057
+ handler: async (args, context) => context.gitlab.getUsers({ query: toQuery(args) })
2058
+ },
2059
+ {
2060
+ name: "gitlab_list_events",
2061
+ title: "List Events",
2062
+ description: "List current user events.",
2063
+ mutating: false,
2064
+ inputSchema: {
2065
+ action: optionalString,
2066
+ target_type: optionalString,
2067
+ before: optionalString,
2068
+ after: optionalString,
2069
+ scope: optionalString,
2070
+ sort: optionalString,
2071
+ ...paginationShape
2072
+ },
2073
+ handler: async (args, context) => context.gitlab.listEvents({ query: toQuery(args) })
2074
+ },
2075
+ {
2076
+ name: "gitlab_get_project_events",
2077
+ title: "Get Project Events",
2078
+ description: "List events for a specific project.",
2079
+ mutating: false,
2080
+ inputSchema: {
2081
+ project_id: z.string().optional(),
2082
+ action: optionalString,
2083
+ target_type: optionalString,
2084
+ before: optionalString,
2085
+ after: optionalString,
2086
+ sort: optionalString,
2087
+ ...paginationShape
2088
+ },
2089
+ handler: async (args, context) => context.gitlab.getProjectEvents(resolveProjectId(args, context, true), {
2090
+ query: toQuery(omit(args, ["project_id"]))
2091
+ })
2092
+ },
2093
+ {
2094
+ name: "gitlab_upload_markdown",
2095
+ title: "Upload Markdown",
2096
+ description: "Upload markdown file/attachment to project.",
2097
+ mutating: true,
2098
+ inputSchema: {
2099
+ project_id: z.string().optional(),
2100
+ content: optionalString,
2101
+ filename: z.string().default("upload.md"),
2102
+ file_path: optionalString
2103
+ },
2104
+ handler: async (args, context) => {
2105
+ const projectId = resolveProjectId(args, context, true);
2106
+ const filePath = getOptionalString(args, "file_path");
2107
+ if (filePath) {
2108
+ return context.gitlab.uploadMarkdownFile(projectId, filePath);
2109
+ }
2110
+ const content = getOptionalString(args, "content");
2111
+ if (!content) {
2112
+ throw new Error("Either file_path or content must be provided");
2113
+ }
2114
+ return context.gitlab.uploadMarkdown(projectId, content, getString(args, "filename"));
2115
+ }
2116
+ },
2117
+ {
2118
+ name: "gitlab_download_attachment",
2119
+ title: "Download Attachment",
2120
+ description: "Download attachment by URL/path and return base64.",
2121
+ mutating: false,
2122
+ inputSchema: {
2123
+ project_id: z.string().optional(),
2124
+ url_or_path: optionalString,
2125
+ secret: optionalString,
2126
+ filename: optionalString
2127
+ },
2128
+ handler: async (args, context) => {
2129
+ const urlOrPath = getOptionalString(args, "url_or_path");
2130
+ if (urlOrPath) {
2131
+ const projectId = resolveProjectId(args, context, false);
2132
+ const upload = parseProjectUploadReference(urlOrPath);
2133
+ if (context.env.GITLAB_ALLOWED_PROJECT_IDS.length > 0) {
2134
+ if (!projectId) {
2135
+ throw new Error("project_id is required when GITLAB_ALLOWED_PROJECT_IDS is configured");
2136
+ }
2137
+ if (!upload) {
2138
+ throw new Error("In project-scoped mode, url_or_path must be a GitLab upload URL/path like '/uploads/<secret>/<filename>'");
2139
+ }
2140
+ const apiRelativePath = `api/v4/projects/${encodeURIComponent(projectId)}/uploads/${encodeURIComponent(upload.secret)}/${encodeURIComponent(upload.filename)}`;
2141
+ return context.gitlab.downloadAttachment(apiRelativePath);
2142
+ }
2143
+ if (projectId && upload) {
2144
+ const apiRelativePath = `api/v4/projects/${encodeURIComponent(projectId)}/uploads/${encodeURIComponent(upload.secret)}/${encodeURIComponent(upload.filename)}`;
2145
+ return context.gitlab.downloadAttachment(apiRelativePath);
2146
+ }
2147
+ return context.gitlab.downloadAttachment(urlOrPath);
2148
+ }
2149
+ const secret = getOptionalString(args, "secret");
2150
+ const filename = getOptionalString(args, "filename");
2151
+ if (!secret || !filename) {
2152
+ throw new Error("Either url_or_path must be provided, or both secret and filename must be provided");
2153
+ }
2154
+ const projectId = resolveProjectId(args, context, true);
2155
+ const apiRelativePath = `api/v4/projects/${encodeURIComponent(projectId)}/uploads/${encodeURIComponent(secret)}/${encodeURIComponent(filename)}`;
2156
+ return context.gitlab.downloadAttachment(apiRelativePath);
2157
+ }
2158
+ },
2159
+ {
2160
+ name: "gitlab_execute_graphql_query",
2161
+ title: "Execute GraphQL Query",
2162
+ description: "Execute read-only GraphQL query.",
2163
+ mutating: false,
2164
+ inputSchema: {
2165
+ query: z.string().min(1),
2166
+ variables: optionalRecord
2167
+ },
2168
+ handler: async (args, context) => {
2169
+ const query = getString(args, "query");
2170
+ if (containsGraphqlMutation(query)) {
2171
+ throw new Error("Mutation detected. Use gitlab_execute_graphql_mutation for mutation operations.");
2172
+ }
2173
+ return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
2174
+ }
2175
+ },
2176
+ {
2177
+ name: "gitlab_execute_graphql_mutation",
2178
+ title: "Execute GraphQL Mutation",
2179
+ description: "Execute GraphQL mutation (disabled in read-only mode).",
2180
+ mutating: true,
2181
+ inputSchema: {
2182
+ query: z.string().min(1),
2183
+ variables: optionalRecord
2184
+ },
2185
+ handler: async (args, context) => {
2186
+ const query = getString(args, "query");
2187
+ if (!containsGraphqlMutation(query)) {
2188
+ throw new Error("No mutation detected. Use gitlab_execute_graphql_query for queries.");
2189
+ }
2190
+ return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
2191
+ }
2192
+ },
2193
+ {
2194
+ name: "gitlab_execute_graphql",
2195
+ title: "Execute GraphQL (Compat)",
2196
+ description: "Backward-compatible GraphQL executor. Mutation payloads still honor read-only policy.",
2197
+ mutating: false,
2198
+ inputSchema: {
2199
+ query: z.string().min(1),
2200
+ variables: optionalRecord
2201
+ },
2202
+ handler: async (args, context) => {
2203
+ const query = getString(args, "query");
2204
+ if (containsGraphqlMutation(query)) {
2205
+ context.policy.assertCanExecute({
2206
+ name: "gitlab_execute_graphql",
2207
+ mutating: true
2208
+ });
2209
+ }
2210
+ return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
2211
+ }
2212
+ }
2213
+ ];
2214
+ }
2215
+ function assertAuthReady(context) {
2216
+ const auth = getSessionAuth();
2217
+ if (context.env.REMOTE_AUTHORIZATION) {
2218
+ const token = auth?.token;
2219
+ if (!token) {
2220
+ throw new Error("Missing remote authorization token for this session");
2221
+ }
2222
+ if (context.env.ENABLE_DYNAMIC_API_URL && !auth?.apiUrl) {
2223
+ throw new Error("Missing remote API URL for this session");
2224
+ }
2225
+ return;
2226
+ }
2227
+ const hasFallbackAuth = Boolean(context.env.GITLAB_PERSONAL_ACCESS_TOKEN) ||
2228
+ Boolean(context.env.GITLAB_USE_OAUTH && context.env.GITLAB_OAUTH_CLIENT_ID) ||
2229
+ Boolean(context.env.GITLAB_TOKEN_SCRIPT) ||
2230
+ Boolean(context.env.GITLAB_TOKEN_FILE) ||
2231
+ Boolean(context.env.GITLAB_AUTH_COOKIE_PATH);
2232
+ if (!hasFallbackAuth) {
2233
+ throw new Error("Authentication required: set GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_TOKEN_SCRIPT, GITLAB_TOKEN_FILE, or GITLAB_AUTH_COOKIE_PATH");
2234
+ }
2235
+ }
2236
+ export function containsGraphqlMutation(query) {
2237
+ if (!query.trim()) {
2238
+ return false;
2239
+ }
2240
+ try {
2241
+ const document = parse(query, { noLocation: true });
2242
+ return document.definitions.some((definition) => definition.kind === Kind.OPERATION_DEFINITION && definition.operation === "mutation");
2243
+ }
2244
+ catch {
2245
+ // Keep a conservative fallback for malformed GraphQL payloads.
2246
+ const normalized = query
2247
+ .replace(/#[^\n]*/g, " ")
2248
+ .replace(/"""[\s\S]*?"""/g, " ")
2249
+ .replace(/"(?:\\.|[^"\\])*"/g, " ");
2250
+ return /\bmutation\b\s*(?:[A-Za-z_][A-Za-z0-9_]*)?\s*(?:\([^)]*\))?\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s*)*\{/i.test(normalized);
2251
+ }
2252
+ }
2253
+ export function parseProjectUploadReference(input) {
2254
+ const trimmed = input.trim();
2255
+ if (!trimmed) {
2256
+ return undefined;
2257
+ }
2258
+ let pathValue = trimmed;
2259
+ if (/^https?:\/\//i.test(trimmed)) {
2260
+ try {
2261
+ pathValue = new URL(trimmed).pathname;
2262
+ }
2263
+ catch {
2264
+ return undefined;
2265
+ }
2266
+ }
2267
+ const [pathOnly] = pathValue.split(/[?#]/, 1);
2268
+ if (!pathOnly) {
2269
+ return undefined;
2270
+ }
2271
+ const marker = "/uploads/";
2272
+ const markerIndex = pathOnly.lastIndexOf(marker);
2273
+ if (markerIndex < 0) {
2274
+ return undefined;
2275
+ }
2276
+ const suffix = pathOnly.slice(markerIndex + marker.length);
2277
+ const [secret, ...filenameParts] = suffix.split("/").filter((segment) => segment.length > 0);
2278
+ if (!secret || filenameParts.length === 0) {
2279
+ return undefined;
2280
+ }
2281
+ let filename;
2282
+ try {
2283
+ filename = decodeURIComponent(filenameParts.join("/"));
2284
+ }
2285
+ catch {
2286
+ return undefined;
2287
+ }
2288
+ if (!filename) {
2289
+ return undefined;
2290
+ }
2291
+ return { secret, filename };
2292
+ }
2293
+ export function shouldDisableGraphqlTools(allowedProjectIds, allowGraphqlWithProjectScope) {
2294
+ return allowedProjectIds.length > 0 && !allowGraphqlWithProjectScope;
2295
+ }
2296
+ function isGraphqlToolName(name) {
2297
+ return (name === "gitlab_execute_graphql_query" ||
2298
+ name === "gitlab_execute_graphql_mutation" ||
2299
+ name === "gitlab_execute_graphql");
2300
+ }
2301
+ function resolveProjectId(args, context, required) {
2302
+ const fromArgs = getOptionalString(args, "project_id");
2303
+ const allowed = context.env.GITLAB_ALLOWED_PROJECT_IDS;
2304
+ if (allowed.length > 0) {
2305
+ if (fromArgs && !allowed.includes(fromArgs)) {
2306
+ throw new Error(`Project '${fromArgs}' is not in GITLAB_ALLOWED_PROJECT_IDS: ${allowed.join(", ")}`);
2307
+ }
2308
+ if (!fromArgs && allowed.length === 1) {
2309
+ return requireArrayValue(allowed, 0, "GITLAB_ALLOWED_PROJECT_IDS is empty");
2310
+ }
2311
+ if (!fromArgs && allowed.length > 1) {
2312
+ throw new Error(`Multiple allowed projects configured (${allowed.join(", ")}). Please specify project_id.`);
2313
+ }
2314
+ return fromArgs ?? requireArrayValue(allowed, 0, "GITLAB_ALLOWED_PROJECT_IDS is empty");
2315
+ }
2316
+ if (required && !fromArgs) {
2317
+ throw new Error("project_id is required");
2318
+ }
2319
+ return fromArgs ?? "";
2320
+ }
2321
+ function toToolError(error, context) {
2322
+ const detailMode = context?.env.GITLAB_ERROR_DETAIL_MODE ?? "full";
2323
+ if (error instanceof GitLabApiError) {
2324
+ const payload = {
2325
+ error: `GitLab API error ${error.status}`
2326
+ };
2327
+ if (detailMode === "full") {
2328
+ payload.details = redactSensitive(error.details);
2329
+ }
2330
+ return {
2331
+ isError: true,
2332
+ content: [
2333
+ {
2334
+ type: "text",
2335
+ text: formatPayloadText(payload, context)
2336
+ }
2337
+ ]
2338
+ };
2339
+ }
2340
+ if (error instanceof Error) {
2341
+ const message = detailMode === "full" ? error.message : "Request failed";
2342
+ return {
2343
+ isError: true,
2344
+ content: [
2345
+ {
2346
+ type: "text",
2347
+ text: message
2348
+ }
2349
+ ]
2350
+ };
2351
+ }
2352
+ return {
2353
+ isError: true,
2354
+ content: [
2355
+ {
2356
+ type: "text",
2357
+ text: "Unknown error"
2358
+ }
2359
+ ]
2360
+ };
2361
+ }
2362
+ function formatPayloadText(payload, context) {
2363
+ if (!context) {
2364
+ return JSON.stringify(payload, null, 2);
2365
+ }
2366
+ return context.formatter.format(payload).text;
2367
+ }
2368
+ function toStructuredContent(value) {
2369
+ if (typeof value === "object" && value !== null) {
2370
+ if (Array.isArray(value)) {
2371
+ return {
2372
+ items: value,
2373
+ count: value.length
2374
+ };
2375
+ }
2376
+ return value;
2377
+ }
2378
+ return {
2379
+ value
2380
+ };
2381
+ }
2382
+ function omit(args, keys) {
2383
+ const result = {};
2384
+ for (const [key, value] of Object.entries(args)) {
2385
+ if (!keys.includes(key)) {
2386
+ result[key] = value;
2387
+ }
2388
+ }
2389
+ return result;
2390
+ }
2391
+ function toQuery(args) {
2392
+ const output = {};
2393
+ for (const [key, value] of Object.entries(args)) {
2394
+ if (value === undefined || value === null) {
2395
+ continue;
2396
+ }
2397
+ if (Array.isArray(value)) {
2398
+ output[key] = value.join(",");
2399
+ continue;
2400
+ }
2401
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
2402
+ output[key] = value;
2403
+ }
2404
+ }
2405
+ return output;
2406
+ }
2407
+ function toCsvValue(value) {
2408
+ if (value === undefined || value === null) {
2409
+ return undefined;
2410
+ }
2411
+ if (typeof value === "string") {
2412
+ return value;
2413
+ }
2414
+ if (Array.isArray(value)) {
2415
+ const items = value.filter((item) => typeof item === "string");
2416
+ return items.length > 0 ? items.join(",") : undefined;
2417
+ }
2418
+ return undefined;
2419
+ }
2420
+ function pickMergeRequestForSourceBranch(value, sourceBranch, options) {
2421
+ const matches = extractMergeRequestRecords(value).filter((item) => {
2422
+ const candidateBranch = item.source_branch;
2423
+ return typeof candidateBranch === "string" && candidateBranch === sourceBranch;
2424
+ });
2425
+ const opened = matches.filter((item) => item.state === "opened");
2426
+ if (options.requireOpened) {
2427
+ if (opened.length === 1) {
2428
+ return requireArrayValue(opened, 0, `No opened merge request found for source_branch='${sourceBranch}'`);
2429
+ }
2430
+ if (opened.length > 1) {
2431
+ throw new Error(`Multiple opened merge requests found for source_branch='${sourceBranch}'. Please specify merge_request_iid.`);
2432
+ }
2433
+ if (matches.length === 0) {
2434
+ throw new Error(`No merge request found for source_branch='${sourceBranch}'`);
2435
+ }
2436
+ throw new Error(`No opened merge request found for source_branch='${sourceBranch}'`);
2437
+ }
2438
+ if (opened.length === 1) {
2439
+ return requireArrayValue(opened, 0, `No merge request found for source_branch='${sourceBranch}'`);
2440
+ }
2441
+ if (opened.length > 1) {
2442
+ throw new Error(`Multiple opened merge requests found for source_branch='${sourceBranch}'. Please specify merge_request_iid.`);
2443
+ }
2444
+ if (matches.length === 1) {
2445
+ return requireArrayValue(matches, 0, `No merge request found for source_branch='${sourceBranch}'`);
2446
+ }
2447
+ if (matches.length === 0) {
2448
+ throw new Error(`No merge request found for source_branch='${sourceBranch}'`);
2449
+ }
2450
+ throw new Error(`Multiple merge requests found for source_branch='${sourceBranch}'. Please specify merge_request_iid.`);
2451
+ }
2452
+ function extractMergeRequestRecords(value) {
2453
+ if (!Array.isArray(value)) {
2454
+ return [];
2455
+ }
2456
+ return value.filter((item) => typeof item === "object" && item !== null);
2457
+ }
2458
+ function getMergeRequestIid(mergeRequest) {
2459
+ const iid = mergeRequest.iid;
2460
+ if (typeof iid === "number" || typeof iid === "string") {
2461
+ return String(iid);
2462
+ }
2463
+ throw new Error("Matched merge request is missing a valid iid");
2464
+ }
2465
+ function getString(args, key) {
2466
+ const value = args[key];
2467
+ if (typeof value !== "string" || value.length === 0) {
2468
+ throw new Error(`'${key}' must be a non-empty string`);
2469
+ }
2470
+ return value;
2471
+ }
2472
+ function getOptionalString(args, key) {
2473
+ const value = args[key];
2474
+ if (value === undefined) {
2475
+ return undefined;
2476
+ }
2477
+ if (typeof value !== "string") {
2478
+ throw new Error(`'${key}' must be a string`);
2479
+ }
2480
+ return value;
2481
+ }
2482
+ function getOptionalStringArray(args, key) {
2483
+ const value = args[key];
2484
+ if (value === undefined) {
2485
+ return undefined;
2486
+ }
2487
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
2488
+ throw new Error(`'${key}' must be string[]`);
2489
+ }
2490
+ return value;
2491
+ }
2492
+ function getBoolean(args, key) {
2493
+ const value = args[key];
2494
+ if (typeof value !== "boolean") {
2495
+ throw new Error(`'${key}' must be boolean`);
2496
+ }
2497
+ return value;
2498
+ }
2499
+ function getOptionalBoolean(args, key) {
2500
+ const value = args[key];
2501
+ if (value === undefined) {
2502
+ return undefined;
2503
+ }
2504
+ if (typeof value !== "boolean") {
2505
+ throw new Error(`'${key}' must be boolean`);
2506
+ }
2507
+ return value;
2508
+ }
2509
+ function getOptionalNumber(args, key) {
2510
+ const value = args[key];
2511
+ if (value === undefined) {
2512
+ return undefined;
2513
+ }
2514
+ if (typeof value !== "number" || Number.isNaN(value)) {
2515
+ throw new Error(`'${key}' must be number`);
2516
+ }
2517
+ return value;
2518
+ }
2519
+ function getArray(args, key) {
2520
+ const value = args[key];
2521
+ if (!Array.isArray(value)) {
2522
+ throw new Error(`'${key}' must be array`);
2523
+ }
2524
+ return value;
2525
+ }
2526
+ function getOptionalNumberArray(args, key) {
2527
+ const value = args[key];
2528
+ if (value === undefined) {
2529
+ return undefined;
2530
+ }
2531
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "number")) {
2532
+ throw new Error(`'${key}' must be number[]`);
2533
+ }
2534
+ return value;
2535
+ }
2536
+ function getOptionalRecord(args, key) {
2537
+ const value = args[key];
2538
+ if (value === undefined) {
2539
+ return undefined;
2540
+ }
2541
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2542
+ throw new Error(`'${key}' must be an object`);
2543
+ }
2544
+ return value;
2545
+ }
2546
+ function requireArrayValue(items, index, errorMessage) {
2547
+ const value = items[index];
2548
+ if (value === undefined) {
2549
+ throw new Error(errorMessage);
2550
+ }
2551
+ return value;
2552
+ }
2553
+ function redactSensitive(value) {
2554
+ if (typeof value === "string") {
2555
+ return value
2556
+ .replace(/\b(glpat-[a-z0-9_-]{10,}|ghp_[a-z0-9]{20,}|eyJ[a-zA-Z0-9._-]{20,})\b/g, "[REDACTED]")
2557
+ .replace(/(private[-_]?token|authorization)["']?\s*[:=]\s*["']?[^"'\s,}]+/gi, "$1=[REDACTED]");
2558
+ }
2559
+ if (Array.isArray(value)) {
2560
+ return value.map((item) => redactSensitive(item));
2561
+ }
2562
+ if (value && typeof value === "object") {
2563
+ const input = value;
2564
+ const output = {};
2565
+ for (const [key, item] of Object.entries(input)) {
2566
+ if (/token|authorization|password|secret/i.test(key)) {
2567
+ output[key] = "[REDACTED]";
2568
+ continue;
2569
+ }
2570
+ output[key] = redactSensitive(item);
2571
+ }
2572
+ return output;
2573
+ }
2574
+ return value;
2575
+ }
2576
+ //# sourceMappingURL=gitlab.js.map