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
package/src/http.ts DELETED
@@ -1,620 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { createServer } from "node:http";
3
-
4
- import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
5
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
7
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
8
- import express from "express";
9
-
10
- import { env } from "./config/env.js";
11
- import { runWithSessionAuth, type SessionAuth } from "./lib/auth-context.js";
12
- import { GitLabClient } from "./lib/gitlab-client.js";
13
- import { logger } from "./lib/logger.js";
14
- import { configureNetworkRuntime } from "./lib/network.js";
15
- import { OutputFormatter } from "./lib/output.js";
16
- import { ToolPolicyEngine } from "./lib/policy.js";
17
- import { GitLabRequestRuntime } from "./lib/request-runtime.js";
18
- import { hasReachedSessionCapacity } from "./lib/session-capacity.js";
19
- import { createMcpServer } from "./server/build-server.js";
20
- import type { AppContext } from "./types/context.js";
21
-
22
- interface SessionState {
23
- sessionId?: string;
24
- server: McpServer;
25
- transport: StreamableHTTPServerTransport;
26
- lastAccessAt: number;
27
- queue: Promise<void>;
28
- activeRequests: number;
29
- closed: boolean;
30
- auth?: SessionAuth;
31
- rateLimit: {
32
- windowStart: number;
33
- count: number;
34
- };
35
- }
36
-
37
- interface SseSessionState {
38
- sessionId: string;
39
- server: McpServer;
40
- transport: SSEServerTransport;
41
- lastAccessAt: number;
42
- closed: boolean;
43
- }
44
-
45
- const requestRuntime = new GitLabRequestRuntime(env, logger);
46
- configureNetworkRuntime(env, logger);
47
-
48
- const context: AppContext = {
49
- env,
50
- logger,
51
- gitlab: new GitLabClient(env.GITLAB_API_URL, env.GITLAB_PERSONAL_ACCESS_TOKEN, {
52
- apiUrls: env.GITLAB_API_URLS,
53
- timeoutMs: env.GITLAB_HTTP_TIMEOUT_MS,
54
- beforeRequest: (requestContext) => requestRuntime.beforeRequest(requestContext)
55
- }),
56
- policy: new ToolPolicyEngine({
57
- readOnlyMode: env.GITLAB_READ_ONLY_MODE,
58
- allowedTools: env.GITLAB_ALLOWED_TOOLS,
59
- deniedToolsRegex: env.GITLAB_DENIED_TOOLS_REGEX
60
- ? new RegExp(env.GITLAB_DENIED_TOOLS_REGEX)
61
- : undefined,
62
- enabledFeatures: {
63
- wiki: env.USE_GITLAB_WIKI,
64
- milestone: env.USE_MILESTONE,
65
- pipeline: env.USE_PIPELINE,
66
- release: env.USE_RELEASE
67
- }
68
- }),
69
- formatter: new OutputFormatter({
70
- responseMode: env.GITLAB_RESPONSE_MODE,
71
- maxBytes: env.GITLAB_MAX_RESPONSE_BYTES
72
- })
73
- };
74
-
75
- const app = createMcpExpressApp({ host: env.HTTP_HOST });
76
- app.use(express.json({ limit: "2mb" }));
77
-
78
- const sessions = new Map<string, SessionState>();
79
- const pendingSessions = new Set<SessionState>();
80
- const sseSessions = new Map<string, SseSessionState>();
81
-
82
- app.get("/healthz", (_req, res) => {
83
- res.status(200).json({
84
- status: sessions.size + sseSessions.size >= env.MAX_SESSIONS ? "degraded" : "ok",
85
- server: env.MCP_SERVER_NAME,
86
- activeSessions: sessions.size,
87
- activeSseSessions: sseSessions.size,
88
- pendingSessions: pendingSessions.size,
89
- maxSessions: env.MAX_SESSIONS,
90
- remoteAuthorization: env.REMOTE_AUTHORIZATION,
91
- readOnlyMode: env.GITLAB_READ_ONLY_MODE,
92
- sseEnabled: env.SSE
93
- });
94
- });
95
-
96
- if (env.SSE) {
97
- app.get("/sse", async (req, res) => {
98
- let sessionId: string | undefined;
99
- try {
100
- const parsedAuth = parseRequestAuth(req);
101
- const fallbackToken = env.REMOTE_AUTHORIZATION ? undefined : env.GITLAB_PERSONAL_ACCESS_TOKEN;
102
-
103
- if (
104
- hasReachedSessionCapacity({
105
- streamableSessions: sessions.size,
106
- pendingSessions: pendingSessions.size,
107
- sseSessions: sseSessions.size,
108
- maxSessions: env.MAX_SESSIONS
109
- })
110
- ) {
111
- res.status(503).send(`Maximum ${env.MAX_SESSIONS} concurrent sessions reached`);
112
- return;
113
- }
114
-
115
- const server = createMcpServer(context);
116
- const transport = new SSEServerTransport("/messages", res);
117
- sessionId = transport.sessionId;
118
- const state: SseSessionState = {
119
- sessionId,
120
- server,
121
- transport,
122
- lastAccessAt: Date.now(),
123
- closed: false
124
- };
125
- sseSessions.set(sessionId, state);
126
- const currentSessionId = sessionId;
127
-
128
- res.on("close", () => {
129
- void closeSseSession(currentSessionId, "client-close");
130
- });
131
-
132
- await runWithSessionAuth(
133
- {
134
- sessionId,
135
- token: parsedAuth?.token ?? fallbackToken,
136
- apiUrl: parsedAuth?.apiUrl ?? env.GITLAB_API_URL,
137
- header: parsedAuth?.header,
138
- updatedAt: Date.now()
139
- },
140
- async () => {
141
- await server.connect(transport);
142
- }
143
- );
144
- logger.info({ sessionId }, "MCP SSE session initialized");
145
- } catch (error) {
146
- if (sessionId) {
147
- await closeSseSession(sessionId, "connect-error");
148
- }
149
- logger.error({ err: error, sessionId }, "Failed to initialize SSE session");
150
- if (!res.headersSent) {
151
- res.status(500).send("Failed to initialize SSE session");
152
- }
153
- }
154
- });
155
-
156
- app.post("/messages", async (req, res) => {
157
- let sessionId: string | undefined;
158
- try {
159
- sessionId = String(req.query.sessionId ?? "");
160
- if (!sessionId) {
161
- res.status(400).send("Missing sessionId");
162
- return;
163
- }
164
-
165
- const session = sseSessions.get(sessionId);
166
- if (!session || session.closed) {
167
- res.status(400).send("No transport found for sessionId");
168
- return;
169
- }
170
-
171
- const parsedAuth = parseRequestAuth(req);
172
- const fallbackToken = env.REMOTE_AUTHORIZATION ? undefined : env.GITLAB_PERSONAL_ACCESS_TOKEN;
173
- session.lastAccessAt = Date.now();
174
-
175
- await runWithSessionAuth(
176
- {
177
- sessionId,
178
- token: parsedAuth?.token ?? fallbackToken,
179
- apiUrl: parsedAuth?.apiUrl ?? env.GITLAB_API_URL,
180
- header: parsedAuth?.header,
181
- updatedAt: Date.now()
182
- },
183
- async () => {
184
- await session.transport.handlePostMessage(req, res);
185
- }
186
- );
187
- } catch (error) {
188
- logger.error({ err: error, sessionId }, "SSE post message failed");
189
- if (!res.headersSent) {
190
- res.status(500).send("SSE message processing failed");
191
- }
192
- }
193
- });
194
- }
195
-
196
- app.all("/mcp", async (req, res) => {
197
- const incomingSessionId = req.header("mcp-session-id") ?? undefined;
198
- const parsedAuth = parseRequestAuth(req);
199
-
200
- try {
201
- if (env.REMOTE_AUTHORIZATION && !parsedAuth?.token) {
202
- res.status(401).json({
203
- jsonrpc: "2.0",
204
- error: {
205
- code: -32010,
206
- message:
207
- "Missing remote authorization token. Provide 'Authorization: Bearer <token>' or 'Private-Token'."
208
- },
209
- id: null
210
- });
211
- return;
212
- }
213
-
214
- if (env.REMOTE_AUTHORIZATION && env.ENABLE_DYNAMIC_API_URL && !parsedAuth?.apiUrl) {
215
- res.status(400).json({
216
- jsonrpc: "2.0",
217
- error: {
218
- code: -32011,
219
- message:
220
- "Missing 'X-GitLab-API-URL' while ENABLE_DYNAMIC_API_URL=true and REMOTE_AUTHORIZATION=true."
221
- },
222
- id: null
223
- });
224
- return;
225
- }
226
-
227
- let session = incomingSessionId ? sessions.get(incomingSessionId) : undefined;
228
-
229
- if (incomingSessionId && !session) {
230
- res.status(404).json({
231
- jsonrpc: "2.0",
232
- error: {
233
- code: -32001,
234
- message: `Unknown session '${incomingSessionId}'`
235
- },
236
- id: null
237
- });
238
- return;
239
- }
240
-
241
- if (!session) {
242
- if (req.method !== "POST") {
243
- res.status(400).json({
244
- jsonrpc: "2.0",
245
- error: {
246
- code: -32000,
247
- message: "Session not initialized. First call must be a POST initialize request."
248
- },
249
- id: null
250
- });
251
- return;
252
- }
253
-
254
- if (
255
- hasReachedSessionCapacity({
256
- streamableSessions: sessions.size,
257
- pendingSessions: pendingSessions.size,
258
- sseSessions: sseSessions.size,
259
- maxSessions: env.MAX_SESSIONS
260
- })
261
- ) {
262
- res.status(503).json({
263
- jsonrpc: "2.0",
264
- error: {
265
- code: -32002,
266
- message: `Maximum ${env.MAX_SESSIONS} concurrent sessions reached`
267
- },
268
- id: null
269
- });
270
- return;
271
- }
272
-
273
- session = await createSession(parsedAuth);
274
- } else {
275
- refreshSessionAuth(session, parsedAuth);
276
- }
277
-
278
- if (!checkSessionRateLimit(session)) {
279
- res.status(429).json({
280
- jsonrpc: "2.0",
281
- error: {
282
- code: -32003,
283
- message: `Rate limit exceeded: max ${env.MAX_REQUESTS_PER_MINUTE} requests/min per session`
284
- },
285
- id: null
286
- });
287
- return;
288
- }
289
-
290
- await enqueueSessionRequest(session, async () => {
291
- const runtimeAuth = buildRuntimeAuth(session);
292
- await runWithSessionAuth(runtimeAuth, async () => {
293
- await session.transport.handleRequest(req, res, req.body);
294
- });
295
- });
296
- } catch (error) {
297
- logger.error(
298
- {
299
- err: error,
300
- method: req.method,
301
- sessionId: incomingSessionId
302
- },
303
- "MCP HTTP request failed"
304
- );
305
-
306
- if (!res.headersSent) {
307
- res.status(500).json({
308
- jsonrpc: "2.0",
309
- error: {
310
- code: -32603,
311
- message: "Internal server error"
312
- },
313
- id: null
314
- });
315
- }
316
- }
317
- });
318
-
319
- const httpServer = createServer(app);
320
-
321
- httpServer.listen(env.HTTP_PORT, env.HTTP_HOST, () => {
322
- logger.info(
323
- {
324
- host: env.HTTP_HOST,
325
- port: env.HTTP_PORT,
326
- transport: env.SSE ? "streamable-http+sse" : "streamable-http",
327
- jsonOnly: env.HTTP_JSON_ONLY,
328
- maxSessions: env.MAX_SESSIONS,
329
- sessionTimeoutSeconds: env.SESSION_TIMEOUT_SECONDS,
330
- remoteAuthEnabled: env.REMOTE_AUTHORIZATION
331
- },
332
- "MCP HTTP server started"
333
- );
334
- });
335
-
336
- const gcInterval = setInterval(() => {
337
- void garbageCollectSessions();
338
- }, 30_000);
339
-
340
- gcInterval.unref();
341
-
342
- process.once("SIGINT", () => {
343
- void shutdown("SIGINT");
344
- });
345
-
346
- process.once("SIGTERM", () => {
347
- void shutdown("SIGTERM");
348
- });
349
-
350
- async function createSession(initialAuth?: SessionAuth): Promise<SessionState> {
351
- const server = createMcpServer(context);
352
- const state: SessionState = {
353
- server,
354
- transport: undefined as unknown as StreamableHTTPServerTransport,
355
- lastAccessAt: Date.now(),
356
- queue: Promise.resolve(),
357
- activeRequests: 0,
358
- closed: false,
359
- auth: initialAuth,
360
- rateLimit: {
361
- windowStart: Date.now(),
362
- count: 0
363
- }
364
- };
365
-
366
- pendingSessions.add(state);
367
-
368
- const transport = new StreamableHTTPServerTransport({
369
- sessionIdGenerator: () => randomUUID(),
370
- enableJsonResponse: env.HTTP_JSON_ONLY,
371
- onsessioninitialized: (sessionId) => {
372
- state.sessionId = sessionId;
373
- state.lastAccessAt = Date.now();
374
- sessions.set(sessionId, state);
375
- pendingSessions.delete(state);
376
- logger.info({ sessionId }, "MCP session initialized");
377
- },
378
- onsessionclosed: async (sessionId) => {
379
- await closeSession(sessionId, "transport-close");
380
- }
381
- });
382
-
383
- state.transport = transport;
384
-
385
- transport.onerror = (error) => {
386
- logger.error({ err: error, sessionId: state.sessionId }, "MCP transport error");
387
- };
388
-
389
- try {
390
- await server.connect(transport);
391
- return state;
392
- } catch (error) {
393
- pendingSessions.delete(state);
394
-
395
- if (state.sessionId) {
396
- await closeSession(state.sessionId, "transport-close");
397
- throw error;
398
- }
399
-
400
- state.closed = true;
401
- try {
402
- await transport.close();
403
- } catch (closeError) {
404
- logger.warn({ err: closeError }, "Failed to close transport after session init failure");
405
- }
406
-
407
- try {
408
- await server.close();
409
- } catch (closeError) {
410
- logger.warn({ err: closeError }, "Failed to close MCP server after session init failure");
411
- }
412
-
413
- throw error;
414
- }
415
- }
416
-
417
- function checkSessionRateLimit(session: SessionState): boolean {
418
- const now = Date.now();
419
- const oneMinute = 60_000;
420
-
421
- if (now - session.rateLimit.windowStart >= oneMinute) {
422
- session.rateLimit.windowStart = now;
423
- session.rateLimit.count = 0;
424
- }
425
-
426
- if (session.rateLimit.count >= env.MAX_REQUESTS_PER_MINUTE) {
427
- return false;
428
- }
429
-
430
- session.rateLimit.count += 1;
431
- return true;
432
- }
433
-
434
- function refreshSessionAuth(session: SessionState, auth?: SessionAuth): void {
435
- if (!auth) {
436
- return;
437
- }
438
-
439
- session.auth = auth;
440
- session.lastAccessAt = Date.now();
441
- }
442
-
443
- function buildRuntimeAuth(session: SessionState): SessionAuth | undefined {
444
- const fallbackToken = env.REMOTE_AUTHORIZATION ? undefined : env.GITLAB_PERSONAL_ACCESS_TOKEN;
445
-
446
- return {
447
- sessionId: session.sessionId,
448
- token: session.auth?.token ?? fallbackToken,
449
- apiUrl: session.auth?.apiUrl ?? env.GITLAB_API_URL,
450
- header: session.auth?.header,
451
- updatedAt: session.auth?.updatedAt ?? Date.now()
452
- };
453
- }
454
-
455
- function parseRequestAuth(req: express.Request): SessionAuth | undefined {
456
- if (!env.REMOTE_AUTHORIZATION) {
457
- return undefined;
458
- }
459
-
460
- const privateToken = req.header("private-token")?.trim();
461
- const authorization = req.header("authorization")?.trim();
462
-
463
- const bearerToken = authorization?.toLowerCase().startsWith("bearer ")
464
- ? authorization.slice(7).trim()
465
- : undefined;
466
-
467
- const token = privateToken || bearerToken;
468
-
469
- let apiUrl: string | undefined;
470
-
471
- if (env.ENABLE_DYNAMIC_API_URL) {
472
- const dynamicApiUrl = req.header("x-gitlab-api-url")?.trim();
473
- if (dynamicApiUrl) {
474
- try {
475
- apiUrl = new URL(dynamicApiUrl).toString();
476
- } catch {
477
- throw new Error(`Invalid x-gitlab-api-url header: '${dynamicApiUrl}'`);
478
- }
479
- }
480
- }
481
-
482
- if (!token && !apiUrl) {
483
- return undefined;
484
- }
485
-
486
- return {
487
- token,
488
- apiUrl,
489
- header: privateToken ? "private-token" : bearerToken ? "authorization" : undefined,
490
- updatedAt: Date.now()
491
- };
492
- }
493
-
494
- async function enqueueSessionRequest(
495
- session: SessionState,
496
- task: () => Promise<void>
497
- ): Promise<void> {
498
- const queued = session.queue.then(async () => {
499
- session.activeRequests += 1;
500
- session.lastAccessAt = Date.now();
501
-
502
- try {
503
- await task();
504
- } finally {
505
- session.activeRequests -= 1;
506
- session.lastAccessAt = Date.now();
507
- }
508
- });
509
-
510
- session.queue = queued.catch(() => undefined);
511
- await queued;
512
- }
513
-
514
- async function garbageCollectSessions(): Promise<void> {
515
- const now = Date.now();
516
- const timeoutMs = env.SESSION_TIMEOUT_SECONDS * 1000;
517
-
518
- for (const [sessionId, session] of sessions) {
519
- if (session.activeRequests > 0 || session.closed) {
520
- continue;
521
- }
522
-
523
- if (now - session.lastAccessAt < timeoutMs) {
524
- continue;
525
- }
526
-
527
- await closeSession(sessionId, "idle-timeout");
528
- }
529
-
530
- for (const [sessionId, session] of sseSessions) {
531
- if (session.closed) {
532
- continue;
533
- }
534
-
535
- if (now - session.lastAccessAt < timeoutMs) {
536
- continue;
537
- }
538
-
539
- await closeSseSession(sessionId, "idle-timeout");
540
- }
541
- }
542
-
543
- async function closeSession(
544
- sessionId: string,
545
- reason: "transport-close" | "idle-timeout" | "shutdown"
546
- ): Promise<void> {
547
- const session = sessions.get(sessionId);
548
- if (!session || session.closed) {
549
- return;
550
- }
551
-
552
- session.closed = true;
553
- sessions.delete(sessionId);
554
-
555
- try {
556
- await session.transport.close();
557
- } catch (error) {
558
- logger.warn({ err: error, sessionId, reason }, "Failed to close transport cleanly");
559
- }
560
-
561
- try {
562
- await session.server.close();
563
- } catch (error) {
564
- logger.warn({ err: error, sessionId, reason }, "Failed to close MCP server cleanly");
565
- }
566
-
567
- logger.info({ sessionId, reason }, "MCP session closed");
568
- }
569
-
570
- async function closeSseSession(
571
- sessionId: string,
572
- reason: "client-close" | "connect-error" | "idle-timeout" | "shutdown"
573
- ): Promise<void> {
574
- const session = sseSessions.get(sessionId);
575
- if (!session || session.closed) {
576
- return;
577
- }
578
-
579
- session.closed = true;
580
- sseSessions.delete(sessionId);
581
-
582
- try {
583
- await session.transport.close();
584
- } catch (error) {
585
- logger.warn({ err: error, sessionId, reason }, "Failed to close SSE transport cleanly");
586
- }
587
-
588
- try {
589
- await session.server.close();
590
- } catch (error) {
591
- logger.warn({ err: error, sessionId, reason }, "Failed to close SSE MCP server cleanly");
592
- }
593
-
594
- logger.info({ sessionId, reason }, "MCP SSE session closed");
595
- }
596
-
597
- async function shutdown(signal: NodeJS.Signals): Promise<void> {
598
- logger.info({ signal }, "Shutting down HTTP server");
599
-
600
- clearInterval(gcInterval);
601
-
602
- const pendingClose = [...sessions.keys()].map((sessionId) => closeSession(sessionId, "shutdown"));
603
- const pendingSseClose = [...sseSessions.keys()].map((sessionId) =>
604
- closeSseSession(sessionId, "shutdown")
605
- );
606
- await Promise.allSettled([...pendingClose, ...pendingSseClose]);
607
-
608
- await new Promise<void>((resolve, reject) => {
609
- httpServer.close((error) => {
610
- if (error) {
611
- reject(error);
612
- return;
613
- }
614
-
615
- resolve();
616
- });
617
- });
618
-
619
- process.exit(0);
620
- }
package/src/index.ts DELETED
@@ -1,77 +0,0 @@
1
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
-
3
- import { env } from "./config/env.js";
4
- import { GitLabClient } from "./lib/gitlab-client.js";
5
- import { logger } from "./lib/logger.js";
6
- import { configureNetworkRuntime } from "./lib/network.js";
7
- import { OutputFormatter } from "./lib/output.js";
8
- import { ToolPolicyEngine } from "./lib/policy.js";
9
- import { GitLabRequestRuntime } from "./lib/request-runtime.js";
10
- import { createMcpServer } from "./server/build-server.js";
11
- import type { AppContext } from "./types/context.js";
12
-
13
- async function main(): Promise<void> {
14
- const deniedToolsRegex = env.GITLAB_DENIED_TOOLS_REGEX
15
- ? new RegExp(env.GITLAB_DENIED_TOOLS_REGEX)
16
- : undefined;
17
- configureNetworkRuntime(env, logger);
18
- const requestRuntime = new GitLabRequestRuntime(env, logger);
19
-
20
- const context: AppContext = {
21
- env,
22
- logger,
23
- gitlab: new GitLabClient(env.GITLAB_API_URL, env.GITLAB_PERSONAL_ACCESS_TOKEN, {
24
- apiUrls: env.GITLAB_API_URLS,
25
- timeoutMs: env.GITLAB_HTTP_TIMEOUT_MS,
26
- beforeRequest: (requestContext) => requestRuntime.beforeRequest(requestContext)
27
- }),
28
- policy: new ToolPolicyEngine({
29
- readOnlyMode: env.GITLAB_READ_ONLY_MODE,
30
- allowedTools: env.GITLAB_ALLOWED_TOOLS,
31
- deniedToolsRegex,
32
- enabledFeatures: {
33
- wiki: env.USE_GITLAB_WIKI,
34
- milestone: env.USE_MILESTONE,
35
- pipeline: env.USE_PIPELINE,
36
- release: env.USE_RELEASE
37
- }
38
- }),
39
- formatter: new OutputFormatter({
40
- responseMode: env.GITLAB_RESPONSE_MODE,
41
- maxBytes: env.GITLAB_MAX_RESPONSE_BYTES
42
- })
43
- };
44
-
45
- const server = createMcpServer(context);
46
- const transport = new StdioServerTransport();
47
-
48
- await server.connect(transport);
49
- logger.info({ transport: "stdio" }, "MCP server started");
50
-
51
- const handleSignal = (signal: NodeJS.Signals) => {
52
- void shutdown(signal, server);
53
- };
54
-
55
- process.once("SIGINT", () => handleSignal("SIGINT"));
56
- process.once("SIGTERM", () => handleSignal("SIGTERM"));
57
- }
58
-
59
- async function shutdown(
60
- signal: NodeJS.Signals,
61
- server: ReturnType<typeof createMcpServer>
62
- ): Promise<void> {
63
- logger.info({ signal }, "Shutting down MCP server");
64
-
65
- try {
66
- await server.close();
67
- } catch (error) {
68
- logger.error({ err: error }, "Server close failed");
69
- }
70
-
71
- process.exit(0);
72
- }
73
-
74
- void main().catch((error) => {
75
- logger.error({ err: error }, "Failed to start MCP server");
76
- process.exit(1);
77
- });
@@ -1,19 +0,0 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
-
3
- export interface SessionAuth {
4
- sessionId?: string;
5
- token?: string;
6
- apiUrl?: string;
7
- header?: "authorization" | "private-token";
8
- updatedAt: number;
9
- }
10
-
11
- const sessionAuthStore = new AsyncLocalStorage<SessionAuth | undefined>();
12
-
13
- export function runWithSessionAuth<T>(auth: SessionAuth | undefined, callback: () => T): T {
14
- return sessionAuthStore.run(auth, callback);
15
- }
16
-
17
- export function getSessionAuth(): SessionAuth | undefined {
18
- return sessionAuthStore.getStore();
19
- }