@zereight/mcp-gitlab 2.0.22 → 2.0.23
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 +32 -4
- package/build/index.js +275 -132
- package/build/schemas.js +42 -11
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -6,12 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
## @zereight/mcp-gitlab
|
|
8
8
|
|
|
9
|
-
[](https://smithery.ai/server/@zereight/gitlab-mcp)
|
|
10
|
-
|
|
11
9
|
GitLab MCP(Model Context Protocol) Server. **Includes bug fixes and improvements over the original GitLab MCP server.**
|
|
12
10
|
|
|
13
|
-
<a href="https://glama.ai/mcp/servers/7jwbk4r6d7"><img width="380" height="200" src="https://glama.ai/mcp/servers/7jwbk4r6d7/badge" alt="gitlab mcp MCP server" /></a>
|
|
14
|
-
|
|
15
11
|
## Usage
|
|
16
12
|
|
|
17
13
|
### Using with Claude App, Cline, Roo Code, Cursor, Kilo Code
|
|
@@ -94,6 +90,38 @@ Then configure the MCP server with OAuth:
|
|
|
94
90
|
}
|
|
95
91
|
```
|
|
96
92
|
|
|
93
|
+
#### Using CLI Arguments (for clients with env var issues)
|
|
94
|
+
|
|
95
|
+
Some MCP clients (like GitHub Copilot CLI) have issues with environment variables. Use CLI arguments instead:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"gitlab": {
|
|
101
|
+
"command": "npx",
|
|
102
|
+
"args": [
|
|
103
|
+
"-y",
|
|
104
|
+
"@zereight/mcp-gitlab",
|
|
105
|
+
"--token=YOUR_GITLAB_TOKEN",
|
|
106
|
+
"--api-url=https://gitlab.com/api/v4"
|
|
107
|
+
],
|
|
108
|
+
"tools": ["*"]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Available CLI arguments:**
|
|
115
|
+
|
|
116
|
+
- `--token` - GitLab Personal Access Token (replaces `GITLAB_PERSONAL_ACCESS_TOKEN`)
|
|
117
|
+
- `--api-url` - GitLab API URL (replaces `GITLAB_API_URL`)
|
|
118
|
+
- `--read-only=true` - Enable read-only mode (replaces `GITLAB_READ_ONLY_MODE`)
|
|
119
|
+
- `--use-wiki=true` - Enable wiki API (replaces `USE_GITLAB_WIKI`)
|
|
120
|
+
- `--use-milestone=true` - Enable milestone API (replaces `USE_MILESTONE`)
|
|
121
|
+
- `--use-pipeline=true` - Enable pipeline API (replaces `USE_PIPELINE`)
|
|
122
|
+
|
|
123
|
+
CLI arguments take precedence over environment variables.
|
|
124
|
+
|
|
97
125
|
#### vscode .vscode/mcp.json
|
|
98
126
|
|
|
99
127
|
**Using OAuth2 (Non-Confidential - Recommended):**
|
package/build/index.js
CHANGED
|
@@ -1,28 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// Parse CLI arguments
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
const cliArgs = {};
|
|
5
|
+
for (let i = 0; i < args.length; i++) {
|
|
6
|
+
const arg = args[i];
|
|
7
|
+
if (arg.startsWith('--')) {
|
|
8
|
+
const [key, value] = arg.slice(2).split('=');
|
|
9
|
+
if (value) {
|
|
10
|
+
cliArgs[key] = value;
|
|
11
|
+
}
|
|
12
|
+
else if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
13
|
+
cliArgs[key] = args[++i];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function getConfig(cliKey, envKey, defaultValue) {
|
|
18
|
+
return cliArgs[cliKey] || process.env[envKey] || defaultValue;
|
|
19
|
+
}
|
|
2
20
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
21
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
4
22
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
23
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
24
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
25
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
26
|
import express from "express";
|
|
9
27
|
import fetchCookie from "fetch-cookie";
|
|
10
|
-
import fs from "fs";
|
|
28
|
+
import fs from "node:fs";
|
|
11
29
|
import { HttpProxyAgent } from "http-proxy-agent";
|
|
12
30
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
13
31
|
import nodeFetch from "node-fetch";
|
|
14
|
-
import path, { dirname } from "path";
|
|
32
|
+
import path, { dirname } from "node:path";
|
|
15
33
|
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
16
34
|
import { CookieJar, parse as parseCookie } from "tough-cookie";
|
|
17
|
-
import { fileURLToPath } from "url";
|
|
35
|
+
import { fileURLToPath, URL } from "node:url";
|
|
18
36
|
import { z } from "zod";
|
|
19
37
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
20
38
|
import { initializeOAuth } from "./oauth.js";
|
|
21
39
|
import { GitLabClientPool } from "./gitlab-client-pool.js";
|
|
22
40
|
// Add type imports for proxy agents
|
|
23
|
-
import { Agent } from "http";
|
|
24
|
-
import { Agent as HttpsAgent } from "https";
|
|
25
|
-
import { URL } from "url";
|
|
41
|
+
import { Agent } from "node:http";
|
|
42
|
+
import { Agent as HttpsAgent } from "node:https";
|
|
26
43
|
import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateLabelSchema, // Added
|
|
27
44
|
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, DeleteDraftNoteSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema,
|
|
28
45
|
// pipeline job schemas
|
|
@@ -32,8 +49,8 @@ GitLabDiscussionNoteSchema, // Added
|
|
|
32
49
|
GitLabDiscussionSchema,
|
|
33
50
|
// Draft Notes Schemas
|
|
34
51
|
GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabTreeSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
35
|
-
ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, MarkdownUploadSchema, DownloadAttachmentSchema, MergeMergeRequestSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema } from "./schemas.js";
|
|
36
|
-
import { randomUUID } from "crypto";
|
|
52
|
+
ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, MarkdownUploadSchema, DownloadAttachmentSchema, MergeMergeRequestSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, } from "./schemas.js";
|
|
53
|
+
import { randomUUID } from "node:crypto";
|
|
37
54
|
import { pino } from "pino";
|
|
38
55
|
const logger = pino({
|
|
39
56
|
level: process.env.LOG_LEVEL || "info",
|
|
@@ -68,8 +85,8 @@ try {
|
|
|
68
85
|
SERVER_VERSION = packageJson.version || SERVER_VERSION;
|
|
69
86
|
}
|
|
70
87
|
}
|
|
71
|
-
catch
|
|
72
|
-
//
|
|
88
|
+
catch {
|
|
89
|
+
// Intentionally ignored: version read failure is non-critical
|
|
73
90
|
}
|
|
74
91
|
const server = new Server({
|
|
75
92
|
name: "better-gitlab-mcp-server",
|
|
@@ -87,9 +104,9 @@ function validateConfiguration() {
|
|
|
87
104
|
// Validate SESSION_TIMEOUT_SECONDS
|
|
88
105
|
const timeoutStr = process.env.SESSION_TIMEOUT_SECONDS;
|
|
89
106
|
if (timeoutStr) {
|
|
90
|
-
const timeout = parseInt(timeoutStr);
|
|
107
|
+
const timeout = Number.parseInt(timeoutStr, 10);
|
|
91
108
|
// Allow values >=1 for testing purposes, but recommend 60-86400 for production
|
|
92
|
-
if (isNaN(timeout) || timeout < 1 || timeout > 86400) {
|
|
109
|
+
if (Number.isNaN(timeout) || timeout < 1 || timeout > 86400) {
|
|
93
110
|
errors.push(`SESSION_TIMEOUT_SECONDS must be between 1 and 86400 seconds, got: ${timeoutStr}`);
|
|
94
111
|
}
|
|
95
112
|
if (timeout < 60) {
|
|
@@ -99,80 +116,85 @@ function validateConfiguration() {
|
|
|
99
116
|
// Validate MAX_SESSIONS
|
|
100
117
|
const maxSessionsStr = process.env.MAX_SESSIONS;
|
|
101
118
|
if (maxSessionsStr) {
|
|
102
|
-
const maxSessions = parseInt(maxSessionsStr);
|
|
103
|
-
if (isNaN(maxSessions) || maxSessions < 1 || maxSessions > 10000) {
|
|
119
|
+
const maxSessions = Number.parseInt(maxSessionsStr, 10);
|
|
120
|
+
if (Number.isNaN(maxSessions) || maxSessions < 1 || maxSessions > 10000) {
|
|
104
121
|
errors.push(`MAX_SESSIONS must be between 1 and 10000, got: ${maxSessionsStr}`);
|
|
105
122
|
}
|
|
106
123
|
}
|
|
107
124
|
// Validate MAX_REQUESTS_PER_MINUTE
|
|
108
125
|
const maxReqStr = process.env.MAX_REQUESTS_PER_MINUTE;
|
|
109
126
|
if (maxReqStr) {
|
|
110
|
-
const maxReq = parseInt(maxReqStr);
|
|
111
|
-
if (isNaN(maxReq) || maxReq < 1 || maxReq > 1000) {
|
|
127
|
+
const maxReq = Number.parseInt(maxReqStr, 10);
|
|
128
|
+
if (Number.isNaN(maxReq) || maxReq < 1 || maxReq > 1000) {
|
|
112
129
|
errors.push(`MAX_REQUESTS_PER_MINUTE must be between 1 and 1000, got: ${maxReqStr}`);
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
132
|
// Validate PORT
|
|
116
|
-
const portStr =
|
|
133
|
+
const portStr = getConfig('port', 'PORT');
|
|
117
134
|
if (portStr) {
|
|
118
|
-
const port = parseInt(portStr);
|
|
119
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
135
|
+
const port = Number.parseInt(portStr, 10);
|
|
136
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
120
137
|
errors.push(`PORT must be between 1 and 65535, got: ${portStr}`);
|
|
121
138
|
}
|
|
122
139
|
}
|
|
123
140
|
// Validate GITLAB_API_URL format
|
|
124
|
-
const apiUrls =
|
|
141
|
+
const apiUrls = getConfig('api-url', 'GITLAB_API_URL')?.split(",") || [];
|
|
125
142
|
if (apiUrls.length > 0) {
|
|
126
143
|
for (const url of apiUrls) {
|
|
127
144
|
try {
|
|
128
145
|
new URL(url.trim());
|
|
129
146
|
}
|
|
130
|
-
catch
|
|
147
|
+
catch {
|
|
131
148
|
errors.push(`GITLAB_API_URL contains an invalid URL: ${url.trim()}`);
|
|
132
149
|
}
|
|
133
150
|
}
|
|
134
151
|
}
|
|
135
152
|
// Validate auth configuration
|
|
136
|
-
const remoteAuth =
|
|
137
|
-
const useOAuth =
|
|
138
|
-
const hasToken = !!
|
|
139
|
-
const hasCookie = !!
|
|
153
|
+
const remoteAuth = getConfig('remote-auth', 'REMOTE_AUTHORIZATION') === "true";
|
|
154
|
+
const useOAuth = getConfig('use-oauth', 'GITLAB_USE_OAUTH') === "true";
|
|
155
|
+
const hasToken = !!getConfig('token', 'GITLAB_PERSONAL_ACCESS_TOKEN');
|
|
156
|
+
const hasCookie = !!getConfig('cookie-path', 'GITLAB_AUTH_COOKIE_PATH');
|
|
140
157
|
if (!remoteAuth && !useOAuth && !hasToken && !hasCookie) {
|
|
141
|
-
errors.push('Either
|
|
158
|
+
errors.push('Either --token, --cookie-path, --use-oauth=true, or --remote-auth=true must be set (or use environment variables)');
|
|
142
159
|
}
|
|
143
|
-
|
|
144
|
-
|
|
160
|
+
const enableDynamicApiUrl = getConfig('enable-dynamic-api-url', 'ENABLE_DYNAMIC_API_URL') === "true";
|
|
161
|
+
if (enableDynamicApiUrl && !remoteAuth) {
|
|
162
|
+
errors.push("ENABLE_DYNAMIC_API_URL=true requires REMOTE_AUTHORIZATION=true");
|
|
145
163
|
}
|
|
146
164
|
if (errors.length > 0) {
|
|
147
|
-
logger.error(
|
|
165
|
+
logger.error("Configuration validation failed:");
|
|
148
166
|
errors.forEach(err => logger.error(` - ${err}`));
|
|
149
167
|
process.exit(1);
|
|
150
168
|
}
|
|
151
|
-
logger.info(
|
|
169
|
+
logger.info("Configuration validation passed");
|
|
152
170
|
}
|
|
153
|
-
const GITLAB_PERSONAL_ACCESS_TOKEN =
|
|
171
|
+
const GITLAB_PERSONAL_ACCESS_TOKEN = getConfig('token', 'GITLAB_PERSONAL_ACCESS_TOKEN');
|
|
154
172
|
let OAUTH_ACCESS_TOKEN = null;
|
|
155
|
-
const GITLAB_AUTH_COOKIE_PATH =
|
|
156
|
-
const USE_OAUTH =
|
|
157
|
-
const IS_OLD =
|
|
158
|
-
const GITLAB_READ_ONLY_MODE =
|
|
159
|
-
const GITLAB_DENIED_TOOLS_REGEX =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const
|
|
173
|
+
const GITLAB_AUTH_COOKIE_PATH = getConfig('cookie-path', 'GITLAB_AUTH_COOKIE_PATH');
|
|
174
|
+
const USE_OAUTH = getConfig('use-oauth', 'GITLAB_USE_OAUTH') === "true";
|
|
175
|
+
const IS_OLD = getConfig('is-old', 'GITLAB_IS_OLD') === "true";
|
|
176
|
+
const GITLAB_READ_ONLY_MODE = getConfig('read-only', 'GITLAB_READ_ONLY_MODE') === "true";
|
|
177
|
+
const GITLAB_DENIED_TOOLS_REGEX = getConfig('denied-tools-regex', 'GITLAB_DENIED_TOOLS_REGEX')
|
|
178
|
+
? new RegExp(getConfig('denied-tools-regex', 'GITLAB_DENIED_TOOLS_REGEX'))
|
|
179
|
+
: undefined;
|
|
180
|
+
const USE_GITLAB_WIKI = getConfig('use-wiki', 'USE_GITLAB_WIKI') === "true";
|
|
181
|
+
const USE_MILESTONE = getConfig('use-milestone', 'USE_MILESTONE') === "true";
|
|
182
|
+
const USE_PIPELINE = getConfig('use-pipeline', 'USE_PIPELINE') === "true";
|
|
183
|
+
const SSE = getConfig('sse', 'SSE') === "true";
|
|
184
|
+
const STREAMABLE_HTTP = getConfig('streamable-http', 'STREAMABLE_HTTP') === "true";
|
|
185
|
+
const REMOTE_AUTHORIZATION = getConfig('remote-auth', 'REMOTE_AUTHORIZATION') === "true";
|
|
186
|
+
const ENABLE_DYNAMIC_API_URL = getConfig('enable-dynamic-api-url', 'ENABLE_DYNAMIC_API_URL') === "true";
|
|
187
|
+
const SESSION_TIMEOUT_SECONDS = Number.parseInt(getConfig('session-timeout', 'SESSION_TIMEOUT_SECONDS', '3600'), 10);
|
|
188
|
+
const HOST = getConfig('host', 'HOST') || '127.0.0.1';
|
|
189
|
+
const PORT = Number.parseInt(getConfig('port', 'PORT', '3002'), 10);
|
|
170
190
|
// Add proxy configuration
|
|
171
|
-
const HTTP_PROXY =
|
|
172
|
-
const HTTPS_PROXY =
|
|
173
|
-
const NODE_TLS_REJECT_UNAUTHORIZED =
|
|
174
|
-
const GITLAB_CA_CERT_PATH =
|
|
175
|
-
const GITLAB_POOL_MAX_SIZE =
|
|
191
|
+
const HTTP_PROXY = getConfig('http-proxy', 'HTTP_PROXY');
|
|
192
|
+
const HTTPS_PROXY = getConfig('https-proxy', 'HTTPS_PROXY');
|
|
193
|
+
const NODE_TLS_REJECT_UNAUTHORIZED = getConfig('tls-reject-unauthorized', 'NODE_TLS_REJECT_UNAUTHORIZED');
|
|
194
|
+
const GITLAB_CA_CERT_PATH = getConfig('ca-cert-path', 'GITLAB_CA_CERT_PATH');
|
|
195
|
+
const GITLAB_POOL_MAX_SIZE = getConfig('pool-max-size', 'GITLAB_POOL_MAX_SIZE')
|
|
196
|
+
? Number.parseInt(getConfig('pool-max-size', 'GITLAB_POOL_MAX_SIZE'), 10)
|
|
197
|
+
: 100;
|
|
176
198
|
let sslOptions = undefined;
|
|
177
199
|
if (NODE_TLS_REJECT_UNAUTHORIZED === "0") {
|
|
178
200
|
sslOptions = { rejectUnauthorized: false };
|
|
@@ -204,7 +226,9 @@ httpsAgent = httpsAgent || new HttpsAgent(sslOptions);
|
|
|
204
226
|
httpAgent = httpAgent || new Agent();
|
|
205
227
|
// Initialize the client pool for managing multiple GitLab instances
|
|
206
228
|
const clientPool = new GitLabClientPool({
|
|
207
|
-
apiUrls: (process.env.GITLAB_API_URL || "https://gitlab.com")
|
|
229
|
+
apiUrls: (process.env.GITLAB_API_URL || "https://gitlab.com")
|
|
230
|
+
.split(",")
|
|
231
|
+
.map(normalizeGitLabApiUrl),
|
|
208
232
|
httpProxy: HTTP_PROXY,
|
|
209
233
|
httpsProxy: HTTPS_PROXY,
|
|
210
234
|
rejectUnauthorized: NODE_TLS_REJECT_UNAUTHORIZED !== "0",
|
|
@@ -235,7 +259,11 @@ const createCookieJar = () => {
|
|
|
235
259
|
if (parts.length >= 7) {
|
|
236
260
|
const [domain, , path, secure, expires, name, value] = parts;
|
|
237
261
|
// Build cookie string in standard format
|
|
238
|
-
const
|
|
262
|
+
const secureFlag = secure === "TRUE" ? "; Secure" : "";
|
|
263
|
+
const expiresFlag = expires === "0"
|
|
264
|
+
? ""
|
|
265
|
+
: `; Expires=${new Date(Number.parseInt(expires, 10) * 1000).toUTCString()}`;
|
|
266
|
+
const cookieStr = `${name}=${value}; Domain=${domain}; Path=${path}${secureFlag}${expiresFlag}`;
|
|
239
267
|
// Use tough-cookie's parse function for robust parsing
|
|
240
268
|
const cookie = parseCookie(cookieStr);
|
|
241
269
|
if (cookie) {
|
|
@@ -276,8 +304,8 @@ async function ensureSessionForRequest() {
|
|
|
276
304
|
// Small delay to ensure cookies are fully processed
|
|
277
305
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
278
306
|
}
|
|
279
|
-
catch
|
|
280
|
-
//
|
|
307
|
+
catch {
|
|
308
|
+
// Intentionally ignored: session establishment errors are non-critical
|
|
281
309
|
}
|
|
282
310
|
}
|
|
283
311
|
}
|
|
@@ -299,9 +327,9 @@ function buildAuthHeaders() {
|
|
|
299
327
|
if (REMOTE_AUTHORIZATION) {
|
|
300
328
|
const ctx = sessionAuthStore.getStore();
|
|
301
329
|
logger.debug({ context: ctx }, "buildAuthHeaders: session context");
|
|
302
|
-
if (ctx
|
|
330
|
+
if (ctx?.token) {
|
|
303
331
|
return {
|
|
304
|
-
[ctx.header]: ctx.header ===
|
|
332
|
+
[ctx.header]: ctx.header === "Authorization" ? `Bearer ${ctx.token}` : ctx.token,
|
|
305
333
|
};
|
|
306
334
|
}
|
|
307
335
|
return {}; // No auth headers if no session context
|
|
@@ -309,7 +337,7 @@ function buildAuthHeaders() {
|
|
|
309
337
|
// Standard mode: prioritize OAuth token, then fall back to environment token
|
|
310
338
|
const token = OAUTH_ACCESS_TOKEN || GITLAB_PERSONAL_ACCESS_TOKEN;
|
|
311
339
|
if (IS_OLD && token) {
|
|
312
|
-
return {
|
|
340
|
+
return { "Private-Token": String(token) };
|
|
313
341
|
}
|
|
314
342
|
if (token) {
|
|
315
343
|
return { Authorization: `Bearer ${token}` };
|
|
@@ -324,7 +352,7 @@ function buildAuthHeaders() {
|
|
|
324
352
|
function getEffectiveApiUrl() {
|
|
325
353
|
if (ENABLE_DYNAMIC_API_URL) {
|
|
326
354
|
const ctx = sessionAuthStore.getStore();
|
|
327
|
-
if (ctx
|
|
355
|
+
if (ctx?.apiUrl) {
|
|
328
356
|
return ctx.apiUrl;
|
|
329
357
|
}
|
|
330
358
|
logger.warn({ ctx }, "getEffectiveApiUrl: No context or apiUrl found, falling back to default");
|
|
@@ -349,7 +377,46 @@ const getFetchConfig = () => {
|
|
|
349
377
|
agent: agent,
|
|
350
378
|
};
|
|
351
379
|
};
|
|
352
|
-
const toJSONSchema = (schema) =>
|
|
380
|
+
const toJSONSchema = (schema) => {
|
|
381
|
+
const jsonSchema = zodToJsonSchema(schema, { $refStrategy: 'none' });
|
|
382
|
+
// Post-process to fix nullable/optional fields that should truly be optional
|
|
383
|
+
function fixNullableOptional(obj) {
|
|
384
|
+
if (obj && typeof obj === 'object') {
|
|
385
|
+
// If this object has properties, process them
|
|
386
|
+
if (obj.properties) {
|
|
387
|
+
const requiredSet = new Set(obj.required || []);
|
|
388
|
+
Object.keys(obj.properties).forEach(key => {
|
|
389
|
+
const prop = obj.properties[key];
|
|
390
|
+
// Handle fields that can be null or omitted
|
|
391
|
+
// If a property has type: ["object", "null"] or anyOf with null, it should not be required
|
|
392
|
+
if (prop.anyOf && prop.anyOf.some((t) => t.type === 'null')) {
|
|
393
|
+
requiredSet.delete(key);
|
|
394
|
+
}
|
|
395
|
+
else if (Array.isArray(prop.type) && prop.type.includes('null')) {
|
|
396
|
+
requiredSet.delete(key);
|
|
397
|
+
}
|
|
398
|
+
// Recursively process nested objects
|
|
399
|
+
obj.properties[key] = fixNullableOptional(prop);
|
|
400
|
+
});
|
|
401
|
+
// Normalize the required array after processing all properties
|
|
402
|
+
if (requiredSet.size > 0) {
|
|
403
|
+
obj.required = Array.from(requiredSet);
|
|
404
|
+
}
|
|
405
|
+
else if (Object.prototype.hasOwnProperty.call(obj, 'required')) {
|
|
406
|
+
delete obj.required;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Process anyOf/allOf/oneOf
|
|
410
|
+
['anyOf', 'allOf', 'oneOf'].forEach(combiner => {
|
|
411
|
+
if (obj[combiner]) {
|
|
412
|
+
obj[combiner] = obj[combiner].map(fixNullableOptional);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return obj;
|
|
417
|
+
}
|
|
418
|
+
return fixNullableOptional(jsonSchema);
|
|
419
|
+
};
|
|
353
420
|
// Define all available tools
|
|
354
421
|
const allTools = [
|
|
355
422
|
{
|
|
@@ -422,6 +489,16 @@ const allTools = [
|
|
|
422
489
|
description: "List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)",
|
|
423
490
|
inputSchema: toJSONSchema(ListMergeRequestDiffsSchema),
|
|
424
491
|
},
|
|
492
|
+
{
|
|
493
|
+
name: "list_merge_request_versions",
|
|
494
|
+
description: "List all versions of a merge request",
|
|
495
|
+
inputSchema: toJSONSchema(ListMergeRequestVersionsSchema),
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "get_merge_request_version",
|
|
499
|
+
description: "Get a specific version of a merge request",
|
|
500
|
+
inputSchema: toJSONSchema(GetMergeRequestVersionSchema),
|
|
501
|
+
},
|
|
425
502
|
{
|
|
426
503
|
name: "get_branch_diffs",
|
|
427
504
|
description: "Get the changes/diffs between two branches or commits in a GitLab project",
|
|
@@ -443,7 +520,7 @@ const allTools = [
|
|
|
443
520
|
inputSchema: toJSONSchema(CreateMergeRequestThreadSchema),
|
|
444
521
|
},
|
|
445
522
|
{
|
|
446
|
-
name:
|
|
523
|
+
name: "resolve_merge_request_thread",
|
|
447
524
|
description: "Resolve a thread on a merge request",
|
|
448
525
|
inputSchema: toJSONSchema(ResolveMergeRequestThreadSchema),
|
|
449
526
|
},
|
|
@@ -483,7 +560,7 @@ const allTools = [
|
|
|
483
560
|
inputSchema: toJSONSchema(GetMergeRequestNoteSchema),
|
|
484
561
|
},
|
|
485
562
|
{
|
|
486
|
-
name:
|
|
563
|
+
name: "get_merge_request_notes",
|
|
487
564
|
description: "List notes for a merge request",
|
|
488
565
|
inputSchema: toJSONSchema(GetMergeRequestNotesSchema),
|
|
489
566
|
},
|
|
@@ -869,12 +946,14 @@ const allTools = [
|
|
|
869
946
|
},
|
|
870
947
|
];
|
|
871
948
|
// Define which tools are read-only
|
|
872
|
-
const readOnlyTools = [
|
|
949
|
+
const readOnlyTools = new Set([
|
|
873
950
|
"search_repositories",
|
|
874
951
|
"execute_graphql",
|
|
875
952
|
"get_file_contents",
|
|
876
953
|
"get_merge_request",
|
|
877
954
|
"get_merge_request_diffs",
|
|
955
|
+
"list_merge_request_versions",
|
|
956
|
+
"get_merge_request_version",
|
|
878
957
|
"get_branch_diffs",
|
|
879
958
|
"mr_discussions",
|
|
880
959
|
"list_issues",
|
|
@@ -919,18 +998,18 @@ const readOnlyTools = [
|
|
|
919
998
|
"list_releases",
|
|
920
999
|
"get_release",
|
|
921
1000
|
"download_release_asset",
|
|
922
|
-
];
|
|
1001
|
+
]);
|
|
923
1002
|
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
|
|
924
|
-
const wikiToolNames = [
|
|
1003
|
+
const wikiToolNames = new Set([
|
|
925
1004
|
"list_wiki_pages",
|
|
926
1005
|
"get_wiki_page",
|
|
927
1006
|
"create_wiki_page",
|
|
928
1007
|
"update_wiki_page",
|
|
929
1008
|
"delete_wiki_page",
|
|
930
1009
|
"upload_wiki_attachment",
|
|
931
|
-
];
|
|
1010
|
+
]);
|
|
932
1011
|
// Define which tools are related to milestones and can be toggled by USE_MILESTONE
|
|
933
|
-
const milestoneToolNames = [
|
|
1012
|
+
const milestoneToolNames = new Set([
|
|
934
1013
|
"list_milestones",
|
|
935
1014
|
"get_milestone",
|
|
936
1015
|
"create_milestone",
|
|
@@ -940,9 +1019,9 @@ const milestoneToolNames = [
|
|
|
940
1019
|
"get_milestone_merge_requests",
|
|
941
1020
|
"promote_milestone",
|
|
942
1021
|
"get_milestone_burndown_events",
|
|
943
|
-
];
|
|
1022
|
+
]);
|
|
944
1023
|
// Define which tools are related to pipelines and can be toggled by USE_PIPELINE
|
|
945
|
-
const pipelineToolNames = [
|
|
1024
|
+
const pipelineToolNames = new Set([
|
|
946
1025
|
"list_pipelines",
|
|
947
1026
|
"get_pipeline",
|
|
948
1027
|
"list_pipeline_jobs",
|
|
@@ -955,7 +1034,7 @@ const pipelineToolNames = [
|
|
|
955
1034
|
"play_pipeline_job",
|
|
956
1035
|
"retry_pipeline_job",
|
|
957
1036
|
"cancel_pipeline_job",
|
|
958
|
-
];
|
|
1037
|
+
]);
|
|
959
1038
|
/**
|
|
960
1039
|
* Smart URL handling for GitLab API
|
|
961
1040
|
*
|
|
@@ -976,11 +1055,17 @@ function normalizeGitLabApiUrl(url) {
|
|
|
976
1055
|
return normalizedUrl;
|
|
977
1056
|
}
|
|
978
1057
|
// Use the normalizeGitLabApiUrl function to handle various URL formats
|
|
979
|
-
const GITLAB_API_URLS = (process.env.GITLAB_API_URL || "https://gitlab.com")
|
|
1058
|
+
const GITLAB_API_URLS = (process.env.GITLAB_API_URL || "https://gitlab.com")
|
|
1059
|
+
.split(",")
|
|
1060
|
+
.map(normalizeGitLabApiUrl);
|
|
980
1061
|
const GITLAB_API_URL = GITLAB_API_URLS[0];
|
|
981
1062
|
const GITLAB_PROJECT_ID = process.env.GITLAB_PROJECT_ID;
|
|
982
|
-
const GITLAB_ALLOWED_PROJECT_IDS = process.env.GITLAB_ALLOWED_PROJECT_IDS?.split(
|
|
983
|
-
|
|
1063
|
+
const GITLAB_ALLOWED_PROJECT_IDS = process.env.GITLAB_ALLOWED_PROJECT_IDS?.split(",")
|
|
1064
|
+
.map(id => id.trim())
|
|
1065
|
+
.filter(Boolean) || [];
|
|
1066
|
+
const GITLAB_COMMIT_FILES_PER_PAGE = process.env.GITLAB_COMMIT_FILES_PER_PAGE
|
|
1067
|
+
? Number.parseInt(process.env.GITLAB_COMMIT_FILES_PER_PAGE, 10)
|
|
1068
|
+
: 20;
|
|
984
1069
|
// Validate authentication configuration
|
|
985
1070
|
if (REMOTE_AUTHORIZATION) {
|
|
986
1071
|
// Remote authorization mode: token comes from HTTP headers
|
|
@@ -1037,11 +1122,11 @@ function getEffectiveProjectId(projectId) {
|
|
|
1037
1122
|
}
|
|
1038
1123
|
// If a project ID is provided, check if it's in the whitelist
|
|
1039
1124
|
if (projectId && !GITLAB_ALLOWED_PROJECT_IDS.includes(projectId)) {
|
|
1040
|
-
throw new Error(`Access denied: Project ${projectId} is not in the allowed project list: ${GITLAB_ALLOWED_PROJECT_IDS.join(
|
|
1125
|
+
throw new Error(`Access denied: Project ${projectId} is not in the allowed project list: ${GITLAB_ALLOWED_PROJECT_IDS.join(", ")}`);
|
|
1041
1126
|
}
|
|
1042
1127
|
// If no project ID provided but we have multiple allowed projects, require an explicit choice
|
|
1043
1128
|
if (!projectId && GITLAB_ALLOWED_PROJECT_IDS.length > 1) {
|
|
1044
|
-
throw new Error(`Multiple projects allowed (${GITLAB_ALLOWED_PROJECT_IDS.join(
|
|
1129
|
+
throw new Error(`Multiple projects allowed (${GITLAB_ALLOWED_PROJECT_IDS.join(", ")}). Please specify a project ID.`);
|
|
1045
1130
|
}
|
|
1046
1131
|
return projectId || GITLAB_ALLOWED_PROJECT_IDS[0];
|
|
1047
1132
|
}
|
|
@@ -1473,18 +1558,22 @@ async function listDiscussions(projectId, resourceType, resourceIid, options = {
|
|
|
1473
1558
|
// Extract pagination headers
|
|
1474
1559
|
const pagination = {
|
|
1475
1560
|
x_next_page: response.headers.get("x-next-page")
|
|
1476
|
-
? parseInt(response.headers.get("x-next-page"))
|
|
1561
|
+
? Number.parseInt(response.headers.get("x-next-page"), 10)
|
|
1477
1562
|
: null,
|
|
1478
|
-
x_page: response.headers.get("x-page")
|
|
1563
|
+
x_page: response.headers.get("x-page")
|
|
1564
|
+
? Number.parseInt(response.headers.get("x-page"), 10)
|
|
1565
|
+
: undefined,
|
|
1479
1566
|
x_per_page: response.headers.get("x-per-page")
|
|
1480
|
-
? parseInt(response.headers.get("x-per-page"))
|
|
1567
|
+
? Number.parseInt(response.headers.get("x-per-page"), 10)
|
|
1481
1568
|
: undefined,
|
|
1482
1569
|
x_prev_page: response.headers.get("x-prev-page")
|
|
1483
|
-
? parseInt(response.headers.get("x-prev-page"))
|
|
1570
|
+
? Number.parseInt(response.headers.get("x-prev-page"), 10)
|
|
1571
|
+
: null,
|
|
1572
|
+
x_total: response.headers.get("x-total")
|
|
1573
|
+
? Number.parseInt(response.headers.get("x-total"), 10)
|
|
1484
1574
|
: null,
|
|
1485
|
-
x_total: response.headers.get("x-total") ? parseInt(response.headers.get("x-total")) : null,
|
|
1486
1575
|
x_total_pages: response.headers.get("x-total-pages")
|
|
1487
|
-
? parseInt(response.headers.get("x-total-pages"))
|
|
1576
|
+
? Number.parseInt(response.headers.get("x-total-pages"), 10)
|
|
1488
1577
|
: null,
|
|
1489
1578
|
};
|
|
1490
1579
|
return PaginatedDiscussionsResponseSchema.parse({
|
|
@@ -1881,10 +1970,10 @@ async function searchProjects(query, page = 1, perPage = 20) {
|
|
|
1881
1970
|
const totalCount = response.headers.get("x-total");
|
|
1882
1971
|
const totalPages = response.headers.get("x-total-pages");
|
|
1883
1972
|
// GitLab API doesn't return these headers for results > 10,000
|
|
1884
|
-
const count = totalCount ? parseInt(totalCount) : projects.length;
|
|
1973
|
+
const count = totalCount ? Number.parseInt(totalCount, 10) : projects.length;
|
|
1885
1974
|
return GitLabSearchResponseSchema.parse({
|
|
1886
1975
|
count,
|
|
1887
|
-
total_pages: totalPages ? parseInt(totalPages) : Math.ceil(count / perPage),
|
|
1976
|
+
total_pages: totalPages ? Number.parseInt(totalPages, 10) : Math.ceil(count / perPage),
|
|
1888
1977
|
current_page: page,
|
|
1889
1978
|
items: projects,
|
|
1890
1979
|
});
|
|
@@ -1906,7 +1995,7 @@ async function createRepository(options) {
|
|
|
1906
1995
|
visibility: options.visibility,
|
|
1907
1996
|
initialize_with_readme: options.initialize_with_readme,
|
|
1908
1997
|
default_branch: "main",
|
|
1909
|
-
path: options.name.toLowerCase().
|
|
1998
|
+
path: options.name.toLowerCase().replaceAll(/\s+/g, "-"),
|
|
1910
1999
|
}),
|
|
1911
2000
|
});
|
|
1912
2001
|
if (!response.ok) {
|
|
@@ -2250,7 +2339,7 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
|
|
|
2250
2339
|
}
|
|
2251
2340
|
// Handle empty response (204 No Content) or successful response
|
|
2252
2341
|
const responseText = await response.text();
|
|
2253
|
-
if (!responseText || responseText.trim() ===
|
|
2342
|
+
if (!responseText || responseText.trim() === "") {
|
|
2254
2343
|
// Return a success indicator for empty responses
|
|
2255
2344
|
return {
|
|
2256
2345
|
id: draftNoteId.toString(),
|
|
@@ -2260,7 +2349,7 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
|
|
|
2260
2349
|
updated_at: new Date().toISOString(),
|
|
2261
2350
|
system: false,
|
|
2262
2351
|
noteable_id: mergeRequestIid.toString(),
|
|
2263
|
-
noteable_type: "MergeRequest"
|
|
2352
|
+
noteable_type: "MergeRequest",
|
|
2264
2353
|
};
|
|
2265
2354
|
}
|
|
2266
2355
|
try {
|
|
@@ -2279,7 +2368,7 @@ async function publishDraftNote(projectId, mergeRequestIid, draftNoteId) {
|
|
|
2279
2368
|
updated_at: new Date().toISOString(),
|
|
2280
2369
|
system: false,
|
|
2281
2370
|
noteable_id: mergeRequestIid.toString(),
|
|
2282
|
-
noteable_type: "MergeRequest"
|
|
2371
|
+
noteable_type: "MergeRequest",
|
|
2283
2372
|
};
|
|
2284
2373
|
}
|
|
2285
2374
|
}
|
|
@@ -2303,7 +2392,7 @@ async function bulkPublishDraftNotes(projectId, mergeRequestIid) {
|
|
|
2303
2392
|
}
|
|
2304
2393
|
// Handle empty response (204 No Content) or successful response
|
|
2305
2394
|
const responseText = await response.text();
|
|
2306
|
-
if (!responseText || responseText.trim() ===
|
|
2395
|
+
if (!responseText || responseText.trim() === "") {
|
|
2307
2396
|
// Return empty array for successful bulk publish with no content
|
|
2308
2397
|
return [];
|
|
2309
2398
|
}
|
|
@@ -2354,7 +2443,6 @@ async function createMergeRequestThread(projectId, mergeRequestIid, body, positi
|
|
|
2354
2443
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
2355
2444
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/discussions`);
|
|
2356
2445
|
const payload = { body };
|
|
2357
|
-
// Add optional parameters if provided
|
|
2358
2446
|
if (position) {
|
|
2359
2447
|
payload.position = position;
|
|
2360
2448
|
}
|
|
@@ -2370,6 +2458,46 @@ async function createMergeRequestThread(projectId, mergeRequestIid, body, positi
|
|
|
2370
2458
|
const data = await response.json();
|
|
2371
2459
|
return GitLabDiscussionSchema.parse(data);
|
|
2372
2460
|
}
|
|
2461
|
+
/**
|
|
2462
|
+
* List all versions of a merge request
|
|
2463
|
+
* 병합 요청의 모든 버전 목록 조회
|
|
2464
|
+
*
|
|
2465
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
2466
|
+
* @param {number} mergeRequestIid - The internal ID of the merge request
|
|
2467
|
+
* @returns {Promise<GitLabMergeRequestVersion[]>} List of merge request versions
|
|
2468
|
+
*/
|
|
2469
|
+
async function listMergeRequestVersions(projectId, mergeRequestIid) {
|
|
2470
|
+
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
2471
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/versions`);
|
|
2472
|
+
const response = await fetch(url.toString(), {
|
|
2473
|
+
...getFetchConfig(),
|
|
2474
|
+
});
|
|
2475
|
+
await handleGitLabError(response);
|
|
2476
|
+
const data = await response.json();
|
|
2477
|
+
return z.array(GitLabMergeRequestVersionSchema).parse(data);
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Get a specific version of a merge request
|
|
2481
|
+
* 병합 요청의 특정 버전 상세 정보 조회
|
|
2482
|
+
*
|
|
2483
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
2484
|
+
* @param {number} mergeRequestIid - The internal ID of the merge request
|
|
2485
|
+
* @param {number} versionId - The ID of the version
|
|
2486
|
+
* @returns {Promise<GitLabMergeRequestVersionDetail>} The merge request version details
|
|
2487
|
+
*/
|
|
2488
|
+
async function getMergeRequestVersion(projectId, mergeRequestIid, versionId, unidiff) {
|
|
2489
|
+
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
2490
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/versions/${versionId}`);
|
|
2491
|
+
if (unidiff !== undefined) {
|
|
2492
|
+
url.searchParams.append("unidiff", String(unidiff));
|
|
2493
|
+
}
|
|
2494
|
+
const response = await fetch(url.toString(), {
|
|
2495
|
+
...getFetchConfig(),
|
|
2496
|
+
});
|
|
2497
|
+
await handleGitLabError(response);
|
|
2498
|
+
const data = await response.json();
|
|
2499
|
+
return GitLabMergeRequestVersionDetailSchema.parse(data);
|
|
2500
|
+
}
|
|
2373
2501
|
/**
|
|
2374
2502
|
* List all namespaces
|
|
2375
2503
|
* 사용 가능한 모든 네임스페이스 목록 조회
|
|
@@ -3027,7 +3155,7 @@ async function cancelPipelineJob(projectId, jobId, force) {
|
|
|
3027
3155
|
projectId = decodeURIComponent(projectId);
|
|
3028
3156
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}/cancel`);
|
|
3029
3157
|
if (force !== undefined) {
|
|
3030
|
-
url.searchParams.append(
|
|
3158
|
+
url.searchParams.append("force", force.toString());
|
|
3031
3159
|
}
|
|
3032
3160
|
const response = await fetch(url.toString(), {
|
|
3033
3161
|
...getFetchConfig(),
|
|
@@ -3744,24 +3872,19 @@ async function downloadReleaseAsset(projectId, tagName, directAssetPath) {
|
|
|
3744
3872
|
return await response.text();
|
|
3745
3873
|
}
|
|
3746
3874
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
3747
|
-
// In remote auth mode, retrieve session context from AsyncLocalStorage
|
|
3748
|
-
// This ensures the context is available even when called from SDK's async chains
|
|
3749
|
-
const sessionContext = REMOTE_AUTHORIZATION ? sessionAuthStore.getStore() : null;
|
|
3750
3875
|
// Apply read-only filter first
|
|
3751
3876
|
const tools0 = GITLAB_READ_ONLY_MODE
|
|
3752
|
-
? allTools.filter(tool => readOnlyTools.
|
|
3877
|
+
? allTools.filter(tool => readOnlyTools.has(tool.name))
|
|
3753
3878
|
: allTools;
|
|
3754
3879
|
// Toggle wiki tools by USE_GITLAB_WIKI flag
|
|
3755
|
-
const tools1 = USE_GITLAB_WIKI
|
|
3756
|
-
? tools0
|
|
3757
|
-
: tools0.filter(tool => !wikiToolNames.includes(tool.name));
|
|
3880
|
+
const tools1 = USE_GITLAB_WIKI ? tools0 : tools0.filter(tool => !wikiToolNames.has(tool.name));
|
|
3758
3881
|
// Toggle milestone tools by USE_MILESTONE flag
|
|
3759
|
-
const tools2 = USE_MILESTONE
|
|
3760
|
-
? tools1
|
|
3761
|
-
: tools1.filter(tool => !milestoneToolNames.includes(tool.name));
|
|
3882
|
+
const tools2 = USE_MILESTONE ? tools1 : tools1.filter(tool => !milestoneToolNames.has(tool.name));
|
|
3762
3883
|
// Toggle pipeline tools by USE_PIPELINE flag
|
|
3763
|
-
let tools = USE_PIPELINE ? tools2 : tools2.filter(tool => !pipelineToolNames.
|
|
3764
|
-
tools = GITLAB_DENIED_TOOLS_REGEX
|
|
3884
|
+
let tools = USE_PIPELINE ? tools2 : tools2.filter(tool => !pipelineToolNames.has(tool.name));
|
|
3885
|
+
tools = GITLAB_DENIED_TOOLS_REGEX
|
|
3886
|
+
? tools.filter(tool => !GITLAB_DENIED_TOOLS_REGEX.test(tool.name))
|
|
3887
|
+
: tools;
|
|
3765
3888
|
// <<< START: Gemini 호환성을 위해 $schema 제거 >>>
|
|
3766
3889
|
tools = tools.map(tool => {
|
|
3767
3890
|
// inputSchema가 존재하고 객체인지 확인
|
|
@@ -4010,21 +4133,21 @@ async function handleToolCall(params) {
|
|
|
4010
4133
|
content: [{ type: "text", text: "Merge request note deleted successfully" }],
|
|
4011
4134
|
};
|
|
4012
4135
|
}
|
|
4013
|
-
case
|
|
4136
|
+
case "get_merge_request_note": {
|
|
4014
4137
|
const args = GetMergeRequestNoteSchema.parse(params.arguments);
|
|
4015
4138
|
const note = await getMergeRequestNote(args.project_id, args.merge_request_iid, args.note_id);
|
|
4016
4139
|
return {
|
|
4017
4140
|
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
4018
4141
|
};
|
|
4019
4142
|
}
|
|
4020
|
-
case
|
|
4143
|
+
case "get_merge_request_notes": {
|
|
4021
4144
|
const args = GetMergeRequestNotesSchema.parse(params.arguments);
|
|
4022
4145
|
const notes = await getMergeRequestNotes(args.project_id, args.merge_request_iid, args.sort, args.order_by);
|
|
4023
4146
|
return {
|
|
4024
4147
|
content: [{ type: "text", text: JSON.stringify(notes, null, 2) }],
|
|
4025
4148
|
};
|
|
4026
4149
|
}
|
|
4027
|
-
case
|
|
4150
|
+
case "update_merge_request_note": {
|
|
4028
4151
|
const args = UpdateMergeRequestNoteSchema.parse(params.arguments);
|
|
4029
4152
|
const note = await updateMergeRequestNote(args.project_id, args.merge_request_iid, args.note_id, args.body);
|
|
4030
4153
|
return {
|
|
@@ -4066,6 +4189,20 @@ async function handleToolCall(params) {
|
|
|
4066
4189
|
content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
|
|
4067
4190
|
};
|
|
4068
4191
|
}
|
|
4192
|
+
case "list_merge_request_versions": {
|
|
4193
|
+
const args = ListMergeRequestVersionsSchema.parse(params.arguments);
|
|
4194
|
+
const versions = await listMergeRequestVersions(args.project_id, args.merge_request_iid);
|
|
4195
|
+
return {
|
|
4196
|
+
content: [{ type: "text", text: JSON.stringify(versions, null, 2) }],
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
case "get_merge_request_version": {
|
|
4200
|
+
const args = GetMergeRequestVersionSchema.parse(params.arguments);
|
|
4201
|
+
const version = await getMergeRequestVersion(args.project_id, args.merge_request_iid, args.version_id, args.unidiff);
|
|
4202
|
+
return {
|
|
4203
|
+
content: [{ type: "text", text: JSON.stringify(version, null, 2) }],
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4069
4206
|
case "update_merge_request": {
|
|
4070
4207
|
const args = UpdateMergeRequestSchema.parse(params.arguments);
|
|
4071
4208
|
const { project_id, merge_request_iid, source_branch, ...options } = args;
|
|
@@ -4749,7 +4886,9 @@ async function handleToolCall(params) {
|
|
|
4749
4886
|
const args = DownloadAttachmentSchema.parse(params.arguments);
|
|
4750
4887
|
const filePath = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
|
|
4751
4888
|
return {
|
|
4752
|
-
content: [
|
|
4889
|
+
content: [
|
|
4890
|
+
{ type: "text", text: JSON.stringify({ success: true, file_path: filePath }, null, 2) },
|
|
4891
|
+
],
|
|
4753
4892
|
};
|
|
4754
4893
|
}
|
|
4755
4894
|
case "list_events": {
|
|
@@ -4921,8 +5060,8 @@ async function startStreamableHTTPServer() {
|
|
|
4921
5060
|
const streamableTransports = {};
|
|
4922
5061
|
const authTimeouts = {};
|
|
4923
5062
|
// Configuration and limits
|
|
4924
|
-
const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS ||
|
|
4925
|
-
const MAX_REQUESTS_PER_MINUTE = parseInt(process.env.MAX_REQUESTS_PER_MINUTE ||
|
|
5063
|
+
const MAX_SESSIONS = Number.parseInt(process.env.MAX_SESSIONS || "1000", 10);
|
|
5064
|
+
const MAX_REQUESTS_PER_MINUTE = Number.parseInt(process.env.MAX_REQUESTS_PER_MINUTE || "60", 10);
|
|
4926
5065
|
// Metrics tracking
|
|
4927
5066
|
const metrics = {
|
|
4928
5067
|
activeSessions: 0,
|
|
@@ -4942,7 +5081,7 @@ async function startStreamableHTTPServer() {
|
|
|
4942
5081
|
// GitLab PAT format: glpat-xxxxx (min 20 chars)
|
|
4943
5082
|
if (token.length < 20)
|
|
4944
5083
|
return false;
|
|
4945
|
-
if (!/^[a-zA-Z0-9_
|
|
5084
|
+
if (!/^[-a-zA-Z0-9_.]+$/.test(token))
|
|
4946
5085
|
return false;
|
|
4947
5086
|
return true;
|
|
4948
5087
|
};
|
|
@@ -4967,9 +5106,9 @@ async function startStreamableHTTPServer() {
|
|
|
4967
5106
|
* Returns null if no auth found or invalid format
|
|
4968
5107
|
*/
|
|
4969
5108
|
const parseAuthHeaders = (req) => {
|
|
4970
|
-
const authHeader = req.headers[
|
|
4971
|
-
const privateToken = req.headers[
|
|
4972
|
-
const dynamicApiUrl = req.headers[
|
|
5109
|
+
const authHeader = req.headers["authorization"] || "";
|
|
5110
|
+
const privateToken = req.headers["private-token"] || "";
|
|
5111
|
+
const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim();
|
|
4973
5112
|
let apiUrl = GITLAB_API_URL; // Default API URL
|
|
4974
5113
|
// Only process dynamic URL if the feature is enabled
|
|
4975
5114
|
if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) {
|
|
@@ -4977,7 +5116,7 @@ async function startStreamableHTTPServer() {
|
|
|
4977
5116
|
new URL(dynamicApiUrl); // Ensure it's a valid URL format
|
|
4978
5117
|
apiUrl = normalizeGitLabApiUrl(dynamicApiUrl);
|
|
4979
5118
|
}
|
|
4980
|
-
catch
|
|
5119
|
+
catch {
|
|
4981
5120
|
logger.warn(`Invalid X-GitLab-API-URL provided: ${dynamicApiUrl}. Auth will fail.`);
|
|
4982
5121
|
return null; // Reject if URL is malformed
|
|
4983
5122
|
}
|
|
@@ -4987,13 +5126,16 @@ async function startStreamableHTTPServer() {
|
|
|
4987
5126
|
let header = null;
|
|
4988
5127
|
if (privateToken) {
|
|
4989
5128
|
token = privateToken.trim();
|
|
4990
|
-
header =
|
|
5129
|
+
header = "Private-Token";
|
|
4991
5130
|
}
|
|
4992
5131
|
else if (authHeader) {
|
|
4993
|
-
|
|
5132
|
+
// Use \S+ instead of .+ to prevent ReDoS attacks
|
|
5133
|
+
// \S+ only matches non-whitespace, so trim() is technically unnecessary,
|
|
5134
|
+
// but we keep it for defensive coding and backward compatibility
|
|
5135
|
+
const match = /^Bearer\s+(\S+)$/i.exec(authHeader);
|
|
4994
5136
|
if (match) {
|
|
4995
5137
|
token = match[1].trim();
|
|
4996
|
-
header =
|
|
5138
|
+
header = "Authorization";
|
|
4997
5139
|
}
|
|
4998
5140
|
}
|
|
4999
5141
|
// Validate token and return AuthData object
|
|
@@ -5048,8 +5190,8 @@ async function startStreamableHTTPServer() {
|
|
|
5048
5190
|
if (REMOTE_AUTHORIZATION && sessionId && !checkRateLimit(sessionId)) {
|
|
5049
5191
|
metrics.rejectedByRateLimit++;
|
|
5050
5192
|
res.status(429).json({
|
|
5051
|
-
error:
|
|
5052
|
-
message: `Maximum ${MAX_REQUESTS_PER_MINUTE} requests per minute allowed
|
|
5193
|
+
error: "Rate limit exceeded",
|
|
5194
|
+
message: `Maximum ${MAX_REQUESTS_PER_MINUTE} requests per minute allowed`,
|
|
5053
5195
|
});
|
|
5054
5196
|
return;
|
|
5055
5197
|
}
|
|
@@ -5057,8 +5199,8 @@ async function startStreamableHTTPServer() {
|
|
|
5057
5199
|
if (!sessionId && Object.keys(streamableTransports).length >= MAX_SESSIONS) {
|
|
5058
5200
|
metrics.rejectedByCapacity++;
|
|
5059
5201
|
res.status(503).json({
|
|
5060
|
-
error:
|
|
5061
|
-
message: `Maximum ${MAX_SESSIONS} concurrent sessions allowed. Please try again later
|
|
5202
|
+
error: "Server capacity reached",
|
|
5203
|
+
message: `Maximum ${MAX_SESSIONS} concurrent sessions allowed. Please try again later.`,
|
|
5062
5204
|
});
|
|
5063
5205
|
return;
|
|
5064
5206
|
}
|
|
@@ -5070,8 +5212,8 @@ async function startStreamableHTTPServer() {
|
|
|
5070
5212
|
if (!authData) {
|
|
5071
5213
|
metrics.authFailures++;
|
|
5072
5214
|
res.status(401).json({
|
|
5073
|
-
error:
|
|
5074
|
-
message:
|
|
5215
|
+
error: "Missing Authorization or Private-Token header",
|
|
5216
|
+
message: "Remote authorization is enabled. Please provide Authorization or Private-Token header.",
|
|
5075
5217
|
});
|
|
5076
5218
|
return;
|
|
5077
5219
|
}
|
|
@@ -5160,7 +5302,7 @@ async function startStreamableHTTPServer() {
|
|
|
5160
5302
|
header: authData.header,
|
|
5161
5303
|
token: authData.token,
|
|
5162
5304
|
lastUsed: authData.lastUsed,
|
|
5163
|
-
apiUrl: authData.apiUrl
|
|
5305
|
+
apiUrl: authData.apiUrl,
|
|
5164
5306
|
};
|
|
5165
5307
|
// Run the entire request handling within AsyncLocalStorage context
|
|
5166
5308
|
await sessionAuthStore.run(ctx, handleRequest);
|
|
@@ -5175,7 +5317,7 @@ async function startStreamableHTTPServer() {
|
|
|
5175
5317
|
res.setHeader("Allow", "POST, DELETE");
|
|
5176
5318
|
res.status(405).json({
|
|
5177
5319
|
error: "Method Not Allowed",
|
|
5178
|
-
message: "GET /mcp is not supported when STREAMABLE_HTTP is enabled. Use POST to communicate with the MCP server."
|
|
5320
|
+
message: "GET /mcp is not supported when STREAMABLE_HTTP is enabled. Use POST to communicate with the MCP server.",
|
|
5179
5321
|
});
|
|
5180
5322
|
});
|
|
5181
5323
|
// Metrics endpoint
|
|
@@ -5191,14 +5333,14 @@ async function startStreamableHTTPServer() {
|
|
|
5191
5333
|
maxRequestsPerMinute: MAX_REQUESTS_PER_MINUTE,
|
|
5192
5334
|
sessionTimeoutSeconds: SESSION_TIMEOUT_SECONDS,
|
|
5193
5335
|
remoteAuthEnabled: REMOTE_AUTHORIZATION,
|
|
5194
|
-
}
|
|
5336
|
+
},
|
|
5195
5337
|
});
|
|
5196
5338
|
});
|
|
5197
5339
|
// Health check endpoint
|
|
5198
5340
|
app.get("/health", (_req, res) => {
|
|
5199
5341
|
const isHealthy = Object.keys(streamableTransports).length < MAX_SESSIONS;
|
|
5200
5342
|
res.status(isHealthy ? 200 : 503).json({
|
|
5201
|
-
status: isHealthy ?
|
|
5343
|
+
status: isHealthy ? "healthy" : "degraded",
|
|
5202
5344
|
activeSessions: Object.keys(streamableTransports).length,
|
|
5203
5345
|
maxSessions: MAX_SESSIONS,
|
|
5204
5346
|
uptime: process.uptime(),
|
|
@@ -5242,7 +5384,7 @@ async function startStreamableHTTPServer() {
|
|
|
5242
5384
|
logger.info(`${signal} received, starting graceful shutdown...`);
|
|
5243
5385
|
// Stop accepting new connections
|
|
5244
5386
|
httpServer.close(() => {
|
|
5245
|
-
logger.info(
|
|
5387
|
+
logger.info("HTTP server closed");
|
|
5246
5388
|
});
|
|
5247
5389
|
// Close all active sessions
|
|
5248
5390
|
const sessionIds = Object.keys(streamableTransports);
|
|
@@ -5267,12 +5409,12 @@ async function startStreamableHTTPServer() {
|
|
|
5267
5409
|
Object.keys(authTimeouts).forEach(sessionId => {
|
|
5268
5410
|
clearAuthTimeout(sessionId);
|
|
5269
5411
|
});
|
|
5270
|
-
logger.info(
|
|
5412
|
+
logger.info("Graceful shutdown complete");
|
|
5271
5413
|
process.exit(0);
|
|
5272
5414
|
};
|
|
5273
5415
|
// Register signal handlers
|
|
5274
|
-
process.on(
|
|
5275
|
-
process.on(
|
|
5416
|
+
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
5417
|
+
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
5276
5418
|
}
|
|
5277
5419
|
/**
|
|
5278
5420
|
* Initialize server with specific transport mode
|
|
@@ -5293,10 +5435,11 @@ async function initializeServerByTransportMode(mode) {
|
|
|
5293
5435
|
logger.warn("Starting GitLab MCP Server with Streamable HTTP transport");
|
|
5294
5436
|
await startStreamableHTTPServer();
|
|
5295
5437
|
break;
|
|
5296
|
-
default:
|
|
5438
|
+
default: {
|
|
5297
5439
|
// This should never happen with proper enum usage, but TypeScript requires it
|
|
5298
5440
|
const exhaustiveCheck = mode;
|
|
5299
5441
|
throw new Error(`Unknown transport mode: ${exhaustiveCheck}`);
|
|
5442
|
+
}
|
|
5300
5443
|
}
|
|
5301
5444
|
}
|
|
5302
5445
|
/**
|
package/build/schemas.js
CHANGED
|
@@ -792,12 +792,12 @@ export const GitLabDiscussionNoteSchema = z.object({
|
|
|
792
792
|
position: z
|
|
793
793
|
.object({
|
|
794
794
|
// Only present for DiffNote
|
|
795
|
-
base_sha: z.string().optional(),
|
|
796
|
-
start_sha: z.string().optional(),
|
|
797
|
-
head_sha: z.string().optional(),
|
|
795
|
+
base_sha: z.string().nullable().optional(),
|
|
796
|
+
start_sha: z.string().nullable().optional(),
|
|
797
|
+
head_sha: z.string().nullable().optional(),
|
|
798
798
|
old_path: z.string().nullable().optional().describe("File path before change"),
|
|
799
799
|
new_path: z.string().nullable().optional().describe("File path after change"),
|
|
800
|
-
position_type: z.enum(["text", "image", "file"]).optional(),
|
|
800
|
+
position_type: z.enum(["text", "image", "file"]).nullable().optional(),
|
|
801
801
|
new_line: z
|
|
802
802
|
.number()
|
|
803
803
|
.nullable()
|
|
@@ -808,11 +808,11 @@ export const GitLabDiscussionNoteSchema = z.object({
|
|
|
808
808
|
.nullable()
|
|
809
809
|
.optional()
|
|
810
810
|
.describe("Line number in the original file (before changes). Used for deleted lines and context lines. Null for newly added lines."),
|
|
811
|
-
line_range: LineRangeSchema.nullable().optional(), //
|
|
812
|
-
width: z.number().optional(), // For image diff notes
|
|
813
|
-
height: z.number().optional(), // For image diff notes
|
|
814
|
-
x: z.number().optional(), // For image diff notes
|
|
815
|
-
y: z.number().optional(), // For image diff notes
|
|
811
|
+
line_range: LineRangeSchema.nullable().optional(), // Accept any value for line_range including null
|
|
812
|
+
width: z.number().nullable().optional(), // For image diff notes
|
|
813
|
+
height: z.number().nullable().optional(), // For image diff notes
|
|
814
|
+
x: z.number().nullable().optional(), // For image diff notes
|
|
815
|
+
y: z.number().nullable().optional(), // For image diff notes
|
|
816
816
|
})
|
|
817
817
|
.passthrough() // Allow additional fields
|
|
818
818
|
.optional(),
|
|
@@ -1049,6 +1049,16 @@ export const ListMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
|
|
|
1049
1049
|
.optional()
|
|
1050
1050
|
.describe("Present diffs in the unified diff format. Default is false. Introduced in GitLab 16.5."),
|
|
1051
1051
|
});
|
|
1052
|
+
// Merge Request Versions API operation schemas
|
|
1053
|
+
export const ListMergeRequestVersionsSchema = ProjectParamsSchema.extend({
|
|
1054
|
+
merge_request_iid: z.coerce.string().describe("The internal ID of the merge request"),
|
|
1055
|
+
});
|
|
1056
|
+
export const GetMergeRequestVersionSchema = ListMergeRequestVersionsSchema.extend({
|
|
1057
|
+
version_id: z.coerce.string().describe("The ID of the merge request diff version"),
|
|
1058
|
+
unidiff: z.boolean()
|
|
1059
|
+
.optional()
|
|
1060
|
+
.describe("Present diffs in the unified diff format. Default is false. Introduced in GitLab 16.5."),
|
|
1061
|
+
});
|
|
1052
1062
|
export const CreateNoteSchema = z.object({
|
|
1053
1063
|
project_id: z.coerce.string().describe("Project ID or namespace/project_path"),
|
|
1054
1064
|
noteable_type: z
|
|
@@ -1418,6 +1428,7 @@ export const MergeRequestThreadPositionCreateSchema = z.object({
|
|
|
1418
1428
|
x: z.number().optional().describe("IMAGE DIFFS ONLY: X coordinate on the image (for position_type='image')."),
|
|
1419
1429
|
y: z.number().optional().describe("IMAGE DIFFS ONLY: Y coordinate on the image (for position_type='image')."),
|
|
1420
1430
|
});
|
|
1431
|
+
// Schema for creating/sending position to GitLab API (stricter)
|
|
1421
1432
|
export const MergeRequestThreadPositionSchema = z.object({
|
|
1422
1433
|
base_sha: z
|
|
1423
1434
|
.string()
|
|
@@ -1454,18 +1465,22 @@ export const MergeRequestThreadPositionSchema = z.object({
|
|
|
1454
1465
|
line_range: LineRangeSchema.nullable().optional().describe("MULTILINE COMMENTS: Specify start/end line positions for commenting on multiple lines. Alternative to single old_line/new_line."),
|
|
1455
1466
|
width: z
|
|
1456
1467
|
.number()
|
|
1468
|
+
.nullable()
|
|
1457
1469
|
.optional()
|
|
1458
1470
|
.describe("IMAGE DIFFS ONLY: Width of the image (for position_type='image')."),
|
|
1459
1471
|
height: z
|
|
1460
1472
|
.number()
|
|
1473
|
+
.nullable()
|
|
1461
1474
|
.optional()
|
|
1462
1475
|
.describe("IMAGE DIFFS ONLY: Height of the image (for position_type='image')."),
|
|
1463
1476
|
x: z
|
|
1464
1477
|
.number()
|
|
1478
|
+
.nullable()
|
|
1465
1479
|
.optional()
|
|
1466
1480
|
.describe("IMAGE DIFFS ONLY: X coordinate on the image (for position_type='image')."),
|
|
1467
1481
|
y: z
|
|
1468
1482
|
.number()
|
|
1483
|
+
.nullable()
|
|
1469
1484
|
.optional()
|
|
1470
1485
|
.describe("IMAGE DIFFS ONLY: Y coordinate on the image (for position_type='image')."),
|
|
1471
1486
|
});
|
|
@@ -1502,7 +1517,7 @@ export const ListDraftNotesSchema = ProjectParamsSchema.extend({
|
|
|
1502
1517
|
export const CreateDraftNoteSchema = ProjectParamsSchema.extend({
|
|
1503
1518
|
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
1504
1519
|
body: z.string().describe("The content of the draft note"),
|
|
1505
|
-
position:
|
|
1520
|
+
position: MergeRequestThreadPositionSchema.optional().describe("Position when creating a diff note"),
|
|
1506
1521
|
resolve_discussion: z.boolean().optional().describe("Whether to resolve the discussion when publishing"),
|
|
1507
1522
|
});
|
|
1508
1523
|
// Update draft note schema
|
|
@@ -1510,7 +1525,7 @@ export const UpdateDraftNoteSchema = ProjectParamsSchema.extend({
|
|
|
1510
1525
|
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
1511
1526
|
draft_note_id: z.coerce.string().describe("The ID of the draft note"),
|
|
1512
1527
|
body: z.string().optional().describe("The content of the draft note"),
|
|
1513
|
-
position:
|
|
1528
|
+
position: MergeRequestThreadPositionSchema.optional().describe("Position when creating a diff note"),
|
|
1514
1529
|
resolve_discussion: z.boolean().optional().describe("Whether to resolve the discussion when publishing"),
|
|
1515
1530
|
});
|
|
1516
1531
|
// Delete draft note schema
|
|
@@ -1785,6 +1800,22 @@ export const GetProjectEventsSchema = z.object({
|
|
|
1785
1800
|
page: z.number().optional().describe("Returns the specified results page. Default: 1"),
|
|
1786
1801
|
per_page: z.number().optional().describe("Number of results per page. Default: 20"),
|
|
1787
1802
|
});
|
|
1803
|
+
// Merge Request Versions schemas - Response schemas based on GitLab API documentation
|
|
1804
|
+
export const GitLabMergeRequestVersionSchema = z.object({
|
|
1805
|
+
id: z.number(),
|
|
1806
|
+
head_commit_sha: z.string(),
|
|
1807
|
+
base_commit_sha: z.string(),
|
|
1808
|
+
start_commit_sha: z.string(),
|
|
1809
|
+
created_at: z.string(),
|
|
1810
|
+
merge_request_id: z.number(),
|
|
1811
|
+
state: z.string(),
|
|
1812
|
+
real_size: z.string(),
|
|
1813
|
+
patch_id_sha: z.string(),
|
|
1814
|
+
});
|
|
1815
|
+
export const GitLabMergeRequestVersionDetailSchema = GitLabMergeRequestVersionSchema.extend({
|
|
1816
|
+
commits: z.array(GitLabCommitSchema),
|
|
1817
|
+
diffs: z.array(GitLabDiffSchema),
|
|
1818
|
+
});
|
|
1788
1819
|
// GraphQL generic execution schema
|
|
1789
1820
|
export const ExecuteGraphQLSchema = z.object({
|
|
1790
1821
|
query: z.string().describe("GraphQL query string"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.23",
|
|
4
4
|
"description": "MCP server for using the GitLab API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "zereight",
|
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
"watch": "tsc --watch",
|
|
28
28
|
"deploy": "npm publish --access public",
|
|
29
29
|
"changelog": "auto-changelog -p",
|
|
30
|
-
"test": "
|
|
31
|
-
"test:
|
|
30
|
+
"test": "npm run test:all",
|
|
31
|
+
"test:all": "npm run build && npm run test:mock && npm run test:live",
|
|
32
|
+
"test:mock": "npx tsx --test test/remote-auth-simple-test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts",
|
|
33
|
+
"test:live": "node test/validate-api.js && tsx test/readonly-mcp-tests.ts",
|
|
32
34
|
"test:remote-auth": "npm run build && npx tsx --test test/remote-auth-simple-test.ts",
|
|
33
|
-
"test:server": "npm run build && node build/test/test-all-transport-server.js",
|
|
34
35
|
"test:mcp:readonly": "tsx test/readonly-mcp-tests.ts",
|
|
35
36
|
"test:oauth": "tsx test/oauth-tests.ts",
|
|
36
37
|
"test:list-merge-requests": "npm run build && tsx test/test-list-merge-requests.ts",
|
|
37
|
-
"test:all": "npm run test && npm run test:mcp:readonly && npm run test:oauth && npm run test:list-merge-requests",
|
|
38
38
|
"lint": "eslint . --ext .ts",
|
|
39
39
|
"lint:fix": "eslint . --ext .ts --fix",
|
|
40
40
|
"format": "prettier --write \"**/*.{js,ts,json,md}\"",
|