gitlab-mcp 1.0.0 → 1.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 (104) hide show
  1. package/LICENSE +21 -0
  2. package/dist/config/env.d.ts +56 -0
  3. package/dist/config/env.js +163 -0
  4. package/dist/config/env.js.map +1 -0
  5. package/dist/http-app.d.ts +45 -0
  6. package/dist/http-app.js +550 -0
  7. package/dist/http-app.js.map +1 -0
  8. package/dist/http.d.ts +2 -0
  9. package/dist/http.js +65 -0
  10. package/dist/http.js.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +65 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/lib/auth-context.d.ts +9 -0
  15. package/dist/lib/auth-context.js +9 -0
  16. package/dist/lib/auth-context.js.map +1 -0
  17. package/dist/lib/gitlab-client.d.ts +331 -0
  18. package/dist/lib/gitlab-client.js +1025 -0
  19. package/dist/lib/gitlab-client.js.map +1 -0
  20. package/dist/lib/logger.d.ts +2 -0
  21. package/dist/lib/logger.js +13 -0
  22. package/dist/lib/logger.js.map +1 -0
  23. package/dist/lib/network.d.ts +3 -0
  24. package/dist/lib/network.js +38 -0
  25. package/dist/lib/network.js.map +1 -0
  26. package/dist/lib/oauth.d.ts +29 -0
  27. package/dist/lib/oauth.js +220 -0
  28. package/dist/lib/oauth.js.map +1 -0
  29. package/dist/lib/output.d.ts +14 -0
  30. package/dist/lib/output.js +38 -0
  31. package/dist/lib/output.js.map +1 -0
  32. package/dist/lib/policy.d.ts +25 -0
  33. package/dist/lib/policy.js +48 -0
  34. package/dist/lib/policy.js.map +1 -0
  35. package/dist/lib/request-runtime.d.ts +26 -0
  36. package/dist/lib/request-runtime.js +323 -0
  37. package/dist/lib/request-runtime.js.map +1 -0
  38. package/dist/lib/sanitize.d.ts +1 -0
  39. package/dist/lib/sanitize.js +21 -0
  40. package/dist/lib/sanitize.js.map +1 -0
  41. package/dist/lib/session-capacity.d.ts +8 -0
  42. package/dist/lib/session-capacity.js +7 -0
  43. package/dist/lib/session-capacity.js.map +1 -0
  44. package/dist/server/build-server.d.ts +3 -0
  45. package/dist/server/build-server.js +13 -0
  46. package/dist/server/build-server.js.map +1 -0
  47. package/dist/tools/gitlab.d.ts +9 -0
  48. package/dist/tools/gitlab.js +2576 -0
  49. package/dist/tools/gitlab.js.map +1 -0
  50. package/dist/tools/health.d.ts +2 -0
  51. package/dist/tools/health.js +21 -0
  52. package/dist/tools/health.js.map +1 -0
  53. package/dist/tools/mr-code-context.d.ts +38 -0
  54. package/dist/tools/mr-code-context.js +330 -0
  55. package/dist/tools/mr-code-context.js.map +1 -0
  56. package/{src/types/context.ts → dist/types/context.d.ts} +5 -6
  57. package/dist/types/context.js +2 -0
  58. package/dist/types/context.js.map +1 -0
  59. package/docs/configuration.md +6 -6
  60. package/docs/mcp-integration-testing-best-practices.md +981 -0
  61. package/package.json +21 -1
  62. package/.dockerignore +0 -7
  63. package/.editorconfig +0 -9
  64. package/.env.example +0 -75
  65. package/.github/workflows/nodejs.yml +0 -31
  66. package/.github/workflows/npm-publish.yml +0 -31
  67. package/.husky/pre-commit +0 -1
  68. package/.nvmrc +0 -1
  69. package/.prettierrc.json +0 -6
  70. package/Dockerfile +0 -20
  71. package/docker-compose.yml +0 -10
  72. package/eslint.config.js +0 -23
  73. package/scripts/get-oauth-token.example.sh +0 -15
  74. package/src/config/env.ts +0 -171
  75. package/src/http.ts +0 -605
  76. package/src/index.ts +0 -77
  77. package/src/lib/auth-context.ts +0 -19
  78. package/src/lib/gitlab-client.ts +0 -1810
  79. package/src/lib/logger.ts +0 -17
  80. package/src/lib/network.ts +0 -45
  81. package/src/lib/oauth.ts +0 -287
  82. package/src/lib/output.ts +0 -51
  83. package/src/lib/policy.ts +0 -78
  84. package/src/lib/request-runtime.ts +0 -376
  85. package/src/lib/sanitize.ts +0 -25
  86. package/src/server/build-server.ts +0 -17
  87. package/src/tools/gitlab.ts +0 -3128
  88. package/src/tools/health.ts +0 -27
  89. package/src/tools/mr-code-context.ts +0 -473
  90. package/tests/auth-context.test.ts +0 -102
  91. package/tests/gitlab-client.test.ts +0 -674
  92. package/tests/graphql-guard.test.ts +0 -121
  93. package/tests/integration/agent-loop.integration.test.ts +0 -552
  94. package/tests/integration/server.integration.test.ts +0 -543
  95. package/tests/mr-code-context.test.ts +0 -600
  96. package/tests/oauth.test.ts +0 -43
  97. package/tests/output.test.ts +0 -186
  98. package/tests/policy.test.ts +0 -324
  99. package/tests/request-runtime.test.ts +0 -252
  100. package/tests/sanitize.test.ts +0 -123
  101. package/tests/upload-reference.test.ts +0 -84
  102. package/tsconfig.build.json +0 -11
  103. package/tsconfig.json +0 -21
  104. package/vitest.config.ts +0 -12
