@zereight/mcp-gitlab 2.1.16 → 2.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/build/index.js +231 -21
- package/build/schemas.js +142 -16
- package/build/test/test-ci-variables.js +306 -0
- package/build/test/test-geteffectiveprojectid.js +42 -0
- package/build/test/test-toolset-filtering.js +3 -0
- package/build/tools/registry.js +74 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -647,6 +647,16 @@ Register the skill directory in your AI client to get optimal tool usage guidanc
|
|
|
647
647
|
154. `search_group_code` - Search for code within a specific GitLab group (requires advanced search or exact code search to be enabled)
|
|
648
648
|
155. `execute_graphql` - Execute a GitLab GraphQL query
|
|
649
649
|
156. `list_merge_request_pipelines` - List pipelines for a merge request with pagination support
|
|
650
|
+
157. `list_project_variables` - List CI/CD variables for a project with optional environment scope filter
|
|
651
|
+
158. `get_project_variable` - Get a single CI/CD variable from a project by key, with optional environment scope filter
|
|
652
|
+
159. `create_project_variable` - Create a new CI/CD variable in a project
|
|
653
|
+
160. `update_project_variable` - Update an existing CI/CD variable in a project, with optional filter to disambiguate by environment scope
|
|
654
|
+
161. `delete_project_variable` - Delete a CI/CD variable from a project, with optional filter to disambiguate by environment scope
|
|
655
|
+
162. `list_group_variables` - List CI/CD variables for a group with optional environment scope filter
|
|
656
|
+
163. `get_group_variable` - Get a single CI/CD variable from a group by key, with optional environment scope filter
|
|
657
|
+
164. `create_group_variable` - Create a new CI/CD variable in a group
|
|
658
|
+
165. `update_group_variable` - Update an existing CI/CD variable in a group, with optional filter to disambiguate by environment scope
|
|
659
|
+
166. `delete_group_variable` - Delete a CI/CD variable from a group, with optional filter to disambiguate by environment scope
|
|
650
660
|
|
|
651
661
|
<!-- TOOLS-END -->
|
|
652
662
|
|
package/build/index.js
CHANGED
|
@@ -139,7 +139,7 @@ CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMerg
|
|
|
139
139
|
GitLabDiscussionNoteSchema, // Added
|
|
140
140
|
GitLabDiscussionSchema,
|
|
141
141
|
// Draft Notes Schemas
|
|
142
|
-
GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, 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, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
142
|
+
GitLabDraftNoteSchema, GitLabForkSchema, GitLabBranchSchema, 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, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListTodosSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
143
143
|
GetMergeRequestFileDiffSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, 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, 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";
|
|
144
144
|
import { randomUUID, createCipheriv, createDecipheriv, randomBytes, createHash } from "node:crypto";
|
|
145
145
|
import { pino } from "pino";
|
|
@@ -1389,16 +1389,9 @@ async function executeGraphQL(query, variables = {}) {
|
|
|
1389
1389
|
* Resolve a project path and issue IID to a work item GraphQL GID.
|
|
1390
1390
|
*/
|
|
1391
1391
|
async function resolveWorkItemGID(projectId, issueIid) {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
const projectUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
|
|
1396
|
-
const projectResponse = await fetch(projectUrl.toString(), {
|
|
1397
|
-
...getFetchConfig(),
|
|
1398
|
-
});
|
|
1399
|
-
await handleGitLabError(projectResponse);
|
|
1400
|
-
const project = await projectResponse.json();
|
|
1401
|
-
const projectPath = project.path_with_namespace;
|
|
1392
|
+
// resolveProjectPath handles both project and group paths (including the
|
|
1393
|
+
// group fallback), so work item tools work for group-level namespaces too.
|
|
1394
|
+
const projectPath = await resolveProjectPath(projectId);
|
|
1402
1395
|
// Resolve work item GID via GraphQL
|
|
1403
1396
|
const data = await executeGraphQL(`query($path: ID!, $iid: String!) {
|
|
1404
1397
|
namespace(fullPath: $path) {
|
|
@@ -1521,15 +1514,7 @@ async function removeIssueParent(projectId, issueIid) {
|
|
|
1521
1514
|
* Requires Premium/Ultimate with configurable statuses enabled.
|
|
1522
1515
|
*/
|
|
1523
1516
|
async function listIssueStatuses(projectId, workItemType = "issue") {
|
|
1524
|
-
|
|
1525
|
-
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
1526
|
-
// Get project path
|
|
1527
|
-
const projectUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}`);
|
|
1528
|
-
const projectResponse = await fetch(projectUrl.toString(), {
|
|
1529
|
-
...getFetchConfig(),
|
|
1530
|
-
});
|
|
1531
|
-
await handleGitLabError(projectResponse);
|
|
1532
|
-
const project = await projectResponse.json();
|
|
1517
|
+
const projectPath = await resolveProjectPath(projectId);
|
|
1533
1518
|
const typeName = WORK_ITEM_TYPE_NAMES[workItemType] || "Issue";
|
|
1534
1519
|
const data = await executeGraphQL(`query($path: ID!, $typeName: IssueType) {
|
|
1535
1520
|
namespace(fullPath: $path) {
|
|
@@ -1557,7 +1542,7 @@ async function listIssueStatuses(projectId, workItemType = "issue") {
|
|
|
1557
1542
|
}
|
|
1558
1543
|
}
|
|
1559
1544
|
}
|
|
1560
|
-
}`, { path:
|
|
1545
|
+
}`, { path: projectPath, typeName: typeName.replace(/ /g, "_").toUpperCase() });
|
|
1561
1546
|
const typeNodes = data.namespace?.workItemTypes?.nodes;
|
|
1562
1547
|
if (!typeNodes || typeNodes.length === 0) {
|
|
1563
1548
|
throw new Error(`Work item type '${typeName}' not found in project`);
|
|
@@ -1953,6 +1938,22 @@ async function resolveProjectPath(projectId) {
|
|
|
1953
1938
|
const projectResponse = await fetch(projectUrl.toString(), {
|
|
1954
1939
|
...getFetchConfig(),
|
|
1955
1940
|
});
|
|
1941
|
+
// On project 404, fall back to groups — but only for path-like identifiers.
|
|
1942
|
+
// Numeric IDs must not fall back: group and project IDs share no namespace,
|
|
1943
|
+
// so a numeric project 404 should fail immediately rather than silently
|
|
1944
|
+
// resolving to an unrelated group with the same integer ID.
|
|
1945
|
+
if (projectResponse.status === 404 && !/^\d+$/.test(effectiveProjectId)) {
|
|
1946
|
+
const groupUrl = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(effectiveProjectId)}`);
|
|
1947
|
+
const groupResponse = await fetch(groupUrl.toString(), {
|
|
1948
|
+
...getFetchConfig(),
|
|
1949
|
+
});
|
|
1950
|
+
if (groupResponse.ok) {
|
|
1951
|
+
const group = await groupResponse.json();
|
|
1952
|
+
return group.full_path;
|
|
1953
|
+
}
|
|
1954
|
+
// Surface the group error
|
|
1955
|
+
await handleGitLabError(groupResponse);
|
|
1956
|
+
}
|
|
1956
1957
|
await handleGitLabError(projectResponse);
|
|
1957
1958
|
const project = await projectResponse.json();
|
|
1958
1959
|
return project.path_with_namespace;
|
|
@@ -5741,6 +5742,139 @@ async function listGroupIterations(groupId, options = {}) {
|
|
|
5741
5742
|
const data = await response.json();
|
|
5742
5743
|
return z.array(GroupIteration).parse(data);
|
|
5743
5744
|
}
|
|
5745
|
+
// --- CI/CD Variables ---
|
|
5746
|
+
async function listProjectVariables(projectId, options = {}) {
|
|
5747
|
+
projectId = decodeURIComponent(projectId);
|
|
5748
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables`);
|
|
5749
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
5750
|
+
if (value === undefined)
|
|
5751
|
+
return;
|
|
5752
|
+
if (key === "filter" && typeof value === "object" && value !== null) {
|
|
5753
|
+
Object.entries(value).forEach(([fKey, fVal]) => {
|
|
5754
|
+
url.searchParams.append(`filter[${fKey}]`, fVal);
|
|
5755
|
+
});
|
|
5756
|
+
}
|
|
5757
|
+
else if (typeof value === "boolean") {
|
|
5758
|
+
url.searchParams.append(key, value ? "true" : "false");
|
|
5759
|
+
}
|
|
5760
|
+
else {
|
|
5761
|
+
url.searchParams.append(key, String(value));
|
|
5762
|
+
}
|
|
5763
|
+
});
|
|
5764
|
+
const response = await fetch(url.toString(), getFetchConfig());
|
|
5765
|
+
await handleGitLabError(response);
|
|
5766
|
+
const data = await response.json();
|
|
5767
|
+
return z.array(GitLabCiVariableSchema).parse(data);
|
|
5768
|
+
}
|
|
5769
|
+
async function getProjectVariable(projectId, key, filter) {
|
|
5770
|
+
projectId = decodeURIComponent(projectId);
|
|
5771
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables/${encodeURIComponent(key)}`);
|
|
5772
|
+
if (filter?.environment_scope) {
|
|
5773
|
+
url.searchParams.append("filter[environment_scope]", filter.environment_scope);
|
|
5774
|
+
}
|
|
5775
|
+
const response = await fetch(url.toString(), getFetchConfig());
|
|
5776
|
+
await handleGitLabError(response);
|
|
5777
|
+
const data = await response.json();
|
|
5778
|
+
return GitLabCiVariableSchema.parse(data);
|
|
5779
|
+
}
|
|
5780
|
+
async function createProjectVariable(projectId, options) {
|
|
5781
|
+
projectId = decodeURIComponent(projectId);
|
|
5782
|
+
const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables`, { ...getFetchConfig(), method: "POST", body: JSON.stringify(options) });
|
|
5783
|
+
await handleGitLabError(response);
|
|
5784
|
+
const data = await response.json();
|
|
5785
|
+
return GitLabCiVariableSchema.parse(data);
|
|
5786
|
+
}
|
|
5787
|
+
async function updateProjectVariable(projectId, key, options) {
|
|
5788
|
+
projectId = decodeURIComponent(projectId);
|
|
5789
|
+
const { filter, ...body } = options;
|
|
5790
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables/${encodeURIComponent(key)}`);
|
|
5791
|
+
if (filter?.environment_scope) {
|
|
5792
|
+
url.searchParams.append("filter[environment_scope]", filter.environment_scope);
|
|
5793
|
+
}
|
|
5794
|
+
const response = await fetch(url.toString(), {
|
|
5795
|
+
...getFetchConfig(),
|
|
5796
|
+
method: "PUT",
|
|
5797
|
+
body: JSON.stringify(body),
|
|
5798
|
+
});
|
|
5799
|
+
await handleGitLabError(response);
|
|
5800
|
+
const data = await response.json();
|
|
5801
|
+
return GitLabCiVariableSchema.parse(data);
|
|
5802
|
+
}
|
|
5803
|
+
async function deleteProjectVariable(projectId, key, filter) {
|
|
5804
|
+
projectId = decodeURIComponent(projectId);
|
|
5805
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/variables/${encodeURIComponent(key)}`);
|
|
5806
|
+
if (filter?.environment_scope) {
|
|
5807
|
+
url.searchParams.append("filter[environment_scope]", filter.environment_scope);
|
|
5808
|
+
}
|
|
5809
|
+
const response = await fetch(url.toString(), { ...getFetchConfig(), method: "DELETE" });
|
|
5810
|
+
await handleGitLabError(response);
|
|
5811
|
+
}
|
|
5812
|
+
async function listGroupVariables(groupId, options = {}) {
|
|
5813
|
+
const encoded = encodeURIComponent(decodeURIComponent(groupId));
|
|
5814
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables`);
|
|
5815
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
5816
|
+
if (value === undefined)
|
|
5817
|
+
return;
|
|
5818
|
+
if (key === "filter" && typeof value === "object" && value !== null) {
|
|
5819
|
+
Object.entries(value).forEach(([fKey, fVal]) => {
|
|
5820
|
+
url.searchParams.append(`filter[${fKey}]`, fVal);
|
|
5821
|
+
});
|
|
5822
|
+
}
|
|
5823
|
+
else if (typeof value === "boolean") {
|
|
5824
|
+
url.searchParams.append(key, value ? "true" : "false");
|
|
5825
|
+
}
|
|
5826
|
+
else {
|
|
5827
|
+
url.searchParams.append(key, String(value));
|
|
5828
|
+
}
|
|
5829
|
+
});
|
|
5830
|
+
const response = await fetch(url.toString(), getFetchConfig());
|
|
5831
|
+
await handleGitLabError(response);
|
|
5832
|
+
const data = await response.json();
|
|
5833
|
+
return z.array(GitLabCiVariableSchema).parse(data);
|
|
5834
|
+
}
|
|
5835
|
+
async function getGroupVariable(groupId, key, filter) {
|
|
5836
|
+
const encoded = encodeURIComponent(decodeURIComponent(groupId));
|
|
5837
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeURIComponent(key)}`);
|
|
5838
|
+
if (filter?.environment_scope) {
|
|
5839
|
+
url.searchParams.append("filter[environment_scope]", filter.environment_scope);
|
|
5840
|
+
}
|
|
5841
|
+
const response = await fetch(url.toString(), getFetchConfig());
|
|
5842
|
+
await handleGitLabError(response);
|
|
5843
|
+
const data = await response.json();
|
|
5844
|
+
return GitLabCiVariableSchema.parse(data);
|
|
5845
|
+
}
|
|
5846
|
+
async function createGroupVariable(groupId, options) {
|
|
5847
|
+
const encoded = encodeURIComponent(decodeURIComponent(groupId));
|
|
5848
|
+
const response = await fetch(`${getEffectiveApiUrl()}/groups/${encoded}/variables`, {
|
|
5849
|
+
...getFetchConfig(),
|
|
5850
|
+
method: "POST",
|
|
5851
|
+
body: JSON.stringify(options),
|
|
5852
|
+
});
|
|
5853
|
+
await handleGitLabError(response);
|
|
5854
|
+
const data = await response.json();
|
|
5855
|
+
return GitLabCiVariableSchema.parse(data);
|
|
5856
|
+
}
|
|
5857
|
+
async function updateGroupVariable(groupId, key, options) {
|
|
5858
|
+
const encoded = encodeURIComponent(decodeURIComponent(groupId));
|
|
5859
|
+
const { filter, ...body } = options;
|
|
5860
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeURIComponent(key)}`);
|
|
5861
|
+
if (filter?.environment_scope) {
|
|
5862
|
+
url.searchParams.append("filter[environment_scope]", filter.environment_scope);
|
|
5863
|
+
}
|
|
5864
|
+
const response = await fetch(url.toString(), { ...getFetchConfig(), method: "PUT", body: JSON.stringify(body) });
|
|
5865
|
+
await handleGitLabError(response);
|
|
5866
|
+
const data = await response.json();
|
|
5867
|
+
return GitLabCiVariableSchema.parse(data);
|
|
5868
|
+
}
|
|
5869
|
+
async function deleteGroupVariable(groupId, key, filter) {
|
|
5870
|
+
const encoded = encodeURIComponent(decodeURIComponent(groupId));
|
|
5871
|
+
const url = new URL(`${getEffectiveApiUrl()}/groups/${encoded}/variables/${encodeURIComponent(key)}`);
|
|
5872
|
+
if (filter?.environment_scope) {
|
|
5873
|
+
url.searchParams.append("filter[environment_scope]", filter.environment_scope);
|
|
5874
|
+
}
|
|
5875
|
+
const response = await fetch(url.toString(), { ...getFetchConfig(), method: "DELETE" });
|
|
5876
|
+
await handleGitLabError(response);
|
|
5877
|
+
}
|
|
5744
5878
|
/**
|
|
5745
5879
|
* Upload a file to a GitLab project for use in markdown content.
|
|
5746
5880
|
*
|
|
@@ -7711,6 +7845,82 @@ async function handleToolCall(params) {
|
|
|
7711
7845
|
content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
|
|
7712
7846
|
};
|
|
7713
7847
|
}
|
|
7848
|
+
// --- CI/CD Variables ---
|
|
7849
|
+
case "list_project_variables": {
|
|
7850
|
+
const args = ListProjectVariablesSchema.parse(params.arguments);
|
|
7851
|
+
const { project_id, ...options } = args;
|
|
7852
|
+
const variables = await listProjectVariables(project_id, options);
|
|
7853
|
+
return { content: [{ type: "text", text: JSON.stringify(variables, null, 2) }] };
|
|
7854
|
+
}
|
|
7855
|
+
case "get_project_variable": {
|
|
7856
|
+
const args = GetProjectVariableSchema.parse(params.arguments);
|
|
7857
|
+
const variable = await getProjectVariable(args.project_id, args.key, args.filter);
|
|
7858
|
+
return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
|
|
7859
|
+
}
|
|
7860
|
+
case "create_project_variable": {
|
|
7861
|
+
const args = CreateProjectVariableSchema.parse(params.arguments);
|
|
7862
|
+
const { project_id, ...options } = args;
|
|
7863
|
+
const variable = await createProjectVariable(project_id, options);
|
|
7864
|
+
return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
|
|
7865
|
+
}
|
|
7866
|
+
case "update_project_variable": {
|
|
7867
|
+
const args = UpdateProjectVariableSchema.parse(params.arguments);
|
|
7868
|
+
const { project_id, key, ...options } = args;
|
|
7869
|
+
const variable = await updateProjectVariable(project_id, key, options);
|
|
7870
|
+
return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
|
|
7871
|
+
}
|
|
7872
|
+
case "delete_project_variable": {
|
|
7873
|
+
const args = DeleteProjectVariableSchema.parse(params.arguments);
|
|
7874
|
+
await deleteProjectVariable(args.project_id, args.key, args.filter);
|
|
7875
|
+
return {
|
|
7876
|
+
content: [
|
|
7877
|
+
{
|
|
7878
|
+
type: "text",
|
|
7879
|
+
text: JSON.stringify({ status: "success", message: `Variable '${args.key}' deleted from project` }, null, 2),
|
|
7880
|
+
},
|
|
7881
|
+
],
|
|
7882
|
+
};
|
|
7883
|
+
}
|
|
7884
|
+
case "list_group_variables": {
|
|
7885
|
+
rejectIfProjectScopedDeployment("list_group_variables");
|
|
7886
|
+
const args = ListGroupVariablesSchema.parse(params.arguments);
|
|
7887
|
+
const { group_id, ...options } = args;
|
|
7888
|
+
const variables = await listGroupVariables(group_id, options);
|
|
7889
|
+
return { content: [{ type: "text", text: JSON.stringify(variables, null, 2) }] };
|
|
7890
|
+
}
|
|
7891
|
+
case "get_group_variable": {
|
|
7892
|
+
rejectIfProjectScopedDeployment("get_group_variable");
|
|
7893
|
+
const args = GetGroupVariableSchema.parse(params.arguments);
|
|
7894
|
+
const variable = await getGroupVariable(args.group_id, args.key, args.filter);
|
|
7895
|
+
return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
|
|
7896
|
+
}
|
|
7897
|
+
case "create_group_variable": {
|
|
7898
|
+
rejectIfProjectScopedDeployment("create_group_variable");
|
|
7899
|
+
const args = CreateGroupVariableSchema.parse(params.arguments);
|
|
7900
|
+
const { group_id, ...options } = args;
|
|
7901
|
+
const variable = await createGroupVariable(group_id, options);
|
|
7902
|
+
return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
|
|
7903
|
+
}
|
|
7904
|
+
case "update_group_variable": {
|
|
7905
|
+
rejectIfProjectScopedDeployment("update_group_variable");
|
|
7906
|
+
const args = UpdateGroupVariableSchema.parse(params.arguments);
|
|
7907
|
+
const { group_id, key, ...options } = args;
|
|
7908
|
+
const variable = await updateGroupVariable(group_id, key, options);
|
|
7909
|
+
return { content: [{ type: "text", text: JSON.stringify(variable, null, 2) }] };
|
|
7910
|
+
}
|
|
7911
|
+
case "delete_group_variable": {
|
|
7912
|
+
rejectIfProjectScopedDeployment("delete_group_variable");
|
|
7913
|
+
const args = DeleteGroupVariableSchema.parse(params.arguments);
|
|
7914
|
+
await deleteGroupVariable(args.group_id, args.key, args.filter);
|
|
7915
|
+
return {
|
|
7916
|
+
content: [
|
|
7917
|
+
{
|
|
7918
|
+
type: "text",
|
|
7919
|
+
text: JSON.stringify({ status: "success", message: `Variable '${args.key}' deleted from group` }, null, 2),
|
|
7920
|
+
},
|
|
7921
|
+
],
|
|
7922
|
+
};
|
|
7923
|
+
}
|
|
7714
7924
|
case "upload_markdown": {
|
|
7715
7925
|
if (IS_REMOTE) {
|
|
7716
7926
|
const args = MarkdownUploadRemoteSchema.parse(params.arguments);
|
package/build/schemas.js
CHANGED
|
@@ -3057,14 +3057,15 @@ export const GitLabTagSignatureSchema = z.object({
|
|
|
3057
3057
|
// --- Work item schemas (GraphQL-based) ---
|
|
3058
3058
|
// Case-insensitive work item type enum (accepts "ISSUE", "Issue", "issue")
|
|
3059
3059
|
const workItemTypeEnum = z.string().transform(v => v.toLowerCase()).pipe(z.enum(["issue", "task", "incident", "test_case", "epic", "key_result", "objective", "requirement", "ticket"]));
|
|
3060
|
+
const ProjectIdOrPathSchema = z.coerce.string().describe("Project ID, URL-encoded project path, or group path (e.g. 'group/subgroup' for group-level work items)");
|
|
3060
3061
|
// Common params for work item tools
|
|
3061
3062
|
const WorkItemParamsSchema = z.object({
|
|
3062
|
-
project_id:
|
|
3063
|
+
project_id: ProjectIdOrPathSchema,
|
|
3063
3064
|
iid: z.coerce.number().describe("The internal ID (IID) of the work item"),
|
|
3064
3065
|
});
|
|
3065
3066
|
export const GetWorkItemSchema = WorkItemParamsSchema;
|
|
3066
3067
|
export const ListWorkItemsSchema = z.object({
|
|
3067
|
-
project_id:
|
|
3068
|
+
project_id: ProjectIdOrPathSchema,
|
|
3068
3069
|
types: z
|
|
3069
3070
|
.array(workItemTypeEnum)
|
|
3070
3071
|
.optional()
|
|
@@ -3096,7 +3097,7 @@ export const ListWorkItemsSchema = z.object({
|
|
|
3096
3097
|
.describe("Cursor for pagination (from previous response's endCursor)"),
|
|
3097
3098
|
});
|
|
3098
3099
|
export const CreateWorkItemSchema = z.object({
|
|
3099
|
-
project_id:
|
|
3100
|
+
project_id: ProjectIdOrPathSchema,
|
|
3100
3101
|
title: z.string().describe("Title of the work item"),
|
|
3101
3102
|
type: workItemTypeEnum
|
|
3102
3103
|
.optional()
|
|
@@ -3166,38 +3167,38 @@ export const UpdateWorkItemSchema = WorkItemParamsSchema.extend({
|
|
|
3166
3167
|
.describe("Incident only: set escalation status"),
|
|
3167
3168
|
});
|
|
3168
3169
|
export const ConvertWorkItemTypeSchema = z.object({
|
|
3169
|
-
project_id:
|
|
3170
|
+
project_id: ProjectIdOrPathSchema,
|
|
3170
3171
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3171
3172
|
new_type: workItemTypeEnum.describe("The target work item type to convert to"),
|
|
3172
3173
|
});
|
|
3173
3174
|
export const ListWorkItemStatusesSchema = z.object({
|
|
3174
|
-
project_id:
|
|
3175
|
+
project_id: ProjectIdOrPathSchema,
|
|
3175
3176
|
work_item_type: workItemTypeEnum
|
|
3176
3177
|
.optional()
|
|
3177
3178
|
.default("issue")
|
|
3178
3179
|
.describe("The work item type to list available statuses for. Defaults to 'issue'."),
|
|
3179
3180
|
});
|
|
3180
3181
|
export const ListWorkItemNotesSchema = z.object({
|
|
3181
|
-
project_id:
|
|
3182
|
+
project_id: ProjectIdOrPathSchema,
|
|
3182
3183
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3183
3184
|
page_size: z.coerce.number().optional().default(20).describe("Number of discussions to return (default 20)"),
|
|
3184
3185
|
after: z.string().optional().describe("Cursor for pagination"),
|
|
3185
3186
|
sort: z.enum(["CREATED_ASC", "CREATED_DESC"]).optional().default("CREATED_ASC").describe("Sort order for discussions"),
|
|
3186
3187
|
});
|
|
3187
3188
|
export const CreateWorkItemNoteSchema = z.object({
|
|
3188
|
-
project_id:
|
|
3189
|
+
project_id: ProjectIdOrPathSchema,
|
|
3189
3190
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3190
3191
|
body: z.string().describe("Note body (Markdown supported)"),
|
|
3191
3192
|
internal: z.coerce.boolean().optional().default(false).describe("Create as internal/confidential note (only visible to project members)"),
|
|
3192
3193
|
discussion_id: z.string().optional().describe("Discussion ID to reply to (for threaded replies). If omitted, creates a new top-level note."),
|
|
3193
3194
|
});
|
|
3194
3195
|
export const MoveWorkItemSchema = z.object({
|
|
3195
|
-
project_id: z.coerce.string().describe("Project ID
|
|
3196
|
+
project_id: z.coerce.string().describe("Project ID, URL-encoded project path, or group path of the source namespace"),
|
|
3196
3197
|
iid: z.coerce.number().describe("The internal ID of the work item to move"),
|
|
3197
|
-
target_project_id: z.coerce.string().describe("Project ID
|
|
3198
|
+
target_project_id: z.coerce.string().describe("Project ID, URL-encoded project path, or group path of the target namespace"),
|
|
3198
3199
|
});
|
|
3199
3200
|
export const ListCustomFieldDefinitionsSchema = z.object({
|
|
3200
|
-
project_id:
|
|
3201
|
+
project_id: ProjectIdOrPathSchema,
|
|
3201
3202
|
work_item_type: workItemTypeEnum
|
|
3202
3203
|
.optional()
|
|
3203
3204
|
.default("issue")
|
|
@@ -3249,23 +3250,23 @@ export const DeleteIssueNoteEmojiReactionSchema = ProjectParamsSchema.extend({
|
|
|
3249
3250
|
});
|
|
3250
3251
|
// --- Emoji Reaction schemas (GraphQL: Work Items) ---
|
|
3251
3252
|
export const CreateWorkItemEmojiReactionSchema = z.object({
|
|
3252
|
-
project_id:
|
|
3253
|
+
project_id: ProjectIdOrPathSchema,
|
|
3253
3254
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3254
3255
|
name: emojiNameField,
|
|
3255
3256
|
});
|
|
3256
3257
|
export const DeleteWorkItemEmojiReactionSchema = z.object({
|
|
3257
|
-
project_id:
|
|
3258
|
+
project_id: ProjectIdOrPathSchema,
|
|
3258
3259
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3259
3260
|
name: emojiNameField,
|
|
3260
3261
|
});
|
|
3261
3262
|
export const CreateWorkItemNoteEmojiReactionSchema = z.object({
|
|
3262
|
-
project_id:
|
|
3263
|
+
project_id: ProjectIdOrPathSchema,
|
|
3263
3264
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3264
3265
|
note_id: z.string().describe("The GraphQL GID of the note (e.g. 'gid://gitlab/Note/123' from list_work_item_notes)"),
|
|
3265
3266
|
name: emojiNameField,
|
|
3266
3267
|
});
|
|
3267
3268
|
export const DeleteWorkItemNoteEmojiReactionSchema = z.object({
|
|
3268
|
-
project_id:
|
|
3269
|
+
project_id: ProjectIdOrPathSchema,
|
|
3269
3270
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3270
3271
|
note_id: z.string().describe("The GraphQL GID of the note (e.g. 'gid://gitlab/Note/123' from list_work_item_notes)"),
|
|
3271
3272
|
name: emojiNameField,
|
|
@@ -3287,11 +3288,11 @@ export const ListIssueNoteEmojiReactionsSchema = ProjectParamsSchema.extend({
|
|
|
3287
3288
|
discussion_id: noteEmojiDiscussionField,
|
|
3288
3289
|
});
|
|
3289
3290
|
export const ListWorkItemEmojiReactionsSchema = z.object({
|
|
3290
|
-
project_id:
|
|
3291
|
+
project_id: ProjectIdOrPathSchema,
|
|
3291
3292
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3292
3293
|
});
|
|
3293
3294
|
export const ListWorkItemNoteEmojiReactionsSchema = z.object({
|
|
3294
|
-
project_id:
|
|
3295
|
+
project_id: ProjectIdOrPathSchema,
|
|
3295
3296
|
iid: z.coerce.number().describe("The internal ID of the work item"),
|
|
3296
3297
|
note_id: z.string().describe("The GraphQL GID of the note (e.g. 'gid://gitlab/Note/123' from list_work_item_notes)"),
|
|
3297
3298
|
});
|
|
@@ -3377,3 +3378,128 @@ export const GetWebhookEventSchema = z
|
|
|
3377
3378
|
message: "Provide exactly one of project_id or group_id",
|
|
3378
3379
|
});
|
|
3379
3380
|
export const HealthCheckSchema = z.object({});
|
|
3381
|
+
// --- CI/CD Variable types ---
|
|
3382
|
+
export const GitLabCiVariableSchema = z.object({
|
|
3383
|
+
variable_type: z.enum(["env_var", "file"]).optional(),
|
|
3384
|
+
key: z.string(),
|
|
3385
|
+
value: z.string().nullable(),
|
|
3386
|
+
protected: z.boolean().optional(),
|
|
3387
|
+
masked: z.boolean().optional(),
|
|
3388
|
+
hidden: z.boolean().optional(),
|
|
3389
|
+
raw: z.boolean().optional(),
|
|
3390
|
+
environment_scope: z.string().optional(),
|
|
3391
|
+
description: z.string().nullable().optional(),
|
|
3392
|
+
});
|
|
3393
|
+
const ciVariableFields = {
|
|
3394
|
+
key: z.string().describe("The key of the variable (must match /[a-zA-Z0-9_]+/)"),
|
|
3395
|
+
value: z.string().describe("The value of the variable"),
|
|
3396
|
+
variable_type: z
|
|
3397
|
+
.enum(["env_var", "file"])
|
|
3398
|
+
.optional()
|
|
3399
|
+
.describe("The type of variable: 'env_var' (default) or 'file'"),
|
|
3400
|
+
protected: z
|
|
3401
|
+
.boolean()
|
|
3402
|
+
.optional()
|
|
3403
|
+
.describe("Whether the variable is only available on protected branches/tags"),
|
|
3404
|
+
masked: z.boolean().optional().describe("Whether the variable value is masked in job logs"),
|
|
3405
|
+
raw: z.boolean().optional().describe("Whether the variable is not expanded (treated as raw string)"),
|
|
3406
|
+
environment_scope: z
|
|
3407
|
+
.string()
|
|
3408
|
+
.optional()
|
|
3409
|
+
.describe("Environment scope (e.g. '*', 'production'). Default: '*'"),
|
|
3410
|
+
description: z.string().optional().describe("Description of the variable"),
|
|
3411
|
+
};
|
|
3412
|
+
export const ListProjectVariablesSchema = z
|
|
3413
|
+
.object({
|
|
3414
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
3415
|
+
filter: z
|
|
3416
|
+
.object({ environment_scope: z.string() })
|
|
3417
|
+
.optional()
|
|
3418
|
+
.describe("Filter by environment scope (e.g. '*', 'production')"),
|
|
3419
|
+
})
|
|
3420
|
+
.merge(PaginationOptionsSchema);
|
|
3421
|
+
export const GetProjectVariableSchema = z.object({
|
|
3422
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
3423
|
+
key: z.string().describe("The key of the variable"),
|
|
3424
|
+
filter: z
|
|
3425
|
+
.object({ environment_scope: z.string() })
|
|
3426
|
+
.optional()
|
|
3427
|
+
.describe("Filter by environment scope"),
|
|
3428
|
+
});
|
|
3429
|
+
export const CreateProjectVariableSchema = z.object({
|
|
3430
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
3431
|
+
...ciVariableFields,
|
|
3432
|
+
});
|
|
3433
|
+
export const UpdateProjectVariableSchema = z.object({
|
|
3434
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
3435
|
+
key: z.string().describe("The key of the variable to update"),
|
|
3436
|
+
value: z.string().describe("The new value of the variable"),
|
|
3437
|
+
variable_type: z.enum(["env_var", "file"]).optional().describe("The type of variable"),
|
|
3438
|
+
protected: z.boolean().optional().describe("Whether the variable is protected"),
|
|
3439
|
+
masked: z.boolean().optional().describe("Whether the variable value is masked in job logs"),
|
|
3440
|
+
raw: z.boolean().optional().describe("Whether the variable is not expanded"),
|
|
3441
|
+
environment_scope: z
|
|
3442
|
+
.string()
|
|
3443
|
+
.optional()
|
|
3444
|
+
.describe("New environment scope to assign to the variable (renames the scope, e.g. '*', 'production'). Use filter.environment_scope to identify which variable to update when multiple share the same key."),
|
|
3445
|
+
description: z.string().optional().describe("Description of the variable"),
|
|
3446
|
+
filter: z
|
|
3447
|
+
.object({ environment_scope: z.string() })
|
|
3448
|
+
.optional()
|
|
3449
|
+
.describe("Identifies which variable to update when multiple variables share the same key across different environment scopes"),
|
|
3450
|
+
});
|
|
3451
|
+
export const DeleteProjectVariableSchema = z.object({
|
|
3452
|
+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
|
|
3453
|
+
key: z.string().describe("The key of the variable to delete"),
|
|
3454
|
+
filter: z
|
|
3455
|
+
.object({ environment_scope: z.string() })
|
|
3456
|
+
.optional()
|
|
3457
|
+
.describe("Filter by environment scope to disambiguate when multiple variables share the same key"),
|
|
3458
|
+
});
|
|
3459
|
+
export const ListGroupVariablesSchema = z
|
|
3460
|
+
.object({
|
|
3461
|
+
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
3462
|
+
filter: z
|
|
3463
|
+
.object({ environment_scope: z.string() })
|
|
3464
|
+
.optional()
|
|
3465
|
+
.describe("Filter by environment scope (e.g. '*', 'production')"),
|
|
3466
|
+
})
|
|
3467
|
+
.merge(PaginationOptionsSchema);
|
|
3468
|
+
export const GetGroupVariableSchema = z.object({
|
|
3469
|
+
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
3470
|
+
key: z.string().describe("The key of the variable"),
|
|
3471
|
+
filter: z
|
|
3472
|
+
.object({ environment_scope: z.string() })
|
|
3473
|
+
.optional()
|
|
3474
|
+
.describe("Filter by environment scope"),
|
|
3475
|
+
});
|
|
3476
|
+
export const CreateGroupVariableSchema = z.object({
|
|
3477
|
+
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
3478
|
+
...ciVariableFields,
|
|
3479
|
+
});
|
|
3480
|
+
export const UpdateGroupVariableSchema = z.object({
|
|
3481
|
+
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
3482
|
+
key: z.string().describe("The key of the variable to update"),
|
|
3483
|
+
value: z.string().describe("The new value of the variable"),
|
|
3484
|
+
variable_type: z.enum(["env_var", "file"]).optional().describe("The type of variable"),
|
|
3485
|
+
protected: z.boolean().optional().describe("Whether the variable is protected"),
|
|
3486
|
+
masked: z.boolean().optional().describe("Whether the variable value is masked in job logs"),
|
|
3487
|
+
raw: z.boolean().optional().describe("Whether the variable is not expanded"),
|
|
3488
|
+
environment_scope: z
|
|
3489
|
+
.string()
|
|
3490
|
+
.optional()
|
|
3491
|
+
.describe("New environment scope to assign to the variable (renames the scope, e.g. '*', 'production'). Use filter.environment_scope to identify which variable to update when multiple share the same key."),
|
|
3492
|
+
description: z.string().optional().describe("Description of the variable"),
|
|
3493
|
+
filter: z
|
|
3494
|
+
.object({ environment_scope: z.string() })
|
|
3495
|
+
.optional()
|
|
3496
|
+
.describe("Identifies which variable to update when multiple variables share the same key across different environment scopes"),
|
|
3497
|
+
});
|
|
3498
|
+
export const DeleteGroupVariableSchema = z.object({
|
|
3499
|
+
group_id: z.coerce.string().describe("Group ID or URL-encoded path"),
|
|
3500
|
+
key: z.string().describe("The key of the variable to delete"),
|
|
3501
|
+
filter: z
|
|
3502
|
+
.object({ environment_scope: z.string() })
|
|
3503
|
+
.optional()
|
|
3504
|
+
.describe("Filter by environment scope to disambiguate when multiple variables share the same key"),
|
|
3505
|
+
});
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { describe, test, before, after } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
|
|
5
|
+
const MOCK_TOKEN = "glpat-mock-token-ci-variables";
|
|
6
|
+
const TEST_PROJECT_ID = "123";
|
|
7
|
+
const TEST_GROUP_ID = "my-group";
|
|
8
|
+
const TEST_VAR_KEY = "DB_URL";
|
|
9
|
+
const TEST_GROUP_VAR_KEY = "SHARED_SECRET";
|
|
10
|
+
const TEST_HIDDEN_VAR_KEY = "HIDDEN_VAR";
|
|
11
|
+
const TEST_SCOPE = "production";
|
|
12
|
+
const MOCK_PROJECT_VARIABLE = {
|
|
13
|
+
variable_type: "env_var",
|
|
14
|
+
key: TEST_VAR_KEY,
|
|
15
|
+
value: "postgres://localhost/db",
|
|
16
|
+
protected: false,
|
|
17
|
+
masked: true,
|
|
18
|
+
raw: false,
|
|
19
|
+
environment_scope: "*",
|
|
20
|
+
description: "Database connection URL",
|
|
21
|
+
};
|
|
22
|
+
const MOCK_GROUP_VARIABLE = {
|
|
23
|
+
variable_type: "env_var",
|
|
24
|
+
key: TEST_GROUP_VAR_KEY,
|
|
25
|
+
value: "s3cr3t",
|
|
26
|
+
protected: false,
|
|
27
|
+
masked: true,
|
|
28
|
+
raw: false,
|
|
29
|
+
environment_scope: "*",
|
|
30
|
+
description: null,
|
|
31
|
+
};
|
|
32
|
+
const MOCK_HIDDEN_PROJECT_VARIABLE = {
|
|
33
|
+
...MOCK_PROJECT_VARIABLE,
|
|
34
|
+
key: TEST_HIDDEN_VAR_KEY,
|
|
35
|
+
value: null,
|
|
36
|
+
hidden: true,
|
|
37
|
+
description: null,
|
|
38
|
+
};
|
|
39
|
+
async function callTool(toolName, args, env) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const proc = spawn("node", ["build/index.js"], {
|
|
42
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
43
|
+
env: { ...process.env, ...env },
|
|
44
|
+
});
|
|
45
|
+
let output = "";
|
|
46
|
+
let errorOutput = "";
|
|
47
|
+
proc.stdout?.on("data", (d) => (output += d));
|
|
48
|
+
proc.stderr?.on("data", (d) => (errorOutput += d));
|
|
49
|
+
proc.on("close", code => {
|
|
50
|
+
if (code !== 0) {
|
|
51
|
+
return reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
|
|
52
|
+
}
|
|
53
|
+
const line = output.split("\n").find(l => l.startsWith("{"));
|
|
54
|
+
if (!line)
|
|
55
|
+
return reject(new Error("No JSON output found"));
|
|
56
|
+
try {
|
|
57
|
+
const response = JSON.parse(line);
|
|
58
|
+
if (response.error) {
|
|
59
|
+
reject(new Error(response.error?.message ?? String(response.error)));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const content = response.result?.content?.[0]?.text;
|
|
63
|
+
resolve(content ? JSON.parse(content) : response.result);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
reject(e);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
proc.stdin?.end(JSON.stringify({
|
|
71
|
+
jsonrpc: "2.0",
|
|
72
|
+
id: 1,
|
|
73
|
+
method: "tools/call",
|
|
74
|
+
params: { name: toolName, arguments: args },
|
|
75
|
+
}) + "\n");
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
describe("CI/CD variable tools", () => {
|
|
79
|
+
let mockServer;
|
|
80
|
+
let mockPort;
|
|
81
|
+
let baseEnv;
|
|
82
|
+
let lastReceivedFilterScope;
|
|
83
|
+
before(async () => {
|
|
84
|
+
mockPort = await findMockServerPort();
|
|
85
|
+
mockServer = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN] });
|
|
86
|
+
// --- Project variable endpoints ---
|
|
87
|
+
mockServer.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/variables`, (_req, res) => {
|
|
88
|
+
res.json([MOCK_PROJECT_VARIABLE]);
|
|
89
|
+
});
|
|
90
|
+
mockServer.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/variables/${TEST_VAR_KEY}`, (req, res) => {
|
|
91
|
+
const scope = req.query["filter[environment_scope]"];
|
|
92
|
+
lastReceivedFilterScope = scope;
|
|
93
|
+
res.json({ ...MOCK_PROJECT_VARIABLE, environment_scope: scope ?? "*" });
|
|
94
|
+
});
|
|
95
|
+
mockServer.addMockHandler("get", `/projects/${TEST_PROJECT_ID}/variables/${TEST_HIDDEN_VAR_KEY}`, (_req, res) => {
|
|
96
|
+
res.json(MOCK_HIDDEN_PROJECT_VARIABLE);
|
|
97
|
+
});
|
|
98
|
+
mockServer.addMockHandler("post", `/projects/${TEST_PROJECT_ID}/variables`, (req, res) => {
|
|
99
|
+
res.status(201).json({ ...MOCK_PROJECT_VARIABLE, ...req.body });
|
|
100
|
+
});
|
|
101
|
+
mockServer.addMockHandler("put", `/projects/${TEST_PROJECT_ID}/variables/${TEST_VAR_KEY}`, (req, res) => {
|
|
102
|
+
const scope = req.query["filter[environment_scope]"];
|
|
103
|
+
lastReceivedFilterScope = scope;
|
|
104
|
+
res.json({ ...MOCK_PROJECT_VARIABLE, ...req.body, environment_scope: scope ?? "*" });
|
|
105
|
+
});
|
|
106
|
+
mockServer.addMockHandler("delete", `/projects/${TEST_PROJECT_ID}/variables/${TEST_VAR_KEY}`, (req, res) => {
|
|
107
|
+
lastReceivedFilterScope = req.query["filter[environment_scope]"];
|
|
108
|
+
res.status(204).send();
|
|
109
|
+
});
|
|
110
|
+
// --- Group variable endpoints ---
|
|
111
|
+
mockServer.addMockHandler("get", `/groups/${TEST_GROUP_ID}/variables`, (_req, res) => {
|
|
112
|
+
res.json([MOCK_GROUP_VARIABLE]);
|
|
113
|
+
});
|
|
114
|
+
mockServer.addMockHandler("get", `/groups/${TEST_GROUP_ID}/variables/${TEST_GROUP_VAR_KEY}`, (_req, res) => {
|
|
115
|
+
res.json(MOCK_GROUP_VARIABLE);
|
|
116
|
+
});
|
|
117
|
+
mockServer.addMockHandler("post", `/groups/${TEST_GROUP_ID}/variables`, (req, res) => {
|
|
118
|
+
res.status(201).json({ ...MOCK_GROUP_VARIABLE, ...req.body });
|
|
119
|
+
});
|
|
120
|
+
mockServer.addMockHandler("put", `/groups/${TEST_GROUP_ID}/variables/${TEST_GROUP_VAR_KEY}`, (req, res) => {
|
|
121
|
+
res.json({ ...MOCK_GROUP_VARIABLE, ...req.body });
|
|
122
|
+
});
|
|
123
|
+
mockServer.addMockHandler("delete", `/groups/${TEST_GROUP_ID}/variables/${TEST_GROUP_VAR_KEY}`, (_req, res) => {
|
|
124
|
+
res.status(204).send();
|
|
125
|
+
});
|
|
126
|
+
await mockServer.start();
|
|
127
|
+
baseEnv = {
|
|
128
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
129
|
+
GITLAB_API_URL: `http://localhost:${mockPort}/api/v4`,
|
|
130
|
+
GITLAB_TOOLSETS: "variables",
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
after(async () => {
|
|
134
|
+
await mockServer.stop();
|
|
135
|
+
});
|
|
136
|
+
// --- Project variable tests ---
|
|
137
|
+
test("list_project_variables returns variable array", async () => {
|
|
138
|
+
const result = await callTool("list_project_variables", { project_id: TEST_PROJECT_ID }, baseEnv);
|
|
139
|
+
assert.ok(Array.isArray(result));
|
|
140
|
+
assert.strictEqual(result.length, 1);
|
|
141
|
+
assert.strictEqual(result[0].key, TEST_VAR_KEY);
|
|
142
|
+
assert.strictEqual(result[0].value, MOCK_PROJECT_VARIABLE.value);
|
|
143
|
+
assert.strictEqual(result[0].masked, true);
|
|
144
|
+
});
|
|
145
|
+
test("get_project_variable returns single variable", async () => {
|
|
146
|
+
const result = await callTool("get_project_variable", { project_id: TEST_PROJECT_ID, key: TEST_VAR_KEY }, baseEnv);
|
|
147
|
+
assert.strictEqual(result.key, TEST_VAR_KEY);
|
|
148
|
+
assert.strictEqual(result.environment_scope, "*");
|
|
149
|
+
assert.strictEqual(result.variable_type, "env_var");
|
|
150
|
+
});
|
|
151
|
+
test("get_project_variable returns hidden variable with null value", async () => {
|
|
152
|
+
const result = await callTool("get_project_variable", { project_id: TEST_PROJECT_ID, key: TEST_HIDDEN_VAR_KEY }, baseEnv);
|
|
153
|
+
assert.strictEqual(result.key, TEST_HIDDEN_VAR_KEY);
|
|
154
|
+
assert.strictEqual(result.value, null);
|
|
155
|
+
assert.strictEqual(result.hidden, true);
|
|
156
|
+
});
|
|
157
|
+
test("create_project_variable returns created variable", async () => {
|
|
158
|
+
const result = await callTool("create_project_variable", {
|
|
159
|
+
project_id: TEST_PROJECT_ID,
|
|
160
|
+
key: TEST_VAR_KEY,
|
|
161
|
+
value: "new-value",
|
|
162
|
+
masked: true,
|
|
163
|
+
}, baseEnv);
|
|
164
|
+
assert.strictEqual(result.key, TEST_VAR_KEY);
|
|
165
|
+
assert.strictEqual(result.value, "new-value");
|
|
166
|
+
});
|
|
167
|
+
test("update_project_variable returns updated variable", async () => {
|
|
168
|
+
const result = await callTool("update_project_variable", {
|
|
169
|
+
project_id: TEST_PROJECT_ID,
|
|
170
|
+
key: TEST_VAR_KEY,
|
|
171
|
+
value: "updated-value",
|
|
172
|
+
}, baseEnv);
|
|
173
|
+
assert.strictEqual(result.value, "updated-value");
|
|
174
|
+
});
|
|
175
|
+
test("delete_project_variable returns success status", async () => {
|
|
176
|
+
const result = await callTool("delete_project_variable", { project_id: TEST_PROJECT_ID, key: TEST_VAR_KEY }, baseEnv);
|
|
177
|
+
assert.strictEqual(result.status, "success");
|
|
178
|
+
assert.ok(result.message.includes(TEST_VAR_KEY));
|
|
179
|
+
});
|
|
180
|
+
// --- Group variable tests ---
|
|
181
|
+
test("list_group_variables returns variable array", async () => {
|
|
182
|
+
const result = await callTool("list_group_variables", { group_id: TEST_GROUP_ID }, baseEnv);
|
|
183
|
+
assert.ok(Array.isArray(result));
|
|
184
|
+
assert.strictEqual(result.length, 1);
|
|
185
|
+
assert.strictEqual(result[0].key, TEST_GROUP_VAR_KEY);
|
|
186
|
+
assert.strictEqual(result[0].masked, true);
|
|
187
|
+
});
|
|
188
|
+
test("get_group_variable returns single variable", async () => {
|
|
189
|
+
const result = await callTool("get_group_variable", { group_id: TEST_GROUP_ID, key: TEST_GROUP_VAR_KEY }, baseEnv);
|
|
190
|
+
assert.strictEqual(result.key, TEST_GROUP_VAR_KEY);
|
|
191
|
+
assert.strictEqual(result.variable_type, "env_var");
|
|
192
|
+
});
|
|
193
|
+
test("create_group_variable returns created variable", async () => {
|
|
194
|
+
const result = await callTool("create_group_variable", {
|
|
195
|
+
group_id: TEST_GROUP_ID,
|
|
196
|
+
key: TEST_GROUP_VAR_KEY,
|
|
197
|
+
value: "new-secret",
|
|
198
|
+
masked: true,
|
|
199
|
+
}, baseEnv);
|
|
200
|
+
assert.strictEqual(result.key, TEST_GROUP_VAR_KEY);
|
|
201
|
+
assert.strictEqual(result.value, "new-secret");
|
|
202
|
+
});
|
|
203
|
+
test("update_group_variable returns updated variable", async () => {
|
|
204
|
+
const result = await callTool("update_group_variable", {
|
|
205
|
+
group_id: TEST_GROUP_ID,
|
|
206
|
+
key: TEST_GROUP_VAR_KEY,
|
|
207
|
+
value: "updated-secret",
|
|
208
|
+
}, baseEnv);
|
|
209
|
+
assert.strictEqual(result.value, "updated-secret");
|
|
210
|
+
});
|
|
211
|
+
test("delete_group_variable returns success status", async () => {
|
|
212
|
+
const result = await callTool("delete_group_variable", { group_id: TEST_GROUP_ID, key: TEST_GROUP_VAR_KEY }, baseEnv);
|
|
213
|
+
assert.strictEqual(result.status, "success");
|
|
214
|
+
assert.ok(result.message.includes(TEST_GROUP_VAR_KEY));
|
|
215
|
+
});
|
|
216
|
+
// --- filter[environment_scope] tests ---
|
|
217
|
+
test("get_project_variable passes filter[environment_scope] to GitLab", async () => {
|
|
218
|
+
lastReceivedFilterScope = undefined;
|
|
219
|
+
const result = await callTool("get_project_variable", { project_id: TEST_PROJECT_ID, key: TEST_VAR_KEY, filter: { environment_scope: TEST_SCOPE } }, baseEnv);
|
|
220
|
+
assert.strictEqual(lastReceivedFilterScope, TEST_SCOPE);
|
|
221
|
+
assert.strictEqual(result.environment_scope, TEST_SCOPE);
|
|
222
|
+
});
|
|
223
|
+
test("update_project_variable passes filter[environment_scope] to GitLab", async () => {
|
|
224
|
+
lastReceivedFilterScope = undefined;
|
|
225
|
+
const result = await callTool("update_project_variable", {
|
|
226
|
+
project_id: TEST_PROJECT_ID,
|
|
227
|
+
key: TEST_VAR_KEY,
|
|
228
|
+
value: "scoped-value",
|
|
229
|
+
filter: { environment_scope: TEST_SCOPE },
|
|
230
|
+
}, baseEnv);
|
|
231
|
+
assert.strictEqual(lastReceivedFilterScope, TEST_SCOPE);
|
|
232
|
+
assert.strictEqual(result.environment_scope, TEST_SCOPE);
|
|
233
|
+
});
|
|
234
|
+
test("delete_project_variable passes filter[environment_scope] to GitLab", async () => {
|
|
235
|
+
lastReceivedFilterScope = undefined;
|
|
236
|
+
const result = await callTool("delete_project_variable", { project_id: TEST_PROJECT_ID, key: TEST_VAR_KEY, filter: { environment_scope: TEST_SCOPE } }, baseEnv);
|
|
237
|
+
assert.strictEqual(lastReceivedFilterScope, TEST_SCOPE);
|
|
238
|
+
assert.strictEqual(result.status, "success");
|
|
239
|
+
});
|
|
240
|
+
// --- Toolset behaviour ---
|
|
241
|
+
test("variables tools are absent when toolset is not activated", async () => {
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
const proc = spawn("node", ["build/index.js"], {
|
|
244
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
245
|
+
env: {
|
|
246
|
+
...process.env,
|
|
247
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
248
|
+
GITLAB_API_URL: `http://localhost:${mockPort}/api/v4`,
|
|
249
|
+
// No GITLAB_TOOLSETS — default toolsets only
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
let output = "";
|
|
253
|
+
proc.stdout?.on("data", (d) => (output += d));
|
|
254
|
+
proc.on("close", () => {
|
|
255
|
+
try {
|
|
256
|
+
const line = output.split("\n").find(l => l.startsWith("{"));
|
|
257
|
+
if (!line)
|
|
258
|
+
return reject(new Error("No JSON output found"));
|
|
259
|
+
const response = JSON.parse(line);
|
|
260
|
+
const names = (response.result?.tools ?? []).map((t) => t.name);
|
|
261
|
+
assert.ok(!names.includes("list_project_variables"), "tool should not be in default toolset");
|
|
262
|
+
assert.ok(!names.includes("create_project_variable"), "tool should not be in default toolset");
|
|
263
|
+
assert.ok(!names.includes("list_group_variables"), "tool should not be in default toolset");
|
|
264
|
+
resolve();
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
reject(e);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
proc.stdin?.end(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }) + "\n");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
test("write tools are absent from tools/list in read-only mode", async () => {
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
const proc = spawn("node", ["build/index.js"], {
|
|
276
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
277
|
+
env: { ...process.env, ...baseEnv, GITLAB_READ_ONLY_MODE: "true" },
|
|
278
|
+
});
|
|
279
|
+
let output = "";
|
|
280
|
+
proc.stdout?.on("data", (d) => (output += d));
|
|
281
|
+
proc.on("close", () => {
|
|
282
|
+
try {
|
|
283
|
+
const line = output.split("\n").find(l => l.startsWith("{"));
|
|
284
|
+
if (!line)
|
|
285
|
+
return reject(new Error("No JSON output found"));
|
|
286
|
+
const response = JSON.parse(line);
|
|
287
|
+
const names = (response.result?.tools ?? []).map((t) => t.name);
|
|
288
|
+
assert.ok(!names.includes("create_project_variable"), "create should be absent in read-only mode");
|
|
289
|
+
assert.ok(!names.includes("update_project_variable"), "update should be absent in read-only mode");
|
|
290
|
+
assert.ok(!names.includes("delete_project_variable"), "delete should be absent in read-only mode");
|
|
291
|
+
assert.ok(!names.includes("create_group_variable"), "create should be absent in read-only mode");
|
|
292
|
+
assert.ok(!names.includes("delete_group_variable"), "delete should be absent in read-only mode");
|
|
293
|
+
assert.ok(names.includes("list_project_variables"), "list should be present in read-only mode");
|
|
294
|
+
assert.ok(names.includes("get_project_variable"), "get should be present in read-only mode");
|
|
295
|
+
assert.ok(names.includes("list_group_variables"), "list should be present in read-only mode");
|
|
296
|
+
assert.ok(names.includes("get_group_variable"), "get should be present in read-only mode");
|
|
297
|
+
resolve();
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
reject(e);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
proc.stdin?.end(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }) + "\n");
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -271,6 +271,7 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
|
|
|
271
271
|
REMOTE_AUTHORIZATION: 'true',
|
|
272
272
|
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
273
273
|
GITLAB_PROJECT_ID: DEFAULT_PROJECT_ID,
|
|
274
|
+
GITLAB_TOOLSETS: 'variables',
|
|
274
275
|
}
|
|
275
276
|
});
|
|
276
277
|
servers.push(server);
|
|
@@ -317,6 +318,26 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
|
|
|
317
318
|
assert.ok(error.message.includes('create_group is not allowed'), 'Should mention create_group');
|
|
318
319
|
}
|
|
319
320
|
});
|
|
321
|
+
test('should reject list_group_variables when GITLAB_PROJECT_ID is set', async () => {
|
|
322
|
+
try {
|
|
323
|
+
await client.callTool('list_group_variables', { group_id: 'my-group' });
|
|
324
|
+
assert.fail('Should have rejected list_group_variables');
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
assert.ok(error instanceof Error);
|
|
328
|
+
assert.ok(error.message.includes('list_group_variables is not allowed'), 'Should mention list_group_variables');
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
test('should reject get_group_variable when GITLAB_PROJECT_ID is set', async () => {
|
|
332
|
+
try {
|
|
333
|
+
await client.callTool('get_group_variable', { group_id: 'my-group', key: 'SHARED_SECRET' });
|
|
334
|
+
assert.fail('Should have rejected get_group_variable');
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
assert.ok(error instanceof Error);
|
|
338
|
+
assert.ok(error.message.includes('get_group_variable is not allowed'), 'Should mention get_group_variable');
|
|
339
|
+
}
|
|
340
|
+
});
|
|
320
341
|
test('should allow get_project (non-mutator) when GITLAB_PROJECT_ID is set', async () => {
|
|
321
342
|
const result = await client.callTool('get_project', { project_id: '' });
|
|
322
343
|
assert.ok(result.content, 'Should have content');
|
|
@@ -348,6 +369,7 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
|
|
|
348
369
|
REMOTE_AUTHORIZATION: 'true',
|
|
349
370
|
GITLAB_API_URL: `${mockGitLabUrl}/api/v4`,
|
|
350
371
|
GITLAB_ALLOWED_PROJECT_IDS: DEFAULT_PROJECT_ID,
|
|
372
|
+
GITLAB_TOOLSETS: 'variables',
|
|
351
373
|
}
|
|
352
374
|
});
|
|
353
375
|
servers.push(server);
|
|
@@ -394,6 +416,26 @@ describe('getEffectiveProjectId', { concurrency: 1 }, () => {
|
|
|
394
416
|
assert.ok(error.message.includes('create_group is not allowed'), 'Should mention create_group');
|
|
395
417
|
}
|
|
396
418
|
});
|
|
419
|
+
test('should reject list_group_variables with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
420
|
+
try {
|
|
421
|
+
await client.callTool('list_group_variables', { group_id: 'my-group' });
|
|
422
|
+
assert.fail('Should have rejected list_group_variables');
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
assert.ok(error instanceof Error);
|
|
426
|
+
assert.ok(error.message.includes('list_group_variables is not allowed'), 'Should mention list_group_variables');
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
test('should reject get_group_variable with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
430
|
+
try {
|
|
431
|
+
await client.callTool('get_group_variable', { group_id: 'my-group', key: 'SHARED_SECRET' });
|
|
432
|
+
assert.fail('Should have rejected get_group_variable');
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
assert.ok(error instanceof Error);
|
|
436
|
+
assert.ok(error.message.includes('get_group_variable is not allowed'), 'Should mention get_group_variable');
|
|
437
|
+
}
|
|
438
|
+
});
|
|
397
439
|
test('should allow get_project (non-mutator) with GITLAB_ALLOWED_PROJECT_IDS', async () => {
|
|
398
440
|
const result = await client.callTool('get_project', { project_id: '' });
|
|
399
441
|
assert.ok(result.content, 'Should have content');
|
|
@@ -33,6 +33,7 @@ const TOOLSET_TOOL_COUNTS = {
|
|
|
33
33
|
workitems: 18,
|
|
34
34
|
webhooks: 3,
|
|
35
35
|
groups: 1,
|
|
36
|
+
variables: 10,
|
|
36
37
|
};
|
|
37
38
|
const LEGACY_PIPELINE_TOOL_COUNT = TOOLSET_TOOL_COUNTS.pipelines + TOOLSET_TOOL_COUNTS.ci;
|
|
38
39
|
const DEFAULT_TOOLSETS = [
|
|
@@ -55,6 +56,7 @@ const NON_DEFAULT_TOOLSETS = [
|
|
|
55
56
|
"workitems",
|
|
56
57
|
"webhooks",
|
|
57
58
|
"search",
|
|
59
|
+
"variables",
|
|
58
60
|
];
|
|
59
61
|
// discover_tools meta-tool is always force-injected (Step 5.5)
|
|
60
62
|
const DISCOVER_TOOLS_COUNT = 1;
|
|
@@ -78,6 +80,7 @@ const TOOLSET_SAMPLE_TOOLS = {
|
|
|
78
80
|
search: ["search_code", "search_project_code", "search_group_code"],
|
|
79
81
|
webhooks: ["list_webhooks", "list_webhook_events", "get_webhook_event"],
|
|
80
82
|
groups: ["create_group"],
|
|
83
|
+
variables: ["list_project_variables", "create_project_variable", "delete_project_variable", "list_group_variables", "create_group_variable", "delete_group_variable"],
|
|
81
84
|
};
|
|
82
85
|
// --- Helpers ---
|
|
83
86
|
async function launchMcpServer(mockGitLabUrl, mcpPort, extraEnv = {}) {
|
package/build/tools/registry.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
2
2
|
import { toJSONSchema } from "../utils/schema.js";
|
|
3
3
|
import { USE_GITLAB_WIKI, USE_MILESTONE, USE_PIPELINE, SSE, STREAMABLE_HTTP, } from "../config.js";
|
|
4
|
-
import { ApproveMergeRequestSchema, BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, ConvertWorkItemTypeSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateGroupSchema, CreateGroupWikiPageSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, MarkAllTodosDoneSchema, ListTodosSchema, MarkTodoDoneSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestNoteSchema, CreateMergeRequestNoteEmojiReactionSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateReleaseEvidenceSchema, CreateReleaseSchema, CreateRepositorySchema, CreateTagSchema, CreateTimelineEventSchema, CreateWikiPageSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, CreateWorkItemSchema, DeleteBranchSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteMergeRequestDiscussionNoteSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, DeleteProjectMilestoneSchema, DeleteReleaseSchema, DeleteTagSchema, DeleteWikiPageSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, DownloadAttachmentSchema, DownloadAttachmentRemoteSchema, DownloadJobArtifactsSchema, DownloadJobArtifactsRemoteSchema, DownloadReleaseAssetSchema, EditProjectMilestoneSchema, ExecuteGraphQLSchema, ForkRepositorySchema, HealthCheckSchema, GetBranchSchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GetDeploymentSchema, GetDraftNoteSchema, GetEnvironmentSchema, GetFileContentsSchema, GetGroupWikiPageSchema, GetIssueLinkSchema, GetIssueSchema, GetJobArtifactFileSchema, GetLabelSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GetMergeRequestDiffsSchema, GetMergeRequestFileDiffSchema, GetMergeRequestNoteSchema, GetMergeRequestNotesSchema, GetMergeRequestSchema, GetMergeRequestVersionSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectEventsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetReleaseSchema, GetRepositoryTreeSchema, GetTagSchema, GetTagSignatureSchema, GetTimelineEventsSchema, GetUsersSchema, GetUserSchema, WhoAmISchema, GetWebhookEventSchema, GetWikiPageSchema, GetWorkItemSchema, ListBranchesSchema, ListCommitsSchema, ListCommitStatusesSchema, ListCustomFieldDefinitionsSchema, ListDeploymentsSchema, ListDraftNotesSchema, ListEnvironmentsSchema, ListEventsSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListGroupWikiPagesSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListJobArtifactsSchema, ListLabelsSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiffsSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestVersionsSchema, ListMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListPipelinesSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListReleasesSchema, ListTagsSchema, ListWebhookEventsSchema, ListWebhooksSchema, ListWikiPagesSchema, ListWorkItemNotesSchema, ListWorkItemStatusesSchema, ListWorkItemsSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, MergeMergeRequestSchema, MoveWorkItemSchema, MyIssuesSchema, PlayPipelineJobSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PushFilesSchema, ResolveMergeRequestThreadSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UnapproveMergeRequestSchema, UpdateDraftNoteSchema, UpdateGroupWikiPageSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateReleaseSchema, UpdateWikiPageSchema, UpdateWorkItemSchema, VerifyNamespaceSchema, } from "../schemas.js";
|
|
4
|
+
import { ApproveMergeRequestSchema, BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, ConvertWorkItemTypeSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateGroupSchema, CreateGroupWikiPageSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, MarkAllTodosDoneSchema, ListTodosSchema, MarkTodoDoneSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestNoteSchema, CreateMergeRequestNoteEmojiReactionSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateReleaseEvidenceSchema, CreateReleaseSchema, CreateRepositorySchema, CreateTagSchema, CreateTimelineEventSchema, CreateWikiPageSchema, CreateWorkItemNoteSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, ListWorkItemEmojiReactionsSchema, ListWorkItemNoteEmojiReactionsSchema, CreateWorkItemSchema, DeleteBranchSchema, DeleteDraftNoteSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteMergeRequestDiscussionNoteSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, DeleteProjectMilestoneSchema, DeleteReleaseSchema, DeleteTagSchema, DeleteWikiPageSchema, DeleteWorkItemEmojiReactionSchema, DeleteWorkItemNoteEmojiReactionSchema, DownloadAttachmentSchema, DownloadAttachmentRemoteSchema, DownloadJobArtifactsSchema, DownloadJobArtifactsRemoteSchema, DownloadReleaseAssetSchema, EditProjectMilestoneSchema, ExecuteGraphQLSchema, ForkRepositorySchema, HealthCheckSchema, GetBranchSchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GetDeploymentSchema, GetDraftNoteSchema, GetEnvironmentSchema, GetFileContentsSchema, GetGroupWikiPageSchema, GetIssueLinkSchema, GetIssueSchema, GetJobArtifactFileSchema, GetLabelSchema, GetMergeRequestApprovalStateSchema, GetMergeRequestConflictsSchema, GetMergeRequestDiffsSchema, GetMergeRequestFileDiffSchema, GetMergeRequestNoteSchema, GetMergeRequestNotesSchema, GetMergeRequestSchema, GetMergeRequestVersionSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema, GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectEventsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetReleaseSchema, GetRepositoryTreeSchema, GetTagSchema, GetTagSignatureSchema, GetTimelineEventsSchema, GetUsersSchema, GetUserSchema, WhoAmISchema, GetWebhookEventSchema, GetWikiPageSchema, GetWorkItemSchema, ListBranchesSchema, ListCommitsSchema, ListCommitStatusesSchema, ListCustomFieldDefinitionsSchema, ListDeploymentsSchema, ListDraftNotesSchema, ListEnvironmentsSchema, ListEventsSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListGroupWikiPagesSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListJobArtifactsSchema, ListLabelsSchema, ListMergeRequestChangedFilesSchema, ListMergeRequestDiffsSchema, ListMergeRequestDiscussionsSchema, ListMergeRequestPipelinesSchema, ListMergeRequestVersionsSchema, ListMergeRequestsSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelineTriggerJobsSchema, ValidateCiLintSchema, ValidateProjectCiLintSchema, ListPipelinesSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListReleasesSchema, ListTagsSchema, ListWebhookEventsSchema, ListWebhooksSchema, ListWikiPagesSchema, ListWorkItemNotesSchema, ListWorkItemStatusesSchema, ListWorkItemsSchema, MarkdownUploadSchema, MarkdownUploadRemoteSchema, MergeMergeRequestSchema, MoveWorkItemSchema, MyIssuesSchema, PlayPipelineJobSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PushFilesSchema, ResolveMergeRequestThreadSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchCodeSchema, SearchGroupCodeSchema, SearchProjectCodeSchema, SearchRepositoriesSchema, UnapproveMergeRequestSchema, UpdateDraftNoteSchema, UpdateGroupWikiPageSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateIssueDescriptionPatchSchema, UpdateLabelSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestSchema, UpdateReleaseSchema, UpdateWikiPageSchema, UpdateWorkItemSchema, VerifyNamespaceSchema, ListProjectVariablesSchema, GetProjectVariableSchema, CreateProjectVariableSchema, UpdateProjectVariableSchema, DeleteProjectVariableSchema, ListGroupVariablesSchema, GetGroupVariableSchema, CreateGroupVariableSchema, UpdateGroupVariableSchema, DeleteGroupVariableSchema, } from "../schemas.js";
|
|
5
5
|
const IS_REMOTE = SSE || STREAMABLE_HTTP;
|
|
6
6
|
// Define all available tools
|
|
7
7
|
export const allTools = [
|
|
@@ -931,6 +931,57 @@ export const allTools = [
|
|
|
931
931
|
description: "Search for code within a specific group (requires advanced search or Zoekt)",
|
|
932
932
|
inputSchema: toJSONSchema(SearchGroupCodeSchema),
|
|
933
933
|
},
|
|
934
|
+
// --- CI/CD Variable tools ---
|
|
935
|
+
{
|
|
936
|
+
name: "list_project_variables",
|
|
937
|
+
description: "List CI/CD variables for a project",
|
|
938
|
+
inputSchema: toJSONSchema(ListProjectVariablesSchema),
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
name: "get_project_variable",
|
|
942
|
+
description: "Get a single CI/CD variable from a project",
|
|
943
|
+
inputSchema: toJSONSchema(GetProjectVariableSchema),
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
name: "create_project_variable",
|
|
947
|
+
description: "Create a CI/CD variable for a project",
|
|
948
|
+
inputSchema: toJSONSchema(CreateProjectVariableSchema),
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
name: "update_project_variable",
|
|
952
|
+
description: "Update an existing CI/CD variable in a project",
|
|
953
|
+
inputSchema: toJSONSchema(UpdateProjectVariableSchema),
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
name: "delete_project_variable",
|
|
957
|
+
description: "Delete a CI/CD variable from a project",
|
|
958
|
+
inputSchema: toJSONSchema(DeleteProjectVariableSchema),
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
name: "list_group_variables",
|
|
962
|
+
description: "List CI/CD variables for a group",
|
|
963
|
+
inputSchema: toJSONSchema(ListGroupVariablesSchema),
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
name: "get_group_variable",
|
|
967
|
+
description: "Get a single CI/CD variable from a group",
|
|
968
|
+
inputSchema: toJSONSchema(GetGroupVariableSchema),
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
name: "create_group_variable",
|
|
972
|
+
description: "Create a CI/CD variable for a group",
|
|
973
|
+
inputSchema: toJSONSchema(CreateGroupVariableSchema),
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
name: "update_group_variable",
|
|
977
|
+
description: "Update an existing CI/CD variable in a group",
|
|
978
|
+
inputSchema: toJSONSchema(UpdateGroupVariableSchema),
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
name: "delete_group_variable",
|
|
982
|
+
description: "Delete a CI/CD variable from a group",
|
|
983
|
+
inputSchema: toJSONSchema(DeleteGroupVariableSchema),
|
|
984
|
+
},
|
|
934
985
|
// --- Meta tool: Dynamic tool discovery ---
|
|
935
986
|
{
|
|
936
987
|
name: "discover_tools",
|
|
@@ -1050,6 +1101,10 @@ export const readOnlyTools = new Set([
|
|
|
1050
1101
|
"list_webhooks",
|
|
1051
1102
|
"list_webhook_events",
|
|
1052
1103
|
"get_webhook_event",
|
|
1104
|
+
"list_project_variables",
|
|
1105
|
+
"get_project_variable",
|
|
1106
|
+
"list_group_variables",
|
|
1107
|
+
"get_group_variable",
|
|
1053
1108
|
]);
|
|
1054
1109
|
// Define which tools are destructive (data loss potential)
|
|
1055
1110
|
export const destructiveTools = new Set([
|
|
@@ -1073,7 +1128,8 @@ export const destructiveTools = new Set([
|
|
|
1073
1128
|
"delete_branch",
|
|
1074
1129
|
"merge_merge_request",
|
|
1075
1130
|
"push_files",
|
|
1076
|
-
"
|
|
1131
|
+
"delete_project_variable",
|
|
1132
|
+
"delete_group_variable",
|
|
1077
1133
|
]);
|
|
1078
1134
|
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
|
|
1079
1135
|
export const wikiToolNames = new Set([
|
|
@@ -1401,6 +1457,22 @@ export const TOOLSET_DEFINITIONS = [
|
|
|
1401
1457
|
isDefault: false,
|
|
1402
1458
|
tools: new Set(["search_code", "search_project_code", "search_group_code"]),
|
|
1403
1459
|
},
|
|
1460
|
+
{
|
|
1461
|
+
id: "variables",
|
|
1462
|
+
isDefault: false,
|
|
1463
|
+
tools: new Set([
|
|
1464
|
+
"list_project_variables",
|
|
1465
|
+
"get_project_variable",
|
|
1466
|
+
"create_project_variable",
|
|
1467
|
+
"update_project_variable",
|
|
1468
|
+
"delete_project_variable",
|
|
1469
|
+
"list_group_variables",
|
|
1470
|
+
"get_group_variable",
|
|
1471
|
+
"create_group_variable",
|
|
1472
|
+
"update_group_variable",
|
|
1473
|
+
"delete_group_variable",
|
|
1474
|
+
]),
|
|
1475
|
+
},
|
|
1404
1476
|
];
|
|
1405
1477
|
// Derived lookup: tool name → toolset ID
|
|
1406
1478
|
export const TOOLSET_BY_TOOL_NAME = new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.17",
|
|
4
4
|
"mcpName": "io.github.zereight/gitlab-mcp",
|
|
5
5
|
"description": "GitLab MCP server for projects, merge requests, issues, pipelines, wiki, releases, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"changelog": "auto-changelog -p",
|
|
52
52
|
"test": "npm run test:all",
|
|
53
53
|
"test:all": "npm run build && npm run test:mock && npm run test:live",
|
|
54
|
-
"test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-upload-markdown.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts && node --import tsx/esm --test test/utils/tool-args.test.ts && node --import tsx/esm --test test/utils/merge-request-position.test.ts && node --import tsx/esm --test test/nullish-tool-arguments-schema.test.ts",
|
|
54
|
+
"test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-upload-markdown.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts && node --import tsx/esm --test test/utils/tool-args.test.ts && node --import tsx/esm --test test/utils/merge-request-position.test.ts && node --import tsx/esm --test test/nullish-tool-arguments-schema.test.ts && node --import tsx/esm --test test/test-ci-variables.ts",
|
|
55
55
|
"test:stateless": "npm run build && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
|
|
56
56
|
"test:mcp-oauth": "npm run build && node --import tsx/esm --test test/mcp-oauth-tests.ts",
|
|
57
57
|
"test:live": "node test/validate-api.js",
|