@zereight/mcp-gitlab 2.0.36 → 2.1.1
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 +69 -332
- package/build/auth-retry.js +81 -0
- package/build/config.js +75 -0
- package/build/customSchemas.js +0 -12
- package/build/gitlab-client-pool.js +0 -1
- package/build/index.js +472 -1650
- package/build/oauth.js +4 -4
- package/build/schemas.js +160 -48
- package/build/test/mcp-oauth-tests.js +49 -0
- package/build/test/schema-tests.js +59 -3
- package/build/test/test-auth-retry.js +188 -0
- package/build/test/test-search-code.js +7 -4
- package/build/test/test-token-optimizations.js +691 -0
- package/build/test/test-toolset-filtering.js +39 -33
- package/build/tools/registry.js +1295 -0
- package/build/utils/helpers.js +57 -0
- package/build/utils/schema.js +49 -0
- package/build/utils/url.js +19 -0
- package/package.json +24 -5
package/build/index.js
CHANGED
|
@@ -1,23 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
}
|
|
20
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { getConfig, ENABLE_DYNAMIC_API_URL, GITLAB_AUTH_COOKIE_PATH, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_READ_ONLY_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, } from "./config.js";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21
4
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
22
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
23
6
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -27,33 +10,29 @@ import express from "express";
|
|
|
27
10
|
import fetchCookie from "fetch-cookie";
|
|
28
11
|
import fs from "node:fs";
|
|
29
12
|
import os from "node:os";
|
|
30
|
-
import { HttpProxyAgent } from "http-proxy-agent";
|
|
31
|
-
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
32
13
|
import nodeFetch from "node-fetch";
|
|
33
14
|
import path, { dirname } from "node:path";
|
|
34
|
-
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
35
15
|
import { CookieJar, parse as parseCookie } from "tough-cookie";
|
|
36
16
|
import { fileURLToPath, URL } from "node:url";
|
|
37
17
|
import { z } from "zod";
|
|
38
|
-
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
39
18
|
import { initializeOAuthClient } from "./oauth.js";
|
|
40
19
|
import { createGitLabOAuthProvider } from "./oauth-proxy.js";
|
|
41
20
|
import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
21
|
+
import { normalizeGitLabApiUrl } from "./utils/url.js";
|
|
22
|
+
import { estimateMergeCommitCount, filterDiffsByPatterns, summarizeWebhookEvents } from "./utils/helpers.js";
|
|
42
23
|
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
|
43
24
|
import { GitLabClientPool } from "./gitlab-client-pool.js";
|
|
44
|
-
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateLabelSchema, // Added
|
|
48
|
-
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema,
|
|
25
|
+
import { allTools, readOnlyTools, destructiveTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, TOOLSET_DEFINITIONS, ALL_TOOLSET_IDS, } from "./tools/registry.js";
|
|
26
|
+
import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, // Added
|
|
27
|
+
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema,
|
|
49
28
|
// pipeline job schemas
|
|
50
29
|
GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
|
|
51
30
|
// Discussion Schemas
|
|
52
31
|
GitLabDiscussionNoteSchema, // Added
|
|
53
32
|
GitLabDiscussionSchema,
|
|
54
33
|
// Draft Notes Schemas
|
|
55
|
-
GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema,
|
|
56
|
-
GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, 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, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, } from "./schemas.js";
|
|
34
|
+
GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
35
|
+
GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, 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, GetWorkItemSchema, ListWorkItemsSchema, CreateWorkItemSchema, UpdateWorkItemSchema, ConvertWorkItemTypeSchema, ListWorkItemStatusesSchema, ListWorkItemNotesSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, } from "./schemas.js";
|
|
57
36
|
import { randomUUID } from "node:crypto";
|
|
58
37
|
import { pino } from "pino";
|
|
59
38
|
const logger = pino({
|
|
@@ -118,10 +97,25 @@ function createServer() {
|
|
|
118
97
|
? toolsAfterLegacy.filter(tool => readOnlyTools.has(tool.name))
|
|
119
98
|
: toolsAfterLegacy;
|
|
120
99
|
// Step 5: Regex denial filter
|
|
121
|
-
|
|
100
|
+
let filteredTools = GITLAB_DENIED_TOOLS_REGEX
|
|
122
101
|
? toolsAfterReadOnly.filter(tool => !GITLAB_DENIED_TOOLS_REGEX.test(tool.name))
|
|
123
|
-
: toolsAfterReadOnly;
|
|
124
|
-
|
|
102
|
+
: [...toolsAfterReadOnly];
|
|
103
|
+
// Step 5.5: Always include discover_tools meta-tool (bypasses toolset filter)
|
|
104
|
+
const discoverTool = allTools.find(t => t.name === "discover_tools");
|
|
105
|
+
const filteredToolNames = new Set(filteredTools.map(t => t.name));
|
|
106
|
+
if (discoverTool && !filteredToolNames.has("discover_tools")) {
|
|
107
|
+
// Respect read-only and regex denial filters
|
|
108
|
+
const passesReadOnly = !GITLAB_READ_ONLY_MODE || readOnlyTools.has("discover_tools");
|
|
109
|
+
const passesRegex = !GITLAB_DENIED_TOOLS_REGEX?.test("discover_tools");
|
|
110
|
+
if (passesReadOnly && passesRegex) {
|
|
111
|
+
filteredTools.push(discoverTool);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Step 5.7: Remove hidden policy tools
|
|
115
|
+
if (hiddenToolSet.size > 0) {
|
|
116
|
+
filteredTools = filteredTools.filter(tool => !hiddenToolSet.has(tool.name));
|
|
117
|
+
}
|
|
118
|
+
const mcpServer = new McpServer({
|
|
125
119
|
name: "better-gitlab-mcp-server",
|
|
126
120
|
version: SERVER_VERSION,
|
|
127
121
|
}, {
|
|
@@ -129,48 +123,190 @@ function createServer() {
|
|
|
129
123
|
tools: {},
|
|
130
124
|
},
|
|
131
125
|
});
|
|
132
|
-
|
|
133
|
-
// Step 6: Gemini $schema cleanup (only dynamic step per request)
|
|
126
|
+
mcpServer.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
127
|
+
// Step 6: Gemini $schema cleanup + annotations (only dynamic step per request)
|
|
134
128
|
// <<< START: Remove $schema for Gemini compatibility >>>
|
|
135
|
-
const tools =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
delete
|
|
143
|
-
return { ...tool, inputSchema: modifiedSchema };
|
|
129
|
+
const tools = filteredTools.map(tool => {
|
|
130
|
+
const modified = { ...tool };
|
|
131
|
+
// Safety net: remove $schema if present (toJSONSchema strips it for zod schemas,
|
|
132
|
+
// but manually-defined schemas like discover_tools may still have it)
|
|
133
|
+
if (modified.inputSchema && typeof modified.inputSchema === "object" && modified.inputSchema !== null) {
|
|
134
|
+
if ("$schema" in modified.inputSchema) {
|
|
135
|
+
modified.inputSchema = { ...modified.inputSchema };
|
|
136
|
+
delete modified.inputSchema.$schema;
|
|
144
137
|
}
|
|
145
138
|
}
|
|
146
|
-
//
|
|
147
|
-
|
|
139
|
+
// Add MCP tool annotations
|
|
140
|
+
modified.annotations = {
|
|
141
|
+
...(readOnlyTools.has(tool.name) ? { readOnlyHint: true } : {}),
|
|
142
|
+
...(destructiveTools.has(tool.name) ? { destructiveHint: true } : {}),
|
|
143
|
+
...(approveToolSet.has(tool.name) ? { confirmationHint: true } : {}),
|
|
144
|
+
openWorldHint: true,
|
|
145
|
+
};
|
|
146
|
+
// Inject _confirmed optional parameter for approve-policy tools
|
|
147
|
+
if (approveToolSet.has(tool.name) && modified.inputSchema?.properties) {
|
|
148
|
+
modified.inputSchema = {
|
|
149
|
+
...modified.inputSchema,
|
|
150
|
+
properties: {
|
|
151
|
+
...modified.inputSchema.properties,
|
|
152
|
+
_confirmed: {
|
|
153
|
+
type: "boolean",
|
|
154
|
+
description: "Set to true to confirm execution of this approval-required tool.",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return modified;
|
|
148
160
|
});
|
|
149
161
|
// <<< END: Remove $schema for Gemini compatibility >>>
|
|
150
162
|
return {
|
|
151
163
|
tools, // return tool list with $schema removed
|
|
152
164
|
};
|
|
153
165
|
});
|
|
154
|
-
|
|
166
|
+
mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
155
167
|
// Manually retrieve the session context using the session ID passed in the request.
|
|
156
168
|
// This is a robust workaround for AsyncLocalStorage context loss.
|
|
157
169
|
const sessionId = request.params.sessionId;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
const toolName = request.params.name;
|
|
171
|
+
const start = Date.now();
|
|
172
|
+
const logCompletion = (result) => {
|
|
173
|
+
const durationMs = Date.now() - start;
|
|
174
|
+
logger.info({ tool: toolName, event: "tool_call_done", durationMs }, `tool_call_done: ${toolName} (${durationMs}ms)`);
|
|
175
|
+
return result;
|
|
176
|
+
};
|
|
177
|
+
const logError = (error) => {
|
|
178
|
+
const durationMs = Date.now() - start;
|
|
179
|
+
logger.error({ tool: toolName, event: "tool_call_error", durationMs, error: error instanceof Error ? error.message : String(error) }, `tool_call_error: ${toolName} (${durationMs}ms)`);
|
|
180
|
+
throw error;
|
|
181
|
+
};
|
|
182
|
+
try {
|
|
183
|
+
// Handle discover_tools meta-tool directly (needs access to mcpServer and filteredTools)
|
|
184
|
+
if (toolName === "discover_tools") {
|
|
185
|
+
const category = request.params.arguments?.category?.trim()?.toLowerCase();
|
|
186
|
+
const currentToolNames = new Set(filteredTools.map(t => t.name));
|
|
187
|
+
if (!category) {
|
|
188
|
+
// List available categories with activation status
|
|
189
|
+
const categories = TOOLSET_DEFINITIONS.map(def => ({
|
|
190
|
+
id: def.id,
|
|
191
|
+
toolCount: def.tools.size,
|
|
192
|
+
active: [...def.tools].some(t => currentToolNames.has(t)),
|
|
193
|
+
isDefault: def.isDefault,
|
|
194
|
+
}));
|
|
195
|
+
return logCompletion({
|
|
196
|
+
content: [{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: JSON.stringify({ categories, hint: "Call discover_tools with a category name to activate it" }, null, 2),
|
|
199
|
+
}],
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (!ALL_TOOLSET_IDS.has(category)) {
|
|
203
|
+
return logCompletion({
|
|
204
|
+
content: [{
|
|
205
|
+
type: "text",
|
|
206
|
+
text: `Unknown category "${category}". Available: ${[...ALL_TOOLSET_IDS].join(", ")}`,
|
|
207
|
+
}],
|
|
208
|
+
isError: true,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const toolsetDef = TOOLSET_DEFINITIONS.find(d => d.id === category);
|
|
212
|
+
if (!toolsetDef) {
|
|
213
|
+
return logCompletion({
|
|
214
|
+
content: [{ type: "text", text: `Category "${category}" not found.` }],
|
|
215
|
+
isError: true,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
// Check if already fully active
|
|
219
|
+
const alreadyActive = [...toolsetDef.tools].every(t => currentToolNames.has(t));
|
|
220
|
+
if (alreadyActive) {
|
|
221
|
+
return logCompletion({
|
|
222
|
+
content: [{
|
|
223
|
+
type: "text",
|
|
224
|
+
text: `Category "${category}" is already active (${toolsetDef.tools.size} tools).`,
|
|
225
|
+
}],
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
// Add tools from this toolset, respecting all filtering policies
|
|
229
|
+
const newTools = [];
|
|
230
|
+
for (const tool of allTools) {
|
|
231
|
+
if (!toolsetDef.tools.has(tool.name))
|
|
232
|
+
continue;
|
|
233
|
+
if (currentToolNames.has(tool.name))
|
|
234
|
+
continue;
|
|
235
|
+
if (GITLAB_READ_ONLY_MODE && !readOnlyTools.has(tool.name))
|
|
236
|
+
continue;
|
|
237
|
+
if (GITLAB_DENIED_TOOLS_REGEX?.test(tool.name))
|
|
238
|
+
continue;
|
|
239
|
+
if (hiddenToolSet.has(tool.name))
|
|
240
|
+
continue;
|
|
241
|
+
newTools.push(tool);
|
|
242
|
+
}
|
|
243
|
+
if (newTools.length === 0) {
|
|
244
|
+
return logCompletion({
|
|
245
|
+
content: [{
|
|
246
|
+
type: "text",
|
|
247
|
+
text: `Category "${category}" has no additional tools to activate (all already active or filtered).`,
|
|
248
|
+
}],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
filteredTools.push(...newTools);
|
|
252
|
+
// Notify client that tool list has changed
|
|
253
|
+
try {
|
|
254
|
+
await mcpServer.server.sendToolListChanged();
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Client may not support notifications - safe to ignore
|
|
258
|
+
}
|
|
259
|
+
const addedNames = newTools.map(t => t.name);
|
|
260
|
+
logger.info({ event: "toolset_activated", category, toolCount: addedNames.length }, `Activated toolset: ${category} (+${addedNames.length} tools)`);
|
|
261
|
+
return logCompletion({
|
|
262
|
+
content: [{
|
|
263
|
+
type: "text",
|
|
264
|
+
text: JSON.stringify({
|
|
265
|
+
activated: category,
|
|
266
|
+
addedTools: addedNames,
|
|
267
|
+
totalTools: filteredTools.length,
|
|
268
|
+
}, null, 2),
|
|
269
|
+
}],
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
// Check approve policy: tool is exposed but requires explicit confirmation
|
|
273
|
+
if (approveToolSet.has(toolName)) {
|
|
274
|
+
const confirmed = request.params.arguments?._confirmed === true;
|
|
275
|
+
if (!confirmed) {
|
|
276
|
+
logger.info({ tool: toolName, event: "tool_call_approval_required" }, `Approval required: ${toolName}`);
|
|
277
|
+
return logCompletion({
|
|
278
|
+
content: [{
|
|
279
|
+
type: "text",
|
|
280
|
+
text: `Tool "${toolName}" requires confirmation. This tool is marked as requiring approval before execution. Re-call with _confirmed: true to proceed.`,
|
|
281
|
+
}],
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
// Strip _confirmed from args before forwarding to handler
|
|
285
|
+
const { _confirmed, ...cleanArgs } = request.params.arguments || {};
|
|
286
|
+
request.params.arguments = cleanArgs;
|
|
287
|
+
}
|
|
288
|
+
if ((REMOTE_AUTHORIZATION || GITLAB_MCP_OAUTH) && sessionId && authBySession[sessionId]) {
|
|
289
|
+
const authData = authBySession[sessionId];
|
|
290
|
+
const sessionContext = {
|
|
291
|
+
sessionId,
|
|
292
|
+
header: authData.header,
|
|
293
|
+
token: authData.token,
|
|
294
|
+
lastUsed: authData.lastUsed,
|
|
295
|
+
apiUrl: authData.apiUrl,
|
|
296
|
+
};
|
|
297
|
+
// Run the handler within the retrieved context
|
|
298
|
+
const result = await sessionAuthStore.run(sessionContext, () => handleToolCall(request.params));
|
|
299
|
+
return logCompletion(result);
|
|
300
|
+
}
|
|
301
|
+
// Fallback for non-remote-auth mode or if session is not found
|
|
302
|
+
const result = await handleToolCall(request.params);
|
|
303
|
+
return logCompletion(result);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
logError(error);
|
|
169
307
|
}
|
|
170
|
-
// Fallback for non-remote-auth mode or if session is not found
|
|
171
|
-
return handleToolCall(request.params);
|
|
172
308
|
});
|
|
173
|
-
return
|
|
309
|
+
return mcpServer;
|
|
174
310
|
}
|
|
175
311
|
/**
|
|
176
312
|
* Validate configuration at startup
|
|
@@ -274,8 +410,6 @@ function validateConfiguration() {
|
|
|
274
410
|
}
|
|
275
411
|
logger.info("Configuration validation passed");
|
|
276
412
|
}
|
|
277
|
-
const GITLAB_PERSONAL_ACCESS_TOKEN = getConfig("token", "GITLAB_PERSONAL_ACCESS_TOKEN");
|
|
278
|
-
const GITLAB_JOB_TOKEN = getConfig("job-token", "GITLAB_JOB_TOKEN");
|
|
279
413
|
let OAUTH_ACCESS_TOKEN = null;
|
|
280
414
|
let oauthClient = null;
|
|
281
415
|
/**
|
|
@@ -299,10 +433,6 @@ async function ensureValidOAuthToken() {
|
|
|
299
433
|
throw error;
|
|
300
434
|
}
|
|
301
435
|
}
|
|
302
|
-
const GITLAB_AUTH_COOKIE_PATH = getConfig("cookie-path", "GITLAB_AUTH_COOKIE_PATH");
|
|
303
|
-
const USE_OAUTH = getConfig("use-oauth", "GITLAB_USE_OAUTH") === "true";
|
|
304
|
-
const IS_OLD = getConfig("is-old", "GITLAB_IS_OLD") === "true";
|
|
305
|
-
const GITLAB_READ_ONLY_MODE = getConfig("read-only", "GITLAB_READ_ONLY_MODE") === "true";
|
|
306
436
|
const GITLAB_DENIED_TOOLS_REGEX = (() => {
|
|
307
437
|
const pattern = getConfig("denied-tools-regex", "GITLAB_DENIED_TOOLS_REGEX");
|
|
308
438
|
if (!pattern)
|
|
@@ -332,64 +462,31 @@ const GITLAB_DENIED_TOOLS_REGEX = (() => {
|
|
|
332
462
|
return undefined;
|
|
333
463
|
}
|
|
334
464
|
})();
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const PORT = Number.parseInt(getConfig("port", "PORT", "3002"), 10);
|
|
354
|
-
// Add proxy configuration
|
|
355
|
-
const HTTP_PROXY = getConfig("http-proxy", "HTTP_PROXY");
|
|
356
|
-
const HTTPS_PROXY = getConfig("https-proxy", "HTTPS_PROXY");
|
|
357
|
-
const NO_PROXY = getConfig("no-proxy", "NO_PROXY");
|
|
358
|
-
const NODE_TLS_REJECT_UNAUTHORIZED = getConfig("tls-reject-unauthorized", "NODE_TLS_REJECT_UNAUTHORIZED");
|
|
359
|
-
const GITLAB_CA_CERT_PATH = getConfig("ca-cert-path", "GITLAB_CA_CERT_PATH");
|
|
360
|
-
const GITLAB_POOL_MAX_SIZE = getConfig("pool-max-size", "GITLAB_POOL_MAX_SIZE")
|
|
361
|
-
? Number.parseInt(getConfig("pool-max-size", "GITLAB_POOL_MAX_SIZE"), 10)
|
|
362
|
-
: 100;
|
|
363
|
-
let sslOptions = undefined;
|
|
364
|
-
if (NODE_TLS_REJECT_UNAUTHORIZED === "0") {
|
|
365
|
-
sslOptions = { rejectUnauthorized: false };
|
|
366
|
-
}
|
|
367
|
-
else if (GITLAB_CA_CERT_PATH) {
|
|
368
|
-
const ca = fs.readFileSync(GITLAB_CA_CERT_PATH);
|
|
369
|
-
sslOptions = { ca };
|
|
370
|
-
}
|
|
371
|
-
// Configure proxy agents if proxies are set
|
|
372
|
-
let httpAgent = undefined;
|
|
373
|
-
let httpsAgent = undefined;
|
|
374
|
-
if (HTTP_PROXY) {
|
|
375
|
-
if (HTTP_PROXY.startsWith("socks")) {
|
|
376
|
-
httpAgent = new SocksProxyAgent(HTTP_PROXY);
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
httpAgent = new HttpProxyAgent(HTTP_PROXY);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (HTTPS_PROXY) {
|
|
383
|
-
if (HTTPS_PROXY.startsWith("socks")) {
|
|
384
|
-
httpsAgent = new SocksProxyAgent(HTTPS_PROXY);
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
// Tool policy: approve / hidden sets
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
const approveToolSet = new Set((GITLAB_TOOL_POLICY_APPROVE_RAW || "")
|
|
469
|
+
.split(",")
|
|
470
|
+
.map(s => s.trim())
|
|
471
|
+
.filter(Boolean));
|
|
472
|
+
const hiddenToolSet = new Set((GITLAB_TOOL_POLICY_HIDDEN_RAW || "")
|
|
473
|
+
.split(",")
|
|
474
|
+
.map(s => s.trim())
|
|
475
|
+
.filter(Boolean));
|
|
476
|
+
// Validate approve/hidden tool names against known tools at startup
|
|
477
|
+
{
|
|
478
|
+
const knownToolNames = new Set(allTools.map(t => t.name));
|
|
479
|
+
for (const name of approveToolSet) {
|
|
480
|
+
if (!knownToolNames.has(name)) {
|
|
481
|
+
logger.warn({ event: "unknown_approve_tool", name }, `GITLAB_TOOL_POLICY_APPROVE contains unknown tool: "${name}"`);
|
|
482
|
+
}
|
|
385
483
|
}
|
|
386
|
-
|
|
387
|
-
|
|
484
|
+
for (const name of hiddenToolSet) {
|
|
485
|
+
if (!knownToolNames.has(name)) {
|
|
486
|
+
logger.warn({ event: "unknown_hidden_tool", name }, `GITLAB_TOOL_POLICY_HIDDEN contains unknown tool: "${name}"`);
|
|
487
|
+
}
|
|
388
488
|
}
|
|
389
489
|
}
|
|
390
|
-
httpsAgent = httpsAgent || new HttpsAgent(sslOptions);
|
|
391
|
-
httpAgent = httpAgent || new Agent();
|
|
392
|
-
// Initialize the client pool for managing multiple GitLab instances
|
|
393
490
|
const clientPool = new GitLabClientPool({
|
|
394
491
|
apiUrls: (getConfig("api-url", "GITLAB_API_URL") || "https://gitlab.com")
|
|
395
492
|
.split(",")
|
|
@@ -449,9 +546,22 @@ const createCookieJar = async () => {
|
|
|
449
546
|
}
|
|
450
547
|
return jar;
|
|
451
548
|
};
|
|
549
|
+
// Auth retry helpers — extracted to auth-retry.ts for testability (no side effects)
|
|
550
|
+
export { headersToPlainObject, isNonReplayableBody, wrapWithAuthRetry, } from "./auth-retry.js";
|
|
551
|
+
import { wrapWithAuthRetry } from "./auth-retry.js";
|
|
552
|
+
/** Build AuthRetryConfig from module globals (lazy — reads globals at call time). */
|
|
553
|
+
function defaultAuthRetryConfig() {
|
|
554
|
+
return {
|
|
555
|
+
isOAuthEnabled: () => USE_OAUTH && oauthClient != null,
|
|
556
|
+
refreshToken: (force) => oauthClient.getAccessToken(force),
|
|
557
|
+
onTokenRefreshed: (token) => { OAUTH_ACCESS_TOKEN = token; },
|
|
558
|
+
buildAuthHeaders,
|
|
559
|
+
logger,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
452
562
|
// Cookie jar and fetch - reloaded when cookie file changes
|
|
453
563
|
let cookieJar = null;
|
|
454
|
-
let fetch = nodeFetch;
|
|
564
|
+
let fetch = wrapWithAuthRetry(nodeFetch, defaultAuthRetryConfig());
|
|
455
565
|
let lastCookieMtime = 0;
|
|
456
566
|
let cookieReloadLock = null; // Mutex to prevent parallel reloads
|
|
457
567
|
// Auth proxies may redirect and set cookies on the first request. We make a throwaway
|
|
@@ -471,7 +581,7 @@ async function reloadCookiesIfChanged() {
|
|
|
471
581
|
lastCookieMtime = mtime;
|
|
472
582
|
const newJar = await createCookieJar();
|
|
473
583
|
cookieJar = newJar;
|
|
474
|
-
fetch = newJar ? fetchCookie(nodeFetch, newJar) : nodeFetch;
|
|
584
|
+
fetch = wrapWithAuthRetry(newJar ? fetchCookie(nodeFetch, newJar) : nodeFetch, defaultAuthRetryConfig());
|
|
475
585
|
initialSessionRequestMade = false;
|
|
476
586
|
}
|
|
477
587
|
}
|
|
@@ -480,7 +590,7 @@ async function reloadCookiesIfChanged() {
|
|
|
480
590
|
if (cookieJar) {
|
|
481
591
|
logger.info("Cookie file removed, clearing cached cookies");
|
|
482
592
|
cookieJar = null;
|
|
483
|
-
fetch = nodeFetch;
|
|
593
|
+
fetch = wrapWithAuthRetry(nodeFetch, defaultAuthRetryConfig());
|
|
484
594
|
lastCookieMtime = 0;
|
|
485
595
|
initialSessionRequestMade = false;
|
|
486
596
|
}
|
|
@@ -586,1190 +696,6 @@ const getFetchConfig = () => {
|
|
|
586
696
|
agent: agent,
|
|
587
697
|
};
|
|
588
698
|
};
|
|
589
|
-
const toJSONSchema = (schema) => {
|
|
590
|
-
const jsonSchema = zodToJsonSchema(schema, { $refStrategy: "none" });
|
|
591
|
-
// Post-process to fix nullable/optional fields that should truly be optional
|
|
592
|
-
function fixNullableOptional(obj) {
|
|
593
|
-
if (obj && typeof obj === "object") {
|
|
594
|
-
// If this object has properties, process them
|
|
595
|
-
if (obj.properties) {
|
|
596
|
-
const requiredSet = new Set(obj.required || []);
|
|
597
|
-
Object.keys(obj.properties).forEach(key => {
|
|
598
|
-
const prop = obj.properties[key];
|
|
599
|
-
// Handle fields that can be null or omitted
|
|
600
|
-
// If a property has type: ["object", "null"] or anyOf with null, it should not be required
|
|
601
|
-
if (prop.anyOf && prop.anyOf.some((t) => t.type === "null")) {
|
|
602
|
-
requiredSet.delete(key);
|
|
603
|
-
}
|
|
604
|
-
else if (Array.isArray(prop.type) && prop.type.includes("null")) {
|
|
605
|
-
requiredSet.delete(key);
|
|
606
|
-
}
|
|
607
|
-
// Recursively process nested objects
|
|
608
|
-
obj.properties[key] = fixNullableOptional(prop);
|
|
609
|
-
});
|
|
610
|
-
// Normalize the required array after processing all properties
|
|
611
|
-
if (requiredSet.size > 0) {
|
|
612
|
-
obj.required = Array.from(requiredSet);
|
|
613
|
-
}
|
|
614
|
-
else if (Object.prototype.hasOwnProperty.call(obj, "required")) {
|
|
615
|
-
delete obj.required;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
// Process anyOf/allOf/oneOf
|
|
619
|
-
["anyOf", "allOf", "oneOf"].forEach(combiner => {
|
|
620
|
-
if (obj[combiner]) {
|
|
621
|
-
obj[combiner] = obj[combiner].map(fixNullableOptional);
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
return obj;
|
|
626
|
-
}
|
|
627
|
-
return fixNullableOptional(jsonSchema);
|
|
628
|
-
};
|
|
629
|
-
// Define all available tools
|
|
630
|
-
const allTools = [
|
|
631
|
-
{
|
|
632
|
-
name: "merge_merge_request",
|
|
633
|
-
description: "Merge a merge request in a GitLab project",
|
|
634
|
-
inputSchema: toJSONSchema(MergeMergeRequestSchema),
|
|
635
|
-
},
|
|
636
|
-
{
|
|
637
|
-
name: "approve_merge_request",
|
|
638
|
-
description: "Approve a merge request. Requires appropriate permissions.",
|
|
639
|
-
inputSchema: toJSONSchema(ApproveMergeRequestSchema),
|
|
640
|
-
},
|
|
641
|
-
{
|
|
642
|
-
name: "unapprove_merge_request",
|
|
643
|
-
description: "Unapprove a previously approved merge request. Requires appropriate permissions.",
|
|
644
|
-
inputSchema: toJSONSchema(UnapproveMergeRequestSchema),
|
|
645
|
-
},
|
|
646
|
-
{
|
|
647
|
-
name: "get_merge_request_approval_state",
|
|
648
|
-
description: "Get merge request approval details including approvers (uses approval_state when available, falls back to approvals endpoint)",
|
|
649
|
-
inputSchema: toJSONSchema(GetMergeRequestApprovalStateSchema),
|
|
650
|
-
},
|
|
651
|
-
{
|
|
652
|
-
name: "get_merge_request_conflicts",
|
|
653
|
-
description: "Get the conflicts of a merge request in a GitLab project",
|
|
654
|
-
inputSchema: toJSONSchema(GetMergeRequestConflictsSchema),
|
|
655
|
-
},
|
|
656
|
-
{
|
|
657
|
-
name: "execute_graphql",
|
|
658
|
-
description: "Execute a GitLab GraphQL query",
|
|
659
|
-
inputSchema: zodToJsonSchema(ExecuteGraphQLSchema),
|
|
660
|
-
},
|
|
661
|
-
{
|
|
662
|
-
name: "create_or_update_file",
|
|
663
|
-
description: "Create or update a single file in a GitLab project",
|
|
664
|
-
inputSchema: toJSONSchema(CreateOrUpdateFileSchema),
|
|
665
|
-
},
|
|
666
|
-
{
|
|
667
|
-
name: "search_repositories",
|
|
668
|
-
description: "Search for GitLab projects",
|
|
669
|
-
inputSchema: toJSONSchema(SearchRepositoriesSchema),
|
|
670
|
-
},
|
|
671
|
-
{
|
|
672
|
-
name: "create_repository",
|
|
673
|
-
description: "Create a new GitLab project",
|
|
674
|
-
inputSchema: toJSONSchema(CreateRepositorySchema),
|
|
675
|
-
},
|
|
676
|
-
{
|
|
677
|
-
name: "get_file_contents",
|
|
678
|
-
description: "Get the contents of a file or directory from a GitLab project",
|
|
679
|
-
inputSchema: toJSONSchema(GetFileContentsSchema),
|
|
680
|
-
},
|
|
681
|
-
{
|
|
682
|
-
name: "push_files",
|
|
683
|
-
description: "Push multiple files to a GitLab project in a single commit",
|
|
684
|
-
inputSchema: toJSONSchema(PushFilesSchema),
|
|
685
|
-
},
|
|
686
|
-
{
|
|
687
|
-
name: "create_issue",
|
|
688
|
-
description: "Create a new issue in a GitLab project",
|
|
689
|
-
inputSchema: toJSONSchema(CreateIssueSchema),
|
|
690
|
-
},
|
|
691
|
-
{
|
|
692
|
-
name: "create_merge_request",
|
|
693
|
-
description: "Create a new merge request in a GitLab project",
|
|
694
|
-
inputSchema: toJSONSchema(CreateMergeRequestSchema),
|
|
695
|
-
},
|
|
696
|
-
{
|
|
697
|
-
name: "fork_repository",
|
|
698
|
-
description: "Fork a GitLab project to your account or specified namespace",
|
|
699
|
-
inputSchema: toJSONSchema(ForkRepositorySchema),
|
|
700
|
-
},
|
|
701
|
-
{
|
|
702
|
-
name: "create_branch",
|
|
703
|
-
description: "Create a new branch in a GitLab project",
|
|
704
|
-
inputSchema: toJSONSchema(CreateBranchSchema),
|
|
705
|
-
},
|
|
706
|
-
{
|
|
707
|
-
name: "get_merge_request",
|
|
708
|
-
description: "Get details of a merge request with compact deployment, commit addition, and approval summaries (Either mergeRequestIid or branchName must be provided)",
|
|
709
|
-
inputSchema: toJSONSchema(GetMergeRequestSchema),
|
|
710
|
-
},
|
|
711
|
-
{
|
|
712
|
-
name: "get_merge_request_diffs",
|
|
713
|
-
description: "Get the changes/diffs of a merge request (Either mergeRequestIid or branchName must be provided)",
|
|
714
|
-
inputSchema: toJSONSchema(GetMergeRequestDiffsSchema),
|
|
715
|
-
},
|
|
716
|
-
{
|
|
717
|
-
name: "list_merge_request_changed_files",
|
|
718
|
-
description: "STEP 1 of code review workflow. " +
|
|
719
|
-
"Returns ONLY the list of changed file paths in a merge request — WITHOUT diff content. " +
|
|
720
|
-
"Call this first to get file paths, then call get_merge_request_file_diff with multiple files in a single batched call (recommended 3-5 files per call). " +
|
|
721
|
-
"This avoids loading the entire diff payload at once and reduces API calls. " +
|
|
722
|
-
"Supports excluded_file_patterns filtering using regex. " +
|
|
723
|
-
"Returns: new_path, old_path, new_file, deleted_file, renamed_file flags for each file. " +
|
|
724
|
-
"(Either mergeRequestIid or branchName must be provided)",
|
|
725
|
-
inputSchema: toJSONSchema(ListMergeRequestChangedFilesSchema),
|
|
726
|
-
},
|
|
727
|
-
{
|
|
728
|
-
name: "list_merge_request_diffs",
|
|
729
|
-
description: "List merge request diffs with pagination support (Either mergeRequestIid or branchName must be provided)",
|
|
730
|
-
inputSchema: toJSONSchema(ListMergeRequestDiffsSchema),
|
|
731
|
-
},
|
|
732
|
-
{
|
|
733
|
-
name: "get_merge_request_file_diff",
|
|
734
|
-
description: "STEP 2 of code review workflow. " +
|
|
735
|
-
"Get diffs for one or more files from a merge request. " +
|
|
736
|
-
"Call list_merge_request_changed_files first to get file paths, then pass them as an array to fetch their diffs efficiently. " +
|
|
737
|
-
"Batching multiple files (recommended 3-5) is supported and preferred over individual requests. " +
|
|
738
|
-
"Returns an array of results - one per requested file path. Files not found are returned with error messages. " +
|
|
739
|
-
"(Either mergeRequestIid or branchName must be provided)",
|
|
740
|
-
inputSchema: toJSONSchema(GetMergeRequestFileDiffSchema),
|
|
741
|
-
},
|
|
742
|
-
{
|
|
743
|
-
name: "list_merge_request_versions",
|
|
744
|
-
description: "List all versions of a merge request",
|
|
745
|
-
inputSchema: toJSONSchema(ListMergeRequestVersionsSchema),
|
|
746
|
-
},
|
|
747
|
-
{
|
|
748
|
-
name: "get_merge_request_version",
|
|
749
|
-
description: "Get a specific version of a merge request",
|
|
750
|
-
inputSchema: toJSONSchema(GetMergeRequestVersionSchema),
|
|
751
|
-
},
|
|
752
|
-
{
|
|
753
|
-
name: "get_branch_diffs",
|
|
754
|
-
description: "Get the changes/diffs between two branches or commits in a GitLab project",
|
|
755
|
-
inputSchema: toJSONSchema(GetBranchDiffsSchema),
|
|
756
|
-
},
|
|
757
|
-
{
|
|
758
|
-
name: "update_merge_request",
|
|
759
|
-
description: "Update a merge request (Either mergeRequestIid or branchName must be provided)",
|
|
760
|
-
inputSchema: toJSONSchema(UpdateMergeRequestSchema),
|
|
761
|
-
},
|
|
762
|
-
{
|
|
763
|
-
name: "create_note",
|
|
764
|
-
description: "Create a new note (comment) to an issue or merge request",
|
|
765
|
-
inputSchema: toJSONSchema(CreateNoteSchema),
|
|
766
|
-
},
|
|
767
|
-
{
|
|
768
|
-
name: "create_merge_request_thread",
|
|
769
|
-
description: "Create a new thread on a merge request",
|
|
770
|
-
inputSchema: toJSONSchema(CreateMergeRequestThreadSchema),
|
|
771
|
-
},
|
|
772
|
-
{
|
|
773
|
-
name: "resolve_merge_request_thread",
|
|
774
|
-
description: "Resolve a thread on a merge request",
|
|
775
|
-
inputSchema: toJSONSchema(ResolveMergeRequestThreadSchema),
|
|
776
|
-
},
|
|
777
|
-
{
|
|
778
|
-
name: "mr_discussions",
|
|
779
|
-
description: "List discussion items for a merge request",
|
|
780
|
-
inputSchema: toJSONSchema(ListMergeRequestDiscussionsSchema),
|
|
781
|
-
},
|
|
782
|
-
{
|
|
783
|
-
name: "delete_merge_request_discussion_note",
|
|
784
|
-
description: "Delete a discussion note on a merge request",
|
|
785
|
-
inputSchema: toJSONSchema(DeleteMergeRequestDiscussionNoteSchema),
|
|
786
|
-
},
|
|
787
|
-
{
|
|
788
|
-
name: "update_merge_request_discussion_note",
|
|
789
|
-
description: "Update a discussion note on a merge request",
|
|
790
|
-
inputSchema: toJSONSchema(UpdateMergeRequestDiscussionNoteSchema),
|
|
791
|
-
},
|
|
792
|
-
{
|
|
793
|
-
name: "create_merge_request_discussion_note",
|
|
794
|
-
description: "Add a new discussion note to an existing merge request thread",
|
|
795
|
-
inputSchema: toJSONSchema(CreateMergeRequestDiscussionNoteSchema),
|
|
796
|
-
},
|
|
797
|
-
{
|
|
798
|
-
name: "create_merge_request_note",
|
|
799
|
-
description: "Add a new note to a merge request",
|
|
800
|
-
inputSchema: toJSONSchema(CreateMergeRequestNoteSchema),
|
|
801
|
-
},
|
|
802
|
-
{
|
|
803
|
-
name: "delete_merge_request_note",
|
|
804
|
-
description: "Delete an existing merge request note",
|
|
805
|
-
inputSchema: toJSONSchema(DeleteMergeRequestNoteSchema),
|
|
806
|
-
},
|
|
807
|
-
{
|
|
808
|
-
name: "get_merge_request_note",
|
|
809
|
-
description: "Get a specific note for a merge request",
|
|
810
|
-
inputSchema: toJSONSchema(GetMergeRequestNoteSchema),
|
|
811
|
-
},
|
|
812
|
-
{
|
|
813
|
-
name: "get_merge_request_notes",
|
|
814
|
-
description: "List notes for a merge request",
|
|
815
|
-
inputSchema: toJSONSchema(GetMergeRequestNotesSchema),
|
|
816
|
-
},
|
|
817
|
-
{
|
|
818
|
-
name: "update_merge_request_note",
|
|
819
|
-
description: "Modify an existing merge request note",
|
|
820
|
-
inputSchema: toJSONSchema(UpdateMergeRequestNoteSchema),
|
|
821
|
-
},
|
|
822
|
-
{
|
|
823
|
-
name: "get_draft_note",
|
|
824
|
-
description: "Get a single draft note from a merge request",
|
|
825
|
-
inputSchema: toJSONSchema(GetDraftNoteSchema),
|
|
826
|
-
},
|
|
827
|
-
{
|
|
828
|
-
name: "list_draft_notes",
|
|
829
|
-
description: "List draft notes for a merge request",
|
|
830
|
-
inputSchema: toJSONSchema(ListDraftNotesSchema),
|
|
831
|
-
},
|
|
832
|
-
{
|
|
833
|
-
name: "create_draft_note",
|
|
834
|
-
description: "Create a draft note for a merge request",
|
|
835
|
-
inputSchema: toJSONSchema(CreateDraftNoteSchema),
|
|
836
|
-
},
|
|
837
|
-
{
|
|
838
|
-
name: "update_draft_note",
|
|
839
|
-
description: "Update an existing draft note",
|
|
840
|
-
inputSchema: toJSONSchema(UpdateDraftNoteSchema),
|
|
841
|
-
},
|
|
842
|
-
{
|
|
843
|
-
name: "delete_draft_note",
|
|
844
|
-
description: "Delete a draft note",
|
|
845
|
-
inputSchema: toJSONSchema(DeleteDraftNoteSchema),
|
|
846
|
-
},
|
|
847
|
-
{
|
|
848
|
-
name: "publish_draft_note",
|
|
849
|
-
description: "Publish a single draft note",
|
|
850
|
-
inputSchema: toJSONSchema(PublishDraftNoteSchema),
|
|
851
|
-
},
|
|
852
|
-
{
|
|
853
|
-
name: "bulk_publish_draft_notes",
|
|
854
|
-
description: "Publish all draft notes for a merge request",
|
|
855
|
-
inputSchema: toJSONSchema(BulkPublishDraftNotesSchema),
|
|
856
|
-
},
|
|
857
|
-
{
|
|
858
|
-
name: "update_issue_note",
|
|
859
|
-
description: "Modify an existing issue thread note",
|
|
860
|
-
inputSchema: toJSONSchema(UpdateIssueNoteSchema),
|
|
861
|
-
},
|
|
862
|
-
{
|
|
863
|
-
name: "create_issue_note",
|
|
864
|
-
description: "Add a note to an issue. Creates a top-level comment, or replies to a discussion thread if discussion_id is provided",
|
|
865
|
-
inputSchema: toJSONSchema(CreateIssueNoteSchema),
|
|
866
|
-
},
|
|
867
|
-
{
|
|
868
|
-
name: "list_issues",
|
|
869
|
-
description: "List issues (default: created by current user only; use scope='all' for all accessible issues)",
|
|
870
|
-
inputSchema: toJSONSchema(ListIssuesSchema),
|
|
871
|
-
},
|
|
872
|
-
{
|
|
873
|
-
name: "my_issues",
|
|
874
|
-
description: "List issues assigned to the authenticated user (defaults to open issues)",
|
|
875
|
-
inputSchema: toJSONSchema(MyIssuesSchema),
|
|
876
|
-
},
|
|
877
|
-
{
|
|
878
|
-
name: "get_issue",
|
|
879
|
-
description: "Get details of a specific issue in a GitLab project",
|
|
880
|
-
inputSchema: toJSONSchema(GetIssueSchema),
|
|
881
|
-
},
|
|
882
|
-
{
|
|
883
|
-
name: "update_issue",
|
|
884
|
-
description: "Update an issue in a GitLab project",
|
|
885
|
-
inputSchema: toJSONSchema(UpdateIssueSchema),
|
|
886
|
-
},
|
|
887
|
-
{
|
|
888
|
-
name: "delete_issue",
|
|
889
|
-
description: "Delete an issue from a GitLab project",
|
|
890
|
-
inputSchema: toJSONSchema(DeleteIssueSchema),
|
|
891
|
-
},
|
|
892
|
-
{
|
|
893
|
-
name: "list_issue_links",
|
|
894
|
-
description: "List all issue links for a specific issue",
|
|
895
|
-
inputSchema: toJSONSchema(ListIssueLinksSchema),
|
|
896
|
-
},
|
|
897
|
-
{
|
|
898
|
-
name: "list_issue_discussions",
|
|
899
|
-
description: "List discussions for an issue in a GitLab project",
|
|
900
|
-
inputSchema: toJSONSchema(ListIssueDiscussionsSchema),
|
|
901
|
-
},
|
|
902
|
-
{
|
|
903
|
-
name: "get_issue_link",
|
|
904
|
-
description: "Get a specific issue link",
|
|
905
|
-
inputSchema: toJSONSchema(GetIssueLinkSchema),
|
|
906
|
-
},
|
|
907
|
-
{
|
|
908
|
-
name: "create_issue_link",
|
|
909
|
-
description: "Create an issue link between two issues",
|
|
910
|
-
inputSchema: toJSONSchema(CreateIssueLinkSchema),
|
|
911
|
-
},
|
|
912
|
-
{
|
|
913
|
-
name: "delete_issue_link",
|
|
914
|
-
description: "Delete an issue link",
|
|
915
|
-
inputSchema: toJSONSchema(DeleteIssueLinkSchema),
|
|
916
|
-
},
|
|
917
|
-
{
|
|
918
|
-
name: "list_namespaces",
|
|
919
|
-
description: "List all namespaces available to the current user",
|
|
920
|
-
inputSchema: toJSONSchema(ListNamespacesSchema),
|
|
921
|
-
},
|
|
922
|
-
{
|
|
923
|
-
name: "get_namespace",
|
|
924
|
-
description: "Get details of a namespace by ID or path",
|
|
925
|
-
inputSchema: toJSONSchema(GetNamespaceSchema),
|
|
926
|
-
},
|
|
927
|
-
{
|
|
928
|
-
name: "verify_namespace",
|
|
929
|
-
description: "Verify if a namespace path exists",
|
|
930
|
-
inputSchema: toJSONSchema(VerifyNamespaceSchema),
|
|
931
|
-
},
|
|
932
|
-
{
|
|
933
|
-
name: "get_project",
|
|
934
|
-
description: "Get details of a specific project",
|
|
935
|
-
inputSchema: toJSONSchema(GetProjectSchema),
|
|
936
|
-
},
|
|
937
|
-
{
|
|
938
|
-
name: "list_projects",
|
|
939
|
-
description: "List projects accessible by the current user",
|
|
940
|
-
inputSchema: toJSONSchema(ListProjectsSchema),
|
|
941
|
-
},
|
|
942
|
-
{
|
|
943
|
-
name: "list_project_members",
|
|
944
|
-
description: "List members of a GitLab project",
|
|
945
|
-
inputSchema: toJSONSchema(ListProjectMembersSchema),
|
|
946
|
-
},
|
|
947
|
-
{
|
|
948
|
-
name: "list_labels",
|
|
949
|
-
description: "List labels for a project",
|
|
950
|
-
inputSchema: toJSONSchema(ListLabelsSchema),
|
|
951
|
-
},
|
|
952
|
-
{
|
|
953
|
-
name: "get_label",
|
|
954
|
-
description: "Get a single label from a project",
|
|
955
|
-
inputSchema: toJSONSchema(GetLabelSchema),
|
|
956
|
-
},
|
|
957
|
-
{
|
|
958
|
-
name: "create_label",
|
|
959
|
-
description: "Create a new label in a project",
|
|
960
|
-
inputSchema: toJSONSchema(CreateLabelSchema),
|
|
961
|
-
},
|
|
962
|
-
{
|
|
963
|
-
name: "update_label",
|
|
964
|
-
description: "Update an existing label in a project",
|
|
965
|
-
inputSchema: toJSONSchema(UpdateLabelSchema),
|
|
966
|
-
},
|
|
967
|
-
{
|
|
968
|
-
name: "delete_label",
|
|
969
|
-
description: "Delete a label from a project",
|
|
970
|
-
inputSchema: toJSONSchema(DeleteLabelSchema),
|
|
971
|
-
},
|
|
972
|
-
{
|
|
973
|
-
name: "list_group_projects",
|
|
974
|
-
description: "List projects in a GitLab group with filtering options",
|
|
975
|
-
inputSchema: toJSONSchema(ListGroupProjectsSchema),
|
|
976
|
-
},
|
|
977
|
-
{
|
|
978
|
-
name: "list_wiki_pages",
|
|
979
|
-
description: "List wiki pages in a GitLab project",
|
|
980
|
-
inputSchema: toJSONSchema(ListWikiPagesSchema),
|
|
981
|
-
},
|
|
982
|
-
{
|
|
983
|
-
name: "get_wiki_page",
|
|
984
|
-
description: "Get details of a specific wiki page",
|
|
985
|
-
inputSchema: toJSONSchema(GetWikiPageSchema),
|
|
986
|
-
},
|
|
987
|
-
{
|
|
988
|
-
name: "create_wiki_page",
|
|
989
|
-
description: "Create a new wiki page in a GitLab project",
|
|
990
|
-
inputSchema: toJSONSchema(CreateWikiPageSchema),
|
|
991
|
-
},
|
|
992
|
-
{
|
|
993
|
-
name: "update_wiki_page",
|
|
994
|
-
description: "Update an existing wiki page in a GitLab project",
|
|
995
|
-
inputSchema: toJSONSchema(UpdateWikiPageSchema),
|
|
996
|
-
},
|
|
997
|
-
{
|
|
998
|
-
name: "delete_wiki_page",
|
|
999
|
-
description: "Delete a wiki page from a GitLab project",
|
|
1000
|
-
inputSchema: toJSONSchema(DeleteWikiPageSchema),
|
|
1001
|
-
},
|
|
1002
|
-
{
|
|
1003
|
-
name: "list_group_wiki_pages",
|
|
1004
|
-
description: "List wiki pages in a GitLab group",
|
|
1005
|
-
inputSchema: toJSONSchema(ListGroupWikiPagesSchema),
|
|
1006
|
-
},
|
|
1007
|
-
{
|
|
1008
|
-
name: "get_group_wiki_page",
|
|
1009
|
-
description: "Get details of a specific group wiki page",
|
|
1010
|
-
inputSchema: toJSONSchema(GetGroupWikiPageSchema),
|
|
1011
|
-
},
|
|
1012
|
-
{
|
|
1013
|
-
name: "create_group_wiki_page",
|
|
1014
|
-
description: "Create a new wiki page in a GitLab group",
|
|
1015
|
-
inputSchema: toJSONSchema(CreateGroupWikiPageSchema),
|
|
1016
|
-
},
|
|
1017
|
-
{
|
|
1018
|
-
name: "update_group_wiki_page",
|
|
1019
|
-
description: "Update an existing wiki page in a GitLab group",
|
|
1020
|
-
inputSchema: toJSONSchema(UpdateGroupWikiPageSchema),
|
|
1021
|
-
},
|
|
1022
|
-
{
|
|
1023
|
-
name: "delete_group_wiki_page",
|
|
1024
|
-
description: "Delete a wiki page from a GitLab group",
|
|
1025
|
-
inputSchema: toJSONSchema(DeleteGroupWikiPageSchema),
|
|
1026
|
-
},
|
|
1027
|
-
{
|
|
1028
|
-
name: "get_repository_tree",
|
|
1029
|
-
description: "Get the repository tree for a GitLab project (list files and directories)",
|
|
1030
|
-
inputSchema: toJSONSchema(GetRepositoryTreeSchema),
|
|
1031
|
-
},
|
|
1032
|
-
{
|
|
1033
|
-
name: "list_pipelines",
|
|
1034
|
-
description: "List pipelines in a GitLab project with filtering options",
|
|
1035
|
-
inputSchema: toJSONSchema(ListPipelinesSchema),
|
|
1036
|
-
},
|
|
1037
|
-
{
|
|
1038
|
-
name: "get_pipeline",
|
|
1039
|
-
description: "Get details of a specific pipeline in a GitLab project",
|
|
1040
|
-
inputSchema: toJSONSchema(GetPipelineSchema),
|
|
1041
|
-
},
|
|
1042
|
-
{
|
|
1043
|
-
name: "list_deployments",
|
|
1044
|
-
description: "List deployments in a GitLab project with filtering options",
|
|
1045
|
-
inputSchema: toJSONSchema(ListDeploymentsSchema),
|
|
1046
|
-
},
|
|
1047
|
-
{
|
|
1048
|
-
name: "get_deployment",
|
|
1049
|
-
description: "Get details of a specific deployment in a GitLab project",
|
|
1050
|
-
inputSchema: toJSONSchema(GetDeploymentSchema),
|
|
1051
|
-
},
|
|
1052
|
-
{
|
|
1053
|
-
name: "list_environments",
|
|
1054
|
-
description: "List environments in a GitLab project",
|
|
1055
|
-
inputSchema: toJSONSchema(ListEnvironmentsSchema),
|
|
1056
|
-
},
|
|
1057
|
-
{
|
|
1058
|
-
name: "get_environment",
|
|
1059
|
-
description: "Get details of a specific environment in a GitLab project",
|
|
1060
|
-
inputSchema: toJSONSchema(GetEnvironmentSchema),
|
|
1061
|
-
},
|
|
1062
|
-
{
|
|
1063
|
-
name: "list_pipeline_jobs",
|
|
1064
|
-
description: "List all jobs in a specific pipeline",
|
|
1065
|
-
inputSchema: toJSONSchema(ListPipelineJobsSchema),
|
|
1066
|
-
},
|
|
1067
|
-
{
|
|
1068
|
-
name: "list_pipeline_trigger_jobs",
|
|
1069
|
-
description: "List all trigger jobs (bridges) in a specific pipeline that trigger downstream pipelines",
|
|
1070
|
-
inputSchema: toJSONSchema(ListPipelineTriggerJobsSchema),
|
|
1071
|
-
},
|
|
1072
|
-
{
|
|
1073
|
-
name: "get_pipeline_job",
|
|
1074
|
-
description: "Get details of a GitLab pipeline job number",
|
|
1075
|
-
inputSchema: toJSONSchema(GetPipelineJobOutputSchema),
|
|
1076
|
-
},
|
|
1077
|
-
{
|
|
1078
|
-
name: "get_pipeline_job_output",
|
|
1079
|
-
description: "Get the output/trace of a GitLab pipeline job with optional pagination to limit context window usage",
|
|
1080
|
-
inputSchema: toJSONSchema(GetPipelineJobOutputSchema),
|
|
1081
|
-
},
|
|
1082
|
-
{
|
|
1083
|
-
name: "create_pipeline",
|
|
1084
|
-
description: "Create a new pipeline for a branch or tag",
|
|
1085
|
-
inputSchema: toJSONSchema(CreatePipelineSchema),
|
|
1086
|
-
},
|
|
1087
|
-
{
|
|
1088
|
-
name: "retry_pipeline",
|
|
1089
|
-
description: "Retry a failed or canceled pipeline",
|
|
1090
|
-
inputSchema: toJSONSchema(RetryPipelineSchema),
|
|
1091
|
-
},
|
|
1092
|
-
{
|
|
1093
|
-
name: "cancel_pipeline",
|
|
1094
|
-
description: "Cancel a running pipeline",
|
|
1095
|
-
inputSchema: toJSONSchema(CancelPipelineSchema),
|
|
1096
|
-
},
|
|
1097
|
-
{
|
|
1098
|
-
name: "play_pipeline_job",
|
|
1099
|
-
description: "Run a manual pipeline job",
|
|
1100
|
-
inputSchema: toJSONSchema(PlayPipelineJobSchema),
|
|
1101
|
-
},
|
|
1102
|
-
{
|
|
1103
|
-
name: "retry_pipeline_job",
|
|
1104
|
-
description: "Retry a failed or canceled pipeline job",
|
|
1105
|
-
inputSchema: toJSONSchema(RetryPipelineJobSchema),
|
|
1106
|
-
},
|
|
1107
|
-
{
|
|
1108
|
-
name: "cancel_pipeline_job",
|
|
1109
|
-
description: "Cancel a running pipeline job",
|
|
1110
|
-
inputSchema: toJSONSchema(CancelPipelineJobSchema),
|
|
1111
|
-
},
|
|
1112
|
-
{
|
|
1113
|
-
name: "list_job_artifacts",
|
|
1114
|
-
description: "List artifact files in a job's artifacts archive. Returns file names, paths, types, and sizes.",
|
|
1115
|
-
inputSchema: toJSONSchema(ListJobArtifactsSchema),
|
|
1116
|
-
},
|
|
1117
|
-
{
|
|
1118
|
-
name: "download_job_artifacts",
|
|
1119
|
-
description: "Download the entire artifact archive (zip) for a job to a local path. Returns the saved file path.",
|
|
1120
|
-
inputSchema: toJSONSchema(DownloadJobArtifactsSchema),
|
|
1121
|
-
},
|
|
1122
|
-
{
|
|
1123
|
-
name: "get_job_artifact_file",
|
|
1124
|
-
description: "Get the content of a single file from a job's artifacts by its path within the archive",
|
|
1125
|
-
inputSchema: toJSONSchema(GetJobArtifactFileSchema),
|
|
1126
|
-
},
|
|
1127
|
-
{
|
|
1128
|
-
name: "list_merge_requests",
|
|
1129
|
-
description: "List merge requests. Without project_id, lists MRs assigned to the authenticated user by default (use scope='all' for all accessible MRs). With project_id, lists MRs for that specific project.",
|
|
1130
|
-
inputSchema: toJSONSchema(ListMergeRequestsSchema),
|
|
1131
|
-
},
|
|
1132
|
-
{
|
|
1133
|
-
name: "list_milestones",
|
|
1134
|
-
description: "List milestones in a GitLab project with filtering options",
|
|
1135
|
-
inputSchema: toJSONSchema(ListProjectMilestonesSchema),
|
|
1136
|
-
},
|
|
1137
|
-
{
|
|
1138
|
-
name: "get_milestone",
|
|
1139
|
-
description: "Get details of a specific milestone",
|
|
1140
|
-
inputSchema: toJSONSchema(GetProjectMilestoneSchema),
|
|
1141
|
-
},
|
|
1142
|
-
{
|
|
1143
|
-
name: "create_milestone",
|
|
1144
|
-
description: "Create a new milestone in a GitLab project",
|
|
1145
|
-
inputSchema: toJSONSchema(CreateProjectMilestoneSchema),
|
|
1146
|
-
},
|
|
1147
|
-
{
|
|
1148
|
-
name: "edit_milestone",
|
|
1149
|
-
description: "Edit an existing milestone in a GitLab project",
|
|
1150
|
-
inputSchema: toJSONSchema(EditProjectMilestoneSchema),
|
|
1151
|
-
},
|
|
1152
|
-
{
|
|
1153
|
-
name: "delete_milestone",
|
|
1154
|
-
description: "Delete a milestone from a GitLab project",
|
|
1155
|
-
inputSchema: toJSONSchema(DeleteProjectMilestoneSchema),
|
|
1156
|
-
},
|
|
1157
|
-
{
|
|
1158
|
-
name: "get_milestone_issue",
|
|
1159
|
-
description: "Get issues associated with a specific milestone",
|
|
1160
|
-
inputSchema: toJSONSchema(GetMilestoneIssuesSchema),
|
|
1161
|
-
},
|
|
1162
|
-
{
|
|
1163
|
-
name: "get_milestone_merge_requests",
|
|
1164
|
-
description: "Get merge requests associated with a specific milestone",
|
|
1165
|
-
inputSchema: toJSONSchema(GetMilestoneMergeRequestsSchema),
|
|
1166
|
-
},
|
|
1167
|
-
{
|
|
1168
|
-
name: "promote_milestone",
|
|
1169
|
-
description: "Promote a milestone to the next stage",
|
|
1170
|
-
inputSchema: toJSONSchema(PromoteProjectMilestoneSchema),
|
|
1171
|
-
},
|
|
1172
|
-
{
|
|
1173
|
-
name: "get_milestone_burndown_events",
|
|
1174
|
-
description: "Get burndown events for a specific milestone",
|
|
1175
|
-
inputSchema: toJSONSchema(GetMilestoneBurndownEventsSchema),
|
|
1176
|
-
},
|
|
1177
|
-
{
|
|
1178
|
-
name: "get_users",
|
|
1179
|
-
description: "Get GitLab user details by usernames",
|
|
1180
|
-
inputSchema: toJSONSchema(GetUsersSchema),
|
|
1181
|
-
},
|
|
1182
|
-
{
|
|
1183
|
-
name: "list_commits",
|
|
1184
|
-
description: "List repository commits with filtering options",
|
|
1185
|
-
inputSchema: toJSONSchema(ListCommitsSchema),
|
|
1186
|
-
},
|
|
1187
|
-
{
|
|
1188
|
-
name: "get_commit",
|
|
1189
|
-
description: "Get details of a specific commit",
|
|
1190
|
-
inputSchema: toJSONSchema(GetCommitSchema),
|
|
1191
|
-
},
|
|
1192
|
-
{
|
|
1193
|
-
name: "get_commit_diff",
|
|
1194
|
-
description: "Get changes/diffs of a specific commit",
|
|
1195
|
-
inputSchema: toJSONSchema(GetCommitDiffSchema),
|
|
1196
|
-
},
|
|
1197
|
-
{
|
|
1198
|
-
name: "list_group_iterations",
|
|
1199
|
-
description: "List group iterations with filtering options",
|
|
1200
|
-
inputSchema: toJSONSchema(ListGroupIterationsSchema),
|
|
1201
|
-
},
|
|
1202
|
-
{
|
|
1203
|
-
name: "upload_markdown",
|
|
1204
|
-
description: "Upload a file to a GitLab project for use in markdown content",
|
|
1205
|
-
inputSchema: toJSONSchema(MarkdownUploadSchema),
|
|
1206
|
-
},
|
|
1207
|
-
{
|
|
1208
|
-
name: "download_attachment",
|
|
1209
|
-
description: "Download an uploaded file from a GitLab project by secret and filename. Image files (png, jpg, gif, webp, svg, bmp, ico) are returned inline as base64 image content so the AI can view them directly. Non-image files are saved to disk. Use local_path to force saving image files to disk instead.",
|
|
1210
|
-
inputSchema: toJSONSchema(DownloadAttachmentSchema),
|
|
1211
|
-
},
|
|
1212
|
-
{
|
|
1213
|
-
name: "list_events",
|
|
1214
|
-
description: "List all events for the currently authenticated user. Note: before/after parameters accept date format YYYY-MM-DD only",
|
|
1215
|
-
inputSchema: toJSONSchema(ListEventsSchema),
|
|
1216
|
-
},
|
|
1217
|
-
{
|
|
1218
|
-
name: "get_project_events",
|
|
1219
|
-
description: "List all visible events for a specified project. Note: before/after parameters accept date format YYYY-MM-DD only",
|
|
1220
|
-
inputSchema: toJSONSchema(GetProjectEventsSchema),
|
|
1221
|
-
},
|
|
1222
|
-
{
|
|
1223
|
-
name: "list_releases",
|
|
1224
|
-
description: "List all releases for a project",
|
|
1225
|
-
inputSchema: toJSONSchema(ListReleasesSchema),
|
|
1226
|
-
},
|
|
1227
|
-
{
|
|
1228
|
-
name: "get_release",
|
|
1229
|
-
description: "Get a release by tag name",
|
|
1230
|
-
inputSchema: toJSONSchema(GetReleaseSchema),
|
|
1231
|
-
},
|
|
1232
|
-
{
|
|
1233
|
-
name: "create_release",
|
|
1234
|
-
description: "Create a new release in a GitLab project",
|
|
1235
|
-
inputSchema: toJSONSchema(CreateReleaseSchema),
|
|
1236
|
-
},
|
|
1237
|
-
{
|
|
1238
|
-
name: "update_release",
|
|
1239
|
-
description: "Update an existing release in a GitLab project",
|
|
1240
|
-
inputSchema: toJSONSchema(UpdateReleaseSchema),
|
|
1241
|
-
},
|
|
1242
|
-
{
|
|
1243
|
-
name: "delete_release",
|
|
1244
|
-
description: "Delete a release from a GitLab project (does not delete the associated tag)",
|
|
1245
|
-
inputSchema: toJSONSchema(DeleteReleaseSchema),
|
|
1246
|
-
},
|
|
1247
|
-
{
|
|
1248
|
-
name: "create_release_evidence",
|
|
1249
|
-
description: "Create release evidence for an existing release (GitLab Premium/Ultimate only)",
|
|
1250
|
-
inputSchema: toJSONSchema(CreateReleaseEvidenceSchema),
|
|
1251
|
-
},
|
|
1252
|
-
{
|
|
1253
|
-
name: "download_release_asset",
|
|
1254
|
-
description: "Download a release asset file by direct asset path",
|
|
1255
|
-
inputSchema: toJSONSchema(DownloadReleaseAssetSchema),
|
|
1256
|
-
},
|
|
1257
|
-
// --- Work item tools (GraphQL-based) ---
|
|
1258
|
-
{
|
|
1259
|
-
name: "get_work_item",
|
|
1260
|
-
description: "Get a single work item with full details including status, hierarchy (parent/children), type, labels, assignees, and all widgets.",
|
|
1261
|
-
inputSchema: toJSONSchema(GetWorkItemSchema),
|
|
1262
|
-
},
|
|
1263
|
-
{
|
|
1264
|
-
name: "list_work_items",
|
|
1265
|
-
description: "List work items in a project with filters (type, state, search, assignees, labels). Returns items with status and hierarchy info.",
|
|
1266
|
-
inputSchema: toJSONSchema(ListWorkItemsSchema),
|
|
1267
|
-
},
|
|
1268
|
-
{
|
|
1269
|
-
name: "create_work_item",
|
|
1270
|
-
description: "Create a new work item (issue, task, incident, test_case, epic, key_result, objective, requirement, ticket). Supports setting title, description, labels, assignees, weight, parent, health status, start/due dates, milestone, and confidentiality.",
|
|
1271
|
-
inputSchema: toJSONSchema(CreateWorkItemSchema),
|
|
1272
|
-
},
|
|
1273
|
-
{
|
|
1274
|
-
name: "update_work_item",
|
|
1275
|
-
description: "Update a work item. Can modify title, description, labels, assignees, weight, state, status, parent hierarchy, children, health status, start/due dates, milestone, confidentiality, linked items, and custom fields.",
|
|
1276
|
-
inputSchema: toJSONSchema(UpdateWorkItemSchema),
|
|
1277
|
-
},
|
|
1278
|
-
{
|
|
1279
|
-
name: "convert_work_item_type",
|
|
1280
|
-
description: "Convert a work item to a different type (e.g. issue to task, task to incident).",
|
|
1281
|
-
inputSchema: toJSONSchema(ConvertWorkItemTypeSchema),
|
|
1282
|
-
},
|
|
1283
|
-
{
|
|
1284
|
-
name: "list_work_item_statuses",
|
|
1285
|
-
description: "List available statuses for a work item type in a project. Requires GitLab Premium/Ultimate with configurable statuses.",
|
|
1286
|
-
inputSchema: toJSONSchema(ListWorkItemStatusesSchema),
|
|
1287
|
-
},
|
|
1288
|
-
{
|
|
1289
|
-
name: "list_custom_field_definitions",
|
|
1290
|
-
description: "List available custom field definitions for a work item type in a project. Returns field names, types, and IDs needed for setting custom fields via update_work_item.",
|
|
1291
|
-
inputSchema: toJSONSchema(ListCustomFieldDefinitionsSchema),
|
|
1292
|
-
},
|
|
1293
|
-
{
|
|
1294
|
-
name: "move_work_item",
|
|
1295
|
-
description: "Move a work item (issue, task, etc.) to a different project. Uses GitLab GraphQL issueMove mutation.",
|
|
1296
|
-
inputSchema: toJSONSchema(MoveWorkItemSchema),
|
|
1297
|
-
},
|
|
1298
|
-
{
|
|
1299
|
-
name: "list_work_item_notes",
|
|
1300
|
-
description: "List notes and discussions on a work item. Returns threaded discussions with author, body, timestamps, and system/internal flags.",
|
|
1301
|
-
inputSchema: toJSONSchema(ListWorkItemNotesSchema),
|
|
1302
|
-
},
|
|
1303
|
-
{
|
|
1304
|
-
name: "create_work_item_note",
|
|
1305
|
-
description: "Add a note/comment to a work item. Supports Markdown, internal notes, and threaded replies.",
|
|
1306
|
-
inputSchema: toJSONSchema(CreateWorkItemNoteSchema),
|
|
1307
|
-
},
|
|
1308
|
-
// --- Incident timeline event tools ---
|
|
1309
|
-
{
|
|
1310
|
-
name: "get_timeline_events",
|
|
1311
|
-
description: "List timeline events for an incident. Returns chronological events with notes, timestamps, and tags (Start time, End time, Impact detected, etc.).",
|
|
1312
|
-
inputSchema: toJSONSchema(GetTimelineEventsSchema),
|
|
1313
|
-
},
|
|
1314
|
-
{
|
|
1315
|
-
name: "create_timeline_event",
|
|
1316
|
-
description: "Create a timeline event on an incident. Supports tags: 'Start time', 'End time', 'Impact detected', 'Response initiated', 'Impact mitigated', 'Cause identified'.",
|
|
1317
|
-
inputSchema: toJSONSchema(CreateTimelineEventSchema),
|
|
1318
|
-
},
|
|
1319
|
-
{
|
|
1320
|
-
name: "list_webhooks",
|
|
1321
|
-
description: "List all configured webhooks for a GitLab project or group. Provide either project_id or group_id.",
|
|
1322
|
-
inputSchema: toJSONSchema(ListWebhooksSchema),
|
|
1323
|
-
},
|
|
1324
|
-
{
|
|
1325
|
-
name: "list_webhook_events",
|
|
1326
|
-
description: "List recent webhook events (past 7 days) for a project or group webhook. Use summary mode for overview, then get_webhook_event for full details.",
|
|
1327
|
-
inputSchema: toJSONSchema(ListWebhookEventsSchema),
|
|
1328
|
-
},
|
|
1329
|
-
{
|
|
1330
|
-
name: "get_webhook_event",
|
|
1331
|
-
description: "Get full details of a specific webhook event by ID, including request/response payloads. Searches up to 500 most recent events.",
|
|
1332
|
-
inputSchema: toJSONSchema(GetWebhookEventSchema),
|
|
1333
|
-
},
|
|
1334
|
-
{
|
|
1335
|
-
name: "search_code",
|
|
1336
|
-
description: "Search for code across all projects on the GitLab instance (requires advanced search or exact code search to be enabled). If exact code search (Zoekt) is enabled, the search query supports rich syntax including file:, lang:, sym: filters.",
|
|
1337
|
-
inputSchema: toJSONSchema(SearchCodeSchema),
|
|
1338
|
-
},
|
|
1339
|
-
{
|
|
1340
|
-
name: "search_project_code",
|
|
1341
|
-
description: "Search for code within a specific GitLab project (requires advanced search or exact code search to be enabled). If exact code search (Zoekt) is enabled, the search query supports rich syntax including file:, lang:, sym: filters.",
|
|
1342
|
-
inputSchema: toJSONSchema(SearchProjectCodeSchema),
|
|
1343
|
-
},
|
|
1344
|
-
{
|
|
1345
|
-
name: "search_group_code",
|
|
1346
|
-
description: "Search for code within a specific GitLab group (requires advanced search or exact code search to be enabled). If exact code search (Zoekt) is enabled, the search query supports rich syntax including file:, lang:, sym: filters.",
|
|
1347
|
-
inputSchema: toJSONSchema(SearchGroupCodeSchema),
|
|
1348
|
-
},
|
|
1349
|
-
];
|
|
1350
|
-
// Define which tools are read-only
|
|
1351
|
-
const readOnlyTools = new Set([
|
|
1352
|
-
"search_repositories",
|
|
1353
|
-
"search_code",
|
|
1354
|
-
"search_project_code",
|
|
1355
|
-
"search_group_code",
|
|
1356
|
-
"execute_graphql",
|
|
1357
|
-
"get_file_contents",
|
|
1358
|
-
"get_merge_request",
|
|
1359
|
-
"get_merge_request_diffs",
|
|
1360
|
-
"list_merge_request_changed_files",
|
|
1361
|
-
"list_merge_request_diffs",
|
|
1362
|
-
"get_merge_request_file_diff",
|
|
1363
|
-
"list_merge_request_versions",
|
|
1364
|
-
"get_merge_request_version",
|
|
1365
|
-
"get_branch_diffs",
|
|
1366
|
-
"get_merge_request_note",
|
|
1367
|
-
"get_merge_request_notes",
|
|
1368
|
-
"get_draft_note",
|
|
1369
|
-
"list_draft_notes",
|
|
1370
|
-
"mr_discussions",
|
|
1371
|
-
"list_issues",
|
|
1372
|
-
"my_issues",
|
|
1373
|
-
"list_merge_requests",
|
|
1374
|
-
"get_issue",
|
|
1375
|
-
"list_issue_links",
|
|
1376
|
-
"list_issue_discussions",
|
|
1377
|
-
"get_issue_link",
|
|
1378
|
-
"list_namespaces",
|
|
1379
|
-
"get_namespace",
|
|
1380
|
-
"verify_namespace",
|
|
1381
|
-
"get_project",
|
|
1382
|
-
"list_projects",
|
|
1383
|
-
"list_project_members",
|
|
1384
|
-
"get_pipeline",
|
|
1385
|
-
"list_pipelines",
|
|
1386
|
-
"list_deployments",
|
|
1387
|
-
"get_deployment",
|
|
1388
|
-
"list_environments",
|
|
1389
|
-
"get_environment",
|
|
1390
|
-
"list_pipeline_jobs",
|
|
1391
|
-
"list_pipeline_trigger_jobs",
|
|
1392
|
-
"get_pipeline_job",
|
|
1393
|
-
"get_pipeline_job_output",
|
|
1394
|
-
"list_job_artifacts",
|
|
1395
|
-
"download_job_artifacts",
|
|
1396
|
-
"get_job_artifact_file",
|
|
1397
|
-
"list_labels",
|
|
1398
|
-
"get_label",
|
|
1399
|
-
"list_group_projects",
|
|
1400
|
-
"get_repository_tree",
|
|
1401
|
-
"list_milestones",
|
|
1402
|
-
"get_milestone",
|
|
1403
|
-
"get_milestone_issue",
|
|
1404
|
-
"get_milestone_merge_requests",
|
|
1405
|
-
"get_milestone_burndown_events",
|
|
1406
|
-
"list_wiki_pages",
|
|
1407
|
-
"get_wiki_page",
|
|
1408
|
-
"list_group_wiki_pages",
|
|
1409
|
-
"get_group_wiki_page",
|
|
1410
|
-
"get_users",
|
|
1411
|
-
"list_commits",
|
|
1412
|
-
"get_commit",
|
|
1413
|
-
"get_commit_diff",
|
|
1414
|
-
"list_group_iterations",
|
|
1415
|
-
"get_group_iteration",
|
|
1416
|
-
"download_attachment",
|
|
1417
|
-
"list_events",
|
|
1418
|
-
"get_project_events",
|
|
1419
|
-
"list_releases",
|
|
1420
|
-
"get_release",
|
|
1421
|
-
"download_release_asset",
|
|
1422
|
-
"get_merge_request_approval_state",
|
|
1423
|
-
"get_work_item",
|
|
1424
|
-
"list_work_items",
|
|
1425
|
-
"list_work_item_statuses",
|
|
1426
|
-
"list_custom_field_definitions",
|
|
1427
|
-
"list_work_item_notes",
|
|
1428
|
-
"get_timeline_events",
|
|
1429
|
-
"get_merge_request_conflicts",
|
|
1430
|
-
"list_webhooks",
|
|
1431
|
-
"list_webhook_events",
|
|
1432
|
-
"get_webhook_event",
|
|
1433
|
-
]);
|
|
1434
|
-
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
|
|
1435
|
-
const wikiToolNames = new Set([
|
|
1436
|
-
"list_wiki_pages",
|
|
1437
|
-
"get_wiki_page",
|
|
1438
|
-
"create_wiki_page",
|
|
1439
|
-
"update_wiki_page",
|
|
1440
|
-
"delete_wiki_page",
|
|
1441
|
-
"list_group_wiki_pages",
|
|
1442
|
-
"get_group_wiki_page",
|
|
1443
|
-
"create_group_wiki_page",
|
|
1444
|
-
"update_group_wiki_page",
|
|
1445
|
-
"delete_group_wiki_page",
|
|
1446
|
-
"upload_wiki_attachment",
|
|
1447
|
-
]);
|
|
1448
|
-
// Define which tools are related to milestones and can be toggled by USE_MILESTONE
|
|
1449
|
-
const milestoneToolNames = new Set([
|
|
1450
|
-
"list_milestones",
|
|
1451
|
-
"get_milestone",
|
|
1452
|
-
"create_milestone",
|
|
1453
|
-
"edit_milestone",
|
|
1454
|
-
"delete_milestone",
|
|
1455
|
-
"get_milestone_issue",
|
|
1456
|
-
"get_milestone_merge_requests",
|
|
1457
|
-
"promote_milestone",
|
|
1458
|
-
"get_milestone_burndown_events",
|
|
1459
|
-
]);
|
|
1460
|
-
// Define which tools are related to pipelines and can be toggled by USE_PIPELINE
|
|
1461
|
-
const pipelineToolNames = new Set([
|
|
1462
|
-
"list_pipelines",
|
|
1463
|
-
"get_pipeline",
|
|
1464
|
-
"list_deployments",
|
|
1465
|
-
"get_deployment",
|
|
1466
|
-
"list_environments",
|
|
1467
|
-
"get_environment",
|
|
1468
|
-
"list_pipeline_jobs",
|
|
1469
|
-
"list_pipeline_trigger_jobs",
|
|
1470
|
-
"get_pipeline_job",
|
|
1471
|
-
"get_pipeline_job_output",
|
|
1472
|
-
"create_pipeline",
|
|
1473
|
-
"retry_pipeline",
|
|
1474
|
-
"cancel_pipeline",
|
|
1475
|
-
"play_pipeline_job",
|
|
1476
|
-
"retry_pipeline_job",
|
|
1477
|
-
"cancel_pipeline_job",
|
|
1478
|
-
"list_job_artifacts",
|
|
1479
|
-
"download_job_artifacts",
|
|
1480
|
-
"get_job_artifact_file",
|
|
1481
|
-
]);
|
|
1482
|
-
const TOOLSET_DEFINITIONS = [
|
|
1483
|
-
{
|
|
1484
|
-
id: "merge_requests",
|
|
1485
|
-
isDefault: true,
|
|
1486
|
-
tools: new Set([
|
|
1487
|
-
"merge_merge_request",
|
|
1488
|
-
"approve_merge_request",
|
|
1489
|
-
"unapprove_merge_request",
|
|
1490
|
-
"get_merge_request_approval_state",
|
|
1491
|
-
"get_merge_request_conflicts",
|
|
1492
|
-
"get_merge_request",
|
|
1493
|
-
"get_merge_request_diffs",
|
|
1494
|
-
"list_merge_request_changed_files",
|
|
1495
|
-
"list_merge_request_diffs",
|
|
1496
|
-
"get_merge_request_file_diff",
|
|
1497
|
-
"list_merge_request_versions",
|
|
1498
|
-
"get_merge_request_version",
|
|
1499
|
-
"update_merge_request",
|
|
1500
|
-
"create_merge_request",
|
|
1501
|
-
"list_merge_requests",
|
|
1502
|
-
"get_branch_diffs",
|
|
1503
|
-
"mr_discussions",
|
|
1504
|
-
"create_merge_request_note",
|
|
1505
|
-
"update_merge_request_note",
|
|
1506
|
-
"delete_merge_request_note",
|
|
1507
|
-
"get_merge_request_note",
|
|
1508
|
-
"get_merge_request_notes",
|
|
1509
|
-
"delete_merge_request_discussion_note",
|
|
1510
|
-
"update_merge_request_discussion_note",
|
|
1511
|
-
"create_merge_request_discussion_note",
|
|
1512
|
-
"get_draft_note",
|
|
1513
|
-
"list_draft_notes",
|
|
1514
|
-
"create_draft_note",
|
|
1515
|
-
"update_draft_note",
|
|
1516
|
-
"delete_draft_note",
|
|
1517
|
-
"publish_draft_note",
|
|
1518
|
-
"bulk_publish_draft_notes",
|
|
1519
|
-
"create_merge_request_thread",
|
|
1520
|
-
"resolve_merge_request_thread",
|
|
1521
|
-
]),
|
|
1522
|
-
},
|
|
1523
|
-
{
|
|
1524
|
-
id: "issues",
|
|
1525
|
-
isDefault: true,
|
|
1526
|
-
tools: new Set([
|
|
1527
|
-
"create_issue",
|
|
1528
|
-
"list_issues",
|
|
1529
|
-
"my_issues",
|
|
1530
|
-
"get_issue",
|
|
1531
|
-
"update_issue",
|
|
1532
|
-
"delete_issue",
|
|
1533
|
-
"create_issue_note",
|
|
1534
|
-
"update_issue_note",
|
|
1535
|
-
"list_issue_links",
|
|
1536
|
-
"list_issue_discussions",
|
|
1537
|
-
"get_issue_link",
|
|
1538
|
-
"create_issue_link",
|
|
1539
|
-
"delete_issue_link",
|
|
1540
|
-
"create_note",
|
|
1541
|
-
]),
|
|
1542
|
-
},
|
|
1543
|
-
{
|
|
1544
|
-
id: "repositories",
|
|
1545
|
-
isDefault: true,
|
|
1546
|
-
tools: new Set([
|
|
1547
|
-
"search_repositories",
|
|
1548
|
-
"create_repository",
|
|
1549
|
-
"get_file_contents",
|
|
1550
|
-
"push_files",
|
|
1551
|
-
"create_or_update_file",
|
|
1552
|
-
"fork_repository",
|
|
1553
|
-
"get_repository_tree",
|
|
1554
|
-
]),
|
|
1555
|
-
},
|
|
1556
|
-
{
|
|
1557
|
-
id: "branches",
|
|
1558
|
-
isDefault: true,
|
|
1559
|
-
tools: new Set([
|
|
1560
|
-
"create_branch",
|
|
1561
|
-
"list_commits",
|
|
1562
|
-
"get_commit",
|
|
1563
|
-
"get_commit_diff",
|
|
1564
|
-
]),
|
|
1565
|
-
},
|
|
1566
|
-
{
|
|
1567
|
-
id: "projects",
|
|
1568
|
-
isDefault: true,
|
|
1569
|
-
tools: new Set([
|
|
1570
|
-
"get_project",
|
|
1571
|
-
"list_projects",
|
|
1572
|
-
"list_project_members",
|
|
1573
|
-
"list_namespaces",
|
|
1574
|
-
"get_namespace",
|
|
1575
|
-
"verify_namespace",
|
|
1576
|
-
"list_group_projects",
|
|
1577
|
-
"list_group_iterations",
|
|
1578
|
-
]),
|
|
1579
|
-
},
|
|
1580
|
-
{
|
|
1581
|
-
id: "labels",
|
|
1582
|
-
isDefault: true,
|
|
1583
|
-
tools: new Set([
|
|
1584
|
-
"list_labels",
|
|
1585
|
-
"get_label",
|
|
1586
|
-
"create_label",
|
|
1587
|
-
"update_label",
|
|
1588
|
-
"delete_label",
|
|
1589
|
-
]),
|
|
1590
|
-
},
|
|
1591
|
-
{
|
|
1592
|
-
id: "pipelines",
|
|
1593
|
-
isDefault: true,
|
|
1594
|
-
tools: new Set([
|
|
1595
|
-
"list_pipelines",
|
|
1596
|
-
"get_pipeline",
|
|
1597
|
-
"list_deployments",
|
|
1598
|
-
"get_deployment",
|
|
1599
|
-
"list_environments",
|
|
1600
|
-
"get_environment",
|
|
1601
|
-
"list_pipeline_jobs",
|
|
1602
|
-
"list_pipeline_trigger_jobs",
|
|
1603
|
-
"get_pipeline_job",
|
|
1604
|
-
"get_pipeline_job_output",
|
|
1605
|
-
"create_pipeline",
|
|
1606
|
-
"retry_pipeline",
|
|
1607
|
-
"cancel_pipeline",
|
|
1608
|
-
"play_pipeline_job",
|
|
1609
|
-
"retry_pipeline_job",
|
|
1610
|
-
"cancel_pipeline_job",
|
|
1611
|
-
"list_job_artifacts",
|
|
1612
|
-
"download_job_artifacts",
|
|
1613
|
-
"get_job_artifact_file",
|
|
1614
|
-
]),
|
|
1615
|
-
},
|
|
1616
|
-
{
|
|
1617
|
-
id: "milestones",
|
|
1618
|
-
isDefault: true,
|
|
1619
|
-
tools: new Set([
|
|
1620
|
-
"list_milestones",
|
|
1621
|
-
"get_milestone",
|
|
1622
|
-
"create_milestone",
|
|
1623
|
-
"edit_milestone",
|
|
1624
|
-
"delete_milestone",
|
|
1625
|
-
"get_milestone_issue",
|
|
1626
|
-
"get_milestone_merge_requests",
|
|
1627
|
-
"promote_milestone",
|
|
1628
|
-
"get_milestone_burndown_events",
|
|
1629
|
-
]),
|
|
1630
|
-
},
|
|
1631
|
-
{
|
|
1632
|
-
id: "wiki",
|
|
1633
|
-
isDefault: true,
|
|
1634
|
-
tools: new Set([
|
|
1635
|
-
"list_wiki_pages",
|
|
1636
|
-
"get_wiki_page",
|
|
1637
|
-
"create_wiki_page",
|
|
1638
|
-
"update_wiki_page",
|
|
1639
|
-
"delete_wiki_page",
|
|
1640
|
-
"list_group_wiki_pages",
|
|
1641
|
-
"get_group_wiki_page",
|
|
1642
|
-
"create_group_wiki_page",
|
|
1643
|
-
"update_group_wiki_page",
|
|
1644
|
-
"delete_group_wiki_page",
|
|
1645
|
-
]),
|
|
1646
|
-
},
|
|
1647
|
-
{
|
|
1648
|
-
id: "releases",
|
|
1649
|
-
isDefault: true,
|
|
1650
|
-
tools: new Set([
|
|
1651
|
-
"list_releases",
|
|
1652
|
-
"get_release",
|
|
1653
|
-
"create_release",
|
|
1654
|
-
"update_release",
|
|
1655
|
-
"delete_release",
|
|
1656
|
-
"create_release_evidence",
|
|
1657
|
-
"download_release_asset",
|
|
1658
|
-
]),
|
|
1659
|
-
},
|
|
1660
|
-
{
|
|
1661
|
-
id: "users",
|
|
1662
|
-
isDefault: true,
|
|
1663
|
-
tools: new Set([
|
|
1664
|
-
"get_users",
|
|
1665
|
-
"list_events",
|
|
1666
|
-
"get_project_events",
|
|
1667
|
-
"upload_markdown",
|
|
1668
|
-
"download_attachment",
|
|
1669
|
-
]),
|
|
1670
|
-
},
|
|
1671
|
-
{
|
|
1672
|
-
id: "workitems",
|
|
1673
|
-
isDefault: false,
|
|
1674
|
-
tools: new Set([
|
|
1675
|
-
"get_work_item",
|
|
1676
|
-
"list_work_items",
|
|
1677
|
-
"create_work_item",
|
|
1678
|
-
"update_work_item",
|
|
1679
|
-
"convert_work_item_type",
|
|
1680
|
-
"list_work_item_statuses",
|
|
1681
|
-
"list_custom_field_definitions",
|
|
1682
|
-
"move_work_item",
|
|
1683
|
-
"list_work_item_notes",
|
|
1684
|
-
"create_work_item_note",
|
|
1685
|
-
"get_timeline_events",
|
|
1686
|
-
"create_timeline_event",
|
|
1687
|
-
]),
|
|
1688
|
-
},
|
|
1689
|
-
{
|
|
1690
|
-
id: "webhooks",
|
|
1691
|
-
isDefault: false,
|
|
1692
|
-
tools: new Set([
|
|
1693
|
-
"list_webhooks",
|
|
1694
|
-
"list_webhook_events",
|
|
1695
|
-
"get_webhook_event",
|
|
1696
|
-
]),
|
|
1697
|
-
},
|
|
1698
|
-
{
|
|
1699
|
-
id: "search",
|
|
1700
|
-
isDefault: false,
|
|
1701
|
-
tools: new Set(["search_code", "search_project_code", "search_group_code"]),
|
|
1702
|
-
},
|
|
1703
|
-
];
|
|
1704
|
-
// Derived lookup: tool name → toolset ID
|
|
1705
|
-
const TOOLSET_BY_TOOL_NAME = new Map();
|
|
1706
|
-
for (const def of TOOLSET_DEFINITIONS) {
|
|
1707
|
-
for (const tool of def.tools) {
|
|
1708
|
-
if (TOOLSET_BY_TOOL_NAME.has(tool)) {
|
|
1709
|
-
logger.warn(`Tool "${tool}" is defined in multiple toolsets: "${TOOLSET_BY_TOOL_NAME.get(tool)}" and "${def.id}"`);
|
|
1710
|
-
}
|
|
1711
|
-
TOOLSET_BY_TOOL_NAME.set(tool, def.id);
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
const DEFAULT_TOOLSET_IDS = new Set(TOOLSET_DEFINITIONS.filter(d => d.isDefault).map(d => d.id));
|
|
1715
|
-
const ALL_TOOLSET_IDS = new Set(TOOLSET_DEFINITIONS.map(d => d.id));
|
|
1716
|
-
function parseEnabledToolsets(raw) {
|
|
1717
|
-
if (!raw || raw.trim() === "") {
|
|
1718
|
-
return DEFAULT_TOOLSET_IDS;
|
|
1719
|
-
}
|
|
1720
|
-
const trimmed = raw.trim().toLowerCase();
|
|
1721
|
-
if (trimmed === "all") {
|
|
1722
|
-
return ALL_TOOLSET_IDS;
|
|
1723
|
-
}
|
|
1724
|
-
const selected = new Set(trimmed
|
|
1725
|
-
.split(",")
|
|
1726
|
-
.map(s => s.trim())
|
|
1727
|
-
.filter((s) => ALL_TOOLSET_IDS.has(s)));
|
|
1728
|
-
if (selected.size === 0) {
|
|
1729
|
-
logger.warn(`No valid toolsets found in configuration (${raw}). Falling back to default toolsets.`);
|
|
1730
|
-
return DEFAULT_TOOLSET_IDS;
|
|
1731
|
-
}
|
|
1732
|
-
return selected;
|
|
1733
|
-
}
|
|
1734
|
-
function parseIndividualTools(raw) {
|
|
1735
|
-
if (!raw || raw.trim() === "") {
|
|
1736
|
-
return new Set();
|
|
1737
|
-
}
|
|
1738
|
-
const allToolNames = new Set(allTools.map((t) => t.name));
|
|
1739
|
-
const parsed = raw
|
|
1740
|
-
.trim()
|
|
1741
|
-
.split(",")
|
|
1742
|
-
.map(s => s.trim().toLowerCase())
|
|
1743
|
-
.filter(Boolean);
|
|
1744
|
-
const unknown = parsed.filter(name => !allToolNames.has(name));
|
|
1745
|
-
if (unknown.length > 0) {
|
|
1746
|
-
logger.warn(`Unknown tool names in GITLAB_TOOLS (will be ignored): ${unknown.join(", ")}`);
|
|
1747
|
-
}
|
|
1748
|
-
return new Set(parsed);
|
|
1749
|
-
}
|
|
1750
|
-
function buildFeatureFlagOverrides() {
|
|
1751
|
-
const overrides = new Set();
|
|
1752
|
-
if (USE_GITLAB_WIKI) {
|
|
1753
|
-
for (const t of wikiToolNames)
|
|
1754
|
-
overrides.add(t);
|
|
1755
|
-
}
|
|
1756
|
-
if (USE_MILESTONE) {
|
|
1757
|
-
for (const t of milestoneToolNames)
|
|
1758
|
-
overrides.add(t);
|
|
1759
|
-
}
|
|
1760
|
-
if (USE_PIPELINE) {
|
|
1761
|
-
for (const t of pipelineToolNames)
|
|
1762
|
-
overrides.add(t);
|
|
1763
|
-
}
|
|
1764
|
-
return overrides;
|
|
1765
|
-
}
|
|
1766
|
-
function isToolInEnabledToolset(toolName, enabledToolsets) {
|
|
1767
|
-
const toolsetId = TOOLSET_BY_TOOL_NAME.get(toolName);
|
|
1768
|
-
// Tools not in any toolset (e.g. execute_graphql) are excluded by default
|
|
1769
|
-
if (toolsetId === undefined)
|
|
1770
|
-
return false;
|
|
1771
|
-
return enabledToolsets.has(toolsetId);
|
|
1772
|
-
}
|
|
1773
699
|
// Compute at startup
|
|
1774
700
|
const enabledToolsets = parseEnabledToolsets(GITLAB_TOOLSETS_RAW);
|
|
1775
701
|
const individuallyEnabledTools = parseIndividualTools(GITLAB_TOOLS_RAW);
|
|
@@ -1780,25 +706,6 @@ if (GITLAB_TOOLSETS_RAW && (USE_PIPELINE || USE_MILESTONE || USE_GITLAB_WIKI)) {
|
|
|
1780
706
|
"Legacy flags add tools additively on top of the toolset selection and may produce unexpected results.");
|
|
1781
707
|
}
|
|
1782
708
|
const MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT = 10;
|
|
1783
|
-
/**
|
|
1784
|
-
* Smart URL handling for GitLab API
|
|
1785
|
-
*
|
|
1786
|
-
* @param {string | undefined} url - Input GitLab API URL
|
|
1787
|
-
* @returns {string} Normalized GitLab API URL with /api/v4 path
|
|
1788
|
-
*/
|
|
1789
|
-
function normalizeGitLabApiUrl(url) {
|
|
1790
|
-
if (!url) {
|
|
1791
|
-
return "https://gitlab.com/api/v4";
|
|
1792
|
-
}
|
|
1793
|
-
let normalizedUrl = url.trim();
|
|
1794
|
-
if (normalizedUrl.endsWith("/")) {
|
|
1795
|
-
normalizedUrl = normalizedUrl.slice(0, -1);
|
|
1796
|
-
}
|
|
1797
|
-
if (!normalizedUrl.endsWith("/api/v4")) {
|
|
1798
|
-
normalizedUrl = `${normalizedUrl}/api/v4`;
|
|
1799
|
-
}
|
|
1800
|
-
return normalizedUrl;
|
|
1801
|
-
}
|
|
1802
709
|
// Use the normalizeGitLabApiUrl function to handle various URL formats
|
|
1803
710
|
const GITLAB_API_URLS = (getConfig("api-url", "GITLAB_API_URL") || "https://gitlab.com")
|
|
1804
711
|
.split(",")
|
|
@@ -2311,23 +1218,6 @@ async function convertIssueType(projectId, issueIid, newType) {
|
|
|
2311
1218
|
};
|
|
2312
1219
|
}
|
|
2313
1220
|
// --- Work item hierarchy ---
|
|
2314
|
-
/**
|
|
2315
|
-
* Set a parent for a work item (issue hierarchy).
|
|
2316
|
-
*/
|
|
2317
|
-
async function setIssueParent(projectId, issueIid, parentProjectId, parentIssueIid) {
|
|
2318
|
-
const { workItemGID } = await resolveWorkItemGID(projectId, issueIid);
|
|
2319
|
-
const { workItemGID: parentGID } = await resolveWorkItemGID(parentProjectId, parentIssueIid);
|
|
2320
|
-
const data = await executeGraphQL(`mutation($id: WorkItemID!, $parentId: WorkItemID!) {
|
|
2321
|
-
workItemUpdate(input: { id: $id, hierarchyWidget: { parentId: $parentId } }) {
|
|
2322
|
-
workItem { id }
|
|
2323
|
-
errors
|
|
2324
|
-
}
|
|
2325
|
-
}`, { id: workItemGID, parentId: parentGID });
|
|
2326
|
-
if (data.workItemUpdate.errors?.length > 0) {
|
|
2327
|
-
throw new Error(`Failed to set parent: ${data.workItemUpdate.errors.join(", ")}`);
|
|
2328
|
-
}
|
|
2329
|
-
return { id: workItemGID, parentId: parentGID };
|
|
2330
|
-
}
|
|
2331
1221
|
/**
|
|
2332
1222
|
* Remove the parent from a work item.
|
|
2333
1223
|
*/
|
|
@@ -2343,83 +1233,6 @@ async function removeIssueParent(projectId, issueIid) {
|
|
|
2343
1233
|
throw new Error(`Failed to remove parent: ${data.workItemUpdate.errors.join(", ")}`);
|
|
2344
1234
|
}
|
|
2345
1235
|
}
|
|
2346
|
-
/**
|
|
2347
|
-
* List children of a work item (hierarchy widget).
|
|
2348
|
-
*/
|
|
2349
|
-
async function listIssueChildren(projectId, issueIid) {
|
|
2350
|
-
projectId = decodeURIComponent(projectId);
|
|
2351
|
-
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
2352
|
-
// Get project path
|
|
2353
|
-
const projectUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
|
|
2354
|
-
const projectResponse = await fetch(projectUrl.toString(), {
|
|
2355
|
-
...getFetchConfig(),
|
|
2356
|
-
});
|
|
2357
|
-
await handleGitLabError(projectResponse);
|
|
2358
|
-
const project = await projectResponse.json();
|
|
2359
|
-
const data = await executeGraphQL(`query($path: ID!, $iid: String!) {
|
|
2360
|
-
namespace(fullPath: $path) {
|
|
2361
|
-
workItem(iid: $iid) {
|
|
2362
|
-
id
|
|
2363
|
-
title
|
|
2364
|
-
widgets {
|
|
2365
|
-
__typename
|
|
2366
|
-
... on WorkItemWidgetHierarchy {
|
|
2367
|
-
parent {
|
|
2368
|
-
id
|
|
2369
|
-
title
|
|
2370
|
-
webUrl
|
|
2371
|
-
workItemType { name }
|
|
2372
|
-
}
|
|
2373
|
-
children {
|
|
2374
|
-
nodes {
|
|
2375
|
-
id
|
|
2376
|
-
title
|
|
2377
|
-
state
|
|
2378
|
-
webUrl
|
|
2379
|
-
workItemType { name }
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
}`, { path: project.path_with_namespace, iid: String(issueIid) });
|
|
2387
|
-
if (!data.namespace?.workItem) {
|
|
2388
|
-
throw new Error(`Work item #${issueIid} not found`);
|
|
2389
|
-
}
|
|
2390
|
-
// Extract hierarchy widget
|
|
2391
|
-
const hierarchyWidget = data.namespace.workItem.widgets?.find((w) => w.__typename === "WorkItemWidgetHierarchy");
|
|
2392
|
-
return {
|
|
2393
|
-
id: data.namespace.workItem.id,
|
|
2394
|
-
title: data.namespace.workItem.title,
|
|
2395
|
-
parent: hierarchyWidget?.parent || null,
|
|
2396
|
-
children: hierarchyWidget?.children?.nodes || [],
|
|
2397
|
-
};
|
|
2398
|
-
}
|
|
2399
|
-
/**
|
|
2400
|
-
* Add a child to a parent work item.
|
|
2401
|
-
*/
|
|
2402
|
-
async function addIssueChild(projectId, issueIid, childProjectId, childIssueIid) {
|
|
2403
|
-
const { workItemGID: parentGID } = await resolveWorkItemGID(projectId, issueIid);
|
|
2404
|
-
const { workItemGID: childGID } = await resolveWorkItemGID(childProjectId, childIssueIid);
|
|
2405
|
-
const data = await executeGraphQL(`mutation($id: WorkItemID!, $childId: WorkItemID!) {
|
|
2406
|
-
workItemUpdate(input: { id: $id, hierarchyWidget: { childrenIds: [$childId] } }) {
|
|
2407
|
-
workItem { id }
|
|
2408
|
-
errors
|
|
2409
|
-
}
|
|
2410
|
-
}`, { id: parentGID, childId: childGID });
|
|
2411
|
-
if (data.workItemUpdate.errors?.length > 0) {
|
|
2412
|
-
throw new Error(`Failed to add child: ${data.workItemUpdate.errors.join(", ")}`);
|
|
2413
|
-
}
|
|
2414
|
-
return { parentId: parentGID, childId: childGID };
|
|
2415
|
-
}
|
|
2416
|
-
/**
|
|
2417
|
-
* Remove a child from a parent work item by setting the child's parent to null.
|
|
2418
|
-
*/
|
|
2419
|
-
async function removeIssueChild(projectId, issueIid, childProjectId, childIssueIid) {
|
|
2420
|
-
// Removing a child is done by removing the parent from the child
|
|
2421
|
-
await removeIssueParent(childProjectId, childIssueIid);
|
|
2422
|
-
}
|
|
2423
1236
|
// --- Work item status ---
|
|
2424
1237
|
/**
|
|
2425
1238
|
* List available statuses for a work item type in a project.
|
|
@@ -2677,6 +1490,39 @@ async function createWorkItemNote(projectId, iid, body, options = {}) {
|
|
|
2677
1490
|
}
|
|
2678
1491
|
return data.createNote.note;
|
|
2679
1492
|
}
|
|
1493
|
+
// --- Emoji Reactions (GraphQL) ---
|
|
1494
|
+
async function addGraphQLAwardEmoji(awardableId, name) {
|
|
1495
|
+
const data = await executeGraphQL(`mutation($awardableId: AwardableID!, $name: String!) {
|
|
1496
|
+
awardEmojiAdd(input: { awardableId: $awardableId, name: $name }) {
|
|
1497
|
+
awardEmoji { name user { username } }
|
|
1498
|
+
errors
|
|
1499
|
+
}
|
|
1500
|
+
}`, { awardableId, name });
|
|
1501
|
+
if (data.awardEmojiAdd.errors?.length > 0) {
|
|
1502
|
+
throw new Error(`Failed to add emoji reaction: ${data.awardEmojiAdd.errors.join(", ")}`);
|
|
1503
|
+
}
|
|
1504
|
+
return data.awardEmojiAdd.awardEmoji;
|
|
1505
|
+
}
|
|
1506
|
+
async function listGraphQLAwardEmoji(awardableId) {
|
|
1507
|
+
const data = await executeGraphQL(`query($id: AwardableID!) {
|
|
1508
|
+
awardable(id: $id) {
|
|
1509
|
+
awardEmoji { nodes { name user { username } } }
|
|
1510
|
+
}
|
|
1511
|
+
}`, { id: awardableId });
|
|
1512
|
+
return data.awardable?.awardEmoji?.nodes ?? [];
|
|
1513
|
+
}
|
|
1514
|
+
async function removeGraphQLAwardEmoji(awardableId, name) {
|
|
1515
|
+
const data = await executeGraphQL(`mutation($awardableId: AwardableID!, $name: String!) {
|
|
1516
|
+
awardEmojiRemove(input: { awardableId: $awardableId, name: $name }) {
|
|
1517
|
+
awardEmoji { name }
|
|
1518
|
+
errors
|
|
1519
|
+
}
|
|
1520
|
+
}`, { awardableId, name });
|
|
1521
|
+
if (data.awardEmojiRemove.errors?.length > 0) {
|
|
1522
|
+
throw new Error(`Failed to remove emoji reaction: ${data.awardEmojiRemove.errors.join(", ")}`);
|
|
1523
|
+
}
|
|
1524
|
+
return data.awardEmojiRemove.awardEmoji;
|
|
1525
|
+
}
|
|
2680
1526
|
// --- Incident Timeline Events ---
|
|
2681
1527
|
/**
|
|
2682
1528
|
* List timeline events for an incident.
|
|
@@ -2815,35 +1661,6 @@ async function updateIncidentEscalationStatus(projectPath, incidentIid, status)
|
|
|
2815
1661
|
}
|
|
2816
1662
|
return data.issueSetEscalationStatus.issue;
|
|
2817
1663
|
}
|
|
2818
|
-
/**
|
|
2819
|
-
* Set the status of a work item.
|
|
2820
|
-
*/
|
|
2821
|
-
async function setIssueStatus(projectId, issueIid, status) {
|
|
2822
|
-
const { workItemGID } = await resolveWorkItemGID(projectId, issueIid);
|
|
2823
|
-
const data = await executeGraphQL(`mutation($id: WorkItemID!, $status: WorkItemsStatusesStatusID!) {
|
|
2824
|
-
workItemUpdate(input: { id: $id, statusWidget: { status: $status } }) {
|
|
2825
|
-
workItem {
|
|
2826
|
-
id
|
|
2827
|
-
widgets {
|
|
2828
|
-
__typename
|
|
2829
|
-
... on WorkItemWidgetStatus {
|
|
2830
|
-
status { id name category color }
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
errors
|
|
2835
|
-
}
|
|
2836
|
-
}`, { id: workItemGID, status });
|
|
2837
|
-
if (data.workItemUpdate.errors?.length > 0) {
|
|
2838
|
-
throw new Error(`Failed to set status: ${data.workItemUpdate.errors.join(", ")}`);
|
|
2839
|
-
}
|
|
2840
|
-
// Extract the current status from the response
|
|
2841
|
-
const statusWidget = data.workItemUpdate.workItem?.widgets?.find((w) => w.__typename === "WorkItemWidgetStatus");
|
|
2842
|
-
return {
|
|
2843
|
-
id: data.workItemUpdate.workItem.id,
|
|
2844
|
-
status: statusWidget?.status || null,
|
|
2845
|
-
};
|
|
2846
|
-
}
|
|
2847
1664
|
/**
|
|
2848
1665
|
* Resolve a project ID (numeric or path) to its full path_with_namespace.
|
|
2849
1666
|
*/
|
|
@@ -3195,7 +2012,8 @@ async function createWorkItem(projectId, options) {
|
|
|
3195
2012
|
inputValues.push("labelsWidget: { labelIds: $labelIds }");
|
|
3196
2013
|
variables.labelIds = labelIds;
|
|
3197
2014
|
}
|
|
3198
|
-
|
|
2015
|
+
// Incidents don't support the weight widget
|
|
2016
|
+
if (options.weight !== undefined && typeName !== "incident") {
|
|
3199
2017
|
inputFields.push("$weight: Int");
|
|
3200
2018
|
inputValues.push("weightWidget: { weight: $weight }");
|
|
3201
2019
|
variables.weight = options.weight;
|
|
@@ -3454,7 +2272,7 @@ async function updateWorkItem(projectId, iid, options) {
|
|
|
3454
2272
|
if (options.children_to_add && options.children_to_add.length > 0) {
|
|
3455
2273
|
const childGIDs = [];
|
|
3456
2274
|
for (const child of options.children_to_add) {
|
|
3457
|
-
const { workItemGID: childGID } = await resolveWorkItemGID(child.project_id, child.iid);
|
|
2275
|
+
const { workItemGID: childGID } = await resolveWorkItemGID(child.project_id || projectId, child.iid);
|
|
3458
2276
|
childGIDs.push(childGID);
|
|
3459
2277
|
}
|
|
3460
2278
|
const addData = await executeGraphQL(`mutation($id: WorkItemID!, $childrenIds: [WorkItemID!]!) {
|
|
@@ -3469,7 +2287,7 @@ async function updateWorkItem(projectId, iid, options) {
|
|
|
3469
2287
|
// Handle children_to_remove: remove parent from each child
|
|
3470
2288
|
if (options.children_to_remove && options.children_to_remove.length > 0) {
|
|
3471
2289
|
for (const child of options.children_to_remove) {
|
|
3472
|
-
await removeIssueParent(child.project_id, child.iid);
|
|
2290
|
+
await removeIssueParent(child.project_id || projectId, child.iid);
|
|
3473
2291
|
}
|
|
3474
2292
|
}
|
|
3475
2293
|
// Handle linked_items_to_add: use workItemAddLinkedItems mutation
|
|
@@ -3480,7 +2298,7 @@ async function updateWorkItem(projectId, iid, options) {
|
|
|
3480
2298
|
const linkType = item.link_type || "RELATED";
|
|
3481
2299
|
if (!groupedByType[linkType])
|
|
3482
2300
|
groupedByType[linkType] = [];
|
|
3483
|
-
const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id, item.iid);
|
|
2301
|
+
const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id || projectId, item.iid);
|
|
3484
2302
|
groupedByType[linkType].push(targetGID);
|
|
3485
2303
|
}
|
|
3486
2304
|
for (const [linkType, targetGIDs] of Object.entries(groupedByType)) {
|
|
@@ -3498,7 +2316,7 @@ async function updateWorkItem(projectId, iid, options) {
|
|
|
3498
2316
|
if (options.linked_items_to_remove && options.linked_items_to_remove.length > 0) {
|
|
3499
2317
|
const targetGIDs = [];
|
|
3500
2318
|
for (const item of options.linked_items_to_remove) {
|
|
3501
|
-
const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id, item.iid);
|
|
2319
|
+
const { workItemGID: targetGID } = await resolveWorkItemGID(item.project_id || projectId, item.iid);
|
|
3502
2320
|
targetGIDs.push(targetGID);
|
|
3503
2321
|
}
|
|
3504
2322
|
const removeLinkedData = await executeGraphQL(`mutation($id: WorkItemID!, $workItemsIds: [WorkItemID!]!) {
|
|
@@ -3923,6 +2741,42 @@ async function deleteMergeRequestNote(projectId, mergeRequestIid, noteId) {
|
|
|
3923
2741
|
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
3924
2742
|
}
|
|
3925
2743
|
}
|
|
2744
|
+
// --- Emoji Reactions (REST) ---
|
|
2745
|
+
function buildAwardEmojiPath(entity, projectId, entityIid, opts) {
|
|
2746
|
+
projectId = decodeURIComponent(projectId);
|
|
2747
|
+
const pp = encodeURIComponent(getEffectiveProjectId(projectId));
|
|
2748
|
+
let path = `${getEffectiveApiUrl()}/projects/${pp}/${entity}/${entityIid}`;
|
|
2749
|
+
if (opts?.noteId) {
|
|
2750
|
+
path = opts.discussionId
|
|
2751
|
+
? `${path}/discussions/${opts.discussionId}/notes/${opts.noteId}`
|
|
2752
|
+
: `${path}/notes/${opts.noteId}`;
|
|
2753
|
+
}
|
|
2754
|
+
path += "/award_emoji";
|
|
2755
|
+
if (opts?.awardId)
|
|
2756
|
+
path += `/${opts.awardId}`;
|
|
2757
|
+
return path;
|
|
2758
|
+
}
|
|
2759
|
+
async function createRestAwardEmoji(path, name) {
|
|
2760
|
+
const response = await fetch(path, {
|
|
2761
|
+
...getFetchConfig(),
|
|
2762
|
+
method: "POST",
|
|
2763
|
+
body: JSON.stringify({ name }),
|
|
2764
|
+
});
|
|
2765
|
+
await handleGitLabError(response);
|
|
2766
|
+
return response.json();
|
|
2767
|
+
}
|
|
2768
|
+
async function listRestAwardEmoji(path) {
|
|
2769
|
+
const response = await fetch(path, getFetchConfig());
|
|
2770
|
+
await handleGitLabError(response);
|
|
2771
|
+
return response.json();
|
|
2772
|
+
}
|
|
2773
|
+
async function deleteRestAwardEmoji(path) {
|
|
2774
|
+
const response = await fetch(path, {
|
|
2775
|
+
...getFetchConfig(),
|
|
2776
|
+
method: "DELETE",
|
|
2777
|
+
});
|
|
2778
|
+
await handleGitLabError(response);
|
|
2779
|
+
}
|
|
3926
2780
|
async function getMergeRequestNote(projectId, mergeRequestIid, noteId) {
|
|
3927
2781
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
3928
2782
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes/${noteId}`);
|
|
@@ -4052,43 +2906,6 @@ async function createOrUpdateFile(projectId, filePath, content, commitMessage, b
|
|
|
4052
2906
|
const data = await response.json();
|
|
4053
2907
|
return GitLabCreateUpdateFileResponseSchema.parse(data);
|
|
4054
2908
|
}
|
|
4055
|
-
/**
|
|
4056
|
-
* Create a tree structure in a GitLab project repository
|
|
4057
|
-
* 저장소에 트리 구조 생성
|
|
4058
|
-
*
|
|
4059
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
4060
|
-
* @param {FileOperation[]} files - Array of file operations
|
|
4061
|
-
* @param {string} [ref] - The name of the branch, tag or commit
|
|
4062
|
-
* @returns {Promise<GitLabTree>} The created tree
|
|
4063
|
-
*/
|
|
4064
|
-
async function createTree(projectId, files, ref) {
|
|
4065
|
-
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
4066
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/repository/tree`);
|
|
4067
|
-
if (ref) {
|
|
4068
|
-
url.searchParams.append("ref", ref);
|
|
4069
|
-
}
|
|
4070
|
-
const response = await fetch(url.toString(), {
|
|
4071
|
-
...getFetchConfig(),
|
|
4072
|
-
method: "POST",
|
|
4073
|
-
body: JSON.stringify({
|
|
4074
|
-
files: files.map(file => ({
|
|
4075
|
-
file_path: file.path,
|
|
4076
|
-
content: encodeRepoFilePayloadContent(file.content),
|
|
4077
|
-
encoding: GITLAB_REPO_FILE_ENCODING,
|
|
4078
|
-
})),
|
|
4079
|
-
}),
|
|
4080
|
-
});
|
|
4081
|
-
if (response.status === 400) {
|
|
4082
|
-
const errorBody = await response.text();
|
|
4083
|
-
throw new Error(`Invalid request: ${errorBody}`);
|
|
4084
|
-
}
|
|
4085
|
-
if (!response.ok) {
|
|
4086
|
-
const errorBody = await response.text();
|
|
4087
|
-
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
4088
|
-
}
|
|
4089
|
-
const data = await response.json();
|
|
4090
|
-
return GitLabTreeSchema.parse(data);
|
|
4091
|
-
}
|
|
4092
2909
|
/**
|
|
4093
2910
|
* Create a commit in a GitLab project repository
|
|
4094
2911
|
* 저장소에 커밋 생성
|
|
@@ -4310,18 +3127,6 @@ async function getProjectMergeMethod(projectId) {
|
|
|
4310
3127
|
.parse(data).merge_method;
|
|
4311
3128
|
return typeof mergeMethod === "string" ? mergeMethod : null;
|
|
4312
3129
|
}
|
|
4313
|
-
function estimateMergeCommitCount(mergeMethod, sourceCommitCount) {
|
|
4314
|
-
if (sourceCommitCount === 0) {
|
|
4315
|
-
return 0;
|
|
4316
|
-
}
|
|
4317
|
-
if (mergeMethod === "merge") {
|
|
4318
|
-
return 1;
|
|
4319
|
-
}
|
|
4320
|
-
if (mergeMethod === "ff" || mergeMethod === "rebase_merge") {
|
|
4321
|
-
return 0;
|
|
4322
|
-
}
|
|
4323
|
-
return null;
|
|
4324
|
-
}
|
|
4325
3130
|
async function buildMergeRequestCommitAdditionSummary(projectId, mergeRequest) {
|
|
4326
3131
|
try {
|
|
4327
3132
|
const sourceCommitCount = await getMergeRequestSourceCommitCount(projectId, mergeRequest.iid);
|
|
@@ -5171,100 +3976,6 @@ async function getMergeRequestVersion(projectId, mergeRequestIid, versionId, uni
|
|
|
5171
3976
|
const data = await response.json();
|
|
5172
3977
|
return GitLabMergeRequestVersionDetailSchema.parse(data);
|
|
5173
3978
|
}
|
|
5174
|
-
/**
|
|
5175
|
-
* List all namespaces
|
|
5176
|
-
* 사용 가능한 모든 네임스페이스 목록 조회
|
|
5177
|
-
*
|
|
5178
|
-
* @param {Object} options - Options for listing namespaces
|
|
5179
|
-
* @param {string} [options.search] - Search query to filter namespaces
|
|
5180
|
-
* @param {boolean} [options.owned_only] - Only return namespaces owned by the authenticated user
|
|
5181
|
-
* @param {boolean} [options.top_level_only] - Only return top-level namespaces
|
|
5182
|
-
* @returns {Promise<GitLabNamespace[]>} List of namespaces
|
|
5183
|
-
*/
|
|
5184
|
-
async function listNamespaces(options) {
|
|
5185
|
-
const url = new URL(`${getEffectiveApiUrl()}/namespaces`);
|
|
5186
|
-
if (options.search) {
|
|
5187
|
-
url.searchParams.append("search", options.search);
|
|
5188
|
-
}
|
|
5189
|
-
if (options.owned_only) {
|
|
5190
|
-
url.searchParams.append("owned_only", "true");
|
|
5191
|
-
}
|
|
5192
|
-
if (options.top_level_only) {
|
|
5193
|
-
url.searchParams.append("top_level_only", "true");
|
|
5194
|
-
}
|
|
5195
|
-
const response = await fetch(url.toString(), {
|
|
5196
|
-
...getFetchConfig(),
|
|
5197
|
-
});
|
|
5198
|
-
await handleGitLabError(response);
|
|
5199
|
-
const data = await response.json();
|
|
5200
|
-
return z.array(GitLabNamespaceSchema).parse(data);
|
|
5201
|
-
}
|
|
5202
|
-
/**
|
|
5203
|
-
* Get details on a namespace
|
|
5204
|
-
* 네임스페이스 상세 정보 조회
|
|
5205
|
-
*
|
|
5206
|
-
* @param {string} id - The ID or URL-encoded path of the namespace
|
|
5207
|
-
* @returns {Promise<GitLabNamespace>} The namespace details
|
|
5208
|
-
*/
|
|
5209
|
-
async function getNamespace(id) {
|
|
5210
|
-
const url = new URL(`${getEffectiveApiUrl()}/namespaces/${encodeURIComponent(id)}`);
|
|
5211
|
-
const response = await fetch(url.toString(), {
|
|
5212
|
-
...getFetchConfig(),
|
|
5213
|
-
});
|
|
5214
|
-
await handleGitLabError(response);
|
|
5215
|
-
const data = await response.json();
|
|
5216
|
-
return GitLabNamespaceSchema.parse(data);
|
|
5217
|
-
}
|
|
5218
|
-
/**
|
|
5219
|
-
* Verify if a namespace exists
|
|
5220
|
-
* 네임스페이스 존재 여부 확인
|
|
5221
|
-
*
|
|
5222
|
-
* @param {string} namespacePath - The path of the namespace to check
|
|
5223
|
-
* @param {number} [parentId] - The ID of the parent namespace
|
|
5224
|
-
* @returns {Promise<GitLabNamespaceExistsResponse>} The verification result
|
|
5225
|
-
*/
|
|
5226
|
-
async function verifyNamespaceExistence(namespacePath, parentId) {
|
|
5227
|
-
const url = new URL(`${getEffectiveApiUrl()}/namespaces/${encodeURIComponent(namespacePath)}/exists`);
|
|
5228
|
-
if (parentId) {
|
|
5229
|
-
url.searchParams.append("parent_id", parentId.toString());
|
|
5230
|
-
}
|
|
5231
|
-
const response = await fetch(url.toString(), {
|
|
5232
|
-
...getFetchConfig(),
|
|
5233
|
-
});
|
|
5234
|
-
await handleGitLabError(response);
|
|
5235
|
-
const data = await response.json();
|
|
5236
|
-
return GitLabNamespaceExistsResponseSchema.parse(data);
|
|
5237
|
-
}
|
|
5238
|
-
/**
|
|
5239
|
-
* Get a single project
|
|
5240
|
-
* 단일 프로젝트 조회
|
|
5241
|
-
*
|
|
5242
|
-
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
5243
|
-
* @param {Object} options - Options for getting project details
|
|
5244
|
-
* @param {boolean} [options.license] - Include project license data
|
|
5245
|
-
* @param {boolean} [options.statistics] - Include project statistics
|
|
5246
|
-
* @param {boolean} [options.with_custom_attributes] - Include custom attributes in response
|
|
5247
|
-
* @returns {Promise<GitLabProject>} Project details
|
|
5248
|
-
*/
|
|
5249
|
-
async function getProject(projectId, options = {}) {
|
|
5250
|
-
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
5251
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}`);
|
|
5252
|
-
if (options.license) {
|
|
5253
|
-
url.searchParams.append("license", "true");
|
|
5254
|
-
}
|
|
5255
|
-
if (options.statistics) {
|
|
5256
|
-
url.searchParams.append("statistics", "true");
|
|
5257
|
-
}
|
|
5258
|
-
if (options.with_custom_attributes) {
|
|
5259
|
-
url.searchParams.append("with_custom_attributes", "true");
|
|
5260
|
-
}
|
|
5261
|
-
const response = await fetch(url.toString(), {
|
|
5262
|
-
...getFetchConfig(),
|
|
5263
|
-
});
|
|
5264
|
-
await handleGitLabError(response);
|
|
5265
|
-
const data = await response.json();
|
|
5266
|
-
return GitLabRepositorySchema.parse(data);
|
|
5267
|
-
}
|
|
5268
3979
|
/**
|
|
5269
3980
|
* List projects
|
|
5270
3981
|
* 프로젝트 목록 조회
|
|
@@ -5452,6 +4163,8 @@ async function listGroupProjects(options) {
|
|
|
5452
4163
|
url.searchParams.append("with_custom_attributes", options.with_custom_attributes.toString());
|
|
5453
4164
|
if (options.with_security_reports !== undefined)
|
|
5454
4165
|
url.searchParams.append("with_security_reports", options.with_security_reports.toString());
|
|
4166
|
+
if (options.topic)
|
|
4167
|
+
url.searchParams.append("topic", options.topic);
|
|
5455
4168
|
const response = await fetch(url.toString(), {
|
|
5456
4169
|
...getFetchConfig(),
|
|
5457
4170
|
});
|
|
@@ -5484,18 +4197,6 @@ async function listWebhooks(options) {
|
|
|
5484
4197
|
await handleGitLabError(response);
|
|
5485
4198
|
return (await response.json());
|
|
5486
4199
|
}
|
|
5487
|
-
/**
|
|
5488
|
-
* Summarize webhook events by stripping heavy payload fields
|
|
5489
|
-
*/
|
|
5490
|
-
function summarizeWebhookEvents(events) {
|
|
5491
|
-
return events.map(event => ({
|
|
5492
|
-
id: event.id,
|
|
5493
|
-
url: event.url,
|
|
5494
|
-
trigger: event.trigger,
|
|
5495
|
-
response_status: event.response_status,
|
|
5496
|
-
execution_duration: event.execution_duration,
|
|
5497
|
-
}));
|
|
5498
|
-
}
|
|
5499
4200
|
/**
|
|
5500
4201
|
* Fetch a single page of webhook events
|
|
5501
4202
|
*/
|
|
@@ -6880,37 +5581,6 @@ async function downloadReleaseAsset(projectId, tagName, directAssetPath) {
|
|
|
6880
5581
|
}
|
|
6881
5582
|
// Request handlers are now registered inside createServer() factory function
|
|
6882
5583
|
// to ensure each transport connection gets its own Server instance (GHSA-345p-7cg4-v4c7).
|
|
6883
|
-
/**
|
|
6884
|
-
* Filter diffs by excluded file patterns
|
|
6885
|
-
* Safely handles invalid regex patterns by logging and ignoring them
|
|
6886
|
-
*
|
|
6887
|
-
* @param diffs - Array of diff objects with new_path property
|
|
6888
|
-
* @param excludedFilePatterns - Array of regex patterns to exclude
|
|
6889
|
-
* @returns Filtered array of diffs
|
|
6890
|
-
*/
|
|
6891
|
-
function filterDiffsByPatterns(diffs, excludedFilePatterns) {
|
|
6892
|
-
if (!excludedFilePatterns?.length)
|
|
6893
|
-
return diffs;
|
|
6894
|
-
const regexPatterns = excludedFilePatterns
|
|
6895
|
-
.map(pattern => {
|
|
6896
|
-
try {
|
|
6897
|
-
return new RegExp(pattern);
|
|
6898
|
-
}
|
|
6899
|
-
catch (e) {
|
|
6900
|
-
console.warn(`Invalid regex pattern ignored: ${pattern}`);
|
|
6901
|
-
return null;
|
|
6902
|
-
}
|
|
6903
|
-
})
|
|
6904
|
-
.filter((regex) => regex !== null);
|
|
6905
|
-
if (regexPatterns.length === 0)
|
|
6906
|
-
return diffs;
|
|
6907
|
-
const matchesAnyPattern = (path) => {
|
|
6908
|
-
if (!path)
|
|
6909
|
-
return false;
|
|
6910
|
-
return regexPatterns.some(regex => regex.test(path));
|
|
6911
|
-
};
|
|
6912
|
-
return diffs.filter(diff => !matchesAnyPattern(diff.new_path));
|
|
6913
|
-
}
|
|
6914
5584
|
async function handleToolCall(params) {
|
|
6915
5585
|
try {
|
|
6916
5586
|
if (!params.arguments) {
|
|
@@ -6922,7 +5592,16 @@ async function handleToolCall(params) {
|
|
|
6922
5592
|
}
|
|
6923
5593
|
// Lazy OAuth token refresh: only validate/refresh when a tool is actually called
|
|
6924
5594
|
await ensureValidOAuthToken();
|
|
6925
|
-
|
|
5595
|
+
// Normalize common parameter aliases that LLMs send
|
|
5596
|
+
const args = params.arguments;
|
|
5597
|
+
if (args) {
|
|
5598
|
+
// work_item_iid -> iid (for work item tools)
|
|
5599
|
+
if (args.work_item_iid !== undefined && args.iid === undefined) {
|
|
5600
|
+
args.iid = args.work_item_iid;
|
|
5601
|
+
delete args.work_item_iid;
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
logger.info({ tool: params.name, event: "tool_call_start" }, `tool_call_start: ${params.name}`);
|
|
6926
5605
|
switch (params.name) {
|
|
6927
5606
|
case "execute_graphql": {
|
|
6928
5607
|
const args = ExecuteGraphQLSchema.parse(params.arguments);
|
|
@@ -7178,6 +5857,42 @@ async function handleToolCall(params) {
|
|
|
7178
5857
|
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
7179
5858
|
};
|
|
7180
5859
|
}
|
|
5860
|
+
case "list_merge_request_emoji_reactions": {
|
|
5861
|
+
const args = ListMergeRequestEmojiReactionsSchema.parse(params.arguments);
|
|
5862
|
+
const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid);
|
|
5863
|
+
const result = await listRestAwardEmoji(path);
|
|
5864
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
5865
|
+
}
|
|
5866
|
+
case "list_merge_request_note_emoji_reactions": {
|
|
5867
|
+
const args = ListMergeRequestNoteEmojiReactionsSchema.parse(params.arguments);
|
|
5868
|
+
const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { noteId: args.note_id, discussionId: args.discussion_id });
|
|
5869
|
+
const result = await listRestAwardEmoji(path);
|
|
5870
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
5871
|
+
}
|
|
5872
|
+
case "create_merge_request_emoji_reaction": {
|
|
5873
|
+
const args = CreateMergeRequestEmojiReactionSchema.parse(params.arguments);
|
|
5874
|
+
const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid);
|
|
5875
|
+
const result = await createRestAwardEmoji(path, args.name);
|
|
5876
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
5877
|
+
}
|
|
5878
|
+
case "delete_merge_request_emoji_reaction": {
|
|
5879
|
+
const args = DeleteMergeRequestEmojiReactionSchema.parse(params.arguments);
|
|
5880
|
+
const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { awardId: args.award_id });
|
|
5881
|
+
await deleteRestAwardEmoji(path);
|
|
5882
|
+
return { content: [{ type: "text", text: "Merge request emoji reaction deleted successfully" }] };
|
|
5883
|
+
}
|
|
5884
|
+
case "create_merge_request_note_emoji_reaction": {
|
|
5885
|
+
const args = CreateMergeRequestNoteEmojiReactionSchema.parse(params.arguments);
|
|
5886
|
+
const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { noteId: args.note_id, discussionId: args.discussion_id });
|
|
5887
|
+
const result = await createRestAwardEmoji(path, args.name);
|
|
5888
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
5889
|
+
}
|
|
5890
|
+
case "delete_merge_request_note_emoji_reaction": {
|
|
5891
|
+
const args = DeleteMergeRequestNoteEmojiReactionSchema.parse(params.arguments);
|
|
5892
|
+
const path = buildAwardEmojiPath("merge_requests", args.project_id, args.merge_request_iid, { noteId: args.note_id, discussionId: args.discussion_id, awardId: args.award_id });
|
|
5893
|
+
await deleteRestAwardEmoji(path);
|
|
5894
|
+
return { content: [{ type: "text", text: "Merge request note emoji reaction deleted successfully" }] };
|
|
5895
|
+
}
|
|
7181
5896
|
case "update_issue_note": {
|
|
7182
5897
|
const args = UpdateIssueNoteSchema.parse(params.arguments);
|
|
7183
5898
|
const note = await updateIssueNote(args.project_id, args.issue_iid, args.discussion_id, args.note_id, args.body, args.resolved);
|
|
@@ -7192,6 +5907,42 @@ async function handleToolCall(params) {
|
|
|
7192
5907
|
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
7193
5908
|
};
|
|
7194
5909
|
}
|
|
5910
|
+
case "list_issue_emoji_reactions": {
|
|
5911
|
+
const args = ListIssueEmojiReactionsSchema.parse(params.arguments);
|
|
5912
|
+
const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid);
|
|
5913
|
+
const result = await listRestAwardEmoji(path);
|
|
5914
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
5915
|
+
}
|
|
5916
|
+
case "list_issue_note_emoji_reactions": {
|
|
5917
|
+
const args = ListIssueNoteEmojiReactionsSchema.parse(params.arguments);
|
|
5918
|
+
const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, { noteId: args.note_id, discussionId: args.discussion_id });
|
|
5919
|
+
const result = await listRestAwardEmoji(path);
|
|
5920
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
5921
|
+
}
|
|
5922
|
+
case "create_issue_emoji_reaction": {
|
|
5923
|
+
const args = CreateIssueEmojiReactionSchema.parse(params.arguments);
|
|
5924
|
+
const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid);
|
|
5925
|
+
const result = await createRestAwardEmoji(path, args.name);
|
|
5926
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
5927
|
+
}
|
|
5928
|
+
case "delete_issue_emoji_reaction": {
|
|
5929
|
+
const args = DeleteIssueEmojiReactionSchema.parse(params.arguments);
|
|
5930
|
+
const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, { awardId: args.award_id });
|
|
5931
|
+
await deleteRestAwardEmoji(path);
|
|
5932
|
+
return { content: [{ type: "text", text: "Issue emoji reaction deleted successfully" }] };
|
|
5933
|
+
}
|
|
5934
|
+
case "create_issue_note_emoji_reaction": {
|
|
5935
|
+
const args = CreateIssueNoteEmojiReactionSchema.parse(params.arguments);
|
|
5936
|
+
const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, { noteId: args.note_id, discussionId: args.discussion_id });
|
|
5937
|
+
const result = await createRestAwardEmoji(path, args.name);
|
|
5938
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
5939
|
+
}
|
|
5940
|
+
case "delete_issue_note_emoji_reaction": {
|
|
5941
|
+
const args = DeleteIssueNoteEmojiReactionSchema.parse(params.arguments);
|
|
5942
|
+
const path = buildAwardEmojiPath("issues", args.project_id, args.issue_iid, { noteId: args.note_id, discussionId: args.discussion_id, awardId: args.award_id });
|
|
5943
|
+
await deleteRestAwardEmoji(path);
|
|
5944
|
+
return { content: [{ type: "text", text: "Issue note emoji reaction deleted successfully" }] };
|
|
5945
|
+
}
|
|
7195
5946
|
case "get_merge_request": {
|
|
7196
5947
|
const args = GetMergeRequestSchema.parse(params.arguments);
|
|
7197
5948
|
const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid, args.source_branch);
|
|
@@ -7641,6 +6392,39 @@ async function handleToolCall(params) {
|
|
|
7641
6392
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
7642
6393
|
};
|
|
7643
6394
|
}
|
|
6395
|
+
case "list_work_item_emoji_reactions": {
|
|
6396
|
+
const args = ListWorkItemEmojiReactionsSchema.parse(params.arguments);
|
|
6397
|
+
const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
|
|
6398
|
+
const result = await listGraphQLAwardEmoji(workItemGID);
|
|
6399
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
6400
|
+
}
|
|
6401
|
+
case "list_work_item_note_emoji_reactions": {
|
|
6402
|
+
const args = ListWorkItemNoteEmojiReactionsSchema.parse(params.arguments);
|
|
6403
|
+
const result = await listGraphQLAwardEmoji(args.note_id);
|
|
6404
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
6405
|
+
}
|
|
6406
|
+
case "create_work_item_emoji_reaction": {
|
|
6407
|
+
const args = CreateWorkItemEmojiReactionSchema.parse(params.arguments);
|
|
6408
|
+
const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
|
|
6409
|
+
const result = await addGraphQLAwardEmoji(workItemGID, args.name);
|
|
6410
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
6411
|
+
}
|
|
6412
|
+
case "delete_work_item_emoji_reaction": {
|
|
6413
|
+
const args = DeleteWorkItemEmojiReactionSchema.parse(params.arguments);
|
|
6414
|
+
const { workItemGID } = await resolveWorkItemGID(args.project_id, args.iid);
|
|
6415
|
+
const result = await removeGraphQLAwardEmoji(workItemGID, args.name);
|
|
6416
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { status: "success", message: "Work item emoji reaction removed" }, null, 2) }] };
|
|
6417
|
+
}
|
|
6418
|
+
case "create_work_item_note_emoji_reaction": {
|
|
6419
|
+
const args = CreateWorkItemNoteEmojiReactionSchema.parse(params.arguments);
|
|
6420
|
+
const result = await addGraphQLAwardEmoji(args.note_id, args.name);
|
|
6421
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
6422
|
+
}
|
|
6423
|
+
case "delete_work_item_note_emoji_reaction": {
|
|
6424
|
+
const args = DeleteWorkItemNoteEmojiReactionSchema.parse(params.arguments);
|
|
6425
|
+
const result = await removeGraphQLAwardEmoji(args.note_id, args.name);
|
|
6426
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? { status: "success", message: "Work item note emoji reaction removed" }, null, 2) }] };
|
|
6427
|
+
}
|
|
7644
6428
|
case "get_timeline_events": {
|
|
7645
6429
|
const args = GetTimelineEventsSchema.parse(params.arguments);
|
|
7646
6430
|
const result = await getTimelineEvents(args.project_id, args.incident_iid);
|
|
@@ -8396,9 +7180,7 @@ async function startSSEServer() {
|
|
|
8396
7180
|
});
|
|
8397
7181
|
});
|
|
8398
7182
|
const httpServer = app.listen(Number(PORT), HOST, () => {
|
|
8399
|
-
logger.info(
|
|
8400
|
-
const colorGreen = "\x1b[32m";
|
|
8401
|
-
const colorReset = "\x1b[0m";
|
|
7183
|
+
logger.info("GitLab MCP Server running with SSE transport");
|
|
8402
7184
|
logger.info(`${colorGreen}Endpoint: http://${HOST}:${PORT}/sse${colorReset}`);
|
|
8403
7185
|
});
|
|
8404
7186
|
const shutdown = async (signal) => {
|
|
@@ -8578,14 +7360,54 @@ async function startStreamableHTTPServer() {
|
|
|
8578
7360
|
const gitlabBaseUrl = GITLAB_API_URL.replace(/\/api\/v4\/?$/, "").replace(/\/$/, "");
|
|
8579
7361
|
const issuerUrl = new URL(MCP_SERVER_URL);
|
|
8580
7362
|
const oauthProvider = createGitLabOAuthProvider(gitlabBaseUrl, GITLAB_OAUTH_APP_ID, "GitLab MCP Server", GITLAB_READ_ONLY_MODE, GITLAB_OAUTH_SCOPES);
|
|
8581
|
-
|
|
8582
|
-
//
|
|
8583
|
-
//
|
|
7363
|
+
const scopesSupported = GITLAB_OAUTH_SCOPES ?? ["api", "read_api", "read_user"];
|
|
7364
|
+
// When server URL has a path (e.g. behind Kong), the SDK's well-known metadata
|
|
7365
|
+
// advertises root-level endpoints. Override to use path-prefixed endpoints.
|
|
7366
|
+
const issuerPath = issuerUrl.pathname.replace(/\/$/, "");
|
|
7367
|
+
if (issuerPath) {
|
|
7368
|
+
const routedBaseUrl = `${issuerUrl.origin}${issuerPath}`;
|
|
7369
|
+
const authorizationServerMetadata = {
|
|
7370
|
+
issuer: issuerUrl.href,
|
|
7371
|
+
authorization_endpoint: `${routedBaseUrl}/authorize`,
|
|
7372
|
+
token_endpoint: `${routedBaseUrl}/token`,
|
|
7373
|
+
registration_endpoint: `${routedBaseUrl}/register`,
|
|
7374
|
+
revocation_endpoint: `${routedBaseUrl}/revoke`,
|
|
7375
|
+
response_types_supported: ["code"],
|
|
7376
|
+
code_challenge_methods_supported: ["S256"],
|
|
7377
|
+
token_endpoint_auth_methods_supported: ["client_secret_post", "none"],
|
|
7378
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
7379
|
+
scopes_supported: scopesSupported,
|
|
7380
|
+
revocation_endpoint_auth_methods_supported: ["client_secret_post"],
|
|
7381
|
+
};
|
|
7382
|
+
const protectedResourceMetadata = {
|
|
7383
|
+
resource: issuerUrl.href,
|
|
7384
|
+
authorization_servers: [issuerUrl.href],
|
|
7385
|
+
scopes_supported: scopesSupported,
|
|
7386
|
+
resource_name: "GitLab MCP Server",
|
|
7387
|
+
};
|
|
7388
|
+
const authorizationMetadataRoutes = [
|
|
7389
|
+
"/.well-known/oauth-authorization-server",
|
|
7390
|
+
"/.well-known/oauth-authorization-server/*path",
|
|
7391
|
+
];
|
|
7392
|
+
const protectedResourceRoutes = [
|
|
7393
|
+
"/.well-known/oauth-protected-resource",
|
|
7394
|
+
"/.well-known/oauth-protected-resource/*path",
|
|
7395
|
+
];
|
|
7396
|
+
app.get(authorizationMetadataRoutes, (_req, res) => {
|
|
7397
|
+
res.json(authorizationServerMetadata);
|
|
7398
|
+
});
|
|
7399
|
+
app.get(protectedResourceRoutes, (_req, res) => {
|
|
7400
|
+
res.json(protectedResourceMetadata);
|
|
7401
|
+
});
|
|
7402
|
+
logger.info({ issuerPath }, "Serving path-aware OAuth metadata for reverse-proxy deployments");
|
|
7403
|
+
}
|
|
7404
|
+
// Mounts /.well-known/oauth-authorization-server (shadowed above when basePath set),
|
|
7405
|
+
// /.well-known/oauth-protected-resource, /authorize, /token, /register, /revoke
|
|
8584
7406
|
app.use(mcpAuthRouter({
|
|
8585
7407
|
provider: oauthProvider,
|
|
8586
7408
|
issuerUrl,
|
|
8587
7409
|
baseUrl: issuerUrl,
|
|
8588
|
-
scopesSupported
|
|
7410
|
+
scopesSupported,
|
|
8589
7411
|
resourceName: "GitLab MCP Server",
|
|
8590
7412
|
}));
|
|
8591
7413
|
// Expose provider so the /mcp route middleware can reference it
|