@zereight/mcp-gitlab 2.1.24 → 2.1.26
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/README.md +5 -0
- package/build/config.js +1 -0
- package/build/index.js +644 -324
- package/build/oauth.js +65 -3
- package/build/schemas.js +475 -197
- package/build/test/dynamic-api-url-test.js +3 -3
- package/build/test/oauth-tests.js +39 -0
- package/build/test/remote-auth-simple-test.js +13 -2
- package/build/test/schema-tests.js +51 -0
- package/build/test/streamable-http-concurrent-session.test.js +92 -0
- package/build/test/streamable-http-unauthenticated-discovery.test.js +113 -0
- package/build/test/test-ci-catalog.js +177 -0
- package/build/test/test-create-repository.js +120 -0
- package/build/test/test-list-issues.js +15 -3
- package/build/test/test-toolset-filtering.js +6 -5
- package/build/test/test-update-project.js +112 -0
- package/build/test/utils/forwarded-public-base-url.test.js +38 -0
- package/build/tools/registry.js +26 -3
- package/build/utils/forwarded-public-base-url.js +62 -0
- package/build/utils/schema.js +15 -1
- package/package.json +4 -2
package/build/oauth.js
CHANGED
|
@@ -5,6 +5,8 @@ import * as path from "path";
|
|
|
5
5
|
import * as http from "http";
|
|
6
6
|
import * as net from "net";
|
|
7
7
|
import * as url from "url";
|
|
8
|
+
import { execFile } from "child_process";
|
|
9
|
+
import { promisify } from "util";
|
|
8
10
|
import open from "open";
|
|
9
11
|
import pkceChallenge from "pkce-challenge";
|
|
10
12
|
import { pino } from "pino";
|
|
@@ -12,6 +14,7 @@ const logger = pino({
|
|
|
12
14
|
name: "gitlab-mcp-oauth",
|
|
13
15
|
level: process.env.LOG_LEVEL || "info",
|
|
14
16
|
}, pino.destination(2));
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
15
18
|
function escapeHtml(str) {
|
|
16
19
|
const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
17
20
|
return String(str).replace(/[&<>"']/g, c => map[c] || c);
|
|
@@ -456,10 +459,64 @@ export class GitLabOAuth {
|
|
|
456
459
|
});
|
|
457
460
|
});
|
|
458
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* Get an access token from an external command.
|
|
464
|
+
*/
|
|
465
|
+
async getScriptToken() {
|
|
466
|
+
if (!this.config.tokenScript) {
|
|
467
|
+
throw new Error("OAuth token script is not configured");
|
|
468
|
+
}
|
|
469
|
+
const shell = process.platform === "win32" ? process.env.ComSpec || "cmd.exe" : "/bin/sh";
|
|
470
|
+
const args = process.platform === "win32"
|
|
471
|
+
? ["/d", "/s", "/c", this.config.tokenScript]
|
|
472
|
+
: ["-c", this.config.tokenScript];
|
|
473
|
+
const timeoutSeconds = Number.parseInt(process.env.GITLAB_OAUTH_TOKEN_SCRIPT_TIMEOUT_SECONDS || "30", 10);
|
|
474
|
+
const timeoutMs = (Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? timeoutSeconds : 30) * 1000;
|
|
475
|
+
const { stdout } = await execFileAsync(shell, args, {
|
|
476
|
+
timeout: timeoutMs,
|
|
477
|
+
maxBuffer: 1024 * 1024,
|
|
478
|
+
});
|
|
479
|
+
const output = stdout.trim();
|
|
480
|
+
if (!output) {
|
|
481
|
+
throw new Error("OAuth token script produced no output");
|
|
482
|
+
}
|
|
483
|
+
let accessToken = output;
|
|
484
|
+
try {
|
|
485
|
+
const parsed = JSON.parse(output);
|
|
486
|
+
if (typeof parsed === "string") {
|
|
487
|
+
accessToken = parsed;
|
|
488
|
+
}
|
|
489
|
+
else if (parsed && typeof parsed === "object") {
|
|
490
|
+
const value = parsed.access_token ??
|
|
491
|
+
parsed.token;
|
|
492
|
+
if (typeof value !== "string") {
|
|
493
|
+
throw new Error("OAuth token script JSON must include a string access_token or token field");
|
|
494
|
+
}
|
|
495
|
+
accessToken = value;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
if (error instanceof Error && error.message.startsWith("OAuth token script JSON")) {
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
502
|
+
// Plain-token stdout is the common case.
|
|
503
|
+
}
|
|
504
|
+
if (!accessToken.trim()) {
|
|
505
|
+
throw new Error("OAuth token script returned an empty token");
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
access_token: accessToken.trim(),
|
|
509
|
+
created_at: Date.now(),
|
|
510
|
+
token_type: "Bearer",
|
|
511
|
+
};
|
|
512
|
+
}
|
|
459
513
|
/**
|
|
460
514
|
* Get a valid access token, refreshing if necessary
|
|
461
515
|
*/
|
|
462
516
|
async getAccessToken(force = false) {
|
|
517
|
+
if (this.config.tokenScript) {
|
|
518
|
+
return (await this.getScriptToken()).access_token;
|
|
519
|
+
}
|
|
463
520
|
let tokenData = this.loadToken();
|
|
464
521
|
// If no token or expired (or forced), start OAuth flow or refresh
|
|
465
522
|
if (!tokenData) {
|
|
@@ -503,6 +560,9 @@ export class GitLabOAuth {
|
|
|
503
560
|
* Check if a valid token exists
|
|
504
561
|
*/
|
|
505
562
|
hasValidToken() {
|
|
563
|
+
if (this.config.tokenScript) {
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
506
566
|
const tokenData = this.loadToken();
|
|
507
567
|
if (!tokenData) {
|
|
508
568
|
return false;
|
|
@@ -520,16 +580,18 @@ export async function initializeOAuthClient(gitlabUrl = "https://gitlab.com") {
|
|
|
520
580
|
const clientSecret = process.env.GITLAB_OAUTH_CLIENT_SECRET;
|
|
521
581
|
const redirectUri = process.env.GITLAB_OAUTH_REDIRECT_URI || "http://127.0.0.1:8888/callback";
|
|
522
582
|
const tokenStoragePath = process.env.GITLAB_OAUTH_TOKEN_PATH;
|
|
523
|
-
|
|
524
|
-
|
|
583
|
+
const tokenScript = process.env.GITLAB_OAUTH_TOKEN_SCRIPT;
|
|
584
|
+
if (!clientId && !tokenScript) {
|
|
585
|
+
throw new Error("GITLAB_OAUTH_CLIENT_ID or GITLAB_OAUTH_TOKEN_SCRIPT environment variable is required for OAuth authentication");
|
|
525
586
|
}
|
|
526
587
|
const oauth = new GitLabOAuth({
|
|
527
|
-
clientId,
|
|
588
|
+
clientId: clientId || "external-token-script",
|
|
528
589
|
clientSecret,
|
|
529
590
|
redirectUri,
|
|
530
591
|
gitlabUrl,
|
|
531
592
|
scopes: [process.env.GITLAB_READ_ONLY_MODE === "true" ? "read_api" : "api"],
|
|
532
593
|
tokenStoragePath,
|
|
594
|
+
tokenScript,
|
|
533
595
|
});
|
|
534
596
|
// Single call: triggers browser flow if needed, or reads cached token
|
|
535
597
|
const accessToken = await oauth.getAccessToken();
|