@zereight/mcp-gitlab 1.0.62 → 1.0.64

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 CHANGED
@@ -111,6 +111,7 @@ $ sh scripts/image_push.sh docker_user_name
111
111
  - `USE_GITLAB_WIKI`: When set to 'true', enables the wiki-related tools (list_wiki_pages, get_wiki_page, create_wiki_page, update_wiki_page, delete_wiki_page). By default, wiki features are disabled.
112
112
  - `USE_MILESTONE`: When set to 'true', enables the milestone-related tools (list_milestones, get_milestone, create_milestone, edit_milestone, delete_milestone, get_milestone_issue, get_milestone_merge_requests, promote_milestone, get_milestone_burndown_events). By default, milestone features are disabled.
113
113
  - `USE_PIPELINE`: When set to 'true', enables the pipeline-related tools (list_pipelines, get_pipeline, list_pipeline_jobs, get_pipeline_job, get_pipeline_job_output, create_pipeline, retry_pipeline, cancel_pipeline). By default, pipeline features are disabled.
114
+ - `GITLAB_AUTH_COOKIE_PATH`: Path to an authentication cookie file for GitLab instances that require cookie-based authentication. When provided, the cookie will be included in all GitLab API requests.
114
115
 
115
116
  ## Tools 🛠️
116
117
 
package/build/index.js CHANGED
@@ -3,7 +3,9 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
- import fetch from "node-fetch";
6
+ import nodeFetch from "node-fetch";
7
+ import fetchCookie from "fetch-cookie";
8
+ import { CookieJar, parse as parseCookie } from "tough-cookie";
7
9
  import { SocksProxyAgent } from "socks-proxy-agent";
8
10
  import { HttpsProxyAgent } from "https-proxy-agent";
9
11
  import { HttpProxyAgent } from "http-proxy-agent";
@@ -51,6 +53,7 @@ const server = new Server({
51
53
  },
52
54
  });
53
55
  const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
56
+ const GITLAB_AUTH_COOKIE_PATH = process.env.GITLAB_AUTH_COOKIE_PATH;
54
57
  const IS_OLD = process.env.GITLAB_IS_OLD === "true";
55
58
  const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true";
56
59
  const USE_GITLAB_WIKI = process.env.USE_GITLAB_WIKI === "true";
@@ -91,6 +94,76 @@ if (HTTPS_PROXY) {
91
94
  }
92
95
  httpsAgent = httpsAgent || new HttpsAgent(sslOptions);
93
96
  httpAgent = httpAgent || new Agent();
