@zereight/mcp-gitlab 2.1.30 → 2.1.38
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.ko.md +2 -1
- package/README.md +77 -68
- package/README.zh-CN.md +2 -1
- package/build/index.js +366 -67
- package/build/schemas.js +85 -19
- package/build/scripts/generate-tool-docs.js +1 -1
- package/build/test/group-milestone-schema.test.js +45 -0
- package/build/test/mcp-oauth-tests.js +12 -0
- package/build/test/mcp-server-name.test.js +87 -0
- package/build/test/nullable-gitlab-response-fields.test.js +22 -0
- package/build/test/path-segment-encoding.test.js +11 -0
- package/build/test/streamable-http-concurrent-session.test.js +63 -0
- package/build/test/test-geteffectiveprojectid.js +214 -1
- package/build/test/test-permission-mode.js +1 -0
- package/build/test/test-protected-branches.js +22 -3
- package/build/test/test-toolset-filtering.js +2 -2
- package/build/tools/registry.js +64 -1
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -96,13 +96,13 @@ import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middlew
|
|
|
96
96
|
import { GitLabClientPool } from "./gitlab-client-pool.js";
|
|
97
97
|
import { allTools, readOnlyTools, destructiveTools, deleteTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, TOOLSET_DEFINITIONS, ALL_TOOLSET_IDS, } from "./tools/registry.js";
|
|
98
98
|
import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, // Added
|
|
99
|
-
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateGroupSchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteBranchSchema, GetProtectedBranchSchema, ListProtectedBranchesSchema, ProtectBranchSchema, UnprotectBranchSchema, UpdateDefaultBranchSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetBranchSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GitLabBlameEntrySchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema, GitLabCiLintResultSchema, GetPipelineJobOutputSchema, PipelineJobControlSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetUserSchema, GitLabUserFullSchema, WhoAmISchema, GitLabCurrentUserSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCommitStatusSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
|
|
99
|
+
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateGroupMilestoneSchema, CreateRepositorySchema, CreateGroupSchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteBranchSchema, GetProtectedBranchSchema, ListProtectedBranchesSchema, ProtectBranchSchema, UnprotectBranchSchema, UpdateDefaultBranchSchema, DeleteDraftNoteSchema, DeleteGroupMilestoneSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, EditGroupMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetBranchSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GitLabBlameEntrySchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetGroupMilestoneSchema, GetGroupMilestoneIssuesSchema, GetGroupMilestoneMergeRequestsSchema, GetGroupMilestoneBurndownEventsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema, GitLabCiLintResultSchema, GetPipelineJobOutputSchema, PipelineJobControlSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetUserSchema, GitLabUserFullSchema, WhoAmISchema, GitLabCurrentUserSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCommitStatusSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
|
|
100
100
|
// Discussion Schemas
|
|
101
101
|
GitLabDiscussionNoteSchema, // Added
|
|
102
102
|
GitLabDiscussionSchema,
|
|
103
103
|
// Draft Notes Schemas
|
|
104
|
-
GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabTodoSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitStatusesSchema, ListBranchesSchema, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, GitLabCiVariableSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GitLabDependencyProxySchema, GitLabDependencyProxyBlobSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
105
|
-
GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, 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, HealthCheckSchema, } from "./schemas.js";
|
|
104
|
+
GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, GitLabProtectedBranchSchema, GitLabGroupSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestPipelineSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabGroupMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabTodoSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchBlobResultSchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitStatusesSchema, ListBranchesSchema, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, GitLabCiVariableSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, GitLabDependencyProxySchema, GitLabDependencyProxyBlobSchema, GetDependencyProxySettingsSchema, UpdateDependencyProxySettingsSchema, ListDependencyProxyBlobsSchema, PurgeDependencyProxyCacheSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
105
|
+
GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListCiCatalogResourcesSchema, GetCiCatalogResourceSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListGroupMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, GetGroupWikiPageSchema, ListGroupWikiPagesSchema, UpdateGroupWikiPageSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, MarkAllTodosDoneSchema, MarkTodoDoneSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateProjectSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, ListTagsSchema, GetTagSchema, CreateTagSchema, DeleteTagSchema, GetTagSignatureSchema, GitLabTagSchema, GitLabTagSignatureSchema, 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, HealthCheckSchema, } from "./schemas.js";
|
|
106
106
|
import { randomUUID, timingSafeEqual, } from "node:crypto";
|
|
107
107
|
import { pino } from "pino";
|
|
108
108
|
const logger = pino({
|
|
@@ -158,6 +158,7 @@ try {
|
|
|
158
158
|
catch {
|
|
159
159
|
// Intentionally ignored: version read failure is non-critical
|
|
160
160
|
}
|
|
161
|
+
const SERVER_NAME = process.env.MCP_SERVER_NAME?.trim() || "zereight-gitlab-mcp-server";
|
|
161
162
|
/**
|
|
162
163
|
* Create a new MCP Server instance with request handlers registered.
|
|
163
164
|
* Each transport connection gets its own Server instance to prevent
|
|
@@ -203,7 +204,7 @@ function createServer() {
|
|
|
203
204
|
filteredTools = filteredTools.filter(tool => !hiddenToolSet.has(tool.name));
|
|
204
205
|
}
|
|
205
206
|
const mcpServer = new McpServer({
|
|
206
|
-
name:
|
|
207
|
+
name: SERVER_NAME,
|
|
207
208
|
version: SERVER_VERSION,
|
|
208
209
|
}, {
|
|
209
210
|
capabilities: {
|
|
@@ -1598,12 +1599,10 @@ async function executeGraphQL(query, variables = {}) {
|
|
|
1598
1599
|
return json.data;
|
|
1599
1600
|
}
|
|
1600
1601
|
/**
|
|
1601
|
-
* Resolve a
|
|
1602
|
+
* Resolve a namespace path and issue IID to a work item GraphQL GID.
|
|
1602
1603
|
*/
|
|
1603
1604
|
async function resolveWorkItemGID(projectId, issueIid) {
|
|
1604
|
-
|
|
1605
|
-
// group fallback), so work item tools work for group-level namespaces too.
|
|
1606
|
-
const projectPath = await resolveProjectPath(projectId);
|
|
1605
|
+
const { path: projectPath, kind: namespaceKind } = await resolveProjectOrGroupPath(projectId);
|
|
1607
1606
|
// Resolve work item GID via GraphQL
|
|
1608
1607
|
const data = await executeGraphQL(`query($path: ID!, $iid: String!) {
|
|
1609
1608
|
namespace(fullPath: $path) {
|
|
@@ -1613,14 +1612,14 @@ async function resolveWorkItemGID(projectId, issueIid) {
|
|
|
1613
1612
|
}
|
|
1614
1613
|
}`, { path: projectPath, iid: String(issueIid) });
|
|
1615
1614
|
if (!data.namespace?.workItem?.id) {
|
|
1616
|
-
throw new Error(`Work item #${issueIid} not found in
|
|
1615
|
+
throw new Error(`Work item #${issueIid} not found in namespace ${projectPath}`);
|
|
1617
1616
|
}
|
|
1618
|
-
return { workItemGID: data.namespace.workItem.id, projectPath };
|
|
1617
|
+
return { workItemGID: data.namespace.workItem.id, projectPath, namespaceKind };
|
|
1619
1618
|
}
|
|
1620
1619
|
/**
|
|
1621
1620
|
* Resolve label names and usernames to GitLab GIDs in a single GraphQL call.
|
|
1622
1621
|
*/
|
|
1623
|
-
async function resolveNamesToIds(projectPath, labelNames, usernames) {
|
|
1622
|
+
async function resolveNamesToIds(projectPath, namespaceKind, labelNames, usernames) {
|
|
1624
1623
|
if (!labelNames?.length && !usernames?.length) {
|
|
1625
1624
|
return { labelIds: [], userIds: [] };
|
|
1626
1625
|
}
|
|
@@ -1631,21 +1630,23 @@ async function resolveNamesToIds(projectPath, labelNames, usernames) {
|
|
|
1631
1630
|
// group labels, single round trip with no pagination needed.
|
|
1632
1631
|
const varDefs = labelNames.map((_, i) => `$l${i}: String!`).join(", ");
|
|
1633
1632
|
const aliases = labelNames.map((_, i) => `l${i}: labels(title: $l${i}, includeAncestorGroups: true, first: 1) { nodes { id } }`).join(" ");
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1633
|
+
const rootField = namespaceKind === "group" ? "group" : "project";
|
|
1634
|
+
const data = await executeGraphQL(`query($path: ID!, $usernames: [String!]!${varDefs ? `, ${varDefs}` : ""}) {
|
|
1635
|
+
${rootField}(fullPath: $path) { ${aliases || "__typename"} }
|
|
1636
1636
|
users(usernames: $usernames) { nodes { id username } }
|
|
1637
1637
|
}`, { path: projectPath, usernames, ...labelVars });
|
|
1638
|
-
|
|
1639
|
-
|
|
1638
|
+
const labelNamespace = namespaceKind === "group" ? data.group : data.project;
|
|
1639
|
+
if (!labelNamespace) {
|
|
1640
|
+
throw new Error(`Namespace '${projectPath}' not found or inaccessible`);
|
|
1640
1641
|
}
|
|
1641
1642
|
const labelIds = labelNames.map((name, i) => {
|
|
1642
|
-
const nodes =
|
|
1643
|
+
const nodes = labelNamespace[`l${i}`]?.nodes;
|
|
1643
1644
|
if (!nodes?.length)
|
|
1644
|
-
throw new Error(`Label '${name}' not found in
|
|
1645
|
+
throw new Error(`Label '${name}' not found in namespace`);
|
|
1645
1646
|
return nodes[0].id;
|
|
1646
1647
|
});
|
|
1647
1648
|
const userIds = usernames.map(name => {
|
|
1648
|
-
const user = users.nodes.find(u => u.username === name);
|
|
1649
|
+
const user = data.users.nodes.find(u => u.username === name);
|
|
1649
1650
|
if (!user)
|
|
1650
1651
|
throw new Error(`User '${name}' not found`);
|
|
1651
1652
|
return user.id;
|
|
@@ -2151,11 +2152,36 @@ async function updateIncidentEscalationStatus(projectPath, incidentIid, status)
|
|
|
2151
2152
|
return data.issueSetEscalationStatus.issue;
|
|
2152
2153
|
}
|
|
2153
2154
|
/**
|
|
2154
|
-
* Resolve a project ID
|
|
2155
|
+
* Resolve a project ID/path or group ID/path to its full namespace path.
|
|
2156
|
+
* Use group:<id-or-path> or project:<id-or-path> to disambiguate numeric IDs.
|
|
2155
2157
|
*/
|
|
2156
2158
|
async function resolveProjectPath(projectId) {
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
+
const { path } = await resolveProjectOrGroupPath(projectId);
|
|
2160
|
+
return path;
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Resolve a project or group path and identify which GraphQL root field to use.
|
|
2164
|
+
* Bare numeric IDs resolve as projects for backwards compatibility; use group:<id>
|
|
2165
|
+
* when the numeric value is a GitLab group ID.
|
|
2166
|
+
*/
|
|
2167
|
+
async function resolveProjectOrGroupPath(projectId) {
|
|
2168
|
+
const decodedProjectId = decodeURIComponent(projectId);
|
|
2169
|
+
const namespaceMatch = decodedProjectId.match(/^(group|project):(.+)$/i);
|
|
2170
|
+
const explicitKind = namespaceMatch?.[1]?.toLowerCase();
|
|
2171
|
+
const requestedProjectId = namespaceMatch ? namespaceMatch[2] : decodedProjectId;
|
|
2172
|
+
if (explicitKind === "group" && GITLAB_ALLOWED_PROJECT_IDS.length > 0) {
|
|
2173
|
+
throw new Error("group:<id-or-path> cannot be used when GITLAB_ALLOWED_PROJECT_IDS is set, because the project allowlist does not cover groups");
|
|
2174
|
+
}
|
|
2175
|
+
const effectiveProjectId = getEffectiveProjectId(requestedProjectId);
|
|
2176
|
+
if (explicitKind === "group") {
|
|
2177
|
+
const groupUrl = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(effectiveProjectId)}`);
|
|
2178
|
+
const groupResponse = await fetch(groupUrl.toString(), {
|
|
2179
|
+
...getFetchConfig(),
|
|
2180
|
+
});
|
|
2181
|
+
await handleGitLabError(groupResponse);
|
|
2182
|
+
const group = await groupResponse.json();
|
|
2183
|
+
return { path: group.full_path, kind: "group" };
|
|
2184
|
+
}
|
|
2159
2185
|
const projectUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
|
|
2160
2186
|
const projectResponse = await fetch(projectUrl.toString(), {
|
|
2161
2187
|
...getFetchConfig(),
|
|
@@ -2164,21 +2190,24 @@ async function resolveProjectPath(projectId) {
|
|
|
2164
2190
|
// Numeric IDs must not fall back: group and project IDs share no namespace,
|
|
2165
2191
|
// so a numeric project 404 should fail immediately rather than silently
|
|
2166
2192
|
// resolving to an unrelated group with the same integer ID.
|
|
2167
|
-
if (projectResponse.status === 404 &&
|
|
2193
|
+
if (projectResponse.status === 404 &&
|
|
2194
|
+
!explicitKind &&
|
|
2195
|
+
GITLAB_ALLOWED_PROJECT_IDS.length === 0 &&
|
|
2196
|
+
!/^\d+$/.test(effectiveProjectId)) {
|
|
2168
2197
|
const groupUrl = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(effectiveProjectId)}`);
|
|
2169
2198
|
const groupResponse = await fetch(groupUrl.toString(), {
|
|
2170
2199
|
...getFetchConfig(),
|
|
2171
2200
|
});
|
|
2172
2201
|
if (groupResponse.ok) {
|
|
2173
2202
|
const group = await groupResponse.json();
|
|
2174
|
-
return group.full_path;
|
|
2203
|
+
return { path: group.full_path, kind: "group" };
|
|
2175
2204
|
}
|
|
2176
2205
|
// Surface the group error
|
|
2177
2206
|
await handleGitLabError(groupResponse);
|
|
2178
2207
|
}
|
|
2179
2208
|
await handleGitLabError(projectResponse);
|
|
2180
2209
|
const project = await projectResponse.json();
|
|
2181
|
-
return project.path_with_namespace;
|
|
2210
|
+
return { path: project.path_with_namespace, kind: "project" };
|
|
2182
2211
|
}
|
|
2183
2212
|
/**
|
|
2184
2213
|
* Get a single work item with all widget data.
|
|
@@ -2400,10 +2429,11 @@ async function getWorkItem(projectId, iid) {
|
|
|
2400
2429
|
return result;
|
|
2401
2430
|
}
|
|
2402
2431
|
/**
|
|
2403
|
-
* List work items in a project with filters.
|
|
2432
|
+
* List work items in a project or group namespace with filters.
|
|
2404
2433
|
*/
|
|
2405
2434
|
async function listWorkItems(projectId, options) {
|
|
2406
|
-
const
|
|
2435
|
+
const namespace = await resolveProjectOrGroupPath(projectId);
|
|
2436
|
+
const rootField = namespace.kind === "group" ? "group" : "project";
|
|
2407
2437
|
// Map type names to GraphQL enum values
|
|
2408
2438
|
const typeMap = {
|
|
2409
2439
|
issue: "ISSUE",
|
|
@@ -2417,7 +2447,7 @@ async function listWorkItems(projectId, options) {
|
|
|
2417
2447
|
ticket: "TICKET",
|
|
2418
2448
|
};
|
|
2419
2449
|
const variables = {
|
|
2420
|
-
path:
|
|
2450
|
+
path: namespace.path,
|
|
2421
2451
|
first: options.first || 20,
|
|
2422
2452
|
};
|
|
2423
2453
|
if (options.types && options.types.length > 0) {
|
|
@@ -2439,7 +2469,7 @@ async function listWorkItems(projectId, options) {
|
|
|
2439
2469
|
variables.after = options.after;
|
|
2440
2470
|
}
|
|
2441
2471
|
const data = await executeGraphQL(`query($path: ID!, $types: [IssueType!], $state: IssuableState, $search: String, $assigneeUsernames: [String!], $labelName: [String!], $first: Int, $after: String) {
|
|
2442
|
-
|
|
2472
|
+
${rootField}(fullPath: $path) {
|
|
2443
2473
|
workItems(types: $types, state: $state, search: $search, assigneeUsernames: $assigneeUsernames, labelName: $labelName, first: $first, after: $after) {
|
|
2444
2474
|
nodes {
|
|
2445
2475
|
id iid title state webUrl workItemType { name }
|
|
@@ -2458,8 +2488,9 @@ async function listWorkItems(projectId, options) {
|
|
|
2458
2488
|
}
|
|
2459
2489
|
}
|
|
2460
2490
|
}`, variables);
|
|
2461
|
-
const
|
|
2462
|
-
const
|
|
2491
|
+
const workItemsRoot = namespace.kind === "group" ? data.group : data.project;
|
|
2492
|
+
const workItems = workItemsRoot?.workItems?.nodes || [];
|
|
2493
|
+
const pageInfo = workItemsRoot?.workItems?.pageInfo || {};
|
|
2463
2494
|
// Flatten widget data for each item
|
|
2464
2495
|
const items = workItems.map((wi) => {
|
|
2465
2496
|
const widgets = wi.widgets || [];
|
|
@@ -2503,7 +2534,7 @@ async function listWorkItems(projectId, options) {
|
|
|
2503
2534
|
* Create a new work item using GraphQL.
|
|
2504
2535
|
*/
|
|
2505
2536
|
async function createWorkItem(projectId, options) {
|
|
2506
|
-
const projectPath = await
|
|
2537
|
+
const { path: projectPath, kind: namespaceKind } = await resolveProjectOrGroupPath(projectId);
|
|
2507
2538
|
const typeName = options.type || "issue";
|
|
2508
2539
|
const typeGID = await resolveWorkItemTypeGID(projectPath, typeName);
|
|
2509
2540
|
// Build the input dynamically - only include widgets that have values
|
|
@@ -2528,7 +2559,7 @@ async function createWorkItem(projectId, options) {
|
|
|
2528
2559
|
variables.description = options.description;
|
|
2529
2560
|
}
|
|
2530
2561
|
// Resolve label names and usernames to GIDs in a single GraphQL call
|
|
2531
|
-
const { labelIds, userIds } = await resolveNamesToIds(projectPath, options.labels, options.assignee_usernames);
|
|
2562
|
+
const { labelIds, userIds } = await resolveNamesToIds(projectPath, namespaceKind, options.labels, options.assignee_usernames);
|
|
2532
2563
|
if (labelIds.length > 0) {
|
|
2533
2564
|
inputFields.push("$labelIds: [LabelID!]!");
|
|
2534
2565
|
inputValues.push("labelsWidget: { labelIds: $labelIds }");
|
|
@@ -2624,7 +2655,7 @@ async function createWorkItem(projectId, options) {
|
|
|
2624
2655
|
* weight, state, status, parent, and children operations.
|
|
2625
2656
|
*/
|
|
2626
2657
|
async function updateWorkItem(projectId, iid, options) {
|
|
2627
|
-
const { workItemGID, projectPath } = await resolveWorkItemGID(projectId, iid);
|
|
2658
|
+
const { workItemGID, projectPath, namespaceKind } = await resolveWorkItemGID(projectId, iid);
|
|
2628
2659
|
// Build the main workItemUpdate mutation dynamically
|
|
2629
2660
|
const inputParts = ["id: $id"];
|
|
2630
2661
|
const varDefs = ["$id: WorkItemID!"];
|
|
@@ -2643,7 +2674,7 @@ async function updateWorkItem(projectId, iid, options) {
|
|
|
2643
2674
|
const allLabelNames = [...(options.add_labels || []), ...(options.remove_labels || [])];
|
|
2644
2675
|
const needsResolve = allLabelNames.length > 0 || options.assignee_usernames?.length;
|
|
2645
2676
|
const { labelIds: resolvedLabelIds, userIds } = needsResolve
|
|
2646
|
-
? await resolveNamesToIds(projectPath, allLabelNames.length > 0 ? allLabelNames : undefined, options.assignee_usernames)
|
|
2677
|
+
? await resolveNamesToIds(projectPath, namespaceKind, allLabelNames.length > 0 ? allLabelNames : undefined, options.assignee_usernames)
|
|
2647
2678
|
: { labelIds: [], userIds: [] };
|
|
2648
2679
|
if (options.add_labels || options.remove_labels) {
|
|
2649
2680
|
const labelParts = [];
|
|
@@ -4993,7 +5024,7 @@ async function listPipelines(projectId, options = {}) {
|
|
|
4993
5024
|
*/
|
|
4994
5025
|
async function getPipeline(projectId, pipelineId) {
|
|
4995
5026
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
4996
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}`);
|
|
5027
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}`);
|
|
4997
5028
|
const response = await fetch(url.toString(), {
|
|
4998
5029
|
...getFetchConfig(),
|
|
4999
5030
|
});
|
|
@@ -5098,7 +5129,7 @@ async function getEnvironment(projectId, environmentId) {
|
|
|
5098
5129
|
*/
|
|
5099
5130
|
async function listPipelineJobs(projectId, pipelineId, options = {}) {
|
|
5100
5131
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
5101
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/jobs`);
|
|
5132
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/jobs`);
|
|
5102
5133
|
// Add all query parameters
|
|
5103
5134
|
Object.entries(options).forEach(([key, value]) => {
|
|
5104
5135
|
if (value !== undefined) {
|
|
@@ -5130,7 +5161,7 @@ async function listPipelineJobs(projectId, pipelineId, options = {}) {
|
|
|
5130
5161
|
*/
|
|
5131
5162
|
async function listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
|
|
5132
5163
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
5133
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/bridges`);
|
|
5164
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/bridges`);
|
|
5134
5165
|
// Add all query parameters
|
|
5135
5166
|
Object.entries(options).forEach(([key, value]) => {
|
|
5136
5167
|
if (value !== undefined) {
|
|
@@ -5154,7 +5185,7 @@ async function listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
|
|
|
5154
5185
|
}
|
|
5155
5186
|
async function getPipelineJob(projectId, jobId) {
|
|
5156
5187
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
5157
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}`);
|
|
5188
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}`);
|
|
5158
5189
|
const response = await fetch(url.toString(), {
|
|
5159
5190
|
...getFetchConfig(),
|
|
5160
5191
|
});
|
|
@@ -5177,7 +5208,7 @@ async function getPipelineJob(projectId, jobId) {
|
|
|
5177
5208
|
const MAX_JOB_TRACE_LINES = 1000;
|
|
5178
5209
|
async function getPipelineJobOutput(projectId, jobId, limit, offset) {
|
|
5179
5210
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
5180
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}/trace`);
|
|
5211
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/trace`);
|
|
5181
5212
|
const response = await fetch(url.toString(), {
|
|
5182
5213
|
...getFetchConfig(),
|
|
5183
5214
|
headers: {
|
|
@@ -5252,7 +5283,7 @@ async function validateProjectCiLint(projectId, options) {
|
|
|
5252
5283
|
*/
|
|
5253
5284
|
async function listJobArtifacts(projectId, jobId, options = {}) {
|
|
5254
5285
|
projectId = decodeURIComponent(projectId);
|
|
5255
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}/artifacts/tree`);
|
|
5286
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/tree`);
|
|
5256
5287
|
Object.entries(options).forEach(([key, value]) => {
|
|
5257
5288
|
if (value !== undefined) {
|
|
5258
5289
|
if (typeof value === "boolean") {
|
|
@@ -5284,7 +5315,7 @@ async function listJobArtifacts(projectId, jobId, options = {}) {
|
|
|
5284
5315
|
async function downloadJobArtifacts(projectId, jobId, localPath) {
|
|
5285
5316
|
projectId = decodeURIComponent(projectId);
|
|
5286
5317
|
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
5287
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${jobId}/artifacts`);
|
|
5318
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts`);
|
|
5288
5319
|
const response = await fetch(url.toString(), {
|
|
5289
5320
|
...getFetchConfig(),
|
|
5290
5321
|
});
|
|
@@ -5316,7 +5347,7 @@ async function getJobArtifactFile(projectId, jobId, artifactPath) {
|
|
|
5316
5347
|
.split("/")
|
|
5317
5348
|
.map(segment => encodeURIComponent(segment))
|
|
5318
5349
|
.join("/");
|
|
5319
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${jobId}/artifacts/${encodedArtifactPath}`);
|
|
5350
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${encodeGitLabPathSegment(jobId)}/artifacts/${encodedArtifactPath}`);
|
|
5320
5351
|
const response = await fetch(url.toString(), {
|
|
5321
5352
|
...getFetchConfig(),
|
|
5322
5353
|
});
|
|
@@ -5363,7 +5394,7 @@ async function createPipeline(projectId, ref, variables, inputs) {
|
|
|
5363
5394
|
*/
|
|
5364
5395
|
async function retryPipeline(projectId, pipelineId) {
|
|
5365
5396
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
5366
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/retry`);
|
|
5397
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/retry`);
|
|
5367
5398
|
const response = await fetch(url.toString(), {
|
|
5368
5399
|
...getFetchConfig(),
|
|
5369
5400
|
method: "POST",
|
|
@@ -5381,7 +5412,7 @@ async function retryPipeline(projectId, pipelineId) {
|
|
|
5381
5412
|
*/
|
|
5382
5413
|
async function cancelPipeline(projectId, pipelineId) {
|
|
5383
5414
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
5384
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${pipelineId}/cancel`);
|
|
5415
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/cancel`);
|
|
5385
5416
|
const response = await fetch(url.toString(), {
|
|
5386
5417
|
...getFetchConfig(),
|
|
5387
5418
|
method: "POST",
|
|
@@ -5400,7 +5431,7 @@ async function cancelPipeline(projectId, pipelineId) {
|
|
|
5400
5431
|
*/
|
|
5401
5432
|
async function playPipelineJob(projectId, jobId, variables) {
|
|
5402
5433
|
projectId = decodeURIComponent(projectId);
|
|
5403
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}/play`);
|
|
5434
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/play`);
|
|
5404
5435
|
const body = {};
|
|
5405
5436
|
if (variables && variables.length > 0) {
|
|
5406
5437
|
body.job_variables_attributes = variables;
|
|
@@ -5423,7 +5454,7 @@ async function playPipelineJob(projectId, jobId, variables) {
|
|
|
5423
5454
|
*/
|
|
5424
5455
|
async function retryPipelineJob(projectId, jobId) {
|
|
5425
5456
|
projectId = decodeURIComponent(projectId);
|
|
5426
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}/retry`);
|
|
5457
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/retry`);
|
|
5427
5458
|
const response = await fetch(url.toString(), {
|
|
5428
5459
|
...getFetchConfig(),
|
|
5429
5460
|
method: "POST",
|
|
@@ -5442,7 +5473,7 @@ async function retryPipelineJob(projectId, jobId) {
|
|
|
5442
5473
|
*/
|
|
5443
5474
|
async function cancelPipelineJob(projectId, jobId, force) {
|
|
5444
5475
|
projectId = decodeURIComponent(projectId);
|
|
5445
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}/cancel`);
|
|
5476
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/cancel`);
|
|
5446
5477
|
if (force !== undefined) {
|
|
5447
5478
|
url.searchParams.append("force", force.toString());
|
|
5448
5479
|
}
|
|
@@ -5524,7 +5555,7 @@ async function listProjectMilestones(projectId, options) {
|
|
|
5524
5555
|
*/
|
|
5525
5556
|
async function getProjectMilestone(projectId, milestoneId) {
|
|
5526
5557
|
projectId = decodeURIComponent(projectId);
|
|
5527
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}`);
|
|
5558
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
|
|
5528
5559
|
const response = await fetch(url.toString(), {
|
|
5529
5560
|
...getFetchConfig(),
|
|
5530
5561
|
});
|
|
@@ -5559,7 +5590,7 @@ async function createProjectMilestone(projectId, options) {
|
|
|
5559
5590
|
*/
|
|
5560
5591
|
async function editProjectMilestone(projectId, milestoneId, options) {
|
|
5561
5592
|
projectId = decodeURIComponent(projectId);
|
|
5562
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}`);
|
|
5593
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
|
|
5563
5594
|
const response = await fetch(url.toString(), {
|
|
5564
5595
|
...getFetchConfig(),
|
|
5565
5596
|
method: "PUT",
|
|
@@ -5577,7 +5608,7 @@ async function editProjectMilestone(projectId, milestoneId, options) {
|
|
|
5577
5608
|
*/
|
|
5578
5609
|
async function deleteProjectMilestone(projectId, milestoneId) {
|
|
5579
5610
|
projectId = decodeURIComponent(projectId);
|
|
5580
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}`);
|
|
5611
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
|
|
5581
5612
|
const response = await fetch(url.toString(), {
|
|
5582
5613
|
...getFetchConfig(),
|
|
5583
5614
|
method: "DELETE",
|
|
@@ -5590,9 +5621,14 @@ async function deleteProjectMilestone(projectId, milestoneId) {
|
|
|
5590
5621
|
* @param {number} milestoneId - The ID of the milestone
|
|
5591
5622
|
* @returns {Promise<GitLabIssue[]>} List of issues
|
|
5592
5623
|
*/
|
|
5593
|
-
async function getMilestoneIssues(projectId, milestoneId) {
|
|
5624
|
+
async function getMilestoneIssues(projectId, milestoneId, options = {}) {
|
|
5594
5625
|
projectId = decodeURIComponent(projectId);
|
|
5595
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}/issues`);
|
|
5626
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
|
|
5627
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
5628
|
+
if (value !== undefined) {
|
|
5629
|
+
url.searchParams.append(key, String(value));
|
|
5630
|
+
}
|
|
5631
|
+
});
|
|
5596
5632
|
const response = await fetch(url.toString(), {
|
|
5597
5633
|
...getFetchConfig(),
|
|
5598
5634
|
});
|
|
@@ -5606,9 +5642,14 @@ async function getMilestoneIssues(projectId, milestoneId) {
|
|
|
5606
5642
|
* @param {number} milestoneId - The ID of the milestone
|
|
5607
5643
|
* @returns {Promise<GitLabMergeRequest[]>} List of merge requests
|
|
5608
5644
|
*/
|
|
5609
|
-
async function getMilestoneMergeRequests(projectId, milestoneId) {
|
|
5645
|
+
async function getMilestoneMergeRequests(projectId, milestoneId, options = {}) {
|
|
5610
5646
|
projectId = decodeURIComponent(projectId);
|
|
5611
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}/merge_requests`);
|
|
5647
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
|
|
5648
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
5649
|
+
if (value !== undefined) {
|
|
5650
|
+
url.searchParams.append(key, String(value));
|
|
5651
|
+
}
|
|
5652
|
+
});
|
|
5612
5653
|
const response = await fetch(url.toString(), {
|
|
5613
5654
|
...getFetchConfig(),
|
|
5614
5655
|
});
|
|
@@ -5620,18 +5661,18 @@ async function getMilestoneMergeRequests(projectId, milestoneId) {
|
|
|
5620
5661
|
* Promote a project milestone to a group milestone
|
|
5621
5662
|
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
5622
5663
|
* @param {number} milestoneId - The ID of the milestone
|
|
5623
|
-
* @returns {Promise<
|
|
5664
|
+
* @returns {Promise<GitLabGroupMilestones>} Promoted milestone
|
|
5624
5665
|
*/
|
|
5625
5666
|
async function promoteProjectMilestone(projectId, milestoneId) {
|
|
5626
5667
|
projectId = decodeURIComponent(projectId);
|
|
5627
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}/promote`);
|
|
5668
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/promote`);
|
|
5628
5669
|
const response = await fetch(url.toString(), {
|
|
5629
5670
|
...getFetchConfig(),
|
|
5630
5671
|
method: "POST",
|
|
5631
5672
|
});
|
|
5632
5673
|
await handleGitLabError(response);
|
|
5633
5674
|
const data = await response.json();
|
|
5634
|
-
return
|
|
5675
|
+
return GitLabGroupMilestonesSchema.parse(data);
|
|
5635
5676
|
}
|
|
5636
5677
|
/**
|
|
5637
5678
|
* Get all burndown chart events for a single milestone
|
|
@@ -5639,9 +5680,148 @@ async function promoteProjectMilestone(projectId, milestoneId) {
|
|
|
5639
5680
|
* @param {number} milestoneId - The ID of the milestone
|
|
5640
5681
|
* @returns {Promise<any[]>} Burndown chart events
|
|
5641
5682
|
*/
|
|
5642
|
-
async function getMilestoneBurndownEvents(projectId, milestoneId) {
|
|
5683
|
+
async function getMilestoneBurndownEvents(projectId, milestoneId, options = {}) {
|
|
5643
5684
|
projectId = decodeURIComponent(projectId);
|
|
5644
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${milestoneId}/burndown_events`);
|
|
5685
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
|
|
5686
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
5687
|
+
if (value !== undefined) {
|
|
5688
|
+
url.searchParams.append(key, String(value));
|
|
5689
|
+
}
|
|
5690
|
+
});
|
|
5691
|
+
const response = await fetch(url.toString(), {
|
|
5692
|
+
...getFetchConfig(),
|
|
5693
|
+
});
|
|
5694
|
+
await handleGitLabError(response);
|
|
5695
|
+
const data = await response.json();
|
|
5696
|
+
return data;
|
|
5697
|
+
}
|
|
5698
|
+
/**
|
|
5699
|
+
* List milestones in a GitLab group
|
|
5700
|
+
*/
|
|
5701
|
+
async function listGroupMilestones(groupId, options) {
|
|
5702
|
+
groupId = decodeURIComponent(groupId);
|
|
5703
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones`);
|
|
5704
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
5705
|
+
if (value !== undefined) {
|
|
5706
|
+
if (key === "iids" && Array.isArray(value) && value.length > 0) {
|
|
5707
|
+
value.forEach(iid => {
|
|
5708
|
+
url.searchParams.append("iids[]", iid.toString());
|
|
5709
|
+
});
|
|
5710
|
+
}
|
|
5711
|
+
else {
|
|
5712
|
+
url.searchParams.append(key, value.toString());
|
|
5713
|
+
}
|
|
5714
|
+
}
|
|
5715
|
+
});
|
|
5716
|
+
const response = await fetch(url.toString(), {
|
|
5717
|
+
...getFetchConfig(),
|
|
5718
|
+
});
|
|
5719
|
+
await handleGitLabError(response);
|
|
5720
|
+
const data = await response.json();
|
|
5721
|
+
return z.array(GitLabGroupMilestonesSchema).parse(data);
|
|
5722
|
+
}
|
|
5723
|
+
/**
|
|
5724
|
+
* Get a single milestone in a GitLab group
|
|
5725
|
+
*/
|
|
5726
|
+
async function getGroupMilestone(groupId, milestoneId) {
|
|
5727
|
+
groupId = decodeURIComponent(groupId);
|
|
5728
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
|
|
5729
|
+
const response = await fetch(url.toString(), {
|
|
5730
|
+
...getFetchConfig(),
|
|
5731
|
+
});
|
|
5732
|
+
await handleGitLabError(response);
|
|
5733
|
+
const data = await response.json();
|
|
5734
|
+
return GitLabGroupMilestonesSchema.parse(data);
|
|
5735
|
+
}
|
|
5736
|
+
/**
|
|
5737
|
+
* Create a new milestone in a GitLab group
|
|
5738
|
+
*/
|
|
5739
|
+
async function createGroupMilestone(groupId, options) {
|
|
5740
|
+
groupId = decodeURIComponent(groupId);
|
|
5741
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones`);
|
|
5742
|
+
const response = await fetch(url.toString(), {
|
|
5743
|
+
...getFetchConfig(),
|
|
5744
|
+
method: "POST",
|
|
5745
|
+
body: JSON.stringify(options),
|
|
5746
|
+
});
|
|
5747
|
+
await handleGitLabError(response);
|
|
5748
|
+
const data = await response.json();
|
|
5749
|
+
return GitLabGroupMilestonesSchema.parse(data);
|
|
5750
|
+
}
|
|
5751
|
+
/**
|
|
5752
|
+
* Edit an existing milestone in a GitLab group
|
|
5753
|
+
*/
|
|
5754
|
+
async function editGroupMilestone(groupId, milestoneId, options) {
|
|
5755
|
+
groupId = decodeURIComponent(groupId);
|
|
5756
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
|
|
5757
|
+
const response = await fetch(url.toString(), {
|
|
5758
|
+
...getFetchConfig(),
|
|
5759
|
+
method: "PUT",
|
|
5760
|
+
body: JSON.stringify(options),
|
|
5761
|
+
});
|
|
5762
|
+
await handleGitLabError(response);
|
|
5763
|
+
const data = await response.json();
|
|
5764
|
+
return GitLabGroupMilestonesSchema.parse(data);
|
|
5765
|
+
}
|
|
5766
|
+
/**
|
|
5767
|
+
* Delete a milestone from a GitLab group
|
|
5768
|
+
*/
|
|
5769
|
+
async function deleteGroupMilestone(groupId, milestoneId) {
|
|
5770
|
+
groupId = decodeURIComponent(groupId);
|
|
5771
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}`);
|
|
5772
|
+
const response = await fetch(url.toString(), {
|
|
5773
|
+
...getFetchConfig(),
|
|
5774
|
+
method: "DELETE",
|
|
5775
|
+
});
|
|
5776
|
+
await handleGitLabError(response);
|
|
5777
|
+
}
|
|
5778
|
+
/**
|
|
5779
|
+
* Get all issues assigned to a single group milestone
|
|
5780
|
+
*/
|
|
5781
|
+
async function getGroupMilestoneIssues(groupId, milestoneId, options = {}) {
|
|
5782
|
+
groupId = decodeURIComponent(groupId);
|
|
5783
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/issues`);
|
|
5784
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
5785
|
+
if (value !== undefined) {
|
|
5786
|
+
url.searchParams.append(key, String(value));
|
|
5787
|
+
}
|
|
5788
|
+
});
|
|
5789
|
+
const response = await fetch(url.toString(), {
|
|
5790
|
+
...getFetchConfig(),
|
|
5791
|
+
});
|
|
5792
|
+
await handleGitLabError(response);
|
|
5793
|
+
const data = await response.json();
|
|
5794
|
+
return z.array(GitLabIssueSchema).parse(data);
|
|
5795
|
+
}
|
|
5796
|
+
/**
|
|
5797
|
+
* Get all merge requests assigned to a single group milestone
|
|
5798
|
+
*/
|
|
5799
|
+
async function getGroupMilestoneMergeRequests(groupId, milestoneId, options = {}) {
|
|
5800
|
+
groupId = decodeURIComponent(groupId);
|
|
5801
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/merge_requests`);
|
|
5802
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
5803
|
+
if (value !== undefined) {
|
|
5804
|
+
url.searchParams.append(key, String(value));
|
|
5805
|
+
}
|
|
5806
|
+
});
|
|
5807
|
+
const response = await fetch(url.toString(), {
|
|
5808
|
+
...getFetchConfig(),
|
|
5809
|
+
});
|
|
5810
|
+
await handleGitLabError(response);
|
|
5811
|
+
const data = await response.json();
|
|
5812
|
+
return z.array(GitLabMergeRequestSchema).parse(data);
|
|
5813
|
+
}
|
|
5814
|
+
/**
|
|
5815
|
+
* Get burndown chart events for a group milestone
|
|
5816
|
+
*/
|
|
5817
|
+
async function getGroupMilestoneBurndownEvents(groupId, milestoneId, options = {}) {
|
|
5818
|
+
groupId = decodeURIComponent(groupId);
|
|
5819
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(groupId)}/milestones/${encodeGitLabPathSegment(milestoneId)}/burndown_events`);
|
|
5820
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
5821
|
+
if (value !== undefined) {
|
|
5822
|
+
url.searchParams.append(key, String(value));
|
|
5823
|
+
}
|
|
5824
|
+
});
|
|
5645
5825
|
const response = await fetch(url.toString(), {
|
|
5646
5826
|
...getFetchConfig(),
|
|
5647
5827
|
});
|
|
@@ -8327,8 +8507,8 @@ async function handleToolCall(params) {
|
|
|
8327
8507
|
};
|
|
8328
8508
|
}
|
|
8329
8509
|
case "get_milestone_issue": {
|
|
8330
|
-
const { project_id, milestone_id } = GetMilestoneIssuesSchema.parse(params.arguments);
|
|
8331
|
-
const issues = await getMilestoneIssues(project_id, milestone_id);
|
|
8510
|
+
const { project_id, milestone_id, ...options } = GetMilestoneIssuesSchema.parse(params.arguments);
|
|
8511
|
+
const issues = await getMilestoneIssues(project_id, milestone_id, options);
|
|
8332
8512
|
return {
|
|
8333
8513
|
content: [
|
|
8334
8514
|
{
|
|
@@ -8339,8 +8519,8 @@ async function handleToolCall(params) {
|
|
|
8339
8519
|
};
|
|
8340
8520
|
}
|
|
8341
8521
|
case "get_milestone_merge_requests": {
|
|
8342
|
-
const { project_id, milestone_id } = GetMilestoneMergeRequestsSchema.parse(params.arguments);
|
|
8343
|
-
const mergeRequests = await getMilestoneMergeRequests(project_id, milestone_id);
|
|
8522
|
+
const { project_id, milestone_id, ...options } = GetMilestoneMergeRequestsSchema.parse(params.arguments);
|
|
8523
|
+
const mergeRequests = await getMilestoneMergeRequests(project_id, milestone_id, options);
|
|
8344
8524
|
return {
|
|
8345
8525
|
content: [
|
|
8346
8526
|
{
|
|
@@ -8363,8 +8543,107 @@ async function handleToolCall(params) {
|
|
|
8363
8543
|
};
|
|
8364
8544
|
}
|
|
8365
8545
|
case "get_milestone_burndown_events": {
|
|
8366
|
-
const { project_id, milestone_id } = GetMilestoneBurndownEventsSchema.parse(params.arguments);
|
|
8367
|
-
const events = await getMilestoneBurndownEvents(project_id, milestone_id);
|
|
8546
|
+
const { project_id, milestone_id, ...options } = GetMilestoneBurndownEventsSchema.parse(params.arguments);
|
|
8547
|
+
const events = await getMilestoneBurndownEvents(project_id, milestone_id, options);
|
|
8548
|
+
return {
|
|
8549
|
+
content: [
|
|
8550
|
+
{
|
|
8551
|
+
type: "text",
|
|
8552
|
+
text: JSON.stringify(events),
|
|
8553
|
+
},
|
|
8554
|
+
],
|
|
8555
|
+
};
|
|
8556
|
+
}
|
|
8557
|
+
case "list_group_milestones": {
|
|
8558
|
+
const { group_id, ...options } = ListGroupMilestonesSchema.parse(params.arguments);
|
|
8559
|
+
const milestones = await listGroupMilestones(group_id, options);
|
|
8560
|
+
return {
|
|
8561
|
+
content: [
|
|
8562
|
+
{
|
|
8563
|
+
type: "text",
|
|
8564
|
+
text: JSON.stringify(milestones),
|
|
8565
|
+
},
|
|
8566
|
+
],
|
|
8567
|
+
};
|
|
8568
|
+
}
|
|
8569
|
+
case "get_group_milestone": {
|
|
8570
|
+
const { group_id, milestone_id } = GetGroupMilestoneSchema.parse(params.arguments);
|
|
8571
|
+
const milestone = await getGroupMilestone(group_id, milestone_id);
|
|
8572
|
+
return {
|
|
8573
|
+
content: [
|
|
8574
|
+
{
|
|
8575
|
+
type: "text",
|
|
8576
|
+
text: JSON.stringify(milestone),
|
|
8577
|
+
},
|
|
8578
|
+
],
|
|
8579
|
+
};
|
|
8580
|
+
}
|
|
8581
|
+
case "create_group_milestone": {
|
|
8582
|
+
const { group_id, ...options } = CreateGroupMilestoneSchema.parse(params.arguments);
|
|
8583
|
+
const milestone = await createGroupMilestone(group_id, options);
|
|
8584
|
+
return {
|
|
8585
|
+
content: [
|
|
8586
|
+
{
|
|
8587
|
+
type: "text",
|
|
8588
|
+
text: JSON.stringify(milestone),
|
|
8589
|
+
},
|
|
8590
|
+
],
|
|
8591
|
+
};
|
|
8592
|
+
}
|
|
8593
|
+
case "edit_group_milestone": {
|
|
8594
|
+
const { group_id, milestone_id, ...options } = EditGroupMilestoneSchema.parse(params.arguments);
|
|
8595
|
+
const milestone = await editGroupMilestone(group_id, milestone_id, options);
|
|
8596
|
+
return {
|
|
8597
|
+
content: [
|
|
8598
|
+
{
|
|
8599
|
+
type: "text",
|
|
8600
|
+
text: JSON.stringify(milestone),
|
|
8601
|
+
},
|
|
8602
|
+
],
|
|
8603
|
+
};
|
|
8604
|
+
}
|
|
8605
|
+
case "delete_group_milestone": {
|
|
8606
|
+
const { group_id, milestone_id } = DeleteGroupMilestoneSchema.parse(params.arguments);
|
|
8607
|
+
await deleteGroupMilestone(group_id, milestone_id);
|
|
8608
|
+
return {
|
|
8609
|
+
content: [
|
|
8610
|
+
{
|
|
8611
|
+
type: "text",
|
|
8612
|
+
text: JSON.stringify({
|
|
8613
|
+
status: "success",
|
|
8614
|
+
message: "Group milestone deleted successfully",
|
|
8615
|
+
}, null, 2),
|
|
8616
|
+
},
|
|
8617
|
+
],
|
|
8618
|
+
};
|
|
8619
|
+
}
|
|
8620
|
+
case "get_group_milestone_issue": {
|
|
8621
|
+
const { group_id, milestone_id, ...options } = GetGroupMilestoneIssuesSchema.parse(params.arguments);
|
|
8622
|
+
const issues = await getGroupMilestoneIssues(group_id, milestone_id, options);
|
|
8623
|
+
return {
|
|
8624
|
+
content: [
|
|
8625
|
+
{
|
|
8626
|
+
type: "text",
|
|
8627
|
+
text: JSON.stringify(issues),
|
|
8628
|
+
},
|
|
8629
|
+
],
|
|
8630
|
+
};
|
|
8631
|
+
}
|
|
8632
|
+
case "get_group_milestone_merge_requests": {
|
|
8633
|
+
const { group_id, milestone_id, ...options } = GetGroupMilestoneMergeRequestsSchema.parse(params.arguments);
|
|
8634
|
+
const mergeRequests = await getGroupMilestoneMergeRequests(group_id, milestone_id, options);
|
|
8635
|
+
return {
|
|
8636
|
+
content: [
|
|
8637
|
+
{
|
|
8638
|
+
type: "text",
|
|
8639
|
+
text: JSON.stringify(mergeRequests),
|
|
8640
|
+
},
|
|
8641
|
+
],
|
|
8642
|
+
};
|
|
8643
|
+
}
|
|
8644
|
+
case "get_group_milestone_burndown_events": {
|
|
8645
|
+
const { group_id, milestone_id, ...options } = GetGroupMilestoneBurndownEventsSchema.parse(params.arguments);
|
|
8646
|
+
const events = await getGroupMilestoneBurndownEvents(group_id, milestone_id, options);
|
|
8368
8647
|
return {
|
|
8369
8648
|
content: [
|
|
8370
8649
|
{
|
|
@@ -9565,6 +9844,22 @@ async function startStreamableHTTPServer() {
|
|
|
9565
9844
|
});
|
|
9566
9845
|
logger.info({ issuerPath }, "Serving path-aware OAuth metadata for reverse-proxy deployments");
|
|
9567
9846
|
}
|
|
9847
|
+
// RFC 9728 path-suffixed discovery: clients connecting to /mcp will
|
|
9848
|
+
// request /.well-known/oauth-protected-resource/mcp to discover the
|
|
9849
|
+
// resource identifier. Register this route so the resource field matches
|
|
9850
|
+
// the actual transport URL the client connects to - regardless of
|
|
9851
|
+
// whether the server is deployed behind a path-stripping reverse proxy.
|
|
9852
|
+
if (!issuerPath) {
|
|
9853
|
+
const mcpResourceUrl = `${issuerUrl.origin}/mcp`;
|
|
9854
|
+
app.get("/.well-known/oauth-protected-resource/mcp", (_req, res) => {
|
|
9855
|
+
res.json({
|
|
9856
|
+
resource: mcpResourceUrl,
|
|
9857
|
+
authorization_servers: [issuerUrl.href],
|
|
9858
|
+
scopes_supported: scopesSupported,
|
|
9859
|
+
resource_name: "GitLab MCP Server",
|
|
9860
|
+
});
|
|
9861
|
+
});
|
|
9862
|
+
}
|
|
9568
9863
|
// Mounts /.well-known/oauth-authorization-server (shadowed above when basePath set),
|
|
9569
9864
|
// /.well-known/oauth-protected-resource, /authorize, /token, /register, /revoke
|
|
9570
9865
|
// Some proxies include the port in X-Forwarded-For (e.g. "1.2.3.4:5678" or
|
|
@@ -9964,10 +10259,14 @@ async function startStreamableHTTPServer() {
|
|
|
9964
10259
|
});
|
|
9965
10260
|
// Health check endpoint
|
|
9966
10261
|
app.get("/health", (_req, res) => {
|
|
9967
|
-
const
|
|
10262
|
+
const activeSessions = Object.keys(streamableTransports).length;
|
|
10263
|
+
const isHealthy = activeSessions < MAX_SESSIONS;
|
|
10264
|
+
if (!isHealthy) {
|
|
10265
|
+
logger.warn({ activeSessions, maxSessions: MAX_SESSIONS }, "Health check degraded: active session capacity reached");
|
|
10266
|
+
}
|
|
9968
10267
|
res.status(isHealthy ? 200 : 503).json({
|
|
9969
10268
|
status: isHealthy ? "healthy" : "degraded",
|
|
9970
|
-
activeSessions
|
|
10269
|
+
activeSessions,
|
|
9971
10270
|
maxSessions: MAX_SESSIONS,
|
|
9972
10271
|
uptime: process.uptime(),
|
|
9973
10272
|
});
|