@zereight/mcp-gitlab 2.1.9 โ†’ 2.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/schemas.js CHANGED
@@ -1404,6 +1404,15 @@ export const GetBranchDiffsSchema = ProjectParamsSchema.extend({
1404
1404
  .describe('Array of regex patterns to exclude files from the diff results. Each pattern is a JavaScript-compatible regular expression that matches file paths to ignore. Examples: ["^vendor/", "^test/mocks/", "\\.spec\\.ts$", "package-lock\\.json"]'),
1405
1405
  });
1406
1406
  export const GetMergeRequestSchema = ProjectParamsSchema.extend({
1407
+ project_id: z
1408
+ .preprocess(value => (value === undefined || value === null ? value : String(value)), z
1409
+ .string({
1410
+ required_error: "project_id is required",
1411
+ invalid_type_error: "project_id is required",
1412
+ })
1413
+ .refine(value => value === "" || value.trim().length > 0, "project_id is required")
1414
+ .transform(value => (value === "" ? value : value.trim())))
1415
+ .describe("Project ID or complete URL-encoded path to project"),
1407
1416
  merge_request_iid: z.coerce.string().optional().describe("The IID of a merge request"),
1408
1417
  source_branch: z.string().optional().describe("Source branch name"),
1409
1418
  });
@@ -1821,7 +1830,8 @@ export const ListProjectsSchema = z
1821
1830
  })
1822
1831
  .merge(PaginationOptionsSchema);
1823
1832
  // Label operation schemas
1824
- export const ListLabelsSchema = z.object({
1833
+ export const ListLabelsSchema = z
1834
+ .object({
1825
1835
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1826
1836
  with_counts: z
1827
1837
  .coerce.boolean()
@@ -1829,7 +1839,8 @@ export const ListLabelsSchema = z.object({
1829
1839
  .describe("Whether or not to include issue and merge request counts"),
1830
1840
  include_ancestor_groups: z.coerce.boolean().optional().describe("Include ancestor groups"),
1831
1841
  search: z.string().optional().describe("Keyword to filter labels by"),
1832
- });
1842
+ })
1843
+ .merge(PaginationOptionsSchema);
1833
1844
  export const GetLabelSchema = z.object({
1834
1845
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1835
1846
  label_id: z.coerce.string().describe("The ID or title of a project's label"),
@@ -417,7 +417,7 @@ async function validateToolCalls(client, mockServer, expectedToken) {
417
417
  { name: 'get_merge_request', params: { project_id: '1', merge_request_iid: '1' } },
418
418
  { name: 'list_merge_requests', params: { project_id: '1' } },
419
419
  { name: 'get_repository_tree', params: { project_id: '1' } },
420
- { name: 'list_labels', params: { project_id: '1' } },
420
+ { name: 'list_labels', params: { project_id: '1', page: 2, per_page: 50 } },
421
421
  { name: 'list_pipelines', params: { project_id: '1' } },
422
422
  { name: 'list_commits', params: { project_id: '1' } },
423
423
  ];
@@ -468,6 +468,10 @@ async function validateToolCalls(client, mockServer, expectedToken) {
468
468
  else {
469
469
  assert.strictEqual(req.headers['private-token'], expectedToken);
470
470
  }
471
+ if (tool.name === 'list_labels') {
472
+ assert.strictEqual(req.query.page, '2');
473
+ assert.strictEqual(req.query.per_page, '50');
474
+ }
471
475
  res.json(mockResponse);
472
476
  });
473
477
  const result = await client.callTool(tool.name, tool.params);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ts-node
2
- import { GetFileContentsSchema, GitLabFileContentSchema, GitLabRepositorySchema, CreatePipelineSchema, CreateCommitStatusSchema, ListCommitStatusesSchema, CreateIssueNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateIssueEmojiReactionSchema, DeleteMergeRequestEmojiReactionSchema, DeleteIssueEmojiReactionSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, CreateIssueSchema, ListIssuesSchema, ListMergeRequestsSchema, GitLabMergeRequestSchema, GitLabTreeItemSchema, GetMergeRequestSchema, GetRepositoryTreeSchema } from '../schemas.js';
2
+ import { GetFileContentsSchema, GitLabFileContentSchema, GitLabRepositorySchema, CreatePipelineSchema, CreateCommitStatusSchema, ListCommitStatusesSchema, CreateIssueNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateIssueEmojiReactionSchema, DeleteMergeRequestEmojiReactionSchema, DeleteIssueEmojiReactionSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, CreateIssueSchema, ListIssuesSchema, ListMergeRequestsSchema, ListLabelsSchema, GitLabMergeRequestSchema, GitLabTreeItemSchema, GetMergeRequestSchema, GetRepositoryTreeSchema } from '../schemas.js';
3
3
  function runGetFileContentsSchemaTests() {
4
4
  console.log('๐Ÿงช Testing GetFileContentsSchema...');
5
5
  const cases = [
@@ -534,11 +534,36 @@ function runGetMergeRequestSchemaTests() {
534
534
  input: { project_id: 'my/project', merge_request_iid: 24 },
535
535
  expected: { project_id: 'my/project', merge_request_iid: '24' },
536
536
  },
537
+ {
538
+ name: 'schema:get_merge_request:coerced-project-id',
539
+ input: { project_id: 123, merge_request_iid: '42' },
540
+ expected: { project_id: '123', merge_request_iid: '42' },
541
+ },
537
542
  {
538
543
  name: 'schema:get_merge_request:coerced-source-branch',
539
544
  input: { project_id: 'my/project', source_branch: 'feature' },
540
545
  expected: { project_id: 'my/project', source_branch: 'feature' },
541
546
  },
547
+ {
548
+ name: 'schema:get_merge_request:reject-missing-project-id-with-merge-request-iid',
549
+ input: { merge_request_iid: '42' },
550
+ shouldFail: true,
551
+ },
552
+ {
553
+ name: 'schema:get_merge_request:reject-missing-project-id-with-source-branch',
554
+ input: { source_branch: 'feature' },
555
+ shouldFail: true,
556
+ },
557
+ {
558
+ name: 'schema:get_merge_request:allow-empty-project-id-for-default-project',
559
+ input: { project_id: '', merge_request_iid: '42' },
560
+ expected: { project_id: '', merge_request_iid: '42' },
561
+ },
562
+ {
563
+ name: 'schema:get_merge_request:reject-whitespace-project-id',
564
+ input: { project_id: ' ', merge_request_iid: '42' },
565
+ shouldFail: true,
566
+ },
542
567
  ];
543
568
  let passed = 0;
544
569
  let failed = 0;
@@ -876,6 +901,52 @@ function runLabelsCoercionSchemaTests() {
876
901
  console.log(`\nResults: ${passed} passed, ${failed} failed`);
877
902
  return { passed, failed };
878
903
  }
904
+ function runListLabelsSchemaTests() {
905
+ console.log('\n=== List Labels Schema Tests ===');
906
+ const cases = [
907
+ {
908
+ name: 'schema:list_labels:pagination-coercion',
909
+ input: { project_id: 'my/project', page: '2', per_page: '100' },
910
+ expected: { project_id: 'my/project', page: 2, per_page: 100 },
911
+ },
912
+ {
913
+ name: 'schema:list_labels:filters-with-pagination',
914
+ input: { project_id: 'my/project', search: 'backend', with_counts: 'true', page: 3, per_page: 50 },
915
+ expected: { project_id: 'my/project', search: 'backend', with_counts: true, page: 3, per_page: 50 },
916
+ },
917
+ ];
918
+ let passed = 0;
919
+ let failed = 0;
920
+ cases.forEach(testCase => {
921
+ const result = { name: testCase.name, status: 'failed' };
922
+ const parsed = ListLabelsSchema.safeParse(testCase.input);
923
+ if (!parsed.success) {
924
+ result.error = parsed.error?.message || 'Schema validation failed';
925
+ }
926
+ else {
927
+ const matches = Object.entries(testCase.expected).every(([key, value]) => {
928
+ const actual = parsed.data[key];
929
+ return actual === value;
930
+ });
931
+ if (matches) {
932
+ result.status = 'passed';
933
+ }
934
+ else {
935
+ result.error = `Unexpected parsed result: ${JSON.stringify(parsed.data)}`;
936
+ }
937
+ }
938
+ if (result.status === 'passed') {
939
+ passed++;
940
+ console.log(`โœ… ${result.name}`);
941
+ }
942
+ else {
943
+ failed++;
944
+ console.log(`โŒ ${result.name}: ${result.error}`);
945
+ }
946
+ });
947
+ console.log(`\nResults: ${passed} passed, ${failed} failed`);
948
+ return { passed, failed };
949
+ }
879
950
  function runGitLabTreeItemSchemaTests() {
880
951
  console.log('\n=== GitLabTreeItem Schema Tests ===');
881
952
  const cases = [
@@ -1008,10 +1079,11 @@ if (import.meta.url === `file://${process.argv[1]}`) {
1008
1079
  const emojiReactionResult = runEmojiReactionSchemaTests();
1009
1080
  const repositorySchemaResult = runGitLabRepositorySchemaTests();
1010
1081
  const labelsCoercionResult = runLabelsCoercionSchemaTests();
1082
+ const listLabelsResult = runListLabelsSchemaTests();
1011
1083
  const treeItemResult = runGitLabTreeItemSchemaTests();
1012
1084
  const repositoryTreeResult = runGetRepositoryTreeSchemaTests();
1013
- const totalPassed = getFileContentsResult.passed + fileContentResult.passed + createPipelineResult.passed + commitStatusResult.passed + createIssueNoteResult.passed + getMergeRequestResult.passed + gitLabMergeRequestResult.passed + emojiReactionResult.passed + repositorySchemaResult.passed + labelsCoercionResult.passed + treeItemResult.passed + repositoryTreeResult.passed;
1014
- const totalFailed = getFileContentsResult.failed + fileContentResult.failed + createPipelineResult.failed + commitStatusResult.failed + createIssueNoteResult.failed + getMergeRequestResult.failed + gitLabMergeRequestResult.failed + emojiReactionResult.failed + repositorySchemaResult.failed + labelsCoercionResult.failed + treeItemResult.failed + repositoryTreeResult.failed;
1085
+ const totalPassed = getFileContentsResult.passed + fileContentResult.passed + createPipelineResult.passed + commitStatusResult.passed + createIssueNoteResult.passed + getMergeRequestResult.passed + gitLabMergeRequestResult.passed + emojiReactionResult.passed + repositorySchemaResult.passed + labelsCoercionResult.passed + listLabelsResult.passed + treeItemResult.passed + repositoryTreeResult.passed;
1086
+ const totalFailed = getFileContentsResult.failed + fileContentResult.failed + createPipelineResult.failed + commitStatusResult.failed + createIssueNoteResult.failed + getMergeRequestResult.failed + gitLabMergeRequestResult.failed + emojiReactionResult.failed + repositorySchemaResult.failed + labelsCoercionResult.failed + listLabelsResult.failed + treeItemResult.failed + repositoryTreeResult.failed;
1015
1087
  console.log(`\nTotal Results: ${totalPassed} passed, ${totalFailed} failed`);
1016
1088
  if (totalFailed > 0) {
1017
1089
  process.exit(1);
@@ -124,6 +124,24 @@ function runTests() {
124
124
  error: error.message,
125
125
  });
126
126
  }
127
+ // Test 7: Effects fields
128
+ try {
129
+ const schema = z.object({
130
+ effectRequired: z.preprocess(value => value, z.string()),
131
+ effectOptional: z.preprocess(value => value, z.string().optional()),
132
+ });
133
+ const result = toJSONSchema(schema);
134
+ assert(result.required?.includes("effectRequired"), "effectRequired should be in required array");
135
+ assert(!result.required?.includes("effectOptional"), "effectOptional should NOT be in required array");
136
+ results.push({ name: "effects fields", status: "passed" });
137
+ }
138
+ catch (error) {
139
+ results.push({
140
+ name: "effects fields",
141
+ status: "failed",
142
+ error: error.message,
143
+ });
144
+ }
127
145
  // Print results
128
146
  const passed = results.filter((r) => r.status === "passed").length;
129
147
  const failed = results.filter((r) => r.status === "failed").length;
@@ -6,6 +6,23 @@ import { zodToJsonSchema } from "zod-to-json-schema";
6
6
  */
7
7
  export const toJSONSchema = (schema) => {
8
8
  const jsonSchema = zodToJsonSchema(schema, { $refStrategy: "none" });
9
+ const isOptionalLikeField = (zodType) => {
10
+ const def = zodType._def;
11
+ const typeName = def?.typeName;
12
+ if (["ZodOptional", "ZodNullable", "ZodDefault", "ZodCatch"].includes(typeName)) {
13
+ return true;
14
+ }
15
+ if (typeName === "ZodEffects") {
16
+ return isOptionalLikeField(def.schema);
17
+ }
18
+ if (typeName === "ZodBranded") {
19
+ return isOptionalLikeField(def.type);
20
+ }
21
+ if (typeName === "ZodPipeline") {
22
+ return isOptionalLikeField(def.in);
23
+ }
24
+ return false;
25
+ };
9
26
  // Extract required fields from Zod schema
10
27
  const zodRequiredFields = (() => {
11
28
  if (schema instanceof z.ZodObject) {
@@ -13,17 +30,7 @@ export const toJSONSchema = (schema) => {
13
30
  const requiredFields = [];
14
31
  Object.entries(shape).forEach(([key, fieldDef]) => {
15
32
  const zodType = fieldDef;
16
- const typeName = zodType._def?.typeName;
17
- // Check if field is wrapped in zod required types
18
- const isRequired = [
19
- "ZodOptional",
20
- "ZodNullable",
21
- "ZodDefault",
22
- "ZodEffects",
23
- "ZodCatch",
24
- "ZodBranded",
25
- ].includes(typeName);
26
- if (!isRequired) {
33
+ if (!isOptionalLikeField(zodType)) {
27
34
  requiredFields.push(key);
28
35
  }
29
36
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.1.9",
3
+ "version": "2.1.10",
4
4
  "mcpName": "io.github.zereight/gitlab-mcp",
5
5
  "description": "GitLab MCP server for projects, merge requests, issues, pipelines, wiki, releases, and more",
6
6
  "keywords": [