@zereight/mcp-gitlab 2.0.2 → 2.0.4

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.
@@ -20,7 +20,7 @@ export const GitLabPipelineSchema = z.object({
20
20
  duration: z.number().nullable().optional(),
21
21
  started_at: z.string().nullable().optional(),
22
22
  finished_at: z.string().nullable().optional(),
23
- coverage: z.number().nullable().optional(),
23
+ coverage: z.coerce.number().nullable().optional(),
24
24
  user: z
25
25
  .object({
26
26
  id: z.coerce.string(),
@@ -58,7 +58,7 @@ export const GitLabPipelineJobSchema = z.object({
58
58
  name: z.string(),
59
59
  ref: z.string(),
60
60
  tag: flexibleBoolean,
61
- coverage: z.number().nullable().optional(),
61
+ coverage: z.coerce.number().nullable().optional(),
62
62
  created_at: z.string(),
63
63
  started_at: z.string().nullable().optional(),
64
64
  finished_at: z.string().nullable().optional(),
@@ -1578,7 +1578,6 @@ export const GetCommitDiffSchema = z.object({
1578
1578
  project_id: z.coerce.string().describe("Project ID or complete URL-encoded path to project"),
1579
1579
  sha: z.string().describe("The commit hash or name of a repository branch or tag"),
1580
1580
  });
1581
- export const GetCurrentUserSchema = z.object({});
1582
1581
  // Schema for listing issues assigned to the current user
1583
1582
  export const MyIssuesSchema = z.object({
1584
1583
  project_id: z.string().optional().describe("Project ID or URL-encoded path (optional when GITLAB_PROJECT_ID is set)"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "MCP server for using the GitLab API",
5
5
  "license": "MIT",
6
6
  "author": "zereight",
@@ -20,9 +20,7 @@
20
20
  "prepare": "npm run build",
21
21
  "dev": "npm run build && node build/index.js",
22
22
  "watch": "tsc --watch",
23
- "npm:deploy": "npm publish --access public",
24
- "npm:deploy:beta": "npm publish --access public --tag beta",
25
- "generate-tools": "npx ts-node scripts/generate-tools-readme.ts",
23
+ "deploy": "npm publish --access public",
26
24
  "changelog": "auto-changelog -p",
27
25
  "test": "node test/validate-api.js",
28
26
  "test:integration": "node test/validate-api.js",
@@ -33,11 +31,8 @@
33
31
  "format:check": "prettier --check \"**/*.{js,ts,json,md}\""
34
32
  },
35
33
  "dependencies": {
36
- "@modelcontextprotocol/sdk": "1.13.3",
37
- "@noble/hashes": "^1.8.0",
38
- "@types/better-sqlite3": "^7.6.13",
34
+ "@modelcontextprotocol/sdk": "^1.10.0",
39
35
  "@types/node-fetch": "^2.6.12",
40
- "better-sqlite3": "^12.2.0",
41
36
  "express": "^5.1.0",
42
37
  "fetch-cookie": "^3.1.0",
43
38
  "form-data": "^4.0.0",
@@ -47,7 +42,6 @@
47
42
  "pino": "^9.7.0",
48
43
  "pino-pretty": "^13.0.0",
49
44
  "socks-proxy-agent": "^8.0.5",
50
- "sqlite3": "^5.1.7",
51
45
  "tough-cookie": "^5.1.2",
52
46
  "zod-to-json-schema": "^3.23.5"
53
47
  },
@@ -1,41 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { fileURLToPath } from "url";
4
- const __filename = fileURLToPath(import.meta.url);
5
- const __dirname = path.dirname(__filename);
6
- async function main() {
7
- const repoRoot = path.resolve(__dirname, "..");
8
- const indexPath = path.join(repoRoot, "index.ts");
9
- const readmePath = path.join(repoRoot, "README.md");
10
- // 1. Read index.ts
11
- const code = fs.readFileSync(indexPath, "utf-8");
12
- // 2. Extract allTools array block
13
- const match = code.match(/const allTools = \[([\s\S]*?)\];/);
14
- if (!match) {
15
- console.error("Unable to locate allTools array in index.ts");
16
- process.exit(1);
17
- }
18
- const toolsBlock = match[1];
19
- // 3. Parse tool entries
20
- const toolRegex = /name:\s*"([^"]+)",[\s\S]*?description:\s*"([^"]+)"/g;
21
- const tools = [];
22
- let m;
23
- while ((m = toolRegex.exec(toolsBlock)) !== null) {
24
- tools.push({ name: m[1], description: m[2] });
25
- }
26
- // 4. Generate markdown
27
- const lines = tools.map((tool, index) => {
28
- return `${index + 1}. \`${tool.name}\` - ${tool.description}`;
29
- });
30
- const markdown = lines.join("\n");
31
- // 5. Read README.md and replace between markers
32
- const readme = fs.readFileSync(readmePath, "utf-8");
33
- const updated = readme.replace(/<!-- TOOLS-START -->([\s\S]*?)<!-- TOOLS-END -->/, `<!-- TOOLS-START -->\n${markdown}\n<!-- TOOLS-END -->`);
34
- // 6. Write back
35
- fs.writeFileSync(readmePath, updated, "utf-8");
36
- console.log("README.md tools section updated.");
37
- }
38
- main().catch(err => {
39
- console.error(err);
40
- process.exit(1);
41
- });
@@ -1,68 +0,0 @@
1
- import { argon2id } from '@noble/hashes/argon2';
2
- import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils';
3
- import { randomBytes } from 'crypto';
4
- // Default options similar to @node-rs/argon2
5
- const DEFAULT_OPTIONS = {
6
- t: 3, // iterations
7
- m: 65536, // 64MB memory
8
- p: 4, // parallelism
9
- maxmem: 2 ** 32 - 1
10
- };
11
- /**
12
- * Hash a password using argon2id
13
- */
14
- export async function hash(password, options) {
15
- const salt = options?.salt || randomBytes(16);
16
- const passwordBytes = utf8ToBytes(password);
17
- const hashBytes = argon2id(passwordBytes, salt, DEFAULT_OPTIONS);
18
- // Store salt and hash together for verification later
19
- // Format: salt:hash (both as hex)
20
- return `${bytesToHex(salt)}:${bytesToHex(hashBytes)}`;
21
- }
22
- /**
23
- * Synchronous version of hash
24
- */
25
- export function hashSync(password, options) {
26
- const salt = options?.salt || randomBytes(16);
27
- const passwordBytes = utf8ToBytes(password);
28
- const hashBytes = argon2id(passwordBytes, salt, DEFAULT_OPTIONS);
29
- // Store salt and hash together for verification later
30
- // Format: salt:hash (both as hex)
31
- return `${bytesToHex(salt)}:${bytesToHex(hashBytes)}`;
32
- }
33
- /**
34
- * Verify a password against a hash
35
- */
36
- export async function verify(storedHash, password, options) {
37
- try {
38
- // If options.salt is provided, it means we're using the old format
39
- // where salt was provided separately
40
- if (options?.salt) {
41
- const passwordBytes = utf8ToBytes(password);
42
- const hashBytes = argon2id(passwordBytes, options.salt, DEFAULT_OPTIONS);
43
- const newHash = bytesToHex(hashBytes);
44
- // storedHash might be just the hash part without salt prefix
45
- const hashPart = storedHash.includes(':') ? storedHash.split(':')[1] : storedHash;
46
- return newHash === hashPart;
47
- }
48
- // New format: salt:hash
49
- const [saltHex, hashHex] = storedHash.split(':');
50
- if (!saltHex || !hashHex) {
51
- return false;
52
- }
53
- const salt = hexToBytes(saltHex);
54
- const passwordBytes = utf8ToBytes(password);
55
- const computedHashBytes = argon2id(passwordBytes, salt, DEFAULT_OPTIONS);
56
- const computedHashHex = bytesToHex(computedHashBytes);
57
- return computedHashHex === hashHex;
58
- }
59
- catch (error) {
60
- return false;
61
- }
62
- }
63
- // Export as default object to match @node-rs/argon2 interface
64
- export default {
65
- hash,
66
- hashSync,
67
- verify
68
- };
@@ -1,78 +0,0 @@
1
- import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
2
- import { config } from './config.js';
3
- import { createGitLabOAuthProvider } from './oauth.js';
4
- import { logger } from './logger.js';
5
- /**
6
- * Configure authentication middleware based on the configuration
7
- * Supports OAuth2, PAT passthrough, and static PAT modes
8
- */
9
- export async function configureAuthentication(app) {
10
- // Default middleware that does nothing
11
- let authMiddleware = undefined;
12
- // OAuth2 mode
13
- if (config.GITLAB_OAUTH2_CLIENT_ID) {
14
- logger.warn("Configuring GitLab OAuth2 proxy authentication");
15
- logger.warn("Please note that GitLab OAuth2 proxy authentication is not yet fully supported. Use this feature at your own risk");
16
- // Create the provider
17
- const provider = await createGitLabOAuthProvider();
18
- // Add the callback handler route BEFORE the OAuth router
19
- app.get("/callback", (req, res) => provider.handleOAuthCallback(req, res));
20
- // Set up OAuth2 proxy router
21
- const oauth2Router = provider.createOAuth2Router();
22
- app.use(oauth2Router);
23
- // Create token verifier and bearer auth middleware
24
- const tokenVerifier = provider.createTokenVerifier();
25
- const bearerAuthMiddleware = requireBearerAuth({
26
- verifier: tokenVerifier,
27
- resourceMetadataUrl: `${config.GITLAB_OAUTH2_BASE_URL}/.well-known/oauth-protected-resource`
28
- });
29
- authMiddleware = (req, res, next) => {
30
- // Ensure GitLab-Token header is not set in OAuth2 mode
31
- const gitlabToken = req.headers["gitlab-token"];
32
- if (gitlabToken) {
33
- res.status(401).send("Gitlab-Token header must not be set when MCP is running in OAuth2 mode");
34
- return;
35
- }
36
- bearerAuthMiddleware(req, res, next);
37
- };
38
- }
39
- // PAT passthrough mode
40
- else if (config.GITLAB_PAT_PASSTHROUGH) {
41
- logger.info("Configuring GitLab PAT passthrough authentication. Users must set the Gitlab-Token header in their requests");
42
- authMiddleware = (req, res, next) => {
43
- // Check the Gitlab-Token header
44
- const token = req.headers["gitlab-token"];
45
- if (!token) {
46
- res.status(401).send("Please set a Gitlab-Token header in your request");
47
- return;
48
- }
49
- if (typeof token !== "string") {
50
- res.status(401).send("Gitlab-Token must only be set once");
51
- return;
52
- }
53
- req.auth = {
54
- token: token,
55
- clientId: "!passthrough",
56
- scopes: [],
57
- };
58
- next();
59
- };
60
- }
61
- // Static PAT mode
62
- else if (config.GITLAB_PERSONAL_ACCESS_TOKEN) {
63
- logger.info("Configuring static GitLab Personal Access Token authentication");
64
- const accessToken = config.GITLAB_PERSONAL_ACCESS_TOKEN;
65
- authMiddleware = (req, res, next) => {
66
- req.auth = {
67
- token: accessToken,
68
- clientId: "!global",
69
- scopes: [],
70
- };
71
- next();
72
- };
73
- }
74
- if (authMiddleware === undefined) {
75
- throw new Error("No authMiddleware configured. This is a bug. Please report it.");
76
- }
77
- return authMiddleware;
78
- }
@@ -1,44 +0,0 @@
1
- import { config } from "./config.js";
2
- import { CookieJar, parse as parseCookie } from "tough-cookie";
3
- import fs from "fs";
4
- import path from "path";
5
- // Create cookie jar with clean Netscape file parsing
6
- export const createCookieJar = () => {
7
- if (!config.GITLAB_AUTH_COOKIE_PATH)
8
- return undefined;
9
- try {
10
- const cookiePath = config.GITLAB_AUTH_COOKIE_PATH.startsWith("~/")
11
- ? path.join(process.env.HOME || "", config.GITLAB_AUTH_COOKIE_PATH.slice(2))
12
- : config.GITLAB_AUTH_COOKIE_PATH;
13
- const jar = new CookieJar();
14
- const cookieContent = fs.readFileSync(cookiePath, "utf8");
15
- cookieContent.split("\n").forEach(line => {
16
- // Handle #HttpOnly_ prefix
17
- if (line.startsWith("#HttpOnly_")) {
18
- line = line.slice(10);
19
- }
20
- // Skip comments and empty lines
21
- if (line.startsWith("#") || !line.trim()) {
22
- return;
23
- }
24
- // Parse Netscape format: domain, flag, path, secure, expires, name, value
25
- const parts = line.split("\t");
26
- if (parts.length >= 7) {
27
- const [domain, , path, secure, expires, name, value] = parts;
28
- // Build cookie string in standard format
29
- const cookieStr = `${name}=${value}; Domain=${domain}; Path=${path}${secure === "TRUE" ? "; Secure" : ""}${expires !== "0" ? `; Expires=${new Date(parseInt(expires) * 1000).toUTCString()}` : ""}`;
30
- // Use tough-cookie's parse function for robust parsing
31
- const cookie = parseCookie(cookieStr);
32
- if (cookie) {
33
- const url = `${secure === "TRUE" ? "https" : "http"}://${domain.startsWith(".") ? domain.slice(1) : domain}`;
34
- jar.setCookieSync(cookie, url);
35
- }
36
- }
37
- });
38
- return jar;
39
- }
40
- catch (error) {
41
- console.error("Error loading cookie file:", error);
42
- return undefined;
43
- }
44
- };
@@ -1,99 +0,0 @@
1
- export const unsafeDefaultArgon2Salt = "change-me-in-production";
2
- export const config = {
3
- HOST: process.env.HOST || '127.0.0.1',
4
- PORT: process.env.PORT || 3002,
5
- SSE: process.env.SSE === "true",
6
- STREAMABLE_HTTP: process.env.STREAMABLE_HTTP === "true",
7
- IS_OLD: process.env.GITLAB_IS_OLD === "true",
8
- GITLAB_READ_ONLY_MODE: process.env.GITLAB_READ_ONLY_MODE === "true",
9
- USE_GITLAB_WIKI: process.env.USE_GITLAB_WIKI === "true",
10
- USE_MILESTONE: process.env.USE_MILESTONE === "true",
11
- USE_PIPELINE: process.env.USE_PIPELINE === "true",
12
- // Add proxy configuration
13
- HTTP_PROXY: process.env.HTTP_PROXY,
14
- HTTPS_PROXY: process.env.HTTPS_PROXY,
15
- NODE_TLS_REJECT_UNAUTHORIZED: process.env.NODE_TLS_REJECT_UNAUTHORIZED,
16
- GITLAB_CA_CERT_PATH: process.env.GITLAB_CA_CERT_PATH,
17
- // Use the normalizeGitLabApiUrl function to handle various URL formats
18
- GITLAB_API_URL: normalizeGitLabApiUrl(process.env.GITLAB_API_URL || undefined),
19
- GITLAB_PROJECT_ID: process.env.GITLAB_PROJECT_ID,
20
- ARGON2_SALT: process.env.ARGON2_SALT || unsafeDefaultArgon2Salt,
21
- // Configure cookie auth path, for gitlab instances which require it
22
- // TODO: investigate the consequences of this with oauth2 and pat passthrough. should it only be used in PAT mode and not passthrough?
23
- GITLAB_AUTH_COOKIE_PATH: process.env.GITLAB_AUTH_COOKIE_PATH,
24
- // only one of GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_OAUTH2_CLIENT_ID, GITLAB_PAT_PASSTHROUGH
25
- // Gitlab PAT configuration. use this PAT to authenticate all requests
26
- GITLAB_PERSONAL_ACCESS_TOKEN: process.env.GITLAB_PERSONAL_ACCESS_TOKEN,
27
- // Gitlab PAT passthrough. pass through the PRIVATE-TOKEN header to make the request to the Gitlab API
28
- // should be "true" to enable
29
- GITLAB_PAT_PASSTHROUGH: process.env.GITLAB_PAT_PASSTHROUGH === "true",
30
- // GitLab OAuth2 configuration
31
- GITLAB_OAUTH2_CLIENT_ID: process.env.GITLAB_OAUTH2_CLIENT_ID,
32
- GITLAB_OAUTH2_CLIENT_SECRET: process.env.GITLAB_OAUTH2_CLIENT_SECRET,
33
- GITLAB_OAUTH2_REDIRECT_URL: process.env.GITLAB_OAUTH2_REDIRECT_URL,
34
- // we need a database in order for the oauth2 provider to persist clients across restarts.
35
- GITLAB_OAUTH2_DB_PATH: process.env.GITLAB_OAUTH2_DB_PATH || ":memory:",
36
- // base url matters for the redirect url, i think?
37
- GITLAB_OAUTH2_BASE_URL: process.env.GITLAB_OAUTH2_BASE_URL, // http://localhost:3002
38
- // TODO: maybe thse can be formed based off of the ISSUER_URL? im not sure... (could introduce problems if gitlab ever changes these endpoints, though i doubt they will)
39
- GITLAB_OAUTH2_TOKEN_URL: process.env.GITLAB_OAUTH2_TOKEN_URL, // https://gitlab.com/oauth/token
40
- GITLAB_OAUTH2_AUTHORIZATION_URL: process.env.GITLAB_OAUTH2_AUTHORIZATION_URL, // https://gitlab.com/oauth/authorize
41
- GITLAB_OAUTH2_REVOCATION_URL: process.env.GITLAB_OAUTH2_REVOCATION_URL, // https://gitlab.com/oauth/revoke
42
- GITLAB_OAUTH2_INTROSPECTION_URL: process.env.GITLAB_OAUTH2_INTROSPECTION_URL, // https://gitlab.com/oauth/introspect
43
- GITLAB_OAUTH2_REGISTRATION_URL: process.env.GITLAB_OAUTH2_REGISTRATION_URL, // ?
44
- GITLAB_OAUTH2_ISSUER_URL: process.env.GITLAB_OAUTH2_ISSUER_URL, // https://gitlab.com
45
- };
46
- export const validateConfiguration = () => {
47
- // Check if using default ARGON2_SALT
48
- if (config.ARGON2_SALT === unsafeDefaultArgon2Salt) {
49
- console.error('\n' + '='.repeat(80));
50
- console.error('⚠️ WARNING: USING DEFAULT ARGON2_SALT VALUE!');
51
- console.error('='.repeat(80));
52
- console.error('');
53
- console.error('You are using the default ARGON2_SALT value which is INSECURE.');
54
- console.error('This salt is publicly known and makes your password hashes vulnerable.');
55
- console.error('');
56
- console.error('Please set the ARGON2_SALT environment variable to a unique, random value:');
57
- console.error(' export ARGON2_SALT="your-unique-random-salt-here"');
58
- console.error('');
59
- console.error('You can generate a secure salt with:');
60
- console.error(' openssl rand -base64 32');
61
- console.error('');
62
- console.error('='.repeat(80) + '\n');
63
- }
64
- // check that only one of GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_OAUTH2_CLIENT_ID, GITLAB_PAT_PASSTHROUGH is set
65
- const onlyOnOf = [
66
- config.GITLAB_PERSONAL_ACCESS_TOKEN,
67
- config.GITLAB_OAUTH2_CLIENT_ID,
68
- config.GITLAB_PAT_PASSTHROUGH
69
- ];
70
- const countOfSet = onlyOnOf.filter(x => !!x).length;
71
- const allVariableNames = "GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_OAUTH2_CLIENT_ID, GITLAB_PAT_PASSTHROUGH";
72
- if (countOfSet == 0) {
73
- console.error(`One of the following variables must be set: ${allVariableNames}`);
74
- process.exit(1);
75
- }
76
- if (countOfSet > 1) {
77
- console.error(`Only one of the following variables can be set: ${allVariableNames}`);
78
- process.exit(1);
79
- }
80
- };
81
- /**
82
- * Smart URL handling for GitLab API
83
- *
84
- * @param {string | undefined} url - Input GitLab API URL
85
- * @returns {string} Normalized GitLab API URL with /api/v4 path
86
- */
87
- function normalizeGitLabApiUrl(url) {
88
- if (!url) {
89
- return "https://gitlab.com/api/v4";
90
- }
91
- // Remove trailing slash if present
92
- let normalizedUrl = url.endsWith("/") ? url.slice(0, -1) : url;
93
- // Check if URL already has /api/v4
94
- if (!normalizedUrl.endsWith("/api/v4") && !normalizedUrl.endsWith("/api/v4/")) {
95
- // Append /api/v4 if not already present
96
- normalizedUrl = `${normalizedUrl}/api/v4`;
97
- }
98
- return normalizedUrl;
99
- }