gitlab-mcp 1.1.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 (106) 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 +13 -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 -620
  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/lib/session-capacity.ts +0 -14
  87. package/src/server/build-server.ts +0 -17
  88. package/src/tools/gitlab.ts +0 -3135
  89. package/src/tools/health.ts +0 -27
  90. package/src/tools/mr-code-context.ts +0 -473
  91. package/tests/auth-context.test.ts +0 -102
  92. package/tests/gitlab-client.test.ts +0 -672
  93. package/tests/graphql-guard.test.ts +0 -121
  94. package/tests/integration/agent-loop.integration.test.ts +0 -558
  95. package/tests/integration/server.integration.test.ts +0 -543
  96. package/tests/mr-code-context.test.ts +0 -600
  97. package/tests/oauth.test.ts +0 -43
  98. package/tests/output.test.ts +0 -186
  99. package/tests/policy.test.ts +0 -324
  100. package/tests/request-runtime.test.ts +0 -252
  101. package/tests/sanitize.test.ts +0 -123
  102. package/tests/session-capacity.test.ts +0 -49
  103. package/tests/upload-reference.test.ts +0 -88
  104. package/tsconfig.build.json +0 -11
  105. package/tsconfig.json +0 -21
  106. package/vitest.config.ts +0 -12
@@ -1,186 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { OutputFormatter } from "../src/lib/output.js";
4
-
5
- describe("OutputFormatter", () => {
6
- describe("json mode", () => {
7
- it("formats objects with 2-space indentation", () => {
8
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
9
- const result = formatter.format({ hello: "world", count: 42 });
10
-
11
- expect(result.text).toBe(JSON.stringify({ hello: "world", count: 42 }, null, 2));
12
- expect(result.truncated).toBe(false);
13
- expect(result.bytes).toBeGreaterThan(0);
14
- });
15
-
16
- it("formats arrays", () => {
17
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
18
- const result = formatter.format([1, 2, 3]);
19
-
20
- expect(result.text).toBe(JSON.stringify([1, 2, 3], null, 2));
21
- expect(result.truncated).toBe(false);
22
- });
23
-
24
- it("formats null", () => {
25
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
26
- const result = formatter.format(null);
27
-
28
- expect(result.text).toBe("null");
29
- expect(result.truncated).toBe(false);
30
- });
31
-
32
- it("formats primitive strings", () => {
33
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
34
- const result = formatter.format("hello");
35
-
36
- expect(result.text).toBe('"hello"');
37
- expect(result.truncated).toBe(false);
38
- });
39
-
40
- it("formats numbers", () => {
41
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
42
- const result = formatter.format(42);
43
-
44
- expect(result.text).toBe("42");
45
- expect(result.truncated).toBe(false);
46
- });
47
-
48
- it("formats booleans", () => {
49
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
50
- const result = formatter.format(true);
51
-
52
- expect(result.text).toBe("true");
53
- });
54
- });
55
-
56
- describe("compact-json mode", () => {
57
- it("formats without whitespace", () => {
58
- const formatter = new OutputFormatter({ responseMode: "compact-json", maxBytes: 10_000 });
59
- const result = formatter.format({ hello: "world", nested: { a: 1 } });
60
-
61
- expect(result.text).toBe('{"hello":"world","nested":{"a":1}}');
62
- expect(result.truncated).toBe(false);
63
- });
64
-
65
- it("formats arrays compactly", () => {
66
- const formatter = new OutputFormatter({ responseMode: "compact-json", maxBytes: 10_000 });
67
- const result = formatter.format([1, 2, 3]);
68
-
69
- expect(result.text).toBe("[1,2,3]");
70
- });
71
- });
72
-
73
- describe("yaml mode", () => {
74
- it("formats objects as YAML", () => {
75
- const formatter = new OutputFormatter({ responseMode: "yaml", maxBytes: 10_000 });
76
- const result = formatter.format({ name: "test", value: 123 });
77
-
78
- expect(result.text).toContain("name: test");
79
- expect(result.text).toContain("value: 123");
80
- expect(result.truncated).toBe(false);
81
- });
82
-
83
- it("formats nested objects as YAML", () => {
84
- const formatter = new OutputFormatter({ responseMode: "yaml", maxBytes: 10_000 });
85
- const result = formatter.format({ outer: { inner: "value" } });
86
-
87
- expect(result.text).toContain("outer:");
88
- expect(result.text).toContain("inner: value");
89
- });
90
-
91
- it("formats arrays as YAML", () => {
92
- const formatter = new OutputFormatter({ responseMode: "yaml", maxBytes: 10_000 });
93
- const result = formatter.format({ items: ["a", "b", "c"] });
94
-
95
- expect(result.text).toContain("items:");
96
- expect(result.text).toContain("- a");
97
- expect(result.text).toContain("- b");
98
- expect(result.text).toContain("- c");
99
- });
100
- });
101
-
102
- describe("truncation", () => {
103
- it("truncates output exceeding maxBytes", () => {
104
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 20 });
105
- const result = formatter.format({ key: "this is a very long value that exceeds the limit" });
106
-
107
- expect(result.truncated).toBe(true);
108
- expect(result.text).toContain("[truncated");
109
- expect(result.bytes).toBeGreaterThan(20);
110
- });
111
-
112
- it("does not truncate when output fits within maxBytes", () => {
113
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 100_000 });
114
- const result = formatter.format({ small: "data" });
115
-
116
- expect(result.truncated).toBe(false);
117
- expect(result.text).not.toContain("[truncated");
118
- });
119
-
120
- it("reports correct original byte count even when truncated", () => {
121
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10 });
122
- const data = { message: "hello world this is a longer message" };
123
- const result = formatter.format(data);
124
-
125
- const expectedFull = JSON.stringify(data, null, 2);
126
- const expectedBytes = Buffer.byteLength(expectedFull, "utf8");
127
-
128
- expect(result.truncated).toBe(true);
129
- expect(result.bytes).toBe(expectedBytes);
130
- });
131
-
132
- it("handles exact boundary correctly", () => {
133
- const data = { a: 1 };
134
- const serialized = JSON.stringify(data, null, 2);
135
- const exactBytes = Buffer.byteLength(serialized, "utf8");
136
-
137
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: exactBytes });
138
- const result = formatter.format(data);
139
-
140
- expect(result.truncated).toBe(false);
141
- expect(result.bytes).toBe(exactBytes);
142
- });
143
-
144
- it("handles multi-byte characters in truncation", () => {
145
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 30 });
146
- const result = formatter.format({ emoji: "Hello 🌍🌍🌍🌍🌍" });
147
-
148
- expect(result.truncated).toBe(true);
149
- expect(result.text).toContain("[truncated");
150
- });
151
- });
152
-
153
- describe("edge cases", () => {
154
- it("handles empty object", () => {
155
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
156
- const result = formatter.format({});
157
-
158
- expect(result.text).toBe("{}");
159
- expect(result.truncated).toBe(false);
160
- });
161
-
162
- it("handles empty array", () => {
163
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
164
- const result = formatter.format([]);
165
-
166
- expect(result.text).toBe("[]");
167
- expect(result.truncated).toBe(false);
168
- });
169
-
170
- it("throws on undefined because JSON.stringify returns undefined", () => {
171
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
172
- // JSON.stringify(undefined) returns the JS value undefined, which is
173
- // not a valid argument for Buffer.byteLength.
174
- expect(() => formatter.format(undefined)).toThrow();
175
- });
176
-
177
- it("handles deeply nested objects", () => {
178
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
179
- const deep = { a: { b: { c: { d: { e: "deep" } } } } };
180
- const result = formatter.format(deep);
181
-
182
- expect(result.text).toContain('"deep"');
183
- expect(result.truncated).toBe(false);
184
- });
185
- });
186
- });
@@ -1,324 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { ToolPolicyEngine, type ToolPolicyMeta } from "../src/lib/policy.js";
4
-
5
- const defaultFeatures = {
6
- wiki: true,
7
- milestone: true,
8
- pipeline: true,
9
- release: true
10
- };
11
-
12
- describe("ToolPolicyEngine", () => {
13
- describe("filterTools", () => {
14
- it("blocks mutating tools in readonly mode", () => {
15
- const engine = new ToolPolicyEngine({
16
- readOnlyMode: true,
17
- allowedTools: [],
18
- enabledFeatures: defaultFeatures
19
- });
20
-
21
- expect(
22
- engine.filterTools([
23
- { name: "readonly", mutating: false },
24
- { name: "mutate", mutating: true }
25
- ])
26
- ).toEqual([{ name: "readonly", mutating: false }]);
27
- });
28
-
29
- it("allows all tools when no restrictions are set", () => {
30
- const engine = new ToolPolicyEngine({
31
- readOnlyMode: false,
32
- allowedTools: [],
33
- enabledFeatures: defaultFeatures
34
- });
35
-
36
- const tools: ToolPolicyMeta[] = [
37
- { name: "tool_a", mutating: false },
38
- { name: "tool_b", mutating: true },
39
- { name: "tool_c", mutating: false }
40
- ];
41
-
42
- expect(engine.filterTools(tools)).toEqual(tools);
43
- });
44
-
45
- it("applies allowlist and deny regex", () => {
46
- const engine = new ToolPolicyEngine({
47
- readOnlyMode: false,
48
- allowedTools: ["gitlab_get_project", "gitlab_list_projects"],
49
- deniedToolsRegex: /^gitlab_list_/,
50
- enabledFeatures: defaultFeatures
51
- });
52
-
53
- expect(
54
- engine.filterTools([
55
- { name: "gitlab_get_project", mutating: false },
56
- { name: "gitlab_list_projects", mutating: false },
57
- { name: "gitlab_create_issue", mutating: true }
58
- ])
59
- ).toEqual([{ name: "gitlab_get_project", mutating: false }]);
60
- });
61
-
62
- it("supports allowlist names without gitlab_ prefix", () => {
63
- const engine = new ToolPolicyEngine({
64
- readOnlyMode: false,
65
- allowedTools: ["get_project"],
66
- enabledFeatures: defaultFeatures
67
- });
68
-
69
- expect(
70
- engine.filterTools([
71
- { name: "gitlab_get_project", mutating: false },
72
- { name: "gitlab_list_projects", mutating: false }
73
- ])
74
- ).toEqual([{ name: "gitlab_get_project", mutating: false }]);
75
- });
76
-
77
- it("supports allowlist names with gitlab_ prefix", () => {
78
- const engine = new ToolPolicyEngine({
79
- readOnlyMode: false,
80
- allowedTools: ["gitlab_get_project"],
81
- enabledFeatures: defaultFeatures
82
- });
83
-
84
- expect(
85
- engine.filterTools([
86
- { name: "gitlab_get_project", mutating: false },
87
- { name: "gitlab_list_projects", mutating: false }
88
- ])
89
- ).toEqual([{ name: "gitlab_get_project", mutating: false }]);
90
- });
91
-
92
- it("respects feature flags", () => {
93
- const engine = new ToolPolicyEngine({
94
- readOnlyMode: false,
95
- allowedTools: [],
96
- enabledFeatures: {
97
- wiki: false,
98
- milestone: false,
99
- pipeline: true,
100
- release: true
101
- }
102
- });
103
-
104
- expect(
105
- engine.filterTools([
106
- { name: "wiki", mutating: false, requiresFeature: "wiki" },
107
- { name: "pipeline", mutating: false, requiresFeature: "pipeline" }
108
- ])
109
- ).toEqual([{ name: "pipeline", mutating: false, requiresFeature: "pipeline" }]);
110
- });
111
-
112
- it("allows tools without requiresFeature even when features are disabled", () => {
113
- const engine = new ToolPolicyEngine({
114
- readOnlyMode: false,
115
- allowedTools: [],
116
- enabledFeatures: {
117
- wiki: false,
118
- milestone: false,
119
- pipeline: false,
120
- release: false
121
- }
122
- });
123
-
124
- expect(
125
- engine.filterTools([
126
- { name: "generic_tool", mutating: false },
127
- { name: "wiki_tool", mutating: false, requiresFeature: "wiki" }
128
- ])
129
- ).toEqual([{ name: "generic_tool", mutating: false }]);
130
- });
131
-
132
- it("applies deniedToolsRegex without allowlist", () => {
133
- const engine = new ToolPolicyEngine({
134
- readOnlyMode: false,
135
- allowedTools: [],
136
- deniedToolsRegex: /^gitlab_delete_/,
137
- enabledFeatures: defaultFeatures
138
- });
139
-
140
- expect(
141
- engine.filterTools([
142
- { name: "gitlab_get_project", mutating: false },
143
- { name: "gitlab_delete_project", mutating: true },
144
- { name: "gitlab_delete_issue", mutating: true }
145
- ])
146
- ).toEqual([{ name: "gitlab_get_project", mutating: false }]);
147
- });
148
-
149
- it("handles empty tools array", () => {
150
- const engine = new ToolPolicyEngine({
151
- readOnlyMode: false,
152
- allowedTools: [],
153
- enabledFeatures: defaultFeatures
154
- });
155
-
156
- expect(engine.filterTools([])).toEqual([]);
157
- });
158
-
159
- it("combines readOnly mode with feature flags", () => {
160
- const engine = new ToolPolicyEngine({
161
- readOnlyMode: true,
162
- allowedTools: [],
163
- enabledFeatures: {
164
- wiki: true,
165
- milestone: true,
166
- pipeline: true,
167
- release: true
168
- }
169
- });
170
-
171
- expect(
172
- engine.filterTools([
173
- { name: "read_wiki", mutating: false, requiresFeature: "wiki" },
174
- { name: "create_wiki", mutating: true, requiresFeature: "wiki" },
175
- { name: "get_project", mutating: false }
176
- ])
177
- ).toEqual([
178
- { name: "read_wiki", mutating: false, requiresFeature: "wiki" },
179
- { name: "get_project", mutating: false }
180
- ]);
181
- });
182
-
183
- it("handles allowlist with whitespace-padded names", () => {
184
- const engine = new ToolPolicyEngine({
185
- readOnlyMode: false,
186
- allowedTools: [" get_project "],
187
- enabledFeatures: defaultFeatures
188
- });
189
-
190
- expect(
191
- engine.filterTools([
192
- { name: "gitlab_get_project", mutating: false },
193
- { name: "gitlab_list_projects", mutating: false }
194
- ])
195
- ).toEqual([{ name: "gitlab_get_project", mutating: false }]);
196
- });
197
-
198
- it("handles allowlist with empty strings", () => {
199
- const engine = new ToolPolicyEngine({
200
- readOnlyMode: false,
201
- allowedTools: ["", " ", "get_project"],
202
- enabledFeatures: defaultFeatures
203
- });
204
-
205
- expect(
206
- engine.filterTools([
207
- { name: "gitlab_get_project", mutating: false },
208
- { name: "gitlab_list_projects", mutating: false }
209
- ])
210
- ).toEqual([{ name: "gitlab_get_project", mutating: false }]);
211
- });
212
-
213
- it("respects release feature flag", () => {
214
- const engine = new ToolPolicyEngine({
215
- readOnlyMode: false,
216
- allowedTools: [],
217
- enabledFeatures: {
218
- wiki: true,
219
- milestone: true,
220
- pipeline: true,
221
- release: false
222
- }
223
- });
224
-
225
- expect(
226
- engine.filterTools([
227
- { name: "list_releases", mutating: false, requiresFeature: "release" },
228
- { name: "list_pipelines", mutating: false, requiresFeature: "pipeline" }
229
- ])
230
- ).toEqual([{ name: "list_pipelines", mutating: false, requiresFeature: "pipeline" }]);
231
- });
232
- });
233
-
234
- describe("assertCanExecute", () => {
235
- it("does not throw for enabled tools", () => {
236
- const engine = new ToolPolicyEngine({
237
- readOnlyMode: false,
238
- allowedTools: [],
239
- enabledFeatures: defaultFeatures
240
- });
241
-
242
- expect(() => {
243
- engine.assertCanExecute({ name: "any_tool", mutating: false });
244
- }).not.toThrow();
245
- });
246
-
247
- it("throws for disabled tools in readonly mode", () => {
248
- const engine = new ToolPolicyEngine({
249
- readOnlyMode: true,
250
- allowedTools: [],
251
- enabledFeatures: defaultFeatures
252
- });
253
-
254
- expect(() => {
255
- engine.assertCanExecute({ name: "create_issue", mutating: true });
256
- }).toThrow("disabled by policy");
257
- });
258
-
259
- it("throws for tools not in allowlist", () => {
260
- const engine = new ToolPolicyEngine({
261
- readOnlyMode: false,
262
- allowedTools: ["gitlab_get_project"],
263
- enabledFeatures: defaultFeatures
264
- });
265
-
266
- expect(() => {
267
- engine.assertCanExecute({ name: "gitlab_list_projects", mutating: false });
268
- }).toThrow("disabled by policy");
269
- });
270
-
271
- it("throws for tools matching denied regex", () => {
272
- const engine = new ToolPolicyEngine({
273
- readOnlyMode: false,
274
- allowedTools: [],
275
- deniedToolsRegex: /^gitlab_delete_/,
276
- enabledFeatures: defaultFeatures
277
- });
278
-
279
- expect(() => {
280
- engine.assertCanExecute({ name: "gitlab_delete_issue", mutating: true });
281
- }).toThrow("disabled by policy");
282
- });
283
-
284
- it("throws for tools requiring disabled features", () => {
285
- const engine = new ToolPolicyEngine({
286
- readOnlyMode: false,
287
- allowedTools: [],
288
- enabledFeatures: {
289
- wiki: false,
290
- milestone: true,
291
- pipeline: true,
292
- release: true
293
- }
294
- });
295
-
296
- expect(() => {
297
- engine.assertCanExecute({ name: "wiki_tool", mutating: false, requiresFeature: "wiki" });
298
- }).toThrow("disabled by policy");
299
- });
300
- });
301
-
302
- describe("isToolEnabled", () => {
303
- it("returns true for unrestricted tools", () => {
304
- const engine = new ToolPolicyEngine({
305
- readOnlyMode: false,
306
- allowedTools: [],
307
- enabledFeatures: defaultFeatures
308
- });
309
-
310
- expect(engine.isToolEnabled({ name: "any_tool", mutating: false })).toBe(true);
311
- });
312
-
313
- it("returns false for mutating tools in readonly mode", () => {
314
- const engine = new ToolPolicyEngine({
315
- readOnlyMode: true,
316
- allowedTools: [],
317
- enabledFeatures: defaultFeatures
318
- });
319
-
320
- expect(engine.isToolEnabled({ name: "create_something", mutating: true })).toBe(false);
321
- expect(engine.isToolEnabled({ name: "read_something", mutating: false })).toBe(true);
322
- });
323
- });
324
- });