97
+ // Create cookie jar with clean Netscape file parsing
98
+ const createCookieJar = () => {
99
+ if (!GITLAB_AUTH_COOKIE_PATH)
100
+ return null;
101
+ try {
102
+ const cookiePath = GITLAB_AUTH_COOKIE_PATH.startsWith("~/")
103
+ ? path.join(process.env.HOME || "", GITLAB_AUTH_COOKIE_PATH.slice(2))
104
+ : GITLAB_AUTH_COOKIE_PATH;
105
+ const jar = new CookieJar();
106
+ const cookieContent = fs.readFileSync(cookiePath, "utf8");
107
+ cookieContent.split("\n").forEach(line => {
108
+ // Handle #HttpOnly_ prefix
109
+ if (line.startsWith("#HttpOnly_")) {
110
+ line = line.slice(10);
111
+ }
112
+ // Skip comments and empty lines
113
+ if (line.startsWith("#") || !line.trim()) {
114
+ return;
115
+ }
116
+ // Parse Netscape format: domain, flag, path, secure, expires, name, value
117
+ const parts = line.split("\t");
118
+ if (parts.length >= 7) {
119
+ const [domain, , path, secure, expires, name, value] = parts;
120
+ // Build cookie string in standard format
121
+ const cookieStr = `${name}=${value}; Domain=${domain}; Path=${path}${secure === "TRUE" ? "; Secure" : ""}${expires !== "0" ? `; Expires=${new Date(parseInt(expires) * 1000).toUTCString()}` : ""}`;
122
+ // Use tough-cookie's parse function for robust parsing
123
+ const cookie = parseCookie(cookieStr);
124
+ if (cookie) {
125
+ const url = `${secure === "TRUE" ? "https" : "http"}://${domain.startsWith(".") ? domain.slice(1) : domain}`;
126
+ jar.setCookieSync(cookie, url);
127
+ }
128
+ }
129
+ });
130
+ return jar;
131
+ }
132
+ catch (error) {
133
+ console.error("Error loading cookie file:", error);
134
+ return null;
135
+ }
136
+ };
137
+ // Initialize cookie jar and fetch
138
+ const cookieJar = createCookieJar();
139
+ const fetch = cookieJar ? fetchCookie(nodeFetch, cookieJar) : nodeFetch;
140
+ // Ensure session is established for the current request
141
+ async function ensureSessionForRequest() {
142
+ if (!cookieJar || !GITLAB_AUTH_COOKIE_PATH)
143
+ return;
144
+ // Extract the base URL from GITLAB_API_URL
145
+ const apiUrl = new URL(GITLAB_API_URL);
146
+ const baseUrl = `${apiUrl.protocol}//${apiUrl.hostname}`;
147
+ // Check if we already have GitLab session cookies
148
+ const gitlabCookies = cookieJar.getCookiesSync(baseUrl);
149
+ const hasSessionCookie = gitlabCookies.some(cookie => cookie.key === '_gitlab_session' || cookie.key === 'remember_user_token');
150
+ if (!hasSessionCookie) {
151
+ try {
152
+ // Establish session with a lightweight request
153
+ await fetch(`${GITLAB_API_URL}/user`, {
154
+ ...DEFAULT_FETCH_CONFIG,
155
+ redirect: 'follow'
156
+ }).catch(() => {
157
+ // Ignore errors - the important thing is that cookies get set during redirects
158
+ });
159
+ // Small delay to ensure cookies are fully processed
160
+ await new Promise(resolve => setTimeout(resolve, 100));
161
+ }
162
+ catch (error) {
163
+ // Ignore session establishment errors
164
+ }
165
+ }
166
+ }
94
167
  // Modify DEFAULT_HEADERS to include agent configuration
95
168
  const DEFAULT_HEADERS = {
96
169
  Accept: "application/json",
@@ -366,7 +439,7 @@ const allTools = [
366
439
  },
367
440
  {
368
441
  name: "get_pipeline_job_output",
369
- description: "Get the output/trace of a GitLab pipeline job number",
442
+ description: "Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage",
370
443
  inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema),
371
444
  },
