@zereight/mcp-gitlab 2.1.28 → 2.1.30
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 +82 -28
- package/README.md +56 -23
- package/README.zh-CN.md +82 -28
- package/build/config.js +13 -0
- package/build/downloads/proxy.js +199 -0
- package/build/index.js +564 -482
- package/build/oauth-proxy.js +18 -4
- package/build/schemas.js +47 -9
- package/build/scripts/check-skill-sync.js +137 -0
- package/build/scripts/generate-tool-docs.js +7 -2
- 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/mcp-oauth-tests.js +17 -4
- package/build/test/multi-server-test.js +3 -3
- package/build/test/no-proxy-integration-test.js +1 -1
- package/build/test/path-segment-encoding.test.js +27 -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/callback-proxy.test.js +8 -0
- package/build/test/stateless/session-id-integration.test.js +3 -3
- package/build/test/streamable-http-concurrent-session.test.js +1 -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 +7 -7
- 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 +215 -0
- package/build/test/test-protected-branches.js +1 -1
- 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 +1 -1
- 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/redact-sensitive.test.js +32 -0
- package/build/test/utils/server-launcher.js +1 -2
- package/build/test/utils/version-check.test.js +52 -0
- package/build/tools/registry.js +30 -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/redact-sensitive.js +21 -0
- package/build/utils/version-check.js +40 -0
- package/package.json +5 -4
package/build/oauth-proxy.js
CHANGED
|
@@ -67,6 +67,12 @@ const REQUIRED_GITLAB_SCOPES_RO = ["read_api"];
|
|
|
67
67
|
const PENDING_AUTH_MAX_SIZE = 1000;
|
|
68
68
|
const PENDING_AUTH_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
69
69
|
const CLIENT_CACHE_MAX_SIZE = 1000;
|
|
70
|
+
function singleQueryParam(value) {
|
|
71
|
+
return typeof value === "string" ? value : undefined;
|
|
72
|
+
}
|
|
73
|
+
function invalidQueryParam(value) {
|
|
74
|
+
return value != null && typeof value !== "string";
|
|
75
|
+
}
|
|
70
76
|
class BoundedLRUMap {
|
|
71
77
|
_map = new Map();
|
|
72
78
|
_maxSize;
|
|
@@ -503,11 +509,19 @@ class GitLabOAuthServerProvider {
|
|
|
503
509
|
res.status(404).send("Callback proxy mode is not enabled");
|
|
504
510
|
return;
|
|
505
511
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
512
|
+
if (invalidQueryParam(req.query.code) ||
|
|
513
|
+
invalidQueryParam(req.query.state) ||
|
|
514
|
+
invalidQueryParam(req.query.error) ||
|
|
515
|
+
invalidQueryParam(req.query.error_description)) {
|
|
516
|
+
res.status(400).send("Invalid query parameter");
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const code = singleQueryParam(req.query.code);
|
|
520
|
+
const state = singleQueryParam(req.query.state);
|
|
521
|
+
const error = singleQueryParam(req.query.error);
|
|
522
|
+
const errorDescription = singleQueryParam(req.query.error_description) ?? "(no description)";
|
|
509
523
|
if (error) {
|
|
510
|
-
logger.error(`GitLab OAuth error: ${error} — ${
|
|
524
|
+
logger.error(`GitLab OAuth error: ${error} — ${errorDescription}`);
|
|
511
525
|
res.status(400).send("Authorization failed");
|
|
512
526
|
return;
|
|
513
527
|
}
|
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()
|
|
@@ -1704,7 +1704,17 @@ export const GetBranchDiffsSchema = ProjectParamsSchema.extend({
|
|
|
1704
1704
|
.optional()
|
|
1705
1705
|
.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
1706
|
});
|
|
1707
|
-
|
|
1707
|
+
const flexibleBooleanOptional = z.preprocess(val => {
|
|
1708
|
+
if (typeof val !== "string")
|
|
1709
|
+
return val;
|
|
1710
|
+
const normalized = val.trim().toLowerCase();
|
|
1711
|
+
if (normalized === "true")
|
|
1712
|
+
return true;
|
|
1713
|
+
if (normalized === "false")
|
|
1714
|
+
return false;
|
|
1715
|
+
return val;
|
|
1716
|
+
}, z.boolean().optional());
|
|
1717
|
+
const MergeRequestParamsSchema = ProjectParamsSchema.extend({
|
|
1708
1718
|
project_id: z
|
|
1709
1719
|
.preprocess(value => (value === undefined || value === null ? value : String(value)), z
|
|
1710
1720
|
.string({
|
|
@@ -1717,7 +1727,10 @@ export const GetMergeRequestSchema = ProjectParamsSchema.extend({
|
|
|
1717
1727
|
merge_request_iid: z.coerce.string().optional().describe("The IID of a merge request"),
|
|
1718
1728
|
source_branch: z.string().optional().describe("Source branch name"),
|
|
1719
1729
|
});
|
|
1720
|
-
export const
|
|
1730
|
+
export const GetMergeRequestSchema = MergeRequestParamsSchema.extend({
|
|
1731
|
+
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."),
|
|
1732
|
+
});
|
|
1733
|
+
export const UpdateMergeRequestSchema = MergeRequestParamsSchema.extend({
|
|
1721
1734
|
title: z.string().optional().describe("The title of the merge request"),
|
|
1722
1735
|
description: z.string().optional().describe("The description of the merge request"),
|
|
1723
1736
|
target_branch: z.string().optional().describe("The target branch"),
|
|
@@ -1859,14 +1872,14 @@ export const ListMergeRequestPipelinesSchema = ProjectParamsSchema.extend({
|
|
|
1859
1872
|
.transform(value => value.trim()))
|
|
1860
1873
|
.describe("The internal ID of the merge request"),
|
|
1861
1874
|
}).merge(PaginationOptionsSchema);
|
|
1862
|
-
export const GetMergeRequestDiffsSchema =
|
|
1875
|
+
export const GetMergeRequestDiffsSchema = MergeRequestParamsSchema.extend({
|
|
1863
1876
|
view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"),
|
|
1864
1877
|
excluded_file_patterns: z
|
|
1865
1878
|
.array(z.string())
|
|
1866
1879
|
.optional()
|
|
1867
1880
|
.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
1881
|
});
|
|
1869
|
-
export const ListMergeRequestDiffsSchema =
|
|
1882
|
+
export const ListMergeRequestDiffsSchema = MergeRequestParamsSchema.extend({
|
|
1870
1883
|
page: z.coerce.number().optional().describe("Page number for pagination (default: 1)"),
|
|
1871
1884
|
per_page: z.coerce
|
|
1872
1885
|
.number()
|
|
@@ -1877,13 +1890,13 @@ export const ListMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
|
|
|
1877
1890
|
.optional()
|
|
1878
1891
|
.describe("Present diffs in the unified diff format. Default is false. Introduced in GitLab 16.5."),
|
|
1879
1892
|
});
|
|
1880
|
-
export const ListMergeRequestChangedFilesSchema =
|
|
1893
|
+
export const ListMergeRequestChangedFilesSchema = MergeRequestParamsSchema.extend({
|
|
1881
1894
|
excluded_file_patterns: z
|
|
1882
1895
|
.array(z.string())
|
|
1883
1896
|
.optional()
|
|
1884
1897
|
.describe('Array of regex patterns to exclude files. Examples: ["^vendor/", "\\.pb\\.go$"]'),
|
|
1885
1898
|
});
|
|
1886
|
-
export const GetMergeRequestFileDiffSchema =
|
|
1899
|
+
export const GetMergeRequestFileDiffSchema = MergeRequestParamsSchema.extend({
|
|
1887
1900
|
file_paths: z
|
|
1888
1901
|
.array(z.string())
|
|
1889
1902
|
.describe("List of file paths to retrieve diffs for (e.g. ['src/api/users.ts', 'src/repo/user.go']). " +
|
|
@@ -2063,6 +2076,7 @@ export const ListMergeRequestsSchema = z
|
|
|
2063
2076
|
export const GetIssueSchema = z.object({
|
|
2064
2077
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2065
2078
|
issue_iid: z.coerce.string().describe("The internal ID of the project issue"),
|
|
2079
|
+
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
2080
|
});
|
|
2067
2081
|
export const UpdateIssueSchema = z.object({
|
|
2068
2082
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
@@ -2086,6 +2100,7 @@ export const UpdateIssueSchema = z.object({
|
|
|
2086
2100
|
issue_type: z
|
|
2087
2101
|
.preprocess(val => (typeof val === "string" ? val.toLowerCase() : val), z.enum(["issue", "incident", "test_case", "task"]).optional())
|
|
2088
2102
|
.describe("The type of issue. One of issue, incident, test_case or task."),
|
|
2103
|
+
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
2104
|
});
|
|
2090
2105
|
export const DeleteIssueSchema = z.object({
|
|
2091
2106
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
@@ -2321,11 +2336,19 @@ export const ListWikiPagesSchema = z
|
|
|
2321
2336
|
.object({
|
|
2322
2337
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2323
2338
|
with_content: z.coerce.boolean().optional().describe("Include content of the wiki pages"),
|
|
2339
|
+
render_html: z.coerce
|
|
2340
|
+
.boolean()
|
|
2341
|
+
.optional()
|
|
2342
|
+
.describe("Return rendered HTML content and include front_matter (e.g., the custom title)"),
|
|
2324
2343
|
})
|
|
2325
2344
|
.merge(PaginationOptionsSchema);
|
|
2326
2345
|
export const GetWikiPageSchema = z.object({
|
|
2327
2346
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2328
2347
|
slug: z.string().describe("Slug of the wiki page (will be URL-encoded internally)"),
|
|
2348
|
+
render_html: z.coerce
|
|
2349
|
+
.boolean()
|
|
2350
|
+
.optional()
|
|
2351
|
+
.describe("Return rendered HTML content and include front_matter (e.g., the custom title)"),
|
|
2329
2352
|
});
|
|
2330
2353
|
export const CreateWikiPageSchema = z.object({
|
|
2331
2354
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
@@ -2336,7 +2359,10 @@ export const CreateWikiPageSchema = z.object({
|
|
|
2336
2359
|
export const UpdateWikiPageSchema = z.object({
|
|
2337
2360
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2338
2361
|
slug: z.string().describe("Slug of the wiki page (will be URL-encoded internally)"),
|
|
2339
|
-
title: z
|
|
2362
|
+
title: z
|
|
2363
|
+
.string()
|
|
2364
|
+
.optional()
|
|
2365
|
+
.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."),
|
|
2340
2366
|
content: z.string().optional().describe("New content of the wiki page"),
|
|
2341
2367
|
format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
|
|
2342
2368
|
});
|
|
@@ -2350,6 +2376,7 @@ export const GitLabWikiPageSchema = z.object({
|
|
|
2350
2376
|
slug: z.string(),
|
|
2351
2377
|
format: z.string(),
|
|
2352
2378
|
content: z.string().optional(),
|
|
2379
|
+
front_matter: z.record(z.unknown()).optional(),
|
|
2353
2380
|
created_at: z.string().optional(),
|
|
2354
2381
|
updated_at: z.string().optional(),
|
|
2355
2382
|
});
|
|
@@ -2358,11 +2385,19 @@ export const ListGroupWikiPagesSchema = z
|
|
|
2358
2385
|
.object({
|
|
2359
2386
|
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
2360
2387
|
with_content: z.coerce.boolean().optional().describe("Include content of the wiki pages"),
|
|
2388
|
+
render_html: z.coerce
|
|
2389
|
+
.boolean()
|
|
2390
|
+
.optional()
|
|
2391
|
+
.describe("Return rendered HTML content and include front_matter (e.g., the custom title)"),
|
|
2361
2392
|
})
|
|
2362
2393
|
.merge(PaginationOptionsSchema);
|
|
2363
2394
|
export const GetGroupWikiPageSchema = z.object({
|
|
2364
2395
|
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
2365
2396
|
slug: z.string().describe("Slug of the wiki page (will be URL-encoded internally)"),
|
|
2397
|
+
render_html: z.coerce
|
|
2398
|
+
.boolean()
|
|
2399
|
+
.optional()
|
|
2400
|
+
.describe("Return rendered HTML content and include front_matter (e.g., the custom title)"),
|
|
2366
2401
|
});
|
|
2367
2402
|
export const CreateGroupWikiPageSchema = z.object({
|
|
2368
2403
|
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
@@ -2373,7 +2408,10 @@ export const CreateGroupWikiPageSchema = z.object({
|
|
|
2373
2408
|
export const UpdateGroupWikiPageSchema = z.object({
|
|
2374
2409
|
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
2375
2410
|
slug: z.string().describe("Slug of the wiki page (will be URL-encoded internally)"),
|
|
2376
|
-
title: z
|
|
2411
|
+
title: z
|
|
2412
|
+
.string()
|
|
2413
|
+
.optional()
|
|
2414
|
+
.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."),
|
|
2377
2415
|
content: z.string().optional().describe("New content of the wiki page"),
|
|
2378
2416
|
format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
|
|
2379
2417
|
});
|
|
@@ -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");
|
|
@@ -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;
|
|
@@ -25,7 +25,7 @@ describe('Dynamic API URL - Multiple GitLab Instances', () => {
|
|
|
25
25
|
let servers = [];
|
|
26
26
|
before(async () => {
|
|
27
27
|
// Start first mock GitLab server
|
|
28
|
-
const mockPort1 = await findMockServerPort(
|
|
28
|
+
const mockPort1 = await findMockServerPort();
|
|
29
29
|
mockGitLab1 = new MockGitLabServer({
|
|
30
30
|
port: mockPort1,
|
|
31
31
|
validTokens: [MOCK_TOKEN_1]
|
|
@@ -33,7 +33,7 @@ describe('Dynamic API URL - Multiple GitLab Instances', () => {
|
|
|
33
33
|
await mockGitLab1.start();
|
|
34
34
|
mockGitLabUrl1 = mockGitLab1.getUrl();
|
|
35
35
|
// Start second mock GitLab server
|
|
36
|
-
const mockPort2 = await findMockServerPort(
|
|
36
|
+
const mockPort2 = await findMockServerPort();
|
|
37
37
|
mockGitLab2 = new MockGitLabServer({
|
|
38
38
|
port: mockPort2,
|
|
39
39
|
validTokens: [MOCK_TOKEN_2]
|
|
@@ -203,14 +203,14 @@ describe('Dynamic API URL - Connection Pool', () => {
|
|
|
203
203
|
let servers = [];
|
|
204
204
|
before(async () => {
|
|
205
205
|
// Start mock GitLab servers
|
|
206
|
-
const mockPort1 = await findMockServerPort(
|
|
206
|
+
const mockPort1 = await findMockServerPort();
|
|
207
207
|
mockGitLab1 = new MockGitLabServer({
|
|
208
208
|
port: mockPort1,
|
|
209
209
|
validTokens: [MOCK_TOKEN_1]
|
|
210
210
|
});
|
|
211
211
|
await mockGitLab1.start();
|
|
212
212
|
mockGitLabUrl1 = mockGitLab1.getUrl();
|
|
213
|
-
const mockPort2 = await findMockServerPort(
|
|
213
|
+
const mockPort2 = await findMockServerPort();
|
|
214
214
|
mockGitLab2 = new MockGitLabServer({
|
|
215
215
|
port: mockPort2,
|
|
216
216
|
validTokens: [MOCK_TOKEN_2]
|
|
@@ -29,7 +29,7 @@ describe('Dynamic Routing and Authentication Scenarios', () => {
|
|
|
29
29
|
// to avoid launchServer overwriting GITLAB_PERSONAL_ACCESS_TOKEN with a different value
|
|
30
30
|
process.env.GITLAB_TOKEN_TEST = MOCK_TOKEN_DEFAULT;
|
|
31
31
|
process.env.TEST_PROJECT_ID = '1';
|
|
32
|
-
const mockPort = await findMockServerPort(
|
|
32
|
+
const mockPort = await findMockServerPort();
|
|
33
33
|
mockServer = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN_DEFAULT] });
|
|
34
34
|
await mockServer.start();
|
|
35
35
|
const mcpPort = await findAvailablePort(3021);
|
|
@@ -82,7 +82,7 @@ describe('Dynamic Routing and Authentication Scenarios', () => {
|
|
|
82
82
|
let mcpUrl;
|
|
83
83
|
let mockServer;
|
|
84
84
|
before(async () => {
|
|
85
|
-
const mockPort = await findMockServerPort(
|
|
85
|
+
const mockPort = await findMockServerPort();
|
|
86
86
|
mockServer = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN_HEADER] });
|
|
87
87
|
await mockServer.start();
|
|
88
88
|
const mcpPort = await findAvailablePort(3022);
|
|
@@ -146,10 +146,10 @@ describe('Dynamic Routing and Authentication Scenarios', () => {
|
|
|
146
146
|
let defaultMockServer;
|
|
147
147
|
let headerMockServer;
|
|
148
148
|
before(async () => {
|
|
149
|
-
const defaultPort = await findMockServerPort(
|
|
149
|
+
const defaultPort = await findMockServerPort();
|
|
150
150
|
defaultMockServer = new MockGitLabServer({ port: defaultPort, validTokens: [MOCK_TOKEN_DEFAULT, MOCK_TOKEN_HEADER] });
|
|
151
151
|
await defaultMockServer.start();
|
|
152
|
-
const headerPort = await findMockServerPort(
|
|
152
|
+
const headerPort = await findMockServerPort();
|
|
153
153
|
headerMockServer = new MockGitLabServer({ port: headerPort, validTokens: [MOCK_TOKEN_DEFAULT, MOCK_TOKEN_HEADER] });
|
|
154
154
|
await headerMockServer.start();
|
|
155
155
|
const mcpPort = await findAvailablePort(3024);
|
|
@@ -83,7 +83,7 @@ describe("MCP OAuth — Discovery Endpoints", () => {
|
|
|
83
83
|
let mockGitLab;
|
|
84
84
|
let servers = [];
|
|
85
85
|
before(async () => {
|
|
86
|
-
const mockPort = await findMockServerPort(
|
|
86
|
+
const mockPort = await findMockServerPort();
|
|
87
87
|
mockGitLab = new MockGitLabServer({
|
|
88
88
|
port: mockPort,
|
|
89
89
|
validTokens: [MOCK_OAUTH_TOKEN],
|
|
@@ -138,7 +138,7 @@ describe("MCP OAuth — Discovery Endpoints", () => {
|
|
|
138
138
|
console.log(" ✓ Protected resource metadata returned");
|
|
139
139
|
});
|
|
140
140
|
test("path-prefixed MCP_SERVER_URL serves path-aware discovery metadata", async () => {
|
|
141
|
-
const mockPort = await findMockServerPort(
|
|
141
|
+
const mockPort = await findMockServerPort();
|
|
142
142
|
const prefixedMockGitLab = new MockGitLabServer({
|
|
143
143
|
port: mockPort,
|
|
144
144
|
validTokens: [MOCK_OAUTH_TOKEN],
|
|
@@ -196,7 +196,7 @@ describe("MCP OAuth — /mcp Auth Enforcement", () => {
|
|
|
196
196
|
let mockGitLab;
|
|
197
197
|
let servers = [];
|
|
198
198
|
before(async () => {
|
|
199
|
-
const mockPort = await findMockServerPort(
|
|
199
|
+
const mockPort = await findMockServerPort();
|
|
200
200
|
mockGitLab = new MockGitLabServer({
|
|
201
201
|
port: mockPort,
|
|
202
202
|
validTokens: [MOCK_OAUTH_TOKEN],
|
|
@@ -492,7 +492,7 @@ describe("MCP OAuth — Header Auth Fallback", () => {
|
|
|
492
492
|
let mockGitLab;
|
|
493
493
|
let servers = [];
|
|
494
494
|
before(async () => {
|
|
495
|
-
const mockPort = await findMockServerPort(
|
|
495
|
+
const mockPort = await findMockServerPort();
|
|
496
496
|
mockGitLab = new MockGitLabServer({
|
|
497
497
|
port: mockPort,
|
|
498
498
|
// Include both OAuth token and raw tokens so GitLab API calls succeed
|
|
@@ -566,6 +566,19 @@ describe("MCP OAuth — Header Auth Fallback", () => {
|
|
|
566
566
|
assert.notStrictEqual(res.status, 403, "Should not return 403 with valid JOB-TOKEN");
|
|
567
567
|
console.log(` ✓ JOB-TOKEN header accepted (status: ${res.status})`);
|
|
568
568
|
});
|
|
569
|
+
test("POST /mcp with invalid Private-Token is rejected before session creation", async () => {
|
|
570
|
+
const res = await fetch(mcpUrl, {
|
|
571
|
+
method: "POST",
|
|
572
|
+
headers: {
|
|
573
|
+
"Content-Type": "application/json",
|
|
574
|
+
Accept: "application/json, text/event-stream",
|
|
575
|
+
"Private-Token": "glpat-invalid-token-0000",
|
|
576
|
+
},
|
|
577
|
+
body: initBody,
|
|
578
|
+
});
|
|
579
|
+
assert.strictEqual(res.status, 401);
|
|
580
|
+
assert.strictEqual(res.headers.get("mcp-session-id"), null);
|
|
581
|
+
});
|
|
569
582
|
test("POST /mcp with valid OAuth Bearer token still works normally", async () => {
|
|
570
583
|
const res = await fetch(mcpUrl, {
|
|
571
584
|
method: "POST",
|
|
@@ -45,7 +45,7 @@ describe("Single Client Mode (ENABLE_DYNAMIC_API_URL=false)", () => {
|
|
|
45
45
|
let mcpUrl;
|
|
46
46
|
let mockServer1;
|
|
47
47
|
before(async () => {
|
|
48
|
-
const mockPort = await findMockServerPort(
|
|
48
|
+
const mockPort = await findMockServerPort();
|
|
49
49
|
mockServer1 = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN] });
|
|
50
50
|
mockServer1.addMockHandler('get', '/projects/1', (req, res) => { res.json(project1); });
|
|
51
51
|
await mockServer1.start();
|
|
@@ -105,8 +105,8 @@ describe("Dynamic Client Mode (ENABLE_DYNAMIC_API_URL=true)", () => {
|
|
|
105
105
|
let mockServer1;
|
|
106
106
|
let mockServer2;
|
|
107
107
|
before(async () => {
|
|
108
|
-
const port1 = await findMockServerPort(
|
|
109
|
-
const port2 = await findMockServerPort(
|
|
108
|
+
const port1 = await findMockServerPort();
|
|
109
|
+
const port2 = await findMockServerPort();
|
|
110
110
|
mockServer1 = new MockGitLabServer({ port: port1, validTokens: [MOCK_TOKEN] });
|
|
111
111
|
mockServer2 = new MockGitLabServer({ port: port2, validTokens: [MOCK_TOKEN] });
|
|
112
112
|
mockServer1.addMockHandler('get', '/projects/1', (req, res) => { res.json(project1); });
|
|
@@ -22,7 +22,7 @@ describe('NO_PROXY Integration Tests', () => {
|
|
|
22
22
|
let mockGitLabHost;
|
|
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]
|