@zereight/mcp-gitlab 2.1.0 → 2.1.2
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/build/auth-retry.js +81 -0
- package/build/index.js +239 -10
- package/build/oauth.js +4 -4
- package/build/schemas.js +104 -10
- package/build/test/mcp-oauth-tests.js +49 -0
- package/build/test/schema-tests.js +261 -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 +3 -0
- package/build/test/test-toolset-filtering.js +9 -3
- package/build/tools/registry.js +124 -1
- package/package.json +4 -4
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper functions for OAuth 401 auto-retry logic.
|
|
3
|
+
*
|
|
4
|
+
* Extracted into a separate module so they can be unit-tested without
|
|
5
|
+
* importing index.ts (which has heavy side effects: starts the MCP server,
|
|
6
|
+
* reads env vars, etc.).
|
|
7
|
+
*/
|
|
8
|
+
import { Headers } from "node-fetch";
|
|
9
|
+
/**
|
|
10
|
+
* Convert various header representations to a plain Record<string, string>.
|
|
11
|
+
*/
|
|
12
|
+
export function headersToPlainObject(headers) {
|
|
13
|
+
if (!headers)
|
|
14
|
+
return {};
|
|
15
|
+
if (headers instanceof Headers) {
|
|
16
|
+
const obj = {};
|
|
17
|
+
headers.forEach((value, key) => { obj[key] = value; });
|
|
18
|
+
return obj;
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(headers)) {
|
|
21
|
+
return Object.fromEntries(headers);
|
|
22
|
+
}
|
|
23
|
+
return headers;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Detect request bodies that cannot be replayed (streams, FormData).
|
|
27
|
+
*/
|
|
28
|
+
export function isNonReplayableBody(body) {
|
|
29
|
+
if (!body)
|
|
30
|
+
return false;
|
|
31
|
+
// Stream-like objects (has .pipe or .read)
|
|
32
|
+
if (typeof body.pipe === "function" || typeof body.read === "function")
|
|
33
|
+
return true;
|
|
34
|
+
// form-data instances (duck-type check since FormData is dynamically imported)
|
|
35
|
+
if (typeof body.getBuffer === "function" && typeof body.getBoundary === "function")
|
|
36
|
+
return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Wrap a fetch function with automatic OAuth token refresh on 401 responses.
|
|
41
|
+
* On a 401, force-refreshes the OAuth token and retries the request once.
|
|
42
|
+
* The retry calls baseFetch directly (not the wrapper), so infinite loops are impossible.
|
|
43
|
+
* In non-OAuth mode, the wrapper is a transparent pass-through.
|
|
44
|
+
*
|
|
45
|
+
* When called without `config`, falls back to module globals in index.ts.
|
|
46
|
+
* When called with `config` (tests), uses injected dependencies.
|
|
47
|
+
*/
|
|
48
|
+
export function wrapWithAuthRetry(baseFetch, config) {
|
|
49
|
+
let refreshLock = null;
|
|
50
|
+
const log = config.logger ?? { info: () => { }, error: () => { } };
|
|
51
|
+
return (async (url, options) => {
|
|
52
|
+
const response = await baseFetch(url, options);
|
|
53
|
+
if (response.status === 401 && config.isOAuthEnabled()) {
|
|
54
|
+
// Skip retry for non-replayable bodies (streams, FormData) since the first request consumed them
|
|
55
|
+
if (isNonReplayableBody(options?.body)) {
|
|
56
|
+
log.info("Received 401 but request body is not replayable (stream/FormData), skipping retry.");
|
|
57
|
+
return response;
|
|
58
|
+
}
|
|
59
|
+
log.info("Received 401, force-refreshing OAuth token and retrying...");
|
|
60
|
+
try {
|
|
61
|
+
// Mutex: coalesce concurrent refresh attempts into a single in-flight request
|
|
62
|
+
if (!refreshLock) {
|
|
63
|
+
refreshLock = config.refreshToken(true).finally(() => {
|
|
64
|
+
refreshLock = null;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const token = await refreshLock;
|
|
68
|
+
config.onTokenRefreshed(token);
|
|
69
|
+
const retryOptions = {
|
|
70
|
+
...options,
|
|
71
|
+
headers: { ...headersToPlainObject(options?.headers), ...config.buildAuthHeaders() },
|
|
72
|
+
};
|
|
73
|
+
return await baseFetch(url, retryOptions);
|
|
74
|
+
}
|
|
75
|
+
catch (refreshError) {
|
|
76
|
+
log.error("OAuth token refresh failed, returning original 401 response:", refreshError);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return response;
|
|
80
|
+
});
|
|
81
|
+
}
|
package/build/index.js
CHANGED
|
@@ -23,8 +23,8 @@ import { estimateMergeCommitCount, filterDiffsByPatterns, summarizeWebhookEvents
|
|
|
23
23
|
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
|
24
24
|
import { GitLabClientPool } from "./gitlab-client-pool.js";
|
|
25
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, CreateLabelSchema, // Added
|
|
27
|
-
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,
|
|
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,
|
|
28
28
|
// pipeline job schemas
|
|
29
29
|
GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
|
|
30
30
|
// Discussion Schemas
|
|
@@ -32,7 +32,7 @@ GitLabDiscussionNoteSchema, // Added
|
|
|
32
32
|
GitLabDiscussionSchema,
|
|
33
33
|
// Draft Notes Schemas
|
|
34
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, MoveWorkItemSchema, ListCustomFieldDefinitionsSchema, GetTimelineEventsSchema, CreateTimelineEventSchema, ListWebhooksSchema, ListWebhookEventsSchema, GetWebhookEventSchema, } from "./schemas.js";
|
|
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";
|
|
36
36
|
import { randomUUID } from "node:crypto";
|
|
37
37
|
import { pino } from "pino";
|
|
38
38
|
const logger = pino({
|
|
@@ -546,9 +546,22 @@ const createCookieJar = async () => {
|
|
|
546
546
|
}
|
|
547
547
|
return jar;
|
|
548
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
|
+
}
|
|
549
562
|
// Cookie jar and fetch - reloaded when cookie file changes
|
|
550
563
|
let cookieJar = null;
|
|
551
|
-
let fetch = nodeFetch;
|
|
564
|
+
let fetch = wrapWithAuthRetry(nodeFetch, defaultAuthRetryConfig());
|
|
552
565
|
let lastCookieMtime = 0;
|
|
553
566
|
let cookieReloadLock = null; // Mutex to prevent parallel reloads
|
|
554
567
|
// Auth proxies may redirect and set cookies on the first request. We make a throwaway
|
|
@@ -568,7 +581,7 @@ async function reloadCookiesIfChanged() {
|
|
|
568
581
|
lastCookieMtime = mtime;
|
|
569
582
|
const newJar = await createCookieJar();
|
|
570
583
|
cookieJar = newJar;
|
|
571
|
-
fetch = newJar ? fetchCookie(nodeFetch, newJar) : nodeFetch;
|
|
584
|
+
fetch = wrapWithAuthRetry(newJar ? fetchCookie(nodeFetch, newJar) : nodeFetch, defaultAuthRetryConfig());
|
|
572
585
|
initialSessionRequestMade = false;
|
|
573
586
|
}
|
|
574
587
|
}
|
|
@@ -577,7 +590,7 @@ async function reloadCookiesIfChanged() {
|
|
|
577
590
|
if (cookieJar) {
|
|
578
591
|
logger.info("Cookie file removed, clearing cached cookies");
|
|
579
592
|
cookieJar = null;
|
|
580
|
-
fetch = nodeFetch;
|
|
593
|
+
fetch = wrapWithAuthRetry(nodeFetch, defaultAuthRetryConfig());
|
|
581
594
|
lastCookieMtime = 0;
|
|
582
595
|
initialSessionRequestMade = false;
|
|
583
596
|
}
|
|
@@ -1477,6 +1490,39 @@ async function createWorkItemNote(projectId, iid, body, options = {}) {
|
|
|
1477
1490
|
}
|
|
1478
1491
|
return data.createNote.note;
|
|
1479
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
|
+
}
|
|
1480
1526
|
// --- Incident Timeline Events ---
|
|
1481
1527
|
/**
|
|
1482
1528
|
* List timeline events for an incident.
|
|
@@ -2695,6 +2741,42 @@ async function deleteMergeRequestNote(projectId, mergeRequestIid, noteId) {
|
|
|
2695
2741
|
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`);
|
|
2696
2742
|
}
|
|
2697
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
|
+
}
|
|
2698
2780
|
async function getMergeRequestNote(projectId, mergeRequestIid, noteId) {
|
|
2699
2781
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
2700
2782
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/notes/${noteId}`);
|
|
@@ -4081,6 +4163,8 @@ async function listGroupProjects(options) {
|
|
|
4081
4163
|
url.searchParams.append("with_custom_attributes", options.with_custom_attributes.toString());
|
|
4082
4164
|
if (options.with_security_reports !== undefined)
|
|
4083
4165
|
url.searchParams.append("with_security_reports", options.with_security_reports.toString());
|
|
4166
|
+
if (options.topic)
|
|
4167
|
+
url.searchParams.append("topic", options.topic);
|
|
4084
4168
|
const response = await fetch(url.toString(), {
|
|
4085
4169
|
...getFetchConfig(),
|
|
4086
4170
|
});
|
|
@@ -5773,6 +5857,42 @@ async function handleToolCall(params) {
|
|
|
5773
5857
|
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
5774
5858
|
};
|
|
5775
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
|
+
}
|
|
5776
5896
|
case "update_issue_note": {
|
|
5777
5897
|
const args = UpdateIssueNoteSchema.parse(params.arguments);
|
|
5778
5898
|
const note = await updateIssueNote(args.project_id, args.issue_iid, args.discussion_id, args.note_id, args.body, args.resolved);
|
|
@@ -5787,6 +5907,42 @@ async function handleToolCall(params) {
|
|
|
5787
5907
|
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
|
|
5788
5908
|
};
|
|
5789
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
|
+
}
|
|
5790
5946
|
case "get_merge_request": {
|
|
5791
5947
|
const args = GetMergeRequestSchema.parse(params.arguments);
|
|
5792
5948
|
const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid, args.source_branch);
|
|
@@ -6236,6 +6392,39 @@ async function handleToolCall(params) {
|
|
|
6236
6392
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
6237
6393
|
};
|
|
6238
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
|
+
}
|
|
6239
6428
|
case "get_timeline_events": {
|
|
6240
6429
|
const args = GetTimelineEventsSchema.parse(params.arguments);
|
|
6241
6430
|
const result = await getTimelineEvents(args.project_id, args.incident_iid);
|
|
@@ -7171,14 +7360,54 @@ async function startStreamableHTTPServer() {
|
|
|
7171
7360
|
const gitlabBaseUrl = GITLAB_API_URL.replace(/\/api\/v4\/?$/, "").replace(/\/$/, "");
|
|
7172
7361
|
const issuerUrl = new URL(MCP_SERVER_URL);
|
|
7173
7362
|
const oauthProvider = createGitLabOAuthProvider(gitlabBaseUrl, GITLAB_OAUTH_APP_ID, "GitLab MCP Server", GITLAB_READ_ONLY_MODE, GITLAB_OAUTH_SCOPES);
|
|
7174
|
-
|
|
7175
|
-
//
|
|
7176
|
-
//
|
|
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
|
|
7177
7406
|
app.use(mcpAuthRouter({
|
|
7178
7407
|
provider: oauthProvider,
|
|
7179
7408
|
issuerUrl,
|
|
7180
7409
|
baseUrl: issuerUrl,
|
|
7181
|
-
scopesSupported
|
|
7410
|
+
scopesSupported,
|
|
7182
7411
|
resourceName: "GitLab MCP Server",
|
|
7183
7412
|
}));
|
|
7184
7413
|
// Expose provider so the /mcp route middleware can reference it
|
package/build/oauth.js
CHANGED
|
@@ -459,15 +459,15 @@ export class GitLabOAuth {
|
|
|
459
459
|
/**
|
|
460
460
|
* Get a valid access token, refreshing if necessary
|
|
461
461
|
*/
|
|
462
|
-
async getAccessToken() {
|
|
462
|
+
async getAccessToken(force = false) {
|
|
463
463
|
let tokenData = this.loadToken();
|
|
464
|
-
// If no token or expired, start OAuth flow or refresh
|
|
464
|
+
// If no token or expired (or forced), start OAuth flow or refresh
|
|
465
465
|
if (!tokenData) {
|
|
466
466
|
logger.info("No stored token found. Starting OAuth flow...");
|
|
467
467
|
tokenData = await this.startOAuthFlow();
|
|
468
468
|
}
|
|
469
|
-
else if (this.isTokenExpired(tokenData)) {
|
|
470
|
-
logger.info("Token expired. Refreshing...");
|
|
469
|
+
else if (force || this.isTokenExpired(tokenData)) {
|
|
470
|
+
logger.info(force && !this.isTokenExpired(tokenData) ? "Force-refreshing OAuth token..." : "Token expired. Refreshing...");
|
|
471
471
|
if (tokenData.refresh_token) {
|
|
472
472
|
try {
|
|
473
473
|
tokenData = await this.refreshAccessToken(tokenData.refresh_token);
|
package/build/schemas.js
CHANGED
|
@@ -527,7 +527,7 @@ export const GitLabRepositorySchema = z.object({
|
|
|
527
527
|
http_url_to_repo: z.string().optional(),
|
|
528
528
|
created_at: z.string().optional(),
|
|
529
529
|
last_activity_at: z.string().optional(),
|
|
530
|
-
default_branch: z.string().optional(),
|
|
530
|
+
default_branch: z.string().nullable().optional(),
|
|
531
531
|
namespace: z
|
|
532
532
|
.object({
|
|
533
533
|
id: z.coerce.string(),
|
|
@@ -621,7 +621,7 @@ export const FileOperationSchema = z.object({
|
|
|
621
621
|
export const GitLabTreeItemSchema = z.object({
|
|
622
622
|
id: z.string(),
|
|
623
623
|
name: z.string(),
|
|
624
|
-
type: z.enum(["tree", "blob"]),
|
|
624
|
+
type: z.enum(["tree", "blob", "commit"]),
|
|
625
625
|
path: z.string(),
|
|
626
626
|
mode: z.string(),
|
|
627
627
|
});
|
|
@@ -1219,7 +1219,7 @@ export const CreateIssueSchema = ProjectParamsSchema.extend({
|
|
|
1219
1219
|
title: z.string().describe("Issue title"),
|
|
1220
1220
|
description: z.string().optional().describe("Issue description"),
|
|
1221
1221
|
assignee_ids: z.array(z.coerce.number()).optional().describe("Array of user IDs to assign"),
|
|
1222
|
-
labels:
|
|
1222
|
+
labels: coerceStringArray.optional().describe("Array of label names"),
|
|
1223
1223
|
milestone_id: z.coerce.string().optional().describe("Milestone ID to assign"),
|
|
1224
1224
|
issue_type: z
|
|
1225
1225
|
.enum(["issue", "incident", "test_case", "task"])
|
|
@@ -1239,7 +1239,7 @@ const MergeRequestOptionsSchema = {
|
|
|
1239
1239
|
.array(z.coerce.number())
|
|
1240
1240
|
.optional()
|
|
1241
1241
|
.describe("The ID of the users to assign as reviewers of the MR"),
|
|
1242
|
-
labels:
|
|
1242
|
+
labels: coerceStringArray.optional().describe("Labels for the MR"),
|
|
1243
1243
|
draft: z.coerce.boolean().optional().describe("Create as draft merge request"),
|
|
1244
1244
|
allow_collaboration: z.coerce.boolean().optional().describe("Allow commits from upstream members"),
|
|
1245
1245
|
remove_source_branch: z
|
|
@@ -1288,7 +1288,7 @@ export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
|
|
|
1288
1288
|
.array(z.coerce.number())
|
|
1289
1289
|
.optional()
|
|
1290
1290
|
.describe("The ID of the users to assign as reviewers of the MR"),
|
|
1291
|
-
labels:
|
|
1291
|
+
labels: coerceStringArray.optional().describe("Labels for the MR"),
|
|
1292
1292
|
state_event: z
|
|
1293
1293
|
.enum(["close", "reopen"])
|
|
1294
1294
|
.optional()
|
|
@@ -1475,7 +1475,7 @@ export const ListIssuesSchema = z
|
|
|
1475
1475
|
created_after: z.string().optional().describe("Return issues created after the given time"),
|
|
1476
1476
|
created_before: z.string().optional().describe("Return issues created before the given time"),
|
|
1477
1477
|
due_date: z.string().optional().describe("Return issues that have the due date"),
|
|
1478
|
-
labels:
|
|
1478
|
+
labels: coerceStringArray.optional().describe("Array of label names"),
|
|
1479
1479
|
milestone: z.string().optional().describe("Milestone title"),
|
|
1480
1480
|
issue_type: z
|
|
1481
1481
|
.enum(["issue", "incident", "test_case", "task"])
|
|
@@ -1546,7 +1546,7 @@ export const ListMergeRequestsSchema = z
|
|
|
1546
1546
|
.string()
|
|
1547
1547
|
.optional()
|
|
1548
1548
|
.describe("Return merge requests updated before the given time"),
|
|
1549
|
-
labels:
|
|
1549
|
+
labels: coerceStringArray.optional().describe("Array of label names"),
|
|
1550
1550
|
milestone: z.string().optional().describe("Milestone title"),
|
|
1551
1551
|
scope: z
|
|
1552
1552
|
.enum(["created_by_me", "assigned_to_me", "all"])
|
|
@@ -1689,6 +1689,7 @@ export const ListProjectsSchema = z
|
|
|
1689
1689
|
.optional()
|
|
1690
1690
|
.describe("Filter projects with merge requests feature enabled"),
|
|
1691
1691
|
min_access_level: z.coerce.number().optional().describe("Filter by minimum access level"),
|
|
1692
|
+
topic: z.string().optional().describe("Filter by topic (projects tagged with this topic)"),
|
|
1692
1693
|
})
|
|
1693
1694
|
.merge(PaginationOptionsSchema);
|
|
1694
1695
|
// Label operation schemas
|
|
@@ -1760,6 +1761,7 @@ export const ListGroupProjectsSchema = z
|
|
|
1760
1761
|
statistics: z.coerce.boolean().optional().describe("Include project statistics"),
|
|
1761
1762
|
with_custom_attributes: z.coerce.boolean().optional().describe("Include custom attributes"),
|
|
1762
1763
|
with_security_reports: z.coerce.boolean().optional().describe("Include security reports"),
|
|
1764
|
+
topic: z.string().optional().describe("Filter by topic (projects tagged with this topic)"),
|
|
1763
1765
|
})
|
|
1764
1766
|
.merge(PaginationOptionsSchema);
|
|
1765
1767
|
// Add wiki operation schemas
|
|
@@ -2134,7 +2136,7 @@ export const MyIssuesSchema = z.object({
|
|
|
2134
2136
|
.enum(["opened", "closed", "all"])
|
|
2135
2137
|
.optional()
|
|
2136
2138
|
.describe("Return issues with a specific state (default: opened)"),
|
|
2137
|
-
labels:
|
|
2139
|
+
labels: coerceStringArray.optional().describe("Array of label names to filter by"),
|
|
2138
2140
|
milestone: z.string().optional().describe("Milestone title to filter by"),
|
|
2139
2141
|
search: z.string().optional().describe("Search for specific terms in title and description"),
|
|
2140
2142
|
created_after: z
|
|
@@ -2273,8 +2275,8 @@ export const GitLabEventSchema = z
|
|
|
2273
2275
|
created_at: z.string(),
|
|
2274
2276
|
author: GitLabEventAuthorSchema,
|
|
2275
2277
|
author_username: z.string(),
|
|
2276
|
-
imported: z.coerce.boolean(),
|
|
2277
|
-
imported_from: z.string(),
|
|
2278
|
+
imported: z.coerce.boolean().optional(),
|
|
2279
|
+
imported_from: z.string().nullable().optional(),
|
|
2278
2280
|
})
|
|
2279
2281
|
.passthrough(); // Allow additional fields
|
|
2280
2282
|
// List events schema
|
|
@@ -2711,6 +2713,98 @@ export const ListCustomFieldDefinitionsSchema = z.object({
|
|
|
2711
2713
|
.default("issue")
|
|
2712
2714
|
.describe("The work item type to list custom field definitions for. Defaults to 'issue'."),
|
|
2713
2715
|
});
|
|
2716
|
+
// --- Emoji Reaction schemas (REST: MRs and Issues) ---
|
|
2717
|
+
const emojiNameField = z.string().describe("Name of the emoji without colons (e.g. 'thumbsup', 'rocket', 'eyes')");
|
|
2718
|
+
const awardIdField = z.coerce.string().describe("The ID of the emoji reaction to delete");
|
|
2719
|
+
const noteEmojiDiscussionField = z.coerce.string().optional().describe("The ID of a discussion thread. Required for notes that are discussion replies; omit for top-level notes.");
|
|
2720
|
+
export const CreateMergeRequestEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2721
|
+
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2722
|
+
name: emojiNameField,
|
|
2723
|
+
});
|
|
2724
|
+
export const DeleteMergeRequestEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2725
|
+
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2726
|
+
award_id: awardIdField,
|
|
2727
|
+
});
|
|
2728
|
+
export const CreateMergeRequestNoteEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2729
|
+
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2730
|
+
note_id: z.coerce.string().describe("The ID of a note (comment or thread reply)"),
|
|
2731
|
+
discussion_id: noteEmojiDiscussionField,
|
|
2732
|
+
name: emojiNameField,
|
|
2733
|
+
});
|
|
2734
|
+
export const DeleteMergeRequestNoteEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2735
|
+
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2736
|
+
note_id: z.coerce.string().describe("The ID of a note (comment or thread reply)"),
|
|
2737
|
+
discussion_id: noteEmojiDiscussionField,
|
|
2738
|
+
award_id: awardIdField,
|
|
2739
|
+
});
|
|
2740
|
+
export const CreateIssueEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2741
|
+
issue_iid: z.coerce.string().describe("The IID of an issue"),
|
|
2742
|
+
name: emojiNameField,
|
|
2743
|
+
});
|
|
2744
|
+
export const DeleteIssueEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2745
|
+
issue_iid: z.coerce.string().describe("The IID of an issue"),
|
|
2746
|
+
award_id: awardIdField,
|
|
2747
|
+
});
|
|
2748
|
+
export const CreateIssueNoteEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2749
|
+
issue_iid: z.coerce.string().describe("The IID of an issue"),
|
|
2750
|
+
note_id: z.coerce.string().describe("The ID of a note (comment or thread reply)"),
|
|
2751
|
+
discussion_id: noteEmojiDiscussionField,
|
|
2752
|
+
name: emojiNameField,
|
|
2753
|
+
});
|
|
2754
|
+
export const DeleteIssueNoteEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
2755
|
+
issue_iid: z.coerce.string().describe("The IID of an issue"),
|
|
2756
|
+
note_id: z.coerce.string().describe("The ID of a note (comment or thread reply)"),
|
|
2757
|
+
discussion_id: noteEmojiDiscussionField,
|
|
2758
|
+
award_id: awardIdField,
|
|
2759
|
+
});
|
|
2760
|
+
// --- Emoji Reaction schemas (GraphQL: Work Items) ---
|
|
2761
|
+
export const CreateWorkItemEmojiReactionSchema = z.object({
|
|
2762
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2763
|
+
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
2764
|
+
name: emojiNameField,
|
|
2765
|
+
});
|
|
2766
|
+
export const DeleteWorkItemEmojiReactionSchema = z.object({
|
|
2767
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2768
|
+
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
2769
|
+
name: emojiNameField,
|
|
2770
|
+
});
|
|
2771
|
+
export const CreateWorkItemNoteEmojiReactionSchema = z.object({
|
|
2772
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2773
|
+
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
2774
|
+
note_id: z.string().describe("The GraphQL GID of the note (e.g. 'gid://gitlab/Note/123' from list_work_item_notes)"),
|
|
2775
|
+
name: emojiNameField,
|
|
2776
|
+
});
|
|
2777
|
+
export const DeleteWorkItemNoteEmojiReactionSchema = z.object({
|
|
2778
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2779
|
+
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
2780
|
+
note_id: z.string().describe("The GraphQL GID of the note (e.g. 'gid://gitlab/Note/123' from list_work_item_notes)"),
|
|
2781
|
+
name: emojiNameField,
|
|
2782
|
+
});
|
|
2783
|
+
export const ListMergeRequestEmojiReactionsSchema = ProjectParamsSchema.extend({
|
|
2784
|
+
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2785
|
+
});
|
|
2786
|
+
export const ListMergeRequestNoteEmojiReactionsSchema = ProjectParamsSchema.extend({
|
|
2787
|
+
merge_request_iid: z.coerce.string().describe("The IID of a merge request"),
|
|
2788
|
+
note_id: z.coerce.string().describe("The ID of a note (comment or thread reply)"),
|
|
2789
|
+
discussion_id: noteEmojiDiscussionField,
|
|
2790
|
+
});
|
|
2791
|
+
export const ListIssueEmojiReactionsSchema = ProjectParamsSchema.extend({
|
|
2792
|
+
issue_iid: z.coerce.string().describe("The IID of an issue"),
|
|
2793
|
+
});
|
|
2794
|
+
export const ListIssueNoteEmojiReactionsSchema = ProjectParamsSchema.extend({
|
|
2795
|
+
issue_iid: z.coerce.string().describe("The IID of an issue"),
|
|
2796
|
+
note_id: z.coerce.string().describe("The ID of a note (comment or thread reply)"),
|
|
2797
|
+
discussion_id: noteEmojiDiscussionField,
|
|
2798
|
+
});
|
|
2799
|
+
export const ListWorkItemEmojiReactionsSchema = z.object({
|
|
2800
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2801
|
+
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
2802
|
+
});
|
|
2803
|
+
export const ListWorkItemNoteEmojiReactionsSchema = z.object({
|
|
2804
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
2805
|
+
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
2806
|
+
note_id: z.string().describe("The GraphQL GID of the note (e.g. 'gid://gitlab/Note/123' from list_work_item_notes)"),
|
|
2807
|
+
});
|
|
2714
2808
|
// --- Incident Timeline Event schemas ---
|
|
2715
2809
|
export const GetTimelineEventsSchema = z.object({
|
|
2716
2810
|
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|