372
445
  {
@@ -1963,9 +2036,11 @@ async function getPipelineJob(projectId, jobId) {
1963
2036
  *
1964
2037
  * @param {string} projectId - The ID or URL-encoded path of the project
1965
2038
  * @param {number} jobId - The ID of the job
2039
+ * @param {number} limit - Maximum number of lines to return from the end (default: 1000)
2040
+ * @param {number} offset - Number of lines to skip from the end (default: 0)
1966
2041
  * @returns {Promise<string>} The job output/trace
1967
2042
  */
1968
- async function getPipelineJobOutput(projectId, jobId) {
2043
+ async function getPipelineJobOutput(projectId, jobId, limit, offset) {
1969
2044
  projectId = decodeURIComponent(projectId); // Decode project ID
1970
2045
  const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/jobs/${jobId}/trace`);
1971
2046
  const response = await fetch(url.toString(), {
@@ -1979,7 +2054,28 @@ async function getPipelineJobOutput(projectId, jobId) {
1979
2054
  throw new Error(`Job trace not found or job is not finished yet`);
1980
2055
  }
1981
2056
  await handleGitLabError(response);
1982
- return await response.text();
2057
+ const fullTrace = await response.text();
2058
+ // Apply client-side pagination to limit context window usage
2059
+ if (limit !== undefined || offset !== undefined) {
2060
+ const lines = fullTrace.split('\n');
2061
+ const startOffset = offset || 0;
2062
+ const maxLines = limit || 1000;
2063
+ // Return lines from the end, skipping offset lines and limiting to maxLines
2064
+ const startIndex = Math.max(0, lines.length - startOffset - maxLines);
2065
+ const endIndex = lines.length - startOffset;
2066
+ const selectedLines = lines.slice(startIndex, endIndex);
2067
+ const result = selectedLines.join('\n');
2068
+ // Add metadata about truncation
2069
+ if (startIndex > 0 || endIndex < lines.length) {
2070
+ const totalLines = lines.length;
2071
+ const shownLines = selectedLines.length;
2072
+ const skippedFromStart = startIndex;
2073
+ const skippedFromEnd = startOffset;
2074
+ return `[Log truncated: showing ${shownLines} of ${totalLines} lines, skipped ${skippedFromStart} from start, ${skippedFromEnd} from end]\n\n${result}`;
2075
+ }
2076
+ return result;
2077
+ }
2078
+ return fullTrace;
1983
2079
  }
1984
2080
  /**
1985
2081
  * Create a new pipeline
@@ -2339,6 +2435,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2339
2435
  if (!request.params.arguments) {
2340
2436
  throw new Error("Arguments are required");
2341
2437
  }
2438
+ // Ensure session is established for every request if cookie authentication is enabled
2439
+ if (GITLAB_AUTH_COOKIE_PATH) {
2440
+ await ensureSessionForRequest();
2441
+ }
2342
2442
  switch (request.params.name) {
2343
2443
  case "fork_repository": {
2344
2444
  const forkArgs = ForkRepositorySchema.parse(request.params.arguments);
@@ -2823,8 +2923,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2823
2923
  };
2824
2924
  }
2825
2925
  case "get_pipeline_job_output": {
2826
- const { project_id, job_id } = GetPipelineJobOutputSchema.parse(request.params.arguments);
2827
- const jobOutput = await getPipelineJobOutput(project_id, job_id);
2926
+ const { project_id, job_id, limit, offset } = GetPipelineJobOutputSchema.parse(request.params.arguments);
2927
+ const jobOutput = await getPipelineJobOutput(project_id, job_id, limit, offset);
2828
2928
  return {
2829
2929
  content: [
2830
2930
  {
package/build/schemas.js CHANGED
@@ -178,6 +178,8 @@ export const CancelPipelineSchema = z.object({
178
178
  export const GetPipelineJobOutputSchema = z.object({
179
179
  project_id: z.string().describe("Project ID or URL-encoded path"),
180
180
  job_id: z.number().describe("The ID of the job"),
181
+ limit: z.number().optional().describe("Maximum number of lines to return from the end of the log (default: 1000)"),
182
+ offset: z.number().optional().describe("Number of lines to skip from the end of the log (default: 0)"),
181
183
  });
182
184
  // User schemas
183
185
  export const GitLabUserSchema = z.object({
@@ -272,14 +274,14 @@ export const GitLabRepositorySchema = z.object({
272
274
  project_access: z
273
275
  .object({
274
276
  access_level: z.number(),
275
- notification_level: z.number().optional(),
277
+ notification_level: z.number().nullable().optional(),
276
278
  })
277
279
  .optional()
278
280
  .nullable(),
279
281
  group_access: z
280
282
  .object({
281
283
  access_level: z.number(),
282
- notification_level: z.number().optional(),
284
+ notification_level: z.number().nullable().optional(),
283
285
  })
284
286
  .optional()
285
287
  .nullable(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zereight/mcp-gitlab",
3
- "version": "1.0.62",
3
+ "version": "1.0.64",
4
4
  "description": "MCP server for using the GitLab API",
5
5
  "license": "MIT",
6
6
  "author": "zereight",
@@ -33,11 +33,13 @@
33
33
  "@modelcontextprotocol/sdk": "1.8.0",
34
34
  "@types/node-fetch": "^2.6.12",
35
35
  "express": "^5.1.0",
36
+ "fetch-cookie": "^3.1.0",
36
37
  "form-data": "^4.0.0",
37
38
  "http-proxy-agent": "^7.0.2",
38
39
  "https-proxy-agent": "^7.0.6",
39
40
  "node-fetch": "^3.3.2",
40
41
  "socks-proxy-agent": "^8.0.5",
42
+ "tough-cookie": "^5.1.2",
41
43
  "zod-to-json-schema": "^3.23.5"
42
44
  },
43
45
  "devDependencies": {