@zereight/mcp-gitlab 2.1.29 → 2.1.38
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/README.ko.md +72 -20
- package/README.md +120 -82
- package/README.zh-CN.md +72 -20
- package/build/config.js +13 -0
- package/build/downloads/proxy.js +199 -0
- package/build/index.js +850 -490
- package/build/schemas.js +115 -28
- package/build/scripts/check-skill-sync.js +137 -0
- package/build/scripts/generate-tool-docs.js +8 -3
- package/build/server/metrics.js +84 -0
- package/build/server/transport-mode.js +25 -0
- package/build/test/callback-proxy-tests.js +1 -1
- package/build/test/client-pool-test.js +1 -1
- package/build/test/dynamic-api-url-allowlist.test.js +2 -2
- package/build/test/dynamic-api-url-test.js +4 -4
- package/build/test/dynamic-routing-tests.js +4 -4
- package/build/test/group-milestone-schema.test.js +45 -0
- package/build/test/mcp-oauth-tests.js +29 -4
- package/build/test/mcp-server-name.test.js +87 -0
- package/build/test/multi-server-test.js +3 -3
- package/build/test/no-proxy-integration-test.js +1 -1
- package/build/test/nullable-gitlab-response-fields.test.js +22 -0
- package/build/test/path-segment-encoding.test.js +11 -0
- package/build/test/remote-auth-simple-test.js +226 -199
- package/build/test/server/metrics.test.js +47 -0
- package/build/test/sse-auth-guard.test.js +2 -2
- package/build/test/stateless/session-id-integration.test.js +3 -3
- package/build/test/streamable-http-concurrent-session.test.js +64 -1
- package/build/test/streamable-http-dns-rebinding.test.js +185 -0
- package/build/test/streamable-http-static-token-auth.test.js +25 -22
- package/build/test/streamable-http-unauthenticated-discovery.test.js +28 -15
- package/build/test/test-ci-catalog.js +1 -1
- package/build/test/test-ci-lint.js +1 -1
- package/build/test/test-ci-variables.js +8 -5
- package/build/test/test-dependency-proxy.js +11 -7
- package/build/test/test-deployment-tools.js +16 -2
- package/build/test/test-download-attachment.js +1 -1
- package/build/test/test-get-file-blame.js +1 -1
- package/build/test/test-geteffectiveprojectid.js +221 -8
- package/build/test/test-issue-description-patch.js +1 -1
- package/build/test/test-job-artifacts.js +1 -1
- package/build/test/test-list-issues.js +1 -1
- package/build/test/test-list-merge-requests.js +1 -1
- package/build/test/test-list-project-members.js +1 -1
- package/build/test/test-merge-request-approval-state-tools.js +1 -1
- package/build/test/test-merge-request-pipelines.js +1 -1
- package/build/test/test-mr-diffs-filter.js +1 -1
- package/build/test/test-mr-file-diffs.js +2 -2
- package/build/test/test-oauth-proxy-rate-limit.js +1 -1
- package/build/test/test-permission-mode.js +216 -0
- package/build/test/test-protected-branches.js +23 -4
- package/build/test/test-remote-downloads.js +2 -2
- package/build/test/test-search-code.js +1 -1
- package/build/test/test-tags.js +1 -1
- package/build/test/test-todos.js +1 -1
- package/build/test/test-token-optimizations.js +3 -3
- package/build/test/test-toolset-filtering.js +3 -3
- package/build/test/test-update-issue-slim.js +141 -0
- package/build/test/test-upload-markdown.js +1 -1
- package/build/test/utils/download-token.test.js +35 -0
- package/build/test/utils/forwarded-public-base-url.test.js +9 -1
- package/build/test/utils/graphql-query.test.js +64 -1
- package/build/test/utils/mock-gitlab-server.js +24 -31
- package/build/test/utils/server-launcher.js +1 -2
- package/build/test/utils/version-check.test.js +52 -0
- package/build/tools/registry.js +93 -5
- package/build/utils/download-token.js +71 -0
- package/build/utils/forwarded-public-base-url.js +22 -0
- package/build/utils/graphql-query.js +109 -12
- package/build/utils/version-check.js +40 -0
- package/package.json +5 -4
package/build/schemas.js
CHANGED
|
@@ -527,7 +527,7 @@ export const GetPipelineJobOutputSchema = z.object({
|
|
|
527
527
|
limit: z
|
|
528
528
|
.number()
|
|
529
529
|
.optional()
|
|
530
|
-
.describe("Maximum number of lines to return from the end of the log (default: 1000)"),
|
|
530
|
+
.describe("Maximum number of lines to return from the end of the log (default/max: 1000)"),
|
|
531
531
|
offset: z
|
|
532
532
|
.number()
|
|
533
533
|
.optional()
|
|
@@ -939,6 +939,12 @@ export const GitLabMilestonesSchema = z.object({
|
|
|
939
939
|
expired: z.coerce.boolean(),
|
|
940
940
|
web_url: z.string().optional(),
|
|
941
941
|
});
|
|
942
|
+
// Group milestones return group_id (not project_id)
|
|
943
|
+
export const GitLabGroupMilestonesSchema = GitLabMilestonesSchema.omit({
|
|
944
|
+
project_id: true,
|
|
945
|
+
}).extend({
|
|
946
|
+
group_id: z.coerce.string(),
|
|
947
|
+
});
|
|
942
948
|
// Input schemas for operations
|
|
943
949
|
export const CreateRepositoryOptionsSchema = z.object({
|
|
944
950
|
name: z.string(),
|
|
@@ -1026,6 +1032,7 @@ export const CreateBranchOptionsSchema = z.object({
|
|
|
1026
1032
|
ref: z.string(), // The source branch/commit for the new branch
|
|
1027
1033
|
});
|
|
1028
1034
|
export const GitLabCompareResultSchema = z.object({
|
|
1035
|
+
// GitLab may return null commit for empty/no-op compare ranges
|
|
1029
1036
|
commit: z
|
|
1030
1037
|
.object({
|
|
1031
1038
|
id: z.string().optional(),
|
|
@@ -1035,6 +1042,7 @@ export const GitLabCompareResultSchema = z.object({
|
|
|
1035
1042
|
author_email: z.string().optional(),
|
|
1036
1043
|
created_at: z.string().optional(),
|
|
1037
1044
|
})
|
|
1045
|
+
.nullable()
|
|
1038
1046
|
.optional(),
|
|
1039
1047
|
commits: z.array(GitLabCommitSchema),
|
|
1040
1048
|
diffs: z.array(GitLabDiffSchema),
|
|
@@ -1658,8 +1666,9 @@ export const UpdateDefaultBranchSchema = ProjectParamsSchema.extend({
|
|
|
1658
1666
|
export const GitLabProtectedBranchAccessLevelSchema = z.object({
|
|
1659
1667
|
access_level: z.number().nullable().optional(),
|
|
1660
1668
|
access_level_description: z.string().optional(),
|
|
1661
|
-
|
|
1662
|
-
|
|
1669
|
+
// GitLab returns null for role-based access levels (not user-/group-specific)
|
|
1670
|
+
user_id: z.number().nullable().optional(),
|
|
1671
|
+
group_id: z.number().nullable().optional(),
|
|
1663
1672
|
});
|
|
1664
1673
|
export const GitLabProtectedBranchSchema = z.object({
|
|
1665
1674
|
id: z.number().optional(),
|
|
@@ -1704,7 +1713,17 @@ export const GetBranchDiffsSchema = ProjectParamsSchema.extend({
|
|
|
1704
1713
|
.optional()
|
|
1705
1714
|
.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"]'),
|
|
1706
1715
|
});
|
|
1707
|
-
|
|
1716
|
+
const flexibleBooleanOptional = z.preprocess(val => {
|
|
1717
|
+
if (typeof val !== "string")
|
|
1718
|
+
return val;
|
|
1719
|
+
const normalized = val.trim().toLowerCase();
|
|
1720
|
+
if (normalized === "true")
|
|
1721
|
+
return true;
|
|
1722
|
+
if (normalized === "false")
|
|
1723
|
+
return false;
|
|
1724
|
+
return val;
|
|
1725
|
+
}, z.boolean().optional());
|
|
1726
|
+
const MergeRequestParamsSchema = ProjectParamsSchema.extend({
|
|
1708
1727
|
project_id: z
|
|
1709
1728
|
.preprocess(value => (value === undefined || value === null ? value : String(value)), z
|
|
1710
1729
|
.string({
|
|
@@ -1717,7 +1736,10 @@ export const GetMergeRequestSchema = ProjectParamsSchema.extend({
|
|
|
1717
1736
|
merge_request_iid: z.coerce.string().optional().describe("The IID of a merge request"),
|
|
1718
1737
|
source_branch: z.string().optional().describe("Source branch name"),
|
|
1719
1738
|
});
|
|
1720
|
-
export const
|
|
1739
|
+
export const GetMergeRequestSchema = MergeRequestParamsSchema.extend({
|
|
1740
|
+
include_summaries: flexibleBooleanOptional.describe("If true, include deployment_summary, commit_addition_summary and approval_summary (extra API calls, larger response). Default false to reduce token usage."),
|
|
1741
|
+
});
|
|
1742
|
+
export const UpdateMergeRequestSchema = MergeRequestParamsSchema.extend({
|
|
1721
1743
|
title: z.string().optional().describe("The title of the merge request"),
|
|
1722
1744
|
description: z.string().optional().describe("The description of the merge request"),
|
|
1723
1745
|
target_branch: z.string().optional().describe("The target branch"),
|
|
@@ -1859,14 +1881,14 @@ export const ListMergeRequestPipelinesSchema = ProjectParamsSchema.extend({
|
|
|
1859
1881
|
.transform(value => value.trim()))
|
|
1860
1882
|
.describe("The internal ID of the merge request"),
|
|
1861
1883
|
}).merge(PaginationOptionsSchema);
|
|
1862
|
-
export const GetMergeRequestDiffsSchema =
|
|
1884
|
+
export const GetMergeRequestDiffsSchema = MergeRequestParamsSchema.extend({
|
|
1863
1885
|
view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"),
|
|
1864
1886
|
excluded_file_patterns: z
|
|
1865
1887
|
.array(z.string())
|
|
1866
1888
|
.optional()
|
|
1867
1889
|
.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"]'),
|
|
1868
1890
|
});
|
|
1869
|
-
export const ListMergeRequestDiffsSchema =
|
|
1891
|
+
export const ListMergeRequestDiffsSchema = MergeRequestParamsSchema.extend({
|
|
1870
1892
|
page: z.coerce.number().optional().describe("Page number for pagination (default: 1)"),
|
|
1871
1893
|
per_page: z.coerce
|
|
1872
1894
|
.number()
|
|
@@ -1877,13 +1899,13 @@ export const ListMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
|
|
|
1877
1899
|
.optional()
|
|
1878
1900
|
.describe("Present diffs in the unified diff format. Default is false. Introduced in GitLab 16.5."),
|
|
1879
1901
|
});
|
|
1880
|
-
export const ListMergeRequestChangedFilesSchema =
|
|
1902
|
+
export const ListMergeRequestChangedFilesSchema = MergeRequestParamsSchema.extend({
|
|
1881
1903
|
excluded_file_patterns: z
|
|
1882
1904
|
.array(z.string())
|
|
1883
1905
|
.optional()
|
|
1884
1906
|
.describe('Array of regex patterns to exclude files. Examples: ["^vendor/", "\\.pb\\.go$"]'),
|
|
1885
1907
|
});
|
|
1886
|
-
export const GetMergeRequestFileDiffSchema =
|
|
1908
|
+
export const GetMergeRequestFileDiffSchema = MergeRequestParamsSchema.extend({
|
|
1887
1909
|
file_paths: z
|
|
1888
1910
|
.array(z.string())
|
|
1889
1911
|
.describe("List of file paths to retrieve diffs for (e.g. ['src/api/users.ts', 'src/repo/user.go']). " +
|
|
@@ -2063,6 +2085,7 @@ export const ListMergeRequestsSchema = z
|
|
|
2063
2085
|
export const GetIssueSchema = z.object({
|
|
2064
2086
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2065
2087
|
issue_iid: z.coerce.string().describe("The internal ID of the project issue"),
|
|
2088
|
+
full_response: flexibleBooleanOptional.describe("If true, return the complete issue object including the full milestone description. Default returns a slim milestone (id, iid, title, state, web_url) to reduce token usage."),
|
|
2066
2089
|
});
|
|
2067
2090
|
export const UpdateIssueSchema = z.object({
|
|
2068
2091
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
@@ -2086,6 +2109,7 @@ export const UpdateIssueSchema = z.object({
|
|
|
2086
2109
|
issue_type: z
|
|
2087
2110
|
.preprocess(val => (typeof val === "string" ? val.toLowerCase() : val), z.enum(["issue", "incident", "test_case", "task"]).optional())
|
|
2088
2111
|
.describe("The type of issue. One of issue, incident, test_case or task."),
|
|
2112
|
+
full_response: flexibleBooleanOptional.describe("If true, return the complete updated issue object. Default returns a slim confirmation (iid, title, state, web_url, updated_at) to reduce token usage."),
|
|
2089
2113
|
});
|
|
2090
2114
|
export const DeleteIssueSchema = z.object({
|
|
2091
2115
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
@@ -2344,7 +2368,10 @@ export const CreateWikiPageSchema = z.object({
|
|
|
2344
2368
|
export const UpdateWikiPageSchema = z.object({
|
|
2345
2369
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2346
2370
|
slug: z.string().describe("Slug of the wiki page (will be URL-encoded internally)"),
|
|
2347
|
-
title: z
|
|
2371
|
+
title: z
|
|
2372
|
+
.string()
|
|
2373
|
+
.optional()
|
|
2374
|
+
.describe("New title of the wiki page. WARNING: setting this renames the page and changes its slug/URL (for nested pages it can also move the page to a different path), which breaks existing links. To change only the displayed title while keeping the URL, omit this parameter and instead set a `title:` field in the content's YAML front matter."),
|
|
2348
2375
|
content: z.string().optional().describe("New content of the wiki page"),
|
|
2349
2376
|
format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
|
|
2350
2377
|
});
|
|
@@ -2390,7 +2417,10 @@ export const CreateGroupWikiPageSchema = z.object({
|
|
|
2390
2417
|
export const UpdateGroupWikiPageSchema = z.object({
|
|
2391
2418
|
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
2392
2419
|
slug: z.string().describe("Slug of the wiki page (will be URL-encoded internally)"),
|
|
2393
|
-
title: z
|
|
2420
|
+
title: z
|
|
2421
|
+
.string()
|
|
2422
|
+
.optional()
|
|
2423
|
+
.describe("New title of the wiki page. WARNING: setting this renames the page and changes its slug/URL (for nested pages it can also move the page to a different path), which breaks existing links. To change only the displayed title while keeping the URL, omit this parameter and instead set a `title:` field in the content's YAML front matter."),
|
|
2394
2424
|
content: z.string().optional().describe("New content of the wiki page"),
|
|
2395
2425
|
format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
|
|
2396
2426
|
});
|
|
@@ -2651,13 +2681,70 @@ export const EditProjectMilestoneSchema = GetProjectMilestoneSchema.extend({
|
|
|
2651
2681
|
// Schema for deleting a milestone
|
|
2652
2682
|
export const DeleteProjectMilestoneSchema = GetProjectMilestoneSchema;
|
|
2653
2683
|
// Schema for getting issues assigned to a milestone
|
|
2654
|
-
export const GetMilestoneIssuesSchema = GetProjectMilestoneSchema;
|
|
2684
|
+
export const GetMilestoneIssuesSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema);
|
|
2655
2685
|
// Schema for getting merge requests assigned to a milestone
|
|
2656
2686
|
export const GetMilestoneMergeRequestsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema);
|
|
2657
2687
|
// Schema for promoting a project milestone to a group milestone
|
|
2658
2688
|
export const PromoteProjectMilestoneSchema = GetProjectMilestoneSchema;
|
|
2659
2689
|
// Schema for getting burndown chart events for a milestone
|
|
2660
2690
|
export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.merge(PaginationOptionsSchema);
|
|
2691
|
+
// Group milestone schemas (mirror project milestones against /groups/:id/milestones)
|
|
2692
|
+
export const ListGroupMilestonesSchema = z
|
|
2693
|
+
.object({
|
|
2694
|
+
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
2695
|
+
iids: z
|
|
2696
|
+
.array(z.coerce.number())
|
|
2697
|
+
.optional()
|
|
2698
|
+
.describe("Return only the milestones having the given iid"),
|
|
2699
|
+
state: z
|
|
2700
|
+
.enum(["active", "closed"])
|
|
2701
|
+
.optional()
|
|
2702
|
+
.describe("Return only active or closed milestones"),
|
|
2703
|
+
title: z
|
|
2704
|
+
.string()
|
|
2705
|
+
.optional()
|
|
2706
|
+
.describe("Return only milestones with a title matching the provided string"),
|
|
2707
|
+
search: z
|
|
2708
|
+
.string()
|
|
2709
|
+
.optional()
|
|
2710
|
+
.describe("Return only milestones with a title or description matching the provided string"),
|
|
2711
|
+
include_ancestors: z.coerce.boolean().optional().describe("Include ancestor groups"),
|
|
2712
|
+
include_descendants: z.coerce.boolean().optional().describe("Include descendant groups"),
|
|
2713
|
+
updated_before: z
|
|
2714
|
+
.string()
|
|
2715
|
+
.optional()
|
|
2716
|
+
.describe("Return milestones updated before the specified date (ISO 8601 format)"),
|
|
2717
|
+
updated_after: z
|
|
2718
|
+
.string()
|
|
2719
|
+
.optional()
|
|
2720
|
+
.describe("Return milestones updated after the specified date (ISO 8601 format)"),
|
|
2721
|
+
})
|
|
2722
|
+
.merge(PaginationOptionsSchema);
|
|
2723
|
+
export const GetGroupMilestoneSchema = z.object({
|
|
2724
|
+
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
2725
|
+
milestone_id: z.coerce.string().describe("The ID of a group milestone"),
|
|
2726
|
+
});
|
|
2727
|
+
export const CreateGroupMilestoneSchema = z.object({
|
|
2728
|
+
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
2729
|
+
title: z.string().describe("The title of the milestone"),
|
|
2730
|
+
description: z.string().optional().describe("The description of the milestone"),
|
|
2731
|
+
due_date: z.string().optional().describe("The due date of the milestone (YYYY-MM-DD)"),
|
|
2732
|
+
start_date: z.string().optional().describe("The start date of the milestone (YYYY-MM-DD)"),
|
|
2733
|
+
});
|
|
2734
|
+
export const EditGroupMilestoneSchema = GetGroupMilestoneSchema.extend({
|
|
2735
|
+
title: z.string().optional().describe("The title of the milestone"),
|
|
2736
|
+
description: z.string().optional().describe("The description of the milestone"),
|
|
2737
|
+
due_date: z.string().optional().describe("The due date of the milestone (YYYY-MM-DD)"),
|
|
2738
|
+
start_date: z.string().optional().describe("The start date of the milestone (YYYY-MM-DD)"),
|
|
2739
|
+
state_event: z
|
|
2740
|
+
.enum(["close", "activate"])
|
|
2741
|
+
.optional()
|
|
2742
|
+
.describe("The state event of the milestone"),
|
|
2743
|
+
});
|
|
2744
|
+
export const DeleteGroupMilestoneSchema = GetGroupMilestoneSchema;
|
|
2745
|
+
export const GetGroupMilestoneIssuesSchema = GetGroupMilestoneSchema.merge(PaginationOptionsSchema);
|
|
2746
|
+
export const GetGroupMilestoneMergeRequestsSchema = GetGroupMilestoneSchema.merge(PaginationOptionsSchema);
|
|
2747
|
+
export const GetGroupMilestoneBurndownEventsSchema = GetGroupMilestoneSchema.merge(PaginationOptionsSchema);
|
|
2661
2748
|
// Add schemas for commit operations
|
|
2662
2749
|
export const ListCommitsSchema = z.object({
|
|
2663
2750
|
project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
|
|
@@ -3330,17 +3417,17 @@ const workItemTypeEnum = z
|
|
|
3330
3417
|
"requirement",
|
|
3331
3418
|
"ticket",
|
|
3332
3419
|
]));
|
|
3333
|
-
const
|
|
3420
|
+
const NamespaceIdOrPathSchema = z.coerce
|
|
3334
3421
|
.string()
|
|
3335
|
-
.describe("Project ID, URL-encoded project path,
|
|
3422
|
+
.describe("Project ID, URL-encoded project path, group path, or explicit namespace prefix for ambiguous numeric IDs (e.g. 'group/subgroup', 'group:123', or 'project:123')");
|
|
3336
3423
|
// Common params for work item tools
|
|
3337
3424
|
const WorkItemParamsSchema = z.object({
|
|
3338
|
-
project_id:
|
|
3425
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3339
3426
|
iid: z.coerce.number().describe("The internal ID (IID) of the work item"),
|
|
3340
3427
|
});
|
|
3341
3428
|
export const GetWorkItemSchema = WorkItemParamsSchema;
|
|
3342
3429
|
export const ListWorkItemsSchema = z.object({
|
|
3343
|
-
project_id:
|
|
3430
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3344
3431
|
types: z
|
|
3345
3432
|
.array(workItemTypeEnum)
|
|
3346
3433
|
.optional()
|
|
@@ -3360,7 +3447,7 @@ export const ListWorkItemsSchema = z.object({
|
|
|
3360
3447
|
.describe("Cursor for pagination (from previous response's endCursor)"),
|
|
3361
3448
|
});
|
|
3362
3449
|
export const CreateWorkItemSchema = z.object({
|
|
3363
|
-
project_id:
|
|
3450
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3364
3451
|
title: z.string().describe("Title of the work item"),
|
|
3365
3452
|
type: workItemTypeEnum
|
|
3366
3453
|
.optional()
|
|
@@ -3501,19 +3588,19 @@ export const UpdateWorkItemSchema = WorkItemParamsSchema.extend({
|
|
|
3501
3588
|
.describe("Incident only: set escalation status"),
|
|
3502
3589
|
});
|
|
3503
3590
|
export const ConvertWorkItemTypeSchema = z.object({
|
|
3504
|
-
project_id:
|
|
3591
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3505
3592
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3506
3593
|
new_type: workItemTypeEnum.describe("The target work item type to convert to"),
|
|
3507
3594
|
});
|
|
3508
3595
|
export const ListWorkItemStatusesSchema = z.object({
|
|
3509
|
-
project_id:
|
|
3596
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3510
3597
|
work_item_type: workItemTypeEnum
|
|
3511
3598
|
.optional()
|
|
3512
3599
|
.default("issue")
|
|
3513
3600
|
.describe("The work item type to list available statuses for. Defaults to 'issue'."),
|
|
3514
3601
|
});
|
|
3515
3602
|
export const ListWorkItemNotesSchema = z.object({
|
|
3516
|
-
project_id:
|
|
3603
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3517
3604
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3518
3605
|
page_size: z.coerce
|
|
3519
3606
|
.number()
|
|
@@ -3528,7 +3615,7 @@ export const ListWorkItemNotesSchema = z.object({
|
|
|
3528
3615
|
.describe("Sort order for discussions"),
|
|
3529
3616
|
});
|
|
3530
3617
|
export const CreateWorkItemNoteSchema = z.object({
|
|
3531
|
-
project_id:
|
|
3618
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3532
3619
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3533
3620
|
body: z.string().describe("Note body (Markdown supported)"),
|
|
3534
3621
|
internal: z.coerce
|
|
@@ -3551,7 +3638,7 @@ export const MoveWorkItemSchema = z.object({
|
|
|
3551
3638
|
.describe("Project ID, URL-encoded project path, or group path of the target namespace"),
|
|
3552
3639
|
});
|
|
3553
3640
|
export const ListCustomFieldDefinitionsSchema = z.object({
|
|
3554
|
-
project_id:
|
|
3641
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3555
3642
|
work_item_type: workItemTypeEnum
|
|
3556
3643
|
.optional()
|
|
3557
3644
|
.default("issue")
|
|
@@ -3608,17 +3695,17 @@ export const DeleteIssueNoteEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
|
3608
3695
|
});
|
|
3609
3696
|
// --- Emoji Reaction schemas (GraphQL: Work Items) ---
|
|
3610
3697
|
export const CreateWorkItemEmojiReactionSchema = z.object({
|
|
3611
|
-
project_id:
|
|
3698
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3612
3699
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3613
3700
|
name: emojiNameField,
|
|
3614
3701
|
});
|
|
3615
3702
|
export const DeleteWorkItemEmojiReactionSchema = z.object({
|
|
3616
|
-
project_id:
|
|
3703
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3617
3704
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3618
3705
|
name: emojiNameField,
|
|
3619
3706
|
});
|
|
3620
3707
|
export const CreateWorkItemNoteEmojiReactionSchema = z.object({
|
|
3621
|
-
project_id:
|
|
3708
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3622
3709
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3623
3710
|
note_id: z
|
|
3624
3711
|
.string()
|
|
@@ -3626,7 +3713,7 @@ export const CreateWorkItemNoteEmojiReactionSchema = z.object({
|
|
|
3626
3713
|
name: emojiNameField,
|
|
3627
3714
|
});
|
|
3628
3715
|
export const DeleteWorkItemNoteEmojiReactionSchema = z.object({
|
|
3629
|
-
project_id:
|
|
3716
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3630
3717
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3631
3718
|
note_id: z
|
|
3632
3719
|
.string()
|
|
@@ -3650,11 +3737,11 @@ export const ListIssueNoteEmojiReactionsSchema = ProjectParamsSchema.extend({
|
|
|
3650
3737
|
discussion_id: noteEmojiDiscussionField,
|
|
3651
3738
|
});
|
|
3652
3739
|
export const ListWorkItemEmojiReactionsSchema = z.object({
|
|
3653
|
-
project_id:
|
|
3740
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3654
3741
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3655
3742
|
});
|
|
3656
3743
|
export const ListWorkItemNoteEmojiReactionsSchema = z.object({
|
|
3657
|
-
project_id:
|
|
3744
|
+
project_id: NamespaceIdOrPathSchema,
|
|
3658
3745
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3659
3746
|
note_id: z
|
|
3660
3747
|
.string()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Verify skills/gitlab-mcp/ stays in sync with tools/registry.ts.
|
|
4
|
+
*
|
|
5
|
+
* Run with: npm run check:skill-sync
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { TOOLSET_DEFINITIONS, allTools, destructiveTools, DEFAULT_TOOLSET_IDS, } from "../tools/registry.js";
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const REPO_ROOT = join(__dirname, "..");
|
|
13
|
+
const SKILL_PATH = join(REPO_ROOT, "skills", "gitlab-mcp", "SKILL.md");
|
|
14
|
+
const REF_DIR = join(REPO_ROOT, "skills", "gitlab-mcp", "reference");
|
|
15
|
+
// Response field names in reference docs — not MCP tools.
|
|
16
|
+
const REFERENCE_FIELD_ALLOWLIST = new Set([
|
|
17
|
+
"allow_multiple",
|
|
18
|
+
"deleted_file",
|
|
19
|
+
"excluded_file_patterns",
|
|
20
|
+
"group_id",
|
|
21
|
+
"new_file",
|
|
22
|
+
"new_path",
|
|
23
|
+
"old_path",
|
|
24
|
+
"project_id",
|
|
25
|
+
"renamed_file",
|
|
26
|
+
]);
|
|
27
|
+
function fail(message) {
|
|
28
|
+
console.error(`::error::${message}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
function parseToolsetTable(skill) {
|
|
32
|
+
const rows = new Map();
|
|
33
|
+
const rowRe = /^\| ([a-z_]+) \((\d+) tools?\) \| (yes|no) \|/gm;
|
|
34
|
+
let match;
|
|
35
|
+
while ((match = rowRe.exec(skill)) !== null) {
|
|
36
|
+
rows.set(match[1], {
|
|
37
|
+
count: Number(match[2]),
|
|
38
|
+
isDefault: match[3] === "yes",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return rows;
|
|
42
|
+
}
|
|
43
|
+
function parseIntroCounts(skill) {
|
|
44
|
+
const match = skill.match(/providing (\d+) tools: (\d+) tools across (\d+) toolsets/);
|
|
45
|
+
if (!match) {
|
|
46
|
+
fail("skills/gitlab-mcp/SKILL.md intro line is missing or malformed");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
totalTools: Number(match[1]),
|
|
50
|
+
uniqueInToolsets: Number(match[2]),
|
|
51
|
+
toolsetCount: Number(match[3]),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function parseDestructiveTools(skill) {
|
|
55
|
+
const section = skill.match(/## Destructive Tools[^\n]*\n\n(`[^`]+`(?:, `[^`]+`)*)$/m);
|
|
56
|
+
if (!section) {
|
|
57
|
+
fail("skills/gitlab-mcp/SKILL.md destructive tools section is missing or malformed");
|
|
58
|
+
}
|
|
59
|
+
return new Set([...section[1].matchAll(/`([a-z_]+)`/g)].map((m) => m[1]));
|
|
60
|
+
}
|
|
61
|
+
function collectReferenceToolNames() {
|
|
62
|
+
const toolNames = new Set();
|
|
63
|
+
for (const file of readdirSync(REF_DIR).filter((name) => name.endsWith(".md"))) {
|
|
64
|
+
const text = readFileSync(join(REF_DIR, file), "utf8");
|
|
65
|
+
for (const match of text.matchAll(/`([a-z][a-z0-9_]*)`/g)) {
|
|
66
|
+
const name = match[1];
|
|
67
|
+
if (!REFERENCE_FIELD_ALLOWLIST.has(name)) {
|
|
68
|
+
toolNames.add(name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return toolNames;
|
|
73
|
+
}
|
|
74
|
+
const skill = readFileSync(SKILL_PATH, "utf8");
|
|
75
|
+
const errors = [];
|
|
76
|
+
const registryCounts = Object.fromEntries(TOOLSET_DEFINITIONS.map((definition) => [definition.id, definition.tools.size]));
|
|
77
|
+
const skillRows = parseToolsetTable(skill);
|
|
78
|
+
if (skillRows.size !== TOOLSET_DEFINITIONS.length) {
|
|
79
|
+
errors.push(`toolset table has ${skillRows.size} rows; registry has ${TOOLSET_DEFINITIONS.length}`);
|
|
80
|
+
}
|
|
81
|
+
for (const definition of TOOLSET_DEFINITIONS) {
|
|
82
|
+
const row = skillRows.get(definition.id);
|
|
83
|
+
if (!row) {
|
|
84
|
+
errors.push(`missing toolset row for ${definition.id}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const actualCount = registryCounts[definition.id];
|
|
88
|
+
if (row.count !== actualCount) {
|
|
89
|
+
errors.push(`${definition.id}: skill says ${row.count}, registry has ${actualCount}`);
|
|
90
|
+
}
|
|
91
|
+
const expectedDefault = DEFAULT_TOOLSET_IDS.has(definition.id);
|
|
92
|
+
if (row.isDefault !== expectedDefault) {
|
|
93
|
+
errors.push(`${definition.id}: skill default=${row.isDefault}, registry default=${expectedDefault}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const id of skillRows.keys()) {
|
|
97
|
+
if (!registryCounts[id]) {
|
|
98
|
+
errors.push(`unknown toolset in skill table: ${id}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const uniqueInToolsets = new Set(TOOLSET_DEFINITIONS.flatMap((definition) => [...definition.tools]))
|
|
102
|
+
.size;
|
|
103
|
+
const intro = parseIntroCounts(skill);
|
|
104
|
+
if (intro.totalTools !== allTools.length) {
|
|
105
|
+
errors.push(`intro total tools: skill says ${intro.totalTools}, registry has ${allTools.length}`);
|
|
106
|
+
}
|
|
107
|
+
if (intro.uniqueInToolsets !== uniqueInToolsets) {
|
|
108
|
+
errors.push(`intro unique toolset tools: skill says ${intro.uniqueInToolsets}, registry has ${uniqueInToolsets}`);
|
|
109
|
+
}
|
|
110
|
+
if (intro.toolsetCount !== TOOLSET_DEFINITIONS.length) {
|
|
111
|
+
errors.push(`intro toolset count: skill says ${intro.toolsetCount}, registry has ${TOOLSET_DEFINITIONS.length}`);
|
|
112
|
+
}
|
|
113
|
+
const skillDestructive = parseDestructiveTools(skill);
|
|
114
|
+
for (const tool of skillDestructive) {
|
|
115
|
+
if (!destructiveTools.has(tool)) {
|
|
116
|
+
errors.push(`destructive list includes non-destructive tool: ${tool}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const tool of destructiveTools) {
|
|
120
|
+
if (!skillDestructive.has(tool)) {
|
|
121
|
+
errors.push(`destructive list missing registry tool: ${tool}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const registryToolNames = new Set(allTools.map((tool) => tool.name));
|
|
125
|
+
for (const tool of collectReferenceToolNames()) {
|
|
126
|
+
if (!registryToolNames.has(tool)) {
|
|
127
|
+
errors.push(`reference mentions unknown tool: ${tool}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (errors.length > 0) {
|
|
131
|
+
console.error("skills/gitlab-mcp/ is out of sync with tools/registry.ts:");
|
|
132
|
+
for (const error of errors) {
|
|
133
|
+
console.error(`- ${error}`);
|
|
134
|
+
}
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
console.log("skills/gitlab-mcp/ is in sync with tools/registry.ts");
|
|
@@ -77,7 +77,7 @@ const GROUP_META = {
|
|
|
77
77
|
},
|
|
78
78
|
milestones: {
|
|
79
79
|
title: "Milestones",
|
|
80
|
-
blurb: "Project milestone CRUD plus associated issues/MRs and burndown events.",
|
|
80
|
+
blurb: "Project and group milestone CRUD plus associated issues/MRs and burndown events.",
|
|
81
81
|
},
|
|
82
82
|
wiki: {
|
|
83
83
|
title: "Wiki",
|
|
@@ -273,8 +273,13 @@ function buildIndexPage(groupedToolsList) {
|
|
|
273
273
|
"",
|
|
274
274
|
...buildToggleSection(groupedToolsList),
|
|
275
275
|
"",
|
|
276
|
-
"
|
|
277
|
-
"
|
|
276
|
+
"Permission modes control which tools are exposed:",
|
|
277
|
+
"",
|
|
278
|
+
"- `GITLAB_PERMISSION_MODE=readonly` — hides every write tool regardless of toggles.",
|
|
279
|
+
"- `GITLAB_PERMISSION_MODE=modify` — allows create/update but blocks all `delete_*` tools.",
|
|
280
|
+
"- `GITLAB_READ_ONLY_MODE=true` (deprecated) — same as `readonly`; prefer `GITLAB_PERMISSION_MODE=readonly`.",
|
|
281
|
+
"",
|
|
282
|
+
"See [Environment Variables](../configuration/environment-variables.md)",
|
|
278
283
|
"and [CLI Arguments](../getting-started/cli-arguments.md) for the full list.",
|
|
279
284
|
"",
|
|
280
285
|
"## Legend",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export function escapePrometheusLabel(value) {
|
|
2
|
+
return String(value).replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, '\\"');
|
|
3
|
+
}
|
|
4
|
+
export function formatPrometheusMetrics(snapshot) {
|
|
5
|
+
const configLabels = Object.entries({
|
|
6
|
+
max_sessions: snapshot.config.maxSessions,
|
|
7
|
+
max_requests_per_minute: snapshot.config.maxRequestsPerMinute,
|
|
8
|
+
session_timeout_seconds: snapshot.config.sessionTimeoutSeconds,
|
|
9
|
+
remote_auth_enabled: snapshot.config.remoteAuthEnabled,
|
|
10
|
+
mcp_oauth_enabled: snapshot.config.mcpOAuthEnabled,
|
|
11
|
+
stateless_mode_enabled: snapshot.config.statelessModeEnabled,
|
|
12
|
+
stateless_rotation_key: snapshot.config.statelessRotationKey,
|
|
13
|
+
})
|
|
14
|
+
.map(([key, value]) => `${key}="${escapePrometheusLabel(value)}"`)
|
|
15
|
+
.join(",");
|
|
16
|
+
return [
|
|
17
|
+
"# HELP gitlab_mcp_requests_processed_total Total MCP requests processed",
|
|
18
|
+
"# TYPE gitlab_mcp_requests_processed_total counter",
|
|
19
|
+
`gitlab_mcp_requests_processed_total ${snapshot.requestsProcessed}`,
|
|
20
|
+
"",
|
|
21
|
+
"# HELP gitlab_mcp_requests_rejected_total Requests rejected, by reason",
|
|
22
|
+
"# TYPE gitlab_mcp_requests_rejected_total counter",
|
|
23
|
+
`gitlab_mcp_requests_rejected_total{reason="rate_limit"} ${snapshot.rejectedByRateLimit}`,
|
|
24
|
+
`gitlab_mcp_requests_rejected_total{reason="capacity"} ${snapshot.rejectedByCapacity}`,
|
|
25
|
+
"",
|
|
26
|
+
"# HELP gitlab_mcp_auth_failures_total Authentication failures",
|
|
27
|
+
"# TYPE gitlab_mcp_auth_failures_total counter",
|
|
28
|
+
`gitlab_mcp_auth_failures_total ${snapshot.authFailures}`,
|
|
29
|
+
"",
|
|
30
|
+
"# HELP gitlab_mcp_sessions_total Total sessions created",
|
|
31
|
+
"# TYPE gitlab_mcp_sessions_total counter",
|
|
32
|
+
`gitlab_mcp_sessions_total ${snapshot.totalSessions}`,
|
|
33
|
+
"",
|
|
34
|
+
"# HELP gitlab_mcp_sessions_expired_total Sessions expired due to inactivity",
|
|
35
|
+
"# TYPE gitlab_mcp_sessions_expired_total counter",
|
|
36
|
+
`gitlab_mcp_sessions_expired_total ${snapshot.expiredSessions}`,
|
|
37
|
+
"",
|
|
38
|
+
"# HELP gitlab_mcp_active_sessions Currently active sessions",
|
|
39
|
+
"# TYPE gitlab_mcp_active_sessions gauge",
|
|
40
|
+
`gitlab_mcp_active_sessions ${snapshot.activeSessions}`,
|
|
41
|
+
"",
|
|
42
|
+
"# HELP gitlab_mcp_authenticated_sessions Currently authenticated sessions",
|
|
43
|
+
"# TYPE gitlab_mcp_authenticated_sessions gauge",
|
|
44
|
+
`gitlab_mcp_authenticated_sessions ${snapshot.authenticatedSessions}`,
|
|
45
|
+
"",
|
|
46
|
+
"# HELP gitlab_mcp_client_pool_size Current GitLab client pool size",
|
|
47
|
+
"# TYPE gitlab_mcp_client_pool_size gauge",
|
|
48
|
+
`gitlab_mcp_client_pool_size ${snapshot.gitlabClientPool.size}`,
|
|
49
|
+
"",
|
|
50
|
+
"# HELP gitlab_mcp_client_pool_max_size Maximum GitLab client pool size",
|
|
51
|
+
"# TYPE gitlab_mcp_client_pool_max_size gauge",
|
|
52
|
+
`gitlab_mcp_client_pool_max_size ${snapshot.gitlabClientPool.maxSize}`,
|
|
53
|
+
"",
|
|
54
|
+
"# HELP gitlab_mcp_uptime_seconds Process uptime in seconds",
|
|
55
|
+
"# TYPE gitlab_mcp_uptime_seconds gauge",
|
|
56
|
+
`gitlab_mcp_uptime_seconds ${snapshot.uptime}`,
|
|
57
|
+
"",
|
|
58
|
+
"# HELP gitlab_mcp_memory_usage_bytes Node.js memory usage by type",
|
|
59
|
+
"# TYPE gitlab_mcp_memory_usage_bytes gauge",
|
|
60
|
+
...Object.entries(snapshot.memoryUsage).map(([key, value]) => `gitlab_mcp_memory_usage_bytes{type="${escapePrometheusLabel(key)}"} ${value}`),
|
|
61
|
+
"",
|
|
62
|
+
"# HELP gitlab_mcp_stateless_requests_total Stateless MCP requests processed",
|
|
63
|
+
"# TYPE gitlab_mcp_stateless_requests_total counter",
|
|
64
|
+
`gitlab_mcp_stateless_requests_total ${snapshot.statelessRequests}`,
|
|
65
|
+
"",
|
|
66
|
+
"# HELP gitlab_mcp_stateless_auth_total Stateless auth successes, by source",
|
|
67
|
+
"# TYPE gitlab_mcp_stateless_auth_total counter",
|
|
68
|
+
`gitlab_mcp_stateless_auth_total{source="header"} ${snapshot.statelessAuthFromHeader}`,
|
|
69
|
+
`gitlab_mcp_stateless_auth_total{source="sealed_session_id"} ${snapshot.statelessAuthFromSealedSid}`,
|
|
70
|
+
"",
|
|
71
|
+
"# HELP gitlab_mcp_stateless_auth_failures_total Stateless auth failures",
|
|
72
|
+
"# TYPE gitlab_mcp_stateless_auth_failures_total counter",
|
|
73
|
+
`gitlab_mcp_stateless_auth_failures_total ${snapshot.statelessAuthFailures}`,
|
|
74
|
+
"",
|
|
75
|
+
"# HELP gitlab_mcp_stateless_session_id_rotations_total Stateless session id rotations",
|
|
76
|
+
"# TYPE gitlab_mcp_stateless_session_id_rotations_total counter",
|
|
77
|
+
`gitlab_mcp_stateless_session_id_rotations_total ${snapshot.statelessSidRotated}`,
|
|
78
|
+
"",
|
|
79
|
+
"# HELP gitlab_mcp_config_info Static configuration (value is always 1)",
|
|
80
|
+
"# TYPE gitlab_mcp_config_info gauge",
|
|
81
|
+
`gitlab_mcp_config_info{${configLabels}} 1`,
|
|
82
|
+
"",
|
|
83
|
+
].join("\n");
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SSE, STREAMABLE_HTTP } from "../config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Available transport modes for MCP server.
|
|
4
|
+
*/
|
|
5
|
+
export var TransportMode;
|
|
6
|
+
(function (TransportMode) {
|
|
7
|
+
TransportMode["STDIO"] = "stdio";
|
|
8
|
+
TransportMode["SSE"] = "sse";
|
|
9
|
+
TransportMode["STREAMABLE_HTTP"] = "streamable-http";
|
|
10
|
+
})(TransportMode || (TransportMode = {}));
|
|
11
|
+
/**
|
|
12
|
+
* Determine the transport mode based on environment variables and availability.
|
|
13
|
+
*
|
|
14
|
+
* Transport mode priority (highest to lowest):
|
|
15
|
+
* 1. STREAMABLE_HTTP
|
|
16
|
+
* 2. SSE
|
|
17
|
+
* 3. STDIO
|
|
18
|
+
*/
|
|
19
|
+
export function determineTransportMode() {
|
|
20
|
+
if (STREAMABLE_HTTP)
|
|
21
|
+
return TransportMode.STREAMABLE_HTTP;
|
|
22
|
+
if (SSE)
|
|
23
|
+
return TransportMode.SSE;
|
|
24
|
+
return TransportMode.STDIO;
|
|
25
|
+
}
|
|
@@ -95,7 +95,7 @@ describe("Callback Proxy Mode", () => {
|
|
|
95
95
|
// Track the last code+verifier GitLab received so we can verify the proxy PKCE
|
|
96
96
|
let lastTokenRequest = {};
|
|
97
97
|
before(async () => {
|
|
98
|
-
const mockPort = await findMockServerPort(
|
|
98
|
+
const mockPort = await findMockServerPort();
|
|
99
99
|
mockGitLab = new MockGitLabServer({
|
|
100
100
|
port: mockPort,
|
|
101
101
|
validTokens: [MOCK_ACCESS_TOKEN],
|
|
@@ -22,7 +22,7 @@ describe('Client Pool Limits', () => {
|
|
|
22
22
|
let mockGitLabUrl;
|
|
23
23
|
before(async () => {
|
|
24
24
|
// Start mock GitLab server
|
|
25
|
-
const mockPort = await findMockServerPort(
|
|
25
|
+
const mockPort = await findMockServerPort();
|
|
26
26
|
mockGitLab = new MockGitLabServer({
|
|
27
27
|
port: mockPort,
|
|
28
28
|
validTokens: [MOCK_TOKEN]
|
|
@@ -30,10 +30,10 @@ describe("Dynamic API URL allowlist", () => {
|
|
|
30
30
|
let secondaryHit = false;
|
|
31
31
|
let getAttackerHits = () => 0;
|
|
32
32
|
before(async () => {
|
|
33
|
-
const primaryPort = await findMockServerPort(
|
|
33
|
+
const primaryPort = await findMockServerPort();
|
|
34
34
|
primaryGitLab = new MockGitLabServer({ port: primaryPort, validTokens: [MOCK_TOKEN] });
|
|
35
35
|
await primaryGitLab.start();
|
|
36
|
-
const secondaryPort = await findMockServerPort(
|
|
36
|
+
const secondaryPort = await findMockServerPort();
|
|
37
37
|
secondaryGitLab = new MockGitLabServer({ port: secondaryPort, validTokens: [MOCK_TOKEN] });
|
|
38
38
|
secondaryGitLab.addMockHandler("get", "/projects/1/issues", (_req, res) => {
|
|
39
39
|
secondaryHit = true;
|