@zereight/mcp-gitlab 2.1.70 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.ko.md +3 -1
  2. package/README.md +6 -2
  3. package/README.pt-BR.md +1 -1
  4. package/README.zh-CN.md +3 -1
  5. package/build/config.js +12 -4
  6. package/build/deprecated-env.js +47 -0
  7. package/build/index.js +369 -184
  8. package/build/oauth-device-flow.js +80 -0
  9. package/build/oauth.js +135 -18
  10. package/build/schemas.js +1 -1
  11. package/build/server/remote-http-stack.js +39 -0
  12. package/build/tools/discover-activation.js +38 -0
  13. package/build/tools/exposed-tools.js +35 -0
  14. package/build/tools/listed-tools.js +76 -0
  15. package/build/tools/registry.js +226 -57
  16. package/build/tools/tool-profile.js +66 -0
  17. package/build/utils/compact-tool-allowlist.js +14 -0
  18. package/build/utils/logger.js +34 -12
  19. package/package.json +3 -3
  20. package/build/scripts/check-skill-sync.js +0 -143
  21. package/build/scripts/generate-server-card.js +0 -39
  22. package/build/scripts/generate-tool-coverage.js +0 -58
  23. package/build/scripts/generate-tool-docs.js +0 -430
  24. package/build/scripts/tool-coverage/coverage.js +0 -341
  25. package/build/scripts/tool-coverage/coverage.test.js +0 -142
  26. package/build/test/callback-proxy-tests.js +0 -321
  27. package/build/test/cli/args.test.js +0 -175
  28. package/build/test/cli/boolean-flags.test.js +0 -106
  29. package/build/test/cli/compact-mcp-result.test.js +0 -93
  30. package/build/test/cli/credentials.test.js +0 -153
  31. package/build/test/cli/exposure.test.js +0 -104
  32. package/build/test/cli/load-config-snapshot.js +0 -5
  33. package/build/test/cli/output.test.js +0 -74
  34. package/build/test/cli/replay-command.test.js +0 -37
  35. package/build/test/cli/router.test.js +0 -183
  36. package/build/test/client-pool-test.js +0 -109
  37. package/build/test/clients/client.js +0 -34
  38. package/build/test/clients/custom-header-client.js +0 -122
  39. package/build/test/clients/sse-client.js +0 -95
  40. package/build/test/clients/stdio-client.js +0 -113
  41. package/build/test/clients/streamable-http-client.js +0 -95
  42. package/build/test/config-allowed-groups.test.js +0 -97
  43. package/build/test/create-commit-actions.test.js +0 -102
  44. package/build/test/draft-note-schema.test.js +0 -44
  45. package/build/test/dynamic-api-url-allowlist.test.js +0 -121
  46. package/build/test/dynamic-api-url-test.js +0 -304
  47. package/build/test/dynamic-routing-tests.js +0 -495
  48. package/build/test/get-merge-request-discussion-schema.test.js +0 -30
  49. package/build/test/gitlab-api-tool-errors.test.js +0 -112
  50. package/build/test/gitlab-artifact-entry-schema.test.js +0 -44
  51. package/build/test/gitlab-diff-schema.test.js +0 -36
  52. package/build/test/gitlab-pipeline-schedule-schema.test.js +0 -32
  53. package/build/test/gitlab-tag-schema.test.js +0 -51
  54. package/build/test/group-milestone-schema.test.js +0 -45
  55. package/build/test/mcp-oauth-tests.js +0 -905
  56. package/build/test/mcp-server-name.test.js +0 -87
  57. package/build/test/merge-merge-request-schema.test.js +0 -75
  58. package/build/test/multi-server-test.js +0 -190
  59. package/build/test/no-proxy-integration-test.js +0 -183
  60. package/build/test/no-proxy-test.js +0 -149
  61. package/build/test/nullable-gitlab-response-fields.test.js +0 -37
  62. package/build/test/nullish-tool-arguments-schema.test.js +0 -118
  63. package/build/test/oauth-device-flow-tests.js +0 -450
  64. package/build/test/oauth-download-url.test.js +0 -157
  65. package/build/test/oauth-startup.test.js +0 -204
  66. package/build/test/oauth-tests.js +0 -428
  67. package/build/test/path-segment-encoding.test.js +0 -98
  68. package/build/test/remote-auth-simple-test.js +0 -277
  69. package/build/test/remote-auth-tests.js +0 -314
  70. package/build/test/response-masking.test.js +0 -445
  71. package/build/test/schema-tests.js +0 -1679
  72. package/build/test/server/metrics.test.js +0 -47
  73. package/build/test/server/request-helpers.test.js +0 -48
  74. package/build/test/server/version.test.js +0 -13
  75. package/build/test/sse-auth-guard.test.js +0 -96
  76. package/build/test/sse-session-limits.test.js +0 -273
  77. package/build/test/stateless/callback-proxy.test.js +0 -510
  78. package/build/test/stateless/client-id.test.js +0 -177
  79. package/build/test/stateless/codec.test.js +0 -328
  80. package/build/test/stateless/config-ttl.test.js +0 -149
  81. package/build/test/stateless/consumed-proxy-code-cache.test.js +0 -150
  82. package/build/test/stateless/session-id-integration.test.js +0 -675
  83. package/build/test/stateless/session-id.test.js +0 -157
  84. package/build/test/streamable-http-concurrent-session.test.js +0 -171
  85. package/build/test/streamable-http-dns-rebinding.test.js +0 -185
  86. package/build/test/streamable-http-sse-stream.test.js +0 -350
  87. package/build/test/streamable-http-static-token-auth.test.js +0 -157
  88. package/build/test/streamable-http-unauthenticated-discovery.test.js +0 -207
  89. package/build/test/test-all-transport-server.js +0 -255
  90. package/build/test/test-auth-retry.js +0 -195
  91. package/build/test/test-blank-filters.js +0 -156
  92. package/build/test/test-ci-catalog.js +0 -177
  93. package/build/test/test-ci-lint.js +0 -191
  94. package/build/test/test-ci-variables.js +0 -309
  95. package/build/test/test-create-repository.js +0 -120
  96. package/build/test/test-dependency-proxy.js +0 -195
  97. package/build/test/test-deployment-tools.js +0 -463
  98. package/build/test/test-download-attachment.js +0 -190
  99. package/build/test/test-dynamic-project-scope.js +0 -524
  100. package/build/test/test-empty-toolsets.js +0 -535
  101. package/build/test/test-get-file-blame.js +0 -145
  102. package/build/test/test-get-file-contents.js +0 -128
  103. package/build/test/test-geteffectiveprojectid.js +0 -804
  104. package/build/test/test-health-check.js +0 -115
  105. package/build/test/test-issue-description-patch.js +0 -256
  106. package/build/test/test-job-artifacts.js +0 -275
  107. package/build/test/test-json-schema.js +0 -166
  108. package/build/test/test-list-group-members.js +0 -150
  109. package/build/test/test-list-group-merge-requests.js +0 -165
  110. package/build/test/test-list-issues.js +0 -170
  111. package/build/test/test-list-merge-requests.js +0 -201
  112. package/build/test/test-list-milestones.js +0 -145
  113. package/build/test/test-list-project-members.js +0 -132
  114. package/build/test/test-merge-request-approval-state-tools.js +0 -213
  115. package/build/test/test-merge-request-approvals.js +0 -187
  116. package/build/test/test-merge-request-pipelines.js +0 -106
  117. package/build/test/test-mr-diffs-filter.js +0 -132
  118. package/build/test/test-mr-file-diffs.js +0 -251
  119. package/build/test/test-oauth-proxy-rate-limit.js +0 -133
  120. package/build/test/test-orbit.js +0 -155
  121. package/build/test/test-permission-mode.js +0 -424
  122. package/build/test/test-pipeline-schedules.js +0 -484
  123. package/build/test/test-protected-branches.js +0 -174
  124. package/build/test/test-remote-downloads.js +0 -510
  125. package/build/test/test-search-code.js +0 -275
  126. package/build/test/test-snippets.js +0 -735
  127. package/build/test/test-tags.js +0 -220
  128. package/build/test/test-todos.js +0 -195
  129. package/build/test/test-token-optimizations.js +0 -691
  130. package/build/test/test-toolset-filtering.js +0 -499
  131. package/build/test/test-update-issue-slim.js +0 -141
  132. package/build/test/test-update-project.js +0 -112
  133. package/build/test/test-upload-markdown.js +0 -206
  134. package/build/test/test-vulnerabilities.js +0 -399
  135. package/build/test/test-webhooks.js +0 -218
  136. package/build/test/tool-description-quality.test.js +0 -50
  137. package/build/test/utils/bulk-publish-options.test.js +0 -32
  138. package/build/test/utils/download-token.test.js +0 -35
  139. package/build/test/utils/forwarded-public-base-url.test.js +0 -46
  140. package/build/test/utils/gitlab-instance-version.test.js +0 -32
  141. package/build/test/utils/gitlab-version-gate.test.js +0 -74
  142. package/build/test/utils/graphql-query.test.js +0 -168
  143. package/build/test/utils/helpers-path.test.js +0 -104
  144. package/build/test/utils/jmespath-tool-result.test.js +0 -91
  145. package/build/test/utils/masking.test.js +0 -384
  146. package/build/test/utils/merge-request-position.test.js +0 -57
  147. package/build/test/utils/mock-gitlab-server.js +0 -614
  148. package/build/test/utils/proxy-client-ip.test.js +0 -28
  149. package/build/test/utils/redact-sensitive.test.js +0 -32
  150. package/build/test/utils/safe-redirect-fetch.test.js +0 -463
  151. package/build/test/utils/server-launcher.js +0 -231
  152. package/build/test/utils/tool-args.test.js +0 -210
  153. package/build/test/utils/url.test.js +0 -141
  154. package/build/test/utils/version-check.test.js +0 -239
  155. package/build/test/utils/wiki-title.test.js +0 -21
  156. package/build/test-note.js +0 -54
  157. package/build/test-resolve-issue-note.js +0 -127
@@ -1,143 +0,0 @@
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
- "after",
18
- "allow_multiple",
19
- "deleted_file",
20
- "excluded_file_patterns",
21
- "group_id",
22
- "new_file",
23
- "new_path",
24
- "old_path",
25
- "name",
26
- "page",
27
- "per_page",
28
- "pipeline_id",
29
- "job_inputs",
30
- "project_id",
31
- "renamed_file",
32
- ]);
33
- function fail(message) {
34
- console.error(`::error::${message}`);
35
- process.exit(1);
36
- }
37
- function parseToolsetTable(skill) {
38
- const rows = new Map();
39
- const rowRe = /^\| ([a-z_]+) \((\d+) tools?\) \| (yes|no) \|/gm;
40
- let match;
41
- while ((match = rowRe.exec(skill)) !== null) {
42
- rows.set(match[1], {
43
- count: Number(match[2]),
44
- isDefault: match[3] === "yes",
45
- });
46
- }
47
- return rows;
48
- }
49
- function parseIntroCounts(skill) {
50
- const match = skill.match(/providing (\d+) tools: (\d+) tools across (\d+) toolsets/);
51
- if (!match) {
52
- fail("skills/gitlab-mcp/SKILL.md intro line is missing or malformed");
53
- }
54
- return {
55
- totalTools: Number(match[1]),
56
- uniqueInToolsets: Number(match[2]),
57
- toolsetCount: Number(match[3]),
58
- };
59
- }
60
- function parseDestructiveTools(skill) {
61
- const section = skill.match(/## Destructive Tools[^\n]*\n\n(`[^`]+`(?:, `[^`]+`)*)$/m);
62
- if (!section) {
63
- fail("skills/gitlab-mcp/SKILL.md destructive tools section is missing or malformed");
64
- }
65
- return new Set([...section[1].matchAll(/`([a-z_]+)`/g)].map((m) => m[1]));
66
- }
67
- function collectReferenceToolNames() {
68
- const toolNames = new Set();
69
- for (const file of readdirSync(REF_DIR).filter((name) => name.endsWith(".md"))) {
70
- const text = readFileSync(join(REF_DIR, file), "utf8");
71
- for (const match of text.matchAll(/`([a-z][a-z0-9_]*)`/g)) {
72
- const name = match[1];
73
- if (!REFERENCE_FIELD_ALLOWLIST.has(name)) {
74
- toolNames.add(name);
75
- }
76
- }
77
- }
78
- return toolNames;
79
- }
80
- const skill = readFileSync(SKILL_PATH, "utf8");
81
- const errors = [];
82
- const registryCounts = Object.fromEntries(TOOLSET_DEFINITIONS.map((definition) => [definition.id, definition.tools.size]));
83
- const skillRows = parseToolsetTable(skill);
84
- if (skillRows.size !== TOOLSET_DEFINITIONS.length) {
85
- errors.push(`toolset table has ${skillRows.size} rows; registry has ${TOOLSET_DEFINITIONS.length}`);
86
- }
87
- for (const definition of TOOLSET_DEFINITIONS) {
88
- const row = skillRows.get(definition.id);
89
- if (!row) {
90
- errors.push(`missing toolset row for ${definition.id}`);
91
- continue;
92
- }
93
- const actualCount = registryCounts[definition.id];
94
- if (row.count !== actualCount) {
95
- errors.push(`${definition.id}: skill says ${row.count}, registry has ${actualCount}`);
96
- }
97
- const expectedDefault = DEFAULT_TOOLSET_IDS.has(definition.id);
98
- if (row.isDefault !== expectedDefault) {
99
- errors.push(`${definition.id}: skill default=${row.isDefault}, registry default=${expectedDefault}`);
100
- }
101
- }
102
- for (const id of skillRows.keys()) {
103
- if (!registryCounts[id]) {
104
- errors.push(`unknown toolset in skill table: ${id}`);
105
- }
106
- }
107
- const uniqueInToolsets = new Set(TOOLSET_DEFINITIONS.flatMap((definition) => [...definition.tools]))
108
- .size;
109
- const intro = parseIntroCounts(skill);
110
- if (intro.totalTools !== allTools.length) {
111
- errors.push(`intro total tools: skill says ${intro.totalTools}, registry has ${allTools.length}`);
112
- }
113
- if (intro.uniqueInToolsets !== uniqueInToolsets) {
114
- errors.push(`intro unique toolset tools: skill says ${intro.uniqueInToolsets}, registry has ${uniqueInToolsets}`);
115
- }
116
- if (intro.toolsetCount !== TOOLSET_DEFINITIONS.length) {
117
- errors.push(`intro toolset count: skill says ${intro.toolsetCount}, registry has ${TOOLSET_DEFINITIONS.length}`);
118
- }
119
- const skillDestructive = parseDestructiveTools(skill);
120
- for (const tool of skillDestructive) {
121
- if (!destructiveTools.has(tool)) {
122
- errors.push(`destructive list includes non-destructive tool: ${tool}`);
123
- }
124
- }
125
- for (const tool of destructiveTools) {
126
- if (!skillDestructive.has(tool)) {
127
- errors.push(`destructive list missing registry tool: ${tool}`);
128
- }
129
- }
130
- const registryToolNames = new Set(allTools.map((tool) => tool.name));
131
- for (const tool of collectReferenceToolNames()) {
132
- if (!registryToolNames.has(tool)) {
133
- errors.push(`reference mentions unknown tool: ${tool}`);
134
- }
135
- }
136
- if (errors.length > 0) {
137
- console.error("skills/gitlab-mcp/ is out of sync with tools/registry.ts:");
138
- for (const error of errors) {
139
- console.error(`- ${error}`);
140
- }
141
- process.exit(1);
142
- }
143
- console.log("skills/gitlab-mcp/ is in sync with tools/registry.ts");
@@ -1,39 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { allTools } from "../tools/registry.js";
4
- async function main() {
5
- const pkgPath = path.resolve(process.cwd(), "package.json");
6
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
7
- const serverCard = {
8
- serverInfo: {
9
- name: pkg.mcpName || pkg.name,
10
- version: pkg.version,
11
- },
12
- authentication: {
13
- required: true,
14
- schemes: ["bearer", "oauth2"],
15
- },
16
- tools: allTools.map(t => {
17
- const tool = { ...t };
18
- if (tool.inputSchema &&
19
- typeof tool.inputSchema === "object" &&
20
- "$schema" in tool.inputSchema) {
21
- const schema = { ...tool.inputSchema };
22
- delete schema["$schema"];
23
- tool.inputSchema = schema;
24
- }
25
- return tool;
26
- }),
27
- resources: [],
28
- prompts: [],
29
- };
30
- const outDir = path.resolve(process.cwd(), ".well-known", "mcp");
31
- fs.mkdirSync(outDir, { recursive: true });
32
- const outFile = path.join(outDir, "server-card.json");
33
- fs.writeFileSync(outFile, JSON.stringify(serverCard, null, 2), "utf8");
34
- console.log(`Successfully generated ${outFile} (${allTools.length} tools)`);
35
- }
36
- main().catch(err => {
37
- console.error("Failed to generate server card:", err);
38
- process.exit(1);
39
- });
@@ -1,58 +0,0 @@
1
- #!/usr/bin/env tsx
2
- /**
3
- * Generate docs/reference/tool-coverage.md from tools/registry.ts + test/ MCP calls.
4
- *
5
- * Detection rules live in ./tool-coverage/coverage.ts (AST-scoped): a tool is
6
- * invoked when test source sends that name as a `callTool*` argument, as the
7
- * `name` of a first-argument params object (`client.callTool({ name })`), or as
8
- * the `name` of a `params` object inside a `tools/call` payload literal.
9
- *
10
- * Name-only mentions (toolset tables, description-quality) do not count.
11
- * callTool(variable) loops are missed unless the name is also a literal in that call.
12
- *
13
- * Run with: npm run docs:tools
14
- */
15
- import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
16
- import { dirname, join, relative } from "node:path";
17
- import { fileURLToPath } from "node:url";
18
- import { allTools, TOOLSET_DEFINITIONS } from "../tools/registry.js";
19
- import { assertDetectorHealthy, buildCoverageMarkdown, collectInvocations, countInvoked, groupToolsByToolset, percent, SKIP_FILES, } from "./tool-coverage/coverage.js";
20
- const __dirname = dirname(fileURLToPath(import.meta.url));
21
- const REPO_ROOT = join(__dirname, "..");
22
- const TEST_DIR = join(REPO_ROOT, "test");
23
- const OUT_PATH = join(REPO_ROOT, "docs", "reference", "tool-coverage.md");
24
- function walkTestFiles(dir) {
25
- const out = [];
26
- for (const name of readdirSync(dir)) {
27
- const full = join(dir, name);
28
- const stat = statSync(full);
29
- if (stat.isDirectory()) {
30
- out.push(...walkTestFiles(full));
31
- continue;
32
- }
33
- if (/\.(ts|js)$/.test(name) && !SKIP_FILES.has(name)) {
34
- out.push(full);
35
- }
36
- }
37
- return out;
38
- }
39
- function repoPath(abs) {
40
- return relative(REPO_ROOT, abs).split("\\").join("/");
41
- }
42
- function readScannedFiles() {
43
- return walkTestFiles(TEST_DIR).map(abs => ({
44
- path: repoPath(abs),
45
- text: readFileSync(abs, "utf8"),
46
- }));
47
- }
48
- function main() {
49
- const toolNames = allTools.map(tool => tool.name);
50
- const toolNameSet = new Set(toolNames);
51
- const hits = collectInvocations(readScannedFiles(), toolNameSet);
52
- const invokedCount = countInvoked(toolNames, hits);
53
- assertDetectorHealthy(hits, invokedCount);
54
- const grouped = groupToolsByToolset(toolNames, TOOLSET_DEFINITIONS);
55
- writeFileSync(OUT_PATH, buildCoverageMarkdown(toolNames, hits, grouped));
56
- console.log(`generated docs/reference/tool-coverage.md — ${invokedCount}/${toolNames.length} (${percent(invokedCount, toolNames.length)})`);
57
- }
58
- main();
@@ -1,430 +0,0 @@
1
- #!/usr/bin/env tsx
2
- /**
3
- * Generate per-group tool reference pages under docs/tools/ from the
4
- * authoritative tool registry (tools/registry.ts).
5
- *
6
- * Run with: npx tsx scripts/generate-tool-docs.ts
7
- * Or via: make tools-docs
8
- */
9
- import { writeFileSync, mkdirSync, readdirSync, unlinkSync, statSync } from "node:fs";
10
- import { join, dirname } from "node:path";
11
- import { fileURLToPath } from "node:url";
12
- import { allTools, readOnlyTools, TOOLSET_DEFINITIONS } from "../tools/registry.js";
13
- const __dirname = dirname(fileURLToPath(import.meta.url));
14
- const REPO_ROOT = join(__dirname, "..");
15
- const OUT_DIR = join(REPO_ROOT, "docs", "tools");
16
- // Legacy single-group env flags that pre-date GITLAB_TOOLSETS.
17
- // Only three groups have these for backward compatibility; everything else
18
- // opt-in is configured via GITLAB_TOOLSETS / GITLAB_TOOLS / discover_tools.
19
- const LEGACY_TOGGLE_ENV = {
20
- pipelines: "USE_PIPELINE",
21
- milestones: "USE_MILESTONE",
22
- wiki: "USE_GITLAB_WIKI",
23
- };
24
- function isDefaultToolset(id) {
25
- return TOOLSET_DEFINITIONS.find(d => d.id === id)?.isDefault ?? false;
26
- }
27
- /** Returns the availability note for a toolset group, or undefined when it is on by default. */
28
- function computeToggleNote(id) {
29
- if (isDefaultToolset(id))
30
- return undefined;
31
- // Synthetic group for tools not in any TOOLSET_DEFINITIONS entry.
32
- // discover_tools is always exposed; execute_graphql is opt-in via GITLAB_TOOLS.
33
- if (id === "meta") {
34
- return "Mixed availability. `discover_tools` is always exposed (the server re-adds it after every toolset filter). `execute_graphql` is not part of any toolset — enable it explicitly with `GITLAB_TOOLS=execute_graphql`. Tools excluded from the list (toolset / `GITLAB_TOOLS` / `GITLAB_DENIED_TOOLS_REGEX`) are also rejected on direct calls; `GITLAB_TOOL_POLICY_HIDDEN` tools stay callable unless they match `GITLAB_DENIED_TOOLS_REGEX`.";
35
- }
36
- const legacy = LEGACY_TOGGLE_ENV[id];
37
- if (legacy) {
38
- return `Opt-in. Enable via \`GITLAB_TOOLSETS=${id}\` (or \`GITLAB_TOOLSETS=all\`), or use the legacy \`${legacy}=true\` flag for backward compatibility.`;
39
- }
40
- return `Opt-in. Enable via \`GITLAB_TOOLSETS=${id}\` (or \`GITLAB_TOOLSETS=all\`), list individual tools in \`GITLAB_TOOLS=\`, or activate at runtime with the \`discover_tools\` MCP tool.`;
41
- }
42
- const GROUP_META = {
43
- merge_requests: {
44
- title: "Merge Requests",
45
- blurb: "MR lifecycle — create, update, merge, approve, plus diff/conflict inspection and the full discussion/note/draft API.",
46
- },
47
- issues: {
48
- title: "Issues",
49
- blurb: "Issue CRUD, links, discussions and notes, todos, and emoji reactions.",
50
- },
51
- repositories: {
52
- title: "Projects & Files",
53
- blurb: "Project search/creation/fork plus the Files API for reading and writing repository content without shelling out to git.",
54
- },
55
- branches: {
56
- title: "Branches & Commits",
57
- blurb: "Branch management, commit listing/inspection, file blame, and CI commit-status manipulation.",
58
- },
59
- projects: {
60
- title: "Projects & Namespaces",
61
- blurb: "Project/namespace listing, member queries, group iterations, and server health.",
62
- },
63
- labels: {
64
- title: "Labels",
65
- blurb: "Project label CRUD.",
66
- },
67
- ci: {
68
- title: "CI Lint",
69
- blurb: "Validate `.gitlab-ci.yml` snippets and project pipeline configs.",
70
- },
71
- groups: {
72
- title: "Groups",
73
- blurb: "Create new groups and subgroups.",
74
- },
75
- pipelines: {
76
- title: "Pipelines, Jobs & Deployments",
77
- blurb: "Pipeline + job control (trigger, retry, cancel, play manual jobs, fetch logs/artifacts), and the deployments/environments view.",
78
- },
79
- milestones: {
80
- title: "Milestones",
81
- blurb: "Project and group milestone CRUD plus associated issues/MRs and burndown events.",
82
- },
83
- wiki: {
84
- title: "Wiki",
85
- blurb: "Project and group wiki page CRUD. Attachment uploads where supported.",
86
- },
87
- releases: {
88
- title: "Releases",
89
- blurb: "Release lifecycle, release evidence, and asset download.",
90
- },
91
- tags: {
92
- title: "Tags",
93
- blurb: "Tag listing, creation, deletion, and signature inspection.",
94
- },
95
- users: {
96
- title: "Users & Events",
97
- blurb: "User lookup, the authenticated user (`whoami`), event streams, and markdown attachment upload/download.",
98
- },
99
- workitems: {
100
- title: "Work Items",
101
- blurb: "Modern unified API for issues, tasks, incidents, and other typed work items — including notes, emoji reactions, and incident timeline events.",
102
- },
103
- webhooks: {
104
- title: "Webhooks",
105
- blurb: "Create, update, and delete project or group webhooks, and inspect recent webhook events.",
106
- },
107
- search: {
108
- title: "Search",
109
- blurb: "Code search across all visible projects, a specific project, or a specific group.",
110
- },
111
- variables: {
112
- title: "Variables",
113
- blurb: "Project and group CI/CD variable CRUD.",
114
- },
115
- dependency_proxy: {
116
- title: "Dependency Proxy",
117
- blurb: "Inspect and manage the GitLab dependency proxy cache settings, blob storage, and purge operations.",
118
- },
119
- vulnerabilities: {
120
- title: "Vulnerabilities",
121
- blurb: "AI-assisted vulnerability triage — list findings, inspect details, dismiss with reason, or confirm for remediation. Backed by the GitLab GraphQL API; requires GitLab Ultimate.",
122
- },
123
- orbit: {
124
- title: "GitLab Orbit",
125
- blurb: "Query the Orbit SDLC knowledge graph (Beta; Premium/Ultimate). Graph queries consume GitLab credits; schema, status, and tool listing are free.",
126
- },
127
- snippets: {
128
- title: "Snippets",
129
- blurb: "Snippet CRUD — list, get (with optional file content), create, update, and delete personal or project snippets.",
130
- },
131
- };
132
- const GROUP_ORDER = [
133
- "projects",
134
- "repositories",
135
- "branches",
136
- "groups",
137
- "merge_requests",
138
- "issues",
139
- "labels",
140
- "workitems",
141
- "ci",
142
- "pipelines",
143
- "milestones",
144
- "wiki",
145
- "releases",
146
- "tags",
147
- "users",
148
- "variables",
149
- "webhooks",
150
- "search",
151
- "dependency_proxy",
152
- "vulnerabilities",
153
- "orbit",
154
- "snippets",
155
- ];
156
- // Authoritative classification — uses the `readOnlyTools` set from
157
- // tools/registry.ts. That set is what the server itself consults to decide
158
- // which tools survive `GITLAB_READ_ONLY_MODE=true`, so the badges here
159
- // match real runtime behavior exactly (no prefix heuristics).
160
- function classify(name) {
161
- return readOnlyTools.has(name) ? "read" : "write";
162
- }
163
- function rwBadge(name) {
164
- return classify(name) === "read" ? "📖 Read-only" : "✏️ Writes";
165
- }
166
- function describeType(prop) {
167
- if (prop.enum) {
168
- return `enum (\`${prop.enum.map(String).join("` \\| `")}\`)`;
169
- }
170
- if (prop.anyOf || prop.oneOf) {
171
- const variants = (prop.anyOf || prop.oneOf || []).map(describeType).filter(Boolean);
172
- return variants.join(" \\| ") || "any";
173
- }
174
- if (Array.isArray(prop.type)) {
175
- return prop.type.join(" \\| ");
176
- }
177
- if (prop.type === "array") {
178
- return `array<${prop.items ? describeType(prop.items) : "any"}>`;
179
- }
180
- if (prop.format && prop.type) {
181
- return `${prop.type} (${prop.format})`;
182
- }
183
- return prop.type || "any";
184
- }
185
- function escapePipe(text) {
186
- return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
187
- }
188
- function paramTable(schema) {
189
- if (!schema || !schema.properties || Object.keys(schema.properties).length === 0) {
190
- return "_No parameters._";
191
- }
192
- const required = new Set(schema.required || []);
193
- const rows = ["| Parameter | Type | Required | Description |", "|---|---|:-:|---|"];
194
- for (const [name, prop] of Object.entries(schema.properties)) {
195
- const type = describeType(prop);
196
- const req = required.has(name) ? "✓" : "";
197
- const desc = escapePipe(prop.description || "");
198
- rows.push(`| \`${name}\` | ${type} | ${req} | ${desc} |`);
199
- }
200
- return rows.join("\n");
201
- }
202
- function toolSection(name, description, schema) {
203
- return [
204
- `### \`${name}\``,
205
- "",
206
- `*${rwBadge(name)}*`,
207
- "",
208
- description,
209
- "",
210
- "**Parameters**",
211
- "",
212
- paramTable(schema),
213
- "",
214
- ].join("\n");
215
- }
216
- function buildGroupPage(id, toolNames) {
217
- const meta = GROUP_META[id];
218
- const lines = [`# ${meta.title}`, "", meta.blurb, ""];
219
- const toggle = computeToggleNote(id);
220
- if (toggle) {
221
- lines.push(`!!! note "Feature toggle"`);
222
- lines.push(` ${toggle}`);
223
- lines.push("");
224
- }
225
- // Quick index
226
- lines.push("## Tools in this group");
227
- lines.push("");
228
- for (const name of toolNames) {
229
- lines.push(`- [\`${name}\`](#${name}) — ${rwBadge(name)}`);
230
- }
231
- lines.push("");
232
- lines.push("---");
233
- lines.push("");
234
- for (const name of toolNames) {
235
- const tool = allTools.find(t => t.name === name);
236
- if (!tool) {
237
- throw new Error(`Tool '${name}' referenced in toolset '${id}' but missing from allTools registry`);
238
- }
239
- lines.push(toolSection(name, tool.description, tool.inputSchema));
240
- }
241
- return lines.join("\n");
242
- }
243
- function buildToggleSection(groupedToolsList) {
244
- const grouped = groupedToolsList.filter(([id]) => GROUP_META[id]);
245
- const defaults = grouped.filter(([id]) => isDefaultToolset(id));
246
- const optins = grouped.filter(([id]) => !isDefaultToolset(id));
247
- const formatList = (items) => items
248
- .map(([id]) => {
249
- const slug = id.replace(/_/g, "-");
250
- const legacy = LEGACY_TOGGLE_ENV[id];
251
- const suffix = legacy ? ` (also \`${legacy}=true\`)` : "";
252
- return `[${GROUP_META[id].title}](${slug}.md)${suffix}`;
253
- })
254
- .join(", ");
255
- return [
256
- "| Status | Groups |",
257
- "|---|---|",
258
- `| **Default** — always exposed | ${formatList(defaults)} |`,
259
- `| **Opt-in** — must be enabled | ${formatList(optins)} |`,
260
- "",
261
- "**How to enable opt-in groups** (any one is sufficient):",
262
- "",
263
- "- `GITLAB_TOOLSETS=<group,…>` — comma-separated toolset IDs.",
264
- "- `GITLAB_TOOLSETS=all` — enables every group.",
265
- "- `GITLAB_TOOLS=<tool,…>` — enables individual tools regardless of group.",
266
- "- `USE_PIPELINE=true` / `USE_MILESTONE=true` / `USE_GITLAB_WIKI=true` —" +
267
- " legacy single-group flags (Pipelines, Milestones, Wiki only).",
268
- "- Call the `discover_tools` MCP tool at runtime to activate categories" +
269
- " for the current session.",
270
- ];
271
- }
272
- function buildIndexPage(groupedToolsList) {
273
- const lines = [
274
- "# Tools Reference",
275
- "",
276
- "Complete catalog of every tool the GitLab MCP server exposes.",
277
- "",
278
- "Which of these have an MCP `tools/call` test is tracked in",
279
- "[Tool invocation coverage](../reference/tool-coverage.md).",
280
- "",
281
- "> **Setup first** — if you haven't connected your Personal Access Token or",
282
- "> OAuth credentials yet, follow one of the [client setup guides](../clients/claude-code.md)",
283
- "> or read [Getting Started](../getting-started/index.md). Tools listed below",
284
- "> will be unavailable until the server is authenticated.",
285
- "",
286
- "## Feature toggles",
287
- "",
288
- "Toolsets are split into a **default** set (exposed automatically) and an",
289
- "**opt-in** set (must be explicitly enabled). The lists below are derived",
290
- "directly from `TOOLSET_DEFINITIONS` in",
291
- "[`tools/registry.ts`](https://github.com/zereight/gitlab-mcp/blob/main/tools/registry.ts).",
292
- "",
293
- ...buildToggleSection(groupedToolsList),
294
- "",
295
- "Permission modes control which tools are exposed:",
296
- "",
297
- "- `GITLAB_PERMISSION_MODE=readonly` — hides every write tool regardless of toggles.",
298
- "- `GITLAB_PERMISSION_MODE=modify` — allows create/update but blocks delete and teardown tools: every `delete_*` tool, `erase_pipeline_job`, `purge_dependency_proxy_cache`, the destructive teardown verbs `cancel_pipeline`, `cancel_pipeline_job`, `stop_environment`, `stop_stale_environments`, `unprotect_branch`, plus `push_files` `delete`/`move` actions.",
299
- "- `GITLAB_READ_ONLY_MODE=true` (deprecated) — same as `readonly`; prefer `GITLAB_PERMISSION_MODE=readonly`.",
300
- "",
301
- "The `modify` guard applies to typed tools (`tools/list` and `tools/call`) and to destructive mutations sent through `execute_graphql`: any top-level mutation field whose name contains a deletion verb (`delete`, `destroy`, `remove`, `prune`, `purge`, `erase`) or a teardown verb (`revoke`, `cancel`, `stop`, `terminate`, `unprotect`, `disable`, `deactivate`, `drop`, `unschedule`). See [Environment Variables](../configuration/environment-variables.md#gitlab_permission_mode).",
302
- "",
303
- "See [Environment Variables](../configuration/environment-variables.md)",
304
- "and [CLI Arguments](../getting-started/cli-arguments.md) for the full list.",
305
- "",
306
- "## Legend",
307
- "",
308
- "| Marker | Meaning |",
309
- "|---|---|",
310
- "| 📖 | **Read-only** — fetches data, does not modify GitLab state. Safe to invoke freely. |",
311
- "| ✏️ | **Writes** — creates, updates, or deletes data on GitLab. Confirm intent before running. |",
312
- "",
313
- "## Browse by group",
314
- "",
315
- "Each group has its own page with full parameter tables — click any tool name to jump to its details, or click the group title for the per-group view.",
316
- "",
317
- ];
318
- for (const [id, tools] of groupedToolsList) {
319
- const meta = GROUP_META[id];
320
- const slug = id.replace(/_/g, "-");
321
- lines.push(`### [${meta.title}](${slug}.md)`);
322
- lines.push("");
323
- lines.push(`${meta.blurb} *(${tools.length} tools)*`);
324
- lines.push("");
325
- const toggle = computeToggleNote(id);
326
- if (toggle) {
327
- lines.push(`> ${toggle}`);
328
- lines.push("");
329
- }
330
- lines.push("| Tool | What it does | R/W |");
331
- lines.push("|---|---|:-:|");
332
- for (const name of tools) {
333
- const tool = allTools.find(t => t.name === name);
334
- if (!tool)
335
- continue;
336
- const desc = escapePipe(tool.description);
337
- const marker = classify(name) === "read" ? "📖" : "✏️";
338
- lines.push(`| [\`${name}\`](${slug}.md#${name}) | ${desc} | ${marker} |`);
339
- }
340
- lines.push("");
341
- }
342
- lines.push("---");
343
- lines.push("");
344
- lines.push("## Argument schemas");
345
- lines.push("");
346
- lines.push("Each group page includes a parameter table per tool, generated from");
347
- lines.push("the authoritative Zod schemas in");
348
- lines.push("[`schemas.ts`](https://github.com/zereight/gitlab-mcp/blob/main/schemas.ts).");
349
- lines.push("For runtime schema inspection from a connected MCP client, call the");
350
- lines.push("`discover_tools` tool.");
351
- return lines.join("\n");
352
- }
353
- // --- Main -----------------------------------------------------------------
354
- function main() {
355
- mkdirSync(OUT_DIR, { recursive: true });
356
- // Clean previously generated per-group files (keep index.md until last)
357
- for (const file of readdirSync(OUT_DIR)) {
358
- const full = join(OUT_DIR, file);
359
- if (statSync(full).isFile() && file.endsWith(".md")) {
360
- unlinkSync(full);
361
- }
362
- }
363
- const grouped = [];
364
- const allCategorizedNames = new Set();
365
- for (const id of GROUP_ORDER) {
366
- const def = TOOLSET_DEFINITIONS.find(d => d.id === id);
367
- if (!def) {
368
- throw new Error(`Toolset '${id}' listed in GROUP_ORDER but missing from TOOLSET_DEFINITIONS — keep them in sync`);
369
- }
370
- // Preserve insertion order from TOOLSET_DEFINITIONS
371
- const tools = [...def.tools];
372
- for (const t of tools)
373
- allCategorizedNames.add(t);
374
- grouped.push([id, tools]);
375
- const page = buildGroupPage(id, tools);
376
- const slug = id.replace(/_/g, "-");
377
- writeFileSync(join(OUT_DIR, `${slug}.md`), page);
378
- console.log(`generated docs/tools/${slug}.md — ${tools.length} tools`);
379
- }
380
- // Tools NOT in any toolset (e.g., execute_graphql, discover_tools)
381
- const uncategorized = allTools.map(t => t.name).filter(n => !allCategorizedNames.has(n));
382
- if (uncategorized.length > 0) {
383
- const metaToggle = computeToggleNote("meta");
384
- const lines = [
385
- `# Meta & GraphQL`,
386
- "",
387
- "Tools the MCP exposes that aren't tied to a specific GitLab feature group — server diagnostics and the GraphQL escape hatch.",
388
- "",
389
- ];
390
- if (metaToggle) {
391
- lines.push(`!!! note "Feature toggle"`);
392
- lines.push(` ${metaToggle}`);
393
- lines.push("");
394
- }
395
- lines.push("## Tools in this group");
396
- lines.push("");
397
- for (const name of uncategorized) {
398
- lines.push(`- [\`${name}\`](#${name}) — ${rwBadge(name)}`);
399
- }
400
- lines.push("");
401
- lines.push("---");
402
- lines.push("");
403
- for (const name of uncategorized) {
404
- const tool = allTools.find(t => t.name === name);
405
- if (!tool) {
406
- throw new Error(`Uncategorized tool '${name}' missing from allTools registry`);
407
- }
408
- lines.push(toolSection(name, tool.description, tool.inputSchema));
409
- }
410
- writeFileSync(join(OUT_DIR, "meta.md"), lines.join("\n"));
411
- console.log(`generated docs/tools/meta.md — ${uncategorized.length} tools`);
412
- grouped.push(["meta", uncategorized]);
413
- GROUP_META["meta"] = {
414
- title: "Meta & GraphQL",
415
- blurb: "Server diagnostics, tool discovery, and the GraphQL escape hatch.",
416
- };
417
- }
418
- writeFileSync(join(OUT_DIR, "index.md"), buildIndexPage(grouped));
419
- console.log(`generated docs/tools/index.md`);
420
- // Emit nav fragment so you can paste it into mkdocs.yml if needed
421
- console.log("\n--- mkdocs.yml nav fragment ---");
422
- console.log(" - Tools:");
423
- console.log(" - Overview: tools/index.md");
424
- for (const [id] of grouped) {
425
- const meta = GROUP_META[id];
426
- const slug = id.replace(/_/g, "-");
427
- console.log(` - ${meta.title}: tools/${slug}.md`);
428
- }
429
- }
430
- main();