gitlab-mcp 0.1.4 → 1.0.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.
- package/.dockerignore +7 -0
- package/.editorconfig +9 -0
- package/.env.example +75 -0
- package/.github/workflows/nodejs.yml +31 -0
- package/.github/workflows/npm-publish.yml +31 -0
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/.prettierrc.json +6 -0
- package/Dockerfile +20 -0
- package/README.md +416 -251
- package/docker-compose.yml +10 -0
- package/docs/architecture.md +310 -0
- package/docs/authentication.md +299 -0
- package/docs/configuration.md +149 -0
- package/docs/deployment.md +336 -0
- package/docs/tools.md +294 -0
- package/eslint.config.js +23 -0
- package/package.json +70 -32
- package/scripts/get-oauth-token.example.sh +15 -0
- package/src/config/env.ts +171 -0
- package/src/http.ts +605 -0
- package/src/index.ts +77 -0
- package/src/lib/auth-context.ts +19 -0
- package/src/lib/gitlab-client.ts +1810 -0
- package/src/lib/logger.ts +17 -0
- package/src/lib/network.ts +45 -0
- package/src/lib/oauth.ts +287 -0
- package/src/lib/output.ts +51 -0
- package/src/lib/policy.ts +78 -0
- package/src/lib/request-runtime.ts +376 -0
- package/src/lib/sanitize.ts +25 -0
- package/src/server/build-server.ts +17 -0
- package/src/tools/gitlab.ts +3128 -0
- package/src/tools/health.ts +27 -0
- package/src/tools/mr-code-context.ts +473 -0
- package/src/types/context.ts +13 -0
- package/tests/auth-context.test.ts +102 -0
- package/tests/gitlab-client.test.ts +674 -0
- package/tests/graphql-guard.test.ts +121 -0
- package/tests/integration/agent-loop.integration.test.ts +552 -0
- package/tests/integration/server.integration.test.ts +543 -0
- package/tests/mr-code-context.test.ts +600 -0
- package/tests/oauth.test.ts +43 -0
- package/tests/output.test.ts +186 -0
- package/tests/policy.test.ts +324 -0
- package/tests/request-runtime.test.ts +252 -0
- package/tests/sanitize.test.ts +123 -0
- package/tests/upload-reference.test.ts +84 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +12 -0
- package/LICENSE +0 -21
- package/build/index.js +0 -1641
- package/build/schemas.js +0 -684
- package/build/test-note.js +0 -54
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { exec as execCb } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
import fetchCookie from "fetch-cookie";
|
|
8
|
+
import type { Logger } from "pino";
|
|
9
|
+
import { Cookie, CookieJar } from "tough-cookie";
|
|
10
|
+
|
|
11
|
+
import type { AppEnv } from "../config/env.js";
|
|
12
|
+
import type { GitLabBeforeRequestContext, GitLabBeforeRequestResult } from "./gitlab-client.js";
|
|
13
|
+
import { deriveGitLabBaseUrl, GitLabOAuthManager } from "./oauth.js";
|
|
14
|
+
|
|
15
|
+
const execAsync = promisify(execCb);
|
|
16
|
+
const DEFAULT_BROWSER_UA =
|
|
17
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36";
|
|
18
|
+
|
|
19
|
+
interface TokenState {
|
|
20
|
+
value: string;
|
|
21
|
+
expiresAt: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class GitLabRequestRuntime {
|
|
25
|
+
private readonly cookiePath?: string;
|
|
26
|
+
private readonly warmupPath: string;
|
|
27
|
+
private readonly tokenFilePath?: string;
|
|
28
|
+
private readonly tokenScript?: string;
|
|
29
|
+
private readonly oauthManager?: GitLabOAuthManager;
|
|
30
|
+
|
|
31
|
+
private fetchImpl: typeof fetch = fetch;
|
|
32
|
+
private cookieJar: CookieJar | null = null;
|
|
33
|
+
private cookieMtime = 0;
|
|
34
|
+
private cookieReloadLock: Promise<void> | null = null;
|
|
35
|
+
private readonly warmedApiRoots = new Set<string>();
|
|
36
|
+
private cachedToken: TokenState | null = null;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly env: AppEnv,
|
|
40
|
+
private readonly logger: Logger
|
|
41
|
+
) {
|
|
42
|
+
this.cookiePath = resolveHomePath(env.GITLAB_AUTH_COOKIE_PATH);
|
|
43
|
+
this.warmupPath = normalizeWarmupPath(env.GITLAB_COOKIE_WARMUP_PATH);
|
|
44
|
+
this.tokenFilePath = resolveHomePath(env.GITLAB_TOKEN_FILE);
|
|
45
|
+
this.tokenScript = env.GITLAB_TOKEN_SCRIPT?.trim() || undefined;
|
|
46
|
+
|
|
47
|
+
if (env.GITLAB_USE_OAUTH && env.GITLAB_OAUTH_CLIENT_ID) {
|
|
48
|
+
this.oauthManager = new GitLabOAuthManager(
|
|
49
|
+
{
|
|
50
|
+
clientId: env.GITLAB_OAUTH_CLIENT_ID,
|
|
51
|
+
clientSecret: env.GITLAB_OAUTH_CLIENT_SECRET,
|
|
52
|
+
gitlabUrl: env.GITLAB_OAUTH_GITLAB_URL || deriveGitLabBaseUrl(env.GITLAB_API_URL),
|
|
53
|
+
redirectUri: env.GITLAB_OAUTH_REDIRECT_URI || "http://127.0.0.1:8765/callback",
|
|
54
|
+
scopes: parseOauthScopes(env.GITLAB_OAUTH_SCOPES),
|
|
55
|
+
tokenStoragePath: resolveHomePath(env.GITLAB_OAUTH_TOKEN_PATH),
|
|
56
|
+
autoOpenBrowser: env.GITLAB_OAUTH_AUTO_OPEN_BROWSER
|
|
57
|
+
},
|
|
58
|
+
this.logger
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async beforeRequest(context: GitLabBeforeRequestContext): Promise<GitLabBeforeRequestResult> {
|
|
64
|
+
await this.reloadCookiesIfChanged();
|
|
65
|
+
|
|
66
|
+
const headers = new Headers(context.headers);
|
|
67
|
+
this.applyCompatibilityHeaders(headers);
|
|
68
|
+
|
|
69
|
+
let token = context.token;
|
|
70
|
+
if (!token) {
|
|
71
|
+
token = await this.resolveFallbackToken();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (this.cookieJar) {
|
|
75
|
+
await this.ensureSessionWarmup(context.url, headers, token);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
headers,
|
|
80
|
+
token,
|
|
81
|
+
fetchImpl: this.fetchImpl
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async resolveFallbackToken(): Promise<string | undefined> {
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
if (this.cachedToken && this.cachedToken.expiresAt > now) {
|
|
88
|
+
return this.cachedToken.value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.oauthManager) {
|
|
92
|
+
const token = await this.oauthManager.getAccessToken();
|
|
93
|
+
if (token) {
|
|
94
|
+
return token;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (this.tokenScript) {
|
|
99
|
+
const token = await this.loadTokenFromScript(this.tokenScript);
|
|
100
|
+
if (token) {
|
|
101
|
+
const ttlMs = this.env.GITLAB_TOKEN_CACHE_SECONDS * 1000;
|
|
102
|
+
this.cachedToken = {
|
|
103
|
+
value: token,
|
|
104
|
+
expiresAt: now + ttlMs
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return token;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (this.tokenFilePath) {
|
|
111
|
+
const token = await this.loadTokenFromFile(this.tokenFilePath);
|
|
112
|
+
if (token) {
|
|
113
|
+
const ttlMs = this.env.GITLAB_TOKEN_CACHE_SECONDS * 1000;
|
|
114
|
+
this.cachedToken = {
|
|
115
|
+
value: token,
|
|
116
|
+
expiresAt: now + ttlMs
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return token;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async loadTokenFromScript(script: string): Promise<string | undefined> {
|
|
126
|
+
try {
|
|
127
|
+
const { stdout } = await execAsync(script, {
|
|
128
|
+
timeout: this.env.GITLAB_TOKEN_SCRIPT_TIMEOUT_MS,
|
|
129
|
+
maxBuffer: 1024 * 1024
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return parseTokenOutput(stdout);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
this.logger.warn({ err: error }, "Failed to execute GITLAB_TOKEN_SCRIPT");
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private async loadTokenFromFile(tokenFilePath: string): Promise<string | undefined> {
|
|
140
|
+
try {
|
|
141
|
+
const stat = await fs.stat(tokenFilePath);
|
|
142
|
+
|
|
143
|
+
// Group/other bits on token files are rejected unless explicitly allowed.
|
|
144
|
+
if ((stat.mode & 0o077) !== 0 && !this.env.GITLAB_ALLOW_INSECURE_TOKEN_FILE) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Token file '${tokenFilePath}' is too permissive. Set chmod 600 or GITLAB_ALLOW_INSECURE_TOKEN_FILE=true.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const content = (await fs.readFile(tokenFilePath, "utf8")).trim();
|
|
151
|
+
return parseTokenOutput(content);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.logger.warn({ err: error, tokenFilePath }, "Failed to read GITLAB_TOKEN_FILE");
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private applyCompatibilityHeaders(headers: Headers): void {
|
|
159
|
+
const userAgent =
|
|
160
|
+
this.env.GITLAB_USER_AGENT?.trim() ||
|
|
161
|
+
(this.env.GITLAB_CLOUDFLARE_BYPASS ? DEFAULT_BROWSER_UA : undefined);
|
|
162
|
+
if (userAgent && !headers.has("User-Agent")) {
|
|
163
|
+
headers.set("User-Agent", userAgent);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (this.env.GITLAB_CLOUDFLARE_BYPASS) {
|
|
167
|
+
if (!headers.has("Accept-Language")) {
|
|
168
|
+
headers.set("Accept-Language", this.env.GITLAB_ACCEPT_LANGUAGE || "en-US,en;q=0.9");
|
|
169
|
+
}
|
|
170
|
+
if (!headers.has("Cache-Control")) {
|
|
171
|
+
headers.set("Cache-Control", "no-cache");
|
|
172
|
+
}
|
|
173
|
+
if (!headers.has("Pragma")) {
|
|
174
|
+
headers.set("Pragma", "no-cache");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async reloadCookiesIfChanged(): Promise<void> {
|
|
180
|
+
if (!this.cookiePath) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.cookieReloadLock) {
|
|
185
|
+
await this.cookieReloadLock;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.cookieReloadLock = (async () => {
|
|
190
|
+
try {
|
|
191
|
+
const stat = await fs.stat(this.cookiePath!);
|
|
192
|
+
if (stat.mtimeMs === this.cookieMtime) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const cookieContent = await fs.readFile(this.cookiePath!, "utf8");
|
|
197
|
+
const jar = createCookieJarFromNetscape(cookieContent);
|
|
198
|
+
this.cookieJar = jar;
|
|
199
|
+
this.fetchImpl = fetchCookie(fetch, jar) as unknown as typeof fetch;
|
|
200
|
+
this.cookieMtime = stat.mtimeMs;
|
|
201
|
+
this.warmedApiRoots.clear();
|
|
202
|
+
this.logger.info({ cookiePath: this.cookiePath }, "Loaded auth cookies");
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (this.cookieJar) {
|
|
205
|
+
this.logger.warn({ err: error, cookiePath: this.cookiePath }, "Clearing auth cookies");
|
|
206
|
+
}
|
|
207
|
+
this.cookieJar = null;
|
|
208
|
+
this.fetchImpl = fetch;
|
|
209
|
+
this.cookieMtime = 0;
|
|
210
|
+
this.warmedApiRoots.clear();
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await this.cookieReloadLock;
|
|
216
|
+
} finally {
|
|
217
|
+
this.cookieReloadLock = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private async ensureSessionWarmup(url: URL, headers: Headers, token?: string): Promise<void> {
|
|
222
|
+
const apiRoot = resolveApiRoot(url);
|
|
223
|
+
if (!apiRoot) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const apiRootKey = `${url.origin}${apiRoot}`;
|
|
227
|
+
if (this.warmedApiRoots.has(apiRootKey)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const warmupUrl = new URL(`${apiRoot}${this.warmupPath}`, url.origin);
|
|
232
|
+
const warmupHeaders = new Headers(headers);
|
|
233
|
+
if (!warmupHeaders.has("Accept")) {
|
|
234
|
+
warmupHeaders.set("Accept", "application/json");
|
|
235
|
+
}
|
|
236
|
+
if (token && !warmupHeaders.has("PRIVATE-TOKEN")) {
|
|
237
|
+
warmupHeaders.set("PRIVATE-TOKEN", token);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const response = await this.fetchImpl(warmupUrl, {
|
|
242
|
+
method: "GET",
|
|
243
|
+
headers: warmupHeaders,
|
|
244
|
+
redirect: "follow",
|
|
245
|
+
signal: AbortSignal.timeout(Math.min(this.env.GITLAB_HTTP_TIMEOUT_MS, 12_000))
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
if (response.status < 500) {
|
|
249
|
+
this.warmedApiRoots.add(apiRootKey);
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
this.logger.debug({ err: error, warmupUrl: warmupUrl.toString() }, "Cookie warmup failed");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function resolveHomePath(input?: string): string | undefined {
|
|
258
|
+
if (!input) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (input.startsWith("~/")) {
|
|
263
|
+
return path.join(os.homedir(), input.slice(2));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return input;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function normalizeWarmupPath(value: string): string {
|
|
270
|
+
const trimmed = value.trim();
|
|
271
|
+
if (!trimmed) {
|
|
272
|
+
return "/user";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function resolveApiRoot(url: URL): string | undefined {
|
|
279
|
+
const match = url.pathname.match(/^(.*\/api\/v4)(?:\/|$)/);
|
|
280
|
+
return match?.[1];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function createCookieJarFromNetscape(content: string): CookieJar {
|
|
284
|
+
const jar = new CookieJar();
|
|
285
|
+
const lines = content.split("\n");
|
|
286
|
+
|
|
287
|
+
for (let raw of lines) {
|
|
288
|
+
let httpOnly = false;
|
|
289
|
+
if (raw.startsWith("#HttpOnly_")) {
|
|
290
|
+
raw = raw.slice("#HttpOnly_".length);
|
|
291
|
+
httpOnly = true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!raw.trim() || raw.startsWith("#")) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const parts = raw.split("\t");
|
|
299
|
+
if (parts.length < 7) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const domain = parts[0];
|
|
304
|
+
const cookiePath = parts[2];
|
|
305
|
+
const secure = parts[3];
|
|
306
|
+
const expires = parts[4];
|
|
307
|
+
const name = parts[5];
|
|
308
|
+
const value = parts[6];
|
|
309
|
+
if (
|
|
310
|
+
domain === undefined ||
|
|
311
|
+
cookiePath === undefined ||
|
|
312
|
+
secure === undefined ||
|
|
313
|
+
expires === undefined ||
|
|
314
|
+
name === undefined ||
|
|
315
|
+
value === undefined
|
|
316
|
+
) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const secureFlag = secure === "TRUE" ? "; Secure" : "";
|
|
320
|
+
const httpOnlyFlag = httpOnly ? "; HttpOnly" : "";
|
|
321
|
+
const expiresFlag =
|
|
322
|
+
expires === "0"
|
|
323
|
+
? ""
|
|
324
|
+
: `; Expires=${new Date(Number.parseInt(expires, 10) * 1000).toUTCString()}`;
|
|
325
|
+
|
|
326
|
+
const cookieString = `${name}=${value}; Domain=${domain}; Path=${cookiePath}${secureFlag}${httpOnlyFlag}${expiresFlag}`;
|
|
327
|
+
const cookie = Cookie.parse(cookieString);
|
|
328
|
+
if (!cookie) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const normalizedDomain = domain.startsWith(".") ? domain.slice(1) : domain;
|
|
333
|
+
const targetUrl = `${secure === "TRUE" ? "https" : "http"}://${normalizedDomain}`;
|
|
334
|
+
try {
|
|
335
|
+
jar.setCookieSync(cookie, targetUrl);
|
|
336
|
+
} catch {
|
|
337
|
+
// ignore invalid cookies from external files
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return jar;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function parseTokenOutput(rawOutput: string): string | undefined {
|
|
345
|
+
const output = rawOutput.trim();
|
|
346
|
+
if (!output) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const parsed = JSON.parse(output) as Record<string, unknown>;
|
|
352
|
+
const token =
|
|
353
|
+
getStringField(parsed, "token") ||
|
|
354
|
+
getStringField(parsed, "access_token") ||
|
|
355
|
+
getStringField(parsed, "private_token");
|
|
356
|
+
if (token) {
|
|
357
|
+
return token;
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
// Plain string output is valid.
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return output.split(/\r?\n/, 1)[0]?.trim() || undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function getStringField(record: Record<string, unknown>, key: string): string | undefined {
|
|
367
|
+
const value = record[key];
|
|
368
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function parseOauthScopes(rawScopes: string): string[] {
|
|
372
|
+
return rawScopes
|
|
373
|
+
.split(/[,\s]+/)
|
|
374
|
+
.map((scope) => scope.trim())
|
|
375
|
+
.filter((scope) => scope.length > 0);
|
|
376
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function stripNullsDeep<T>(value: T): T {
|
|
2
|
+
if (value === null) {
|
|
3
|
+
return undefined as T;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value.map((item) => stripNullsDeep(item)).filter((item) => item !== undefined) as T;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof value === "object" && value !== undefined) {
|
|
11
|
+
const input = value as Record<string, unknown>;
|
|
12
|
+
const output: Record<string, unknown> = {};
|
|
13
|
+
|
|
14
|
+
for (const [key, item] of Object.entries(input)) {
|
|
15
|
+
const normalized = stripNullsDeep(item);
|
|
16
|
+
if (normalized !== undefined) {
|
|
17
|
+
output[key] = normalized;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return output as T;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
|
|
3
|
+
import { registerGitLabTools } from "../tools/gitlab.js";
|
|
4
|
+
import { registerHealthTool } from "../tools/health.js";
|
|
5
|
+
import type { AppContext } from "../types/context.js";
|
|
6
|
+
|
|
7
|
+
export function createMcpServer(context: AppContext): McpServer {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: context.env.MCP_SERVER_NAME,
|
|
10
|
+
version: context.env.MCP_SERVER_VERSION
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
registerHealthTool(server);
|
|
14
|
+
registerGitLabTools(server, context);
|
|
15
|
+
|
|
16
|
+
return server;
|
|
17
|
+
}
|