@@ -1,252 +0,0 @@
1
- /**
2
- * Tests for helper functions exported or used internally by request-runtime.ts.
3
- * Since some functions are private, we test them indirectly or test the module-level
4
- * exported utilities that are accessible.
5
- *
6
- * For deeper testing we extract testable logic patterns.
7
- */
8
- import { describe, expect, it } from "vitest";
9
- import os from "node:os";
10
- import path from "node:path";
11
-
12
- /**
13
- * Replicate the parseTokenOutput logic for testing.
14
- * This matches the private function in request-runtime.ts.
15
- */
16
- function parseTokenOutput(rawOutput: string): string | undefined {
17
- const output = rawOutput.trim();
18
- if (!output) {
19
- return undefined;
20
- }
21
-
22
- try {
23
- const parsed = JSON.parse(output) as Record<string, unknown>;
24
- const token =
25
- getStringField(parsed, "token") ||
26
- getStringField(parsed, "access_token") ||
27
- getStringField(parsed, "private_token");
28
- if (token) {
29
- return token;
30
- }
31
- } catch {
32
- // Plain string output is valid.
33
- }
34
-
35
- return output.split(/\r?\n/, 1)[0]?.trim() || undefined;
36
- }
37
-
38
- function getStringField(record: Record<string, unknown>, key: string): string | undefined {
39
- const value = record[key];
40
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
41
- }
42
-
43
- /**
44
- * Replicate resolveHomePath for testing.
45
- */
46
- function resolveHomePath(input?: string): string | undefined {
47
- if (!input) {
48
- return undefined;
49
- }
50
-
51
- if (input.startsWith("~/")) {
52
- return path.join(os.homedir(), input.slice(2));
53
- }
54
-
55
- return input;
56
- }
57
-
58
- /**
59
- * Replicate normalizeWarmupPath for testing.
60
- */
61
- function normalizeWarmupPath(value: string): string {
62
- const trimmed = value.trim();
63
- if (!trimmed) {
64
- return "/user";
65
- }
66
-
67
- return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
68
- }
69
-
70
- /**
71
- * Replicate resolveApiRoot for testing.
72
- */
73
- function resolveApiRoot(url: URL): string | undefined {
74
- const match = url.pathname.match(/^(.*\/api\/v4)(?:\/|$)/);
75
- return match?.[1];
76
- }
77
-
78
- /**
79
- * Replicate parseOauthScopes for testing.
80
- */
81
- function parseOauthScopes(rawScopes: string): string[] {
82
- return rawScopes
83
- .split(/[,\s]+/)
84
- .map((scope) => scope.trim())
85
- .filter((scope) => scope.length > 0);
86
- }
87
-
88
- describe("parseTokenOutput", () => {
89
- it("returns undefined for empty string", () => {
90
- expect(parseTokenOutput("")).toBeUndefined();
91
- });
92
-
93
- it("returns undefined for whitespace-only string", () => {
94
- expect(parseTokenOutput(" \n ")).toBeUndefined();
95
- });
96
-
97
- it("parses plain text token", () => {
98
- expect(parseTokenOutput("glpat-abc123")).toBe("glpat-abc123");
99
- });
100
-
101
- it("parses plain text with trailing newline", () => {
102
- expect(parseTokenOutput("glpat-abc123\n")).toBe("glpat-abc123");
103
- });
104
-
105
- it("takes first line of multi-line output", () => {
106
- expect(parseTokenOutput("glpat-abc123\nsome debug info\nmore stuff")).toBe("glpat-abc123");
107
- });
108
-
109
- it("parses JSON with token field", () => {
110
- expect(parseTokenOutput('{"token": "glpat-from-json"}')).toBe("glpat-from-json");
111
- });
112
-
113
- it("parses JSON with access_token field", () => {
114
- expect(parseTokenOutput('{"access_token": "oauth-token-123"}')).toBe("oauth-token-123");
115
- });
116
-
117
- it("parses JSON with private_token field", () => {
118
- expect(parseTokenOutput('{"private_token": "private-123"}')).toBe("private-123");
119
- });
120
-
121
- it("prefers token field over access_token", () => {
122
- expect(parseTokenOutput('{"token": "primary", "access_token": "secondary"}')).toBe("primary");
123
- });
124
-
125
- it("ignores JSON with empty token fields", () => {
126
- expect(parseTokenOutput('{"token": "", "other": "value"}')).toBe(
127
- '{"token": "", "other": "value"}'
128
- );
129
- });
130
-
131
- it("ignores JSON with whitespace-only token fields", () => {
132
- expect(parseTokenOutput('{"token": " "}')).toBe('{"token": " "}');
133
- });
134
-
135
- it("trims whitespace from parsed token values", () => {
136
- expect(parseTokenOutput('{"token": " trimmed "}')).toBe("trimmed");
137
- });
138
-
139
- it("handles JSON with no recognized token fields", () => {
140
- const input = '{"unknown_field": "some-value"}';
141
- // Falls back to first line
142
- expect(parseTokenOutput(input)).toBe(input);
143
- });
144
- });
145
-
146
- describe("resolveHomePath", () => {
147
- it("returns undefined for empty input", () => {
148
- expect(resolveHomePath("")).toBeUndefined();
149
- expect(resolveHomePath(undefined)).toBeUndefined();
150
- });
151
-
152
- it("expands ~ to home directory", () => {
153
- const result = resolveHomePath("~/some/path");
154
- expect(result).toBeDefined();
155
- expect(result).not.toContain("~/");
156
- expect(result).toContain("some/path");
157
- });
158
-
159
- it("returns absolute paths unchanged", () => {
160
- expect(resolveHomePath("/absolute/path")).toBe("/absolute/path");
161
- });
162
-
163
- it("returns relative paths unchanged", () => {
164
- expect(resolveHomePath("relative/path")).toBe("relative/path");
165
- });
166
- });
167
-
168
- describe("normalizeWarmupPath", () => {
169
- it("returns /user for empty string", () => {
170
- expect(normalizeWarmupPath("")).toBe("/user");
171
- });
172
-
173
- it("returns /user for whitespace-only string", () => {
174
- expect(normalizeWarmupPath(" ")).toBe("/user");
175
- });
176
-
177
- it("preserves leading slash", () => {
178
- expect(normalizeWarmupPath("/custom")).toBe("/custom");
179
- });
180
-
181
- it("adds leading slash when missing", () => {
182
- expect(normalizeWarmupPath("custom")).toBe("/custom");
183
- });
184
-
185
- it("trims whitespace", () => {
186
- expect(normalizeWarmupPath(" /user ")).toBe("/user");
187
- });
188
- });
189
-
190
- describe("resolveApiRoot", () => {
191
- it("extracts /api/v4 from standard URL", () => {
192
- const url = new URL("https://gitlab.example.com/api/v4/projects/1");
193
- expect(resolveApiRoot(url)).toBe("/api/v4");
194
- });
195
-
196
- it("extracts subpath /api/v4", () => {
197
- const url = new URL("https://example.com/gitlab/api/v4/projects");
198
- expect(resolveApiRoot(url)).toBe("/gitlab/api/v4");
199
- });
200
-
201
- it("returns undefined for non-API URLs", () => {
202
- const url = new URL("https://gitlab.example.com/group/project");
203
- expect(resolveApiRoot(url)).toBeUndefined();
204
- });
205
-
206
- it("matches when path ends with /api/v4", () => {
207
- const url = new URL("https://gitlab.example.com/api/v4/");
208
- expect(resolveApiRoot(url)).toBe("/api/v4");
209
- });
210
- });
211
-
212
- describe("parseOauthScopes", () => {
213
- it("parses space-separated scopes", () => {
214
- expect(parseOauthScopes("api read_user")).toEqual(["api", "read_user"]);
215
- });
216
-
217
- it("parses comma-separated scopes", () => {
218
- expect(parseOauthScopes("api,read_user,write_repository")).toEqual([
219
- "api",
220
- "read_user",
221
- "write_repository"
222
- ]);
223
- });
224
-
225
- it("handles mixed separators", () => {
226
- expect(parseOauthScopes("api, read_user write_repository")).toEqual([
227
- "api",
228
- "read_user",
229
- "write_repository"
230
- ]);
231
- });
232
-
233
- it("filters empty entries", () => {
234
- expect(parseOauthScopes("api,,read_user, ,write_repository")).toEqual([
235
- "api",
236
- "read_user",
237
- "write_repository"
238
- ]);
239
- });
240
-
241
- it("handles single scope", () => {
242
- expect(parseOauthScopes("api")).toEqual(["api"]);
243
- });
244
-
245
- it("handles empty string", () => {
246
- expect(parseOauthScopes("")).toEqual([]);
247
- });
248
-
249
- it("trims whitespace from scopes", () => {
250
- expect(parseOauthScopes(" api , read_user ")).toEqual(["api", "read_user"]);
251
- });
252
- });
@@ -1,123 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { stripNullsDeep } from "../src/lib/sanitize.js";
4
-
5
- describe("stripNullsDeep", () => {
6
- it("removes null values recursively from objects", () => {
7
- const input = {
8
- title: "demo",
9
- description: null,
10
- nested: {
11
- keep: 1,
12
- drop: null,
13
- arr: [1, null, 2, null, 3]
14
- }
15
- };
16
-
17
- expect(stripNullsDeep(input)).toEqual({
18
- title: "demo",
19
- nested: {
20
- keep: 1,
21
- arr: [1, 2, 3]
22
- }
23
- });
24
- });
25
-
26
- it("returns undefined for top-level null", () => {
27
- expect(stripNullsDeep(null)).toBeUndefined();
28
- });
29
-
30
- it("preserves primitive string values", () => {
31
- expect(stripNullsDeep("hello")).toBe("hello");
32
- });
33
-
34
- it("preserves primitive number values", () => {
35
- expect(stripNullsDeep(42)).toBe(42);
36
- expect(stripNullsDeep(0)).toBe(0);
37
- expect(stripNullsDeep(-1)).toBe(-1);
38
- });
39
-
40
- it("preserves boolean values", () => {
41
- expect(stripNullsDeep(true)).toBe(true);
42
- expect(stripNullsDeep(false)).toBe(false);
43
- });
44
-
45
- it("preserves undefined as-is", () => {
46
- expect(stripNullsDeep(undefined)).toBeUndefined();
47
- });
48
-
49
- it("handles empty object", () => {
50
- expect(stripNullsDeep({})).toEqual({});
51
- });
52
-
53
- it("handles empty array", () => {
54
- expect(stripNullsDeep([])).toEqual([]);
55
- });
56
-
57
- it("handles object with all null values", () => {
58
- expect(stripNullsDeep({ a: null, b: null, c: null })).toEqual({});
59
- });
60
-
61
- it("handles array with all null values", () => {
62
- expect(stripNullsDeep([null, null, null])).toEqual([]);
63
- });
64
-
65
- it("handles deeply nested null removal", () => {
66
- const input = {
67
- level1: {
68
- level2: {
69
- level3: {
70
- keep: "value",
71
- drop: null
72
- }
73
- }
74
- }
75
- };
76
-
77
- expect(stripNullsDeep(input)).toEqual({
78
- level1: {
79
- level2: {
80
- level3: {
81
- keep: "value"
82
- }
83
- }
84
- }
85
- });
86
- });
87
-
88
- it("handles mixed array contents", () => {
89
- const input = [1, "hello", null, true, null, { key: null, keep: "yes" }];
90
-
91
- expect(stripNullsDeep(input)).toEqual([1, "hello", true, { keep: "yes" }]);
92
- });
93
-
94
- it("handles nested arrays within objects", () => {
95
- const input = {
96
- items: [{ name: "a", value: null }, null, { name: "b", value: 1 }]
97
- };
98
-
99
- expect(stripNullsDeep(input)).toEqual({
100
- items: [{ name: "a" }, { name: "b", value: 1 }]
101
- });
102
- });
103
-
104
- it("preserves zero, empty string, and false values", () => {
105
- const input = {
106
- zero: 0,
107
- emptyStr: "",
108
- falsy: false,
109
- nullValue: null
110
- };
111
-
112
- expect(stripNullsDeep(input)).toEqual({
113
- zero: 0,
114
- emptyStr: "",
115
- falsy: false
116
- });
117
- });
118
-
119
- it("handles arrays nested within arrays", () => {
120
- const input = [[null, 1], [2, null], null];
121
- expect(stripNullsDeep(input)).toEqual([[1], [2]]);
122
- });
123
- });
@@ -1,84 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { parseProjectUploadReference } from "../src/tools/gitlab.js";
4
-
5
- describe("parseProjectUploadReference", () => {
6
- it("parses absolute upload URL", () => {
7
- expect(
8
- parseProjectUploadReference("https://gitlab.example.com/group/repo/uploads/abc123/file.txt")
9
- ).toEqual({
10
- secret: "abc123",
11
- filename: "file.txt"
12
- });
13
- });
14
-
15
- it("parses relative upload path", () => {
16
- expect(parseProjectUploadReference("/uploads/abc123/file.txt")).toEqual({
17
- secret: "abc123",
18
- filename: "file.txt"
19
- });
20
- });
21
-
22
- it("parses relative upload path with encoded filename", () => {
23
- expect(parseProjectUploadReference("/uploads/abc123/%E6%B5%8B%E8%AF%95.txt")).toEqual({
24
- secret: "abc123",
25
- filename: "测试.txt"
26
- });
27
- });
28
-
29
- it("returns undefined for non-upload paths", () => {
30
- expect(parseProjectUploadReference("/api/v4/projects/1/issues")).toBeUndefined();
31
- });
32
-
33
- it("returns undefined for empty string", () => {
34
- expect(parseProjectUploadReference("")).toBeUndefined();
35
- });
36
-
37
- it("returns undefined for whitespace-only string", () => {
38
- expect(parseProjectUploadReference(" ")).toBeUndefined();
39
- });
40
-
41
- it("handles URL with query parameters", () => {
42
- const result = parseProjectUploadReference(
43
- "https://gitlab.example.com/group/repo/uploads/abc123/file.txt?inline=true"
44
- );
45
- expect(result).toEqual({
46
- secret: "abc123",
47
- filename: "file.txt"
48
- });
49
- });
50
-
51
- it("handles URL with fragment", () => {
52
- const result = parseProjectUploadReference(
53
- "https://gitlab.example.com/group/repo/uploads/abc123/file.txt#section"
54
- );
55
- expect(result).toEqual({
56
- secret: "abc123",
57
- filename: "file.txt"
58
- });
59
- });
60
-
61
- it("handles URL with nested group paths", () => {
62
- const result = parseProjectUploadReference(
63
- "https://gitlab.example.com/group/subgroup/repo/uploads/secret123/document.pdf"
64
- );
65
- expect(result).toEqual({
66
- secret: "secret123",
67
- filename: "document.pdf"
68
- });
69
- });
70
-
71
- it("returns undefined for invalid URL", () => {
72
- expect(parseProjectUploadReference("not a url at all")).toBeUndefined();
73
- });
74
-
75
- it("handles upload path without leading slash", () => {
76
- // This depends on implementation, but should handle gracefully
77
- const result = parseProjectUploadReference("uploads/abc123/file.txt");
78
- // May or may not match depending on implementation
79
- if (result) {
80
- expect(result.secret).toBe("abc123");
81
- expect(result.filename).toBe("file.txt");
82
- }
83
- });
84
- });
@@ -1,11 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "declaration": true,
5
- "outDir": "dist",
6
- "rootDir": "src",
7
- "sourceMap": true,
8
- "types": ["node"]
9
- },
10
- "include": ["src"]
11
- }
package/tsconfig.json DELETED
@@ -1,21 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "lib": ["ES2022", "DOM"],
7
- "strict": true,
8
- "noUncheckedIndexedAccess": true,
9
- "noImplicitOverride": true,
10
- "useUnknownInCatchVariables": true,
11
- "skipLibCheck": true,
12
- "resolveJsonModule": true,
13
- "esModuleInterop": false,
14
- "forceConsistentCasingInFileNames": true,
15
- "isolatedModules": true,
16
- "verbatimModuleSyntax": true,
17
- "types": ["node", "vitest/globals"]
18
- },
19
- "include": ["src", "tests", "eslint.config.js", "vitest.config.ts"],
20
- "exclude": ["dist", "node_modules"]
21
- }
package/vitest.config.ts DELETED
@@ -1,12 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
-
3
- export default defineConfig({
4
- test: {
5
- environment: "node",
6
- include: ["tests/**/*.test.ts"],
7
- coverage: {
8
- reporter: ["text", "html"],
9
- include: ["src/**/*.ts"]
10
- }
11
- }
12
- });