Skip to content

Commit 9b5c118

Browse files
NIFr3dfrederic.wagner
andauthored
feat: implement gitlab releases endpoint (#259)
* feat: implement release endpoint * fix: revert lint --------- Co-authored-by: frederic.wagner <[email protected]>
1 parent db4f724 commit 9b5c118

File tree

3 files changed

+518
-2
lines changed

3 files changed

+518
-2
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,13 @@ The token is stored per session (identified by `mcp-session-id` header) and reus
363363
86. `download_attachment` - Download an uploaded file from a GitLab project by secret and filename
364364
87. `list_events` - List all events for the currently authenticated user
365365
88. `get_project_events` - List all visible events for a specified project
366+
89. `list_releases` - List all releases for a project
367+
90. `get_release` - Get a release by tag name
368+
91. `create_release` - Create a new release in a GitLab project
369+
92. `update_release` - Update an existing release in a GitLab project
370+
93. `delete_release` - Delete a release from a GitLab project (does not delete the associated tag)
371+
94. `create_release_evidence` - Create release evidence for an existing release (GitLab Premium/Ultimate only)
372+
95. `download_release_asset` - Download a release asset file by direct asset path
366373
<!-- TOOLS-END -->
367374

368375
</details>

index.ts

Lines changed: 328 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,16 @@ import {
197197
ListEventsSchema,
198198
GetProjectEventsSchema,
199199
GitLabEvent,
200-
ExecuteGraphQLSchema
200+
ExecuteGraphQLSchema,
201+
type GitLabRelease,
202+
GitLabReleaseSchema,
203+
ListReleasesSchema,
204+
GetReleaseSchema,
205+
CreateReleaseSchema,
206+
UpdateReleaseSchema,
207+
DeleteReleaseSchema,
208+
CreateReleaseEvidenceSchema,
209+
DownloadReleaseAssetSchema,
201210
} from "./schemas.js";
202211

203212
import { randomUUID } from "crypto";
@@ -973,6 +982,41 @@ const allTools = [
973982
description: "List all visible events for a specified project. Note: before/after parameters accept date format YYYY-MM-DD only",
974983
inputSchema: toJSONSchema(GetProjectEventsSchema),
975984
},
985+
{
986+
name: "list_releases",
987+
description: "List all releases for a project",
988+
inputSchema: toJSONSchema(ListReleasesSchema),
989+
},
990+
{
991+
name: "get_release",
992+
description: "Get a release by tag name",
993+
inputSchema: toJSONSchema(GetReleaseSchema),
994+
},
995+
{
996+
name: "create_release",
997+
description: "Create a new release in a GitLab project",
998+
inputSchema: toJSONSchema(CreateReleaseSchema),
999+
},
1000+
{
1001+
name: "update_release",
1002+
description: "Update an existing release in a GitLab project",
1003+
inputSchema: toJSONSchema(UpdateReleaseSchema),
1004+
},
1005+
{
1006+
name: "delete_release",
1007+
description: "Delete a release from a GitLab project (does not delete the associated tag)",
1008+
inputSchema: toJSONSchema(DeleteReleaseSchema),
1009+
},
1010+
{
1011+
name: "create_release_evidence",
1012+
description: "Create release evidence for an existing release (GitLab Premium/Ultimate only)",
1013+
inputSchema: toJSONSchema(CreateReleaseEvidenceSchema),
1014+
},
1015+
{
1016+
name: "download_release_asset",
1017+
description: "Download a release asset file by direct asset path",
1018+
inputSchema: toJSONSchema(DownloadReleaseAssetSchema),
1019+
},
9761020
];
9771021

9781022
// Define which tools are read-only
@@ -1023,6 +1067,9 @@ const readOnlyTools = [
10231067
"download_attachment",
10241068
"list_events",
10251069
"get_project_events",
1070+
"list_releases",
1071+
"get_release",
1072+
"download_release_asset",
10261073
];
10271074

10281075
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
@@ -4461,6 +4508,200 @@ async function getProjectEvents(projectId: string, options: Omit<z.infer<typeof
44614508
return GitLabEventSchema.array().parse(data);
44624509
}
44634510

4511+
/**
4512+
* List all releases for a project
4513+
*
4514+
* @param projectId The ID or URL-encoded path of the project
4515+
* @param options Optional parameters for listing releases
4516+
* @returns Array of GitLab releases
4517+
*/
4518+
async function listReleases(
4519+
projectId: string,
4520+
options: Omit<z.infer<typeof ListReleasesSchema>, "project_id"> = {}
4521+
): Promise<GitLabRelease[]> {
4522+
const effectiveProjectId = getEffectiveProjectId(projectId);
4523+
const url = new URL(
4524+
`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/releases`
4525+
);
4526+
4527+
// Add query parameters
4528+
Object.entries(options).forEach(([key, value]) => {
4529+
if (value !== undefined) {
4530+
url.searchParams.append(key, value.toString());
4531+
}
4532+
});
4533+
4534+
const response = await fetch(url.toString(), {
4535+
...DEFAULT_FETCH_CONFIG,
4536+
});
4537+
4538+
await handleGitLabError(response);
4539+
4540+
const data = await response.json();
4541+
return GitLabReleaseSchema.array().parse(data);
4542+
}
4543+
4544+
/**
4545+
* Get a release by tag name
4546+
*
4547+
* @param projectId The ID or URL-encoded path of the project
4548+
* @param tagName The Git tag the release is associated with
4549+
* @param includeHtmlDescription If true, includes HTML rendered Markdown
4550+
* @returns GitLab release
4551+
*/
4552+
async function getRelease(
4553+
projectId: string,
4554+
tagName: string,
4555+
includeHtmlDescription?: boolean
4556+
): Promise<GitLabRelease> {
4557+
const effectiveProjectId = getEffectiveProjectId(projectId);
4558+
const url = new URL(
4559+
`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tagName)}`
4560+
);
4561+
4562+
if (includeHtmlDescription !== undefined) {
4563+
url.searchParams.append("include_html_description", includeHtmlDescription.toString());
4564+
}
4565+
4566+
const response = await fetch(url.toString(), {
4567+
...DEFAULT_FETCH_CONFIG,
4568+
});
4569+
4570+
await handleGitLabError(response);
4571+
4572+
const data = await response.json();
4573+
return GitLabReleaseSchema.parse(data);
4574+
}
4575+
4576+
/**
4577+
* Create a new release
4578+
*
4579+
* @param projectId The ID or URL-encoded path of the project
4580+
* @param options Options for creating the release
4581+
* @returns Created GitLab release
4582+
*/
4583+
async function createRelease(
4584+
projectId: string,
4585+
options: Omit<z.infer<typeof CreateReleaseSchema>, "project_id">
4586+
): Promise<GitLabRelease> {
4587+
const effectiveProjectId = getEffectiveProjectId(projectId);
4588+
4589+
const response = await fetch(
4590+
`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/releases`,
4591+
{
4592+
...DEFAULT_FETCH_CONFIG,
4593+
method: "POST",
4594+
body: JSON.stringify(options),
4595+
}
4596+
);
4597+
4598+
await handleGitLabError(response);
4599+
4600+
const data = await response.json();
4601+
return GitLabReleaseSchema.parse(data);
4602+
}
4603+
4604+
/**
4605+
* Update an existing release
4606+
*
4607+
* @param projectId The ID or URL-encoded path of the project
4608+
* @param tagName The Git tag the release is associated with
4609+
* @param options Options for updating the release
4610+
* @returns Updated GitLab release
4611+
*/
4612+
async function updateRelease(
4613+
projectId: string,
4614+
tagName: string,
4615+
options: Omit<z.infer<typeof UpdateReleaseSchema>, "project_id" | "tag_name">
4616+
): Promise<GitLabRelease> {
4617+
const effectiveProjectId = getEffectiveProjectId(projectId);
4618+
4619+
const response = await fetch(
4620+
`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tagName)}`,
4621+
{
4622+
...DEFAULT_FETCH_CONFIG,
4623+
method: "PUT",
4624+
body: JSON.stringify(options),
4625+
}
4626+
);
4627+
4628+
await handleGitLabError(response);
4629+
4630+
const data = await response.json();
4631+
return GitLabReleaseSchema.parse(data);
4632+
}
4633+
4634+
/**
4635+
* Delete a release
4636+
*
4637+
* @param projectId The ID or URL-encoded path of the project
4638+
* @param tagName The Git tag the release is associated with
4639+
* @returns Deleted GitLab release
4640+
*/
4641+
async function deleteRelease(projectId: string, tagName: string): Promise<GitLabRelease> {
4642+
const effectiveProjectId = getEffectiveProjectId(projectId);
4643+
4644+
const response = await fetch(
4645+
`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tagName)}`,
4646+
{
4647+
...DEFAULT_FETCH_CONFIG,
4648+
method: "DELETE",
4649+
}
4650+
);
4651+
4652+
await handleGitLabError(response);
4653+
4654+
const data = await response.json();
4655+
return GitLabReleaseSchema.parse(data);
4656+
}
4657+
4658+
/**
4659+
* Create release evidence (GitLab Premium/Ultimate only)
4660+
*
4661+
* @param projectId The ID or URL-encoded path of the project
4662+
* @param tagName The Git tag the release is associated with
4663+
*/
4664+
async function createReleaseEvidence(projectId: string, tagName: string): Promise<void> {
4665+
const effectiveProjectId = getEffectiveProjectId(projectId);
4666+
4667+
const response = await fetch(
4668+
`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tagName)}/evidence`,
4669+
{
4670+
...DEFAULT_FETCH_CONFIG,
4671+
method: "POST",
4672+
}
4673+
);
4674+
4675+
await handleGitLabError(response);
4676+
}
4677+
4678+
/**
4679+
* Download a release asset
4680+
*
4681+
* @param projectId The ID or URL-encoded path of the project
4682+
* @param tagName The Git tag the release is associated with
4683+
* @param directAssetPath Path to the release asset file
4684+
* @returns The asset file content
4685+
*/
4686+
async function downloadReleaseAsset(
4687+
projectId: string,
4688+
tagName: string,
4689+
directAssetPath: string
4690+
): Promise<string> {
4691+
const effectiveProjectId = getEffectiveProjectId(projectId);
4692+
4693+
const response = await fetch(
4694+
`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/releases/${encodeURIComponent(tagName)}/downloads/${directAssetPath}`,
4695+
{
4696+
...DEFAULT_FETCH_CONFIG,
4697+
}
4698+
);
4699+
4700+
await handleGitLabError(response);
4701+
4702+
return await response.text();
4703+
}
4704+
44644705
server.setRequestHandler(ListToolsRequestSchema, async () => {
44654706
// Apply read-only filter first
44664707
const tools0 = GITLAB_READ_ONLY_MODE
@@ -5651,6 +5892,91 @@ server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
56515892
};
56525893
}
56535894

5895+
case "list_releases": {
5896+
const args = ListReleasesSchema.parse(request.params.arguments);
5897+
const { project_id, ...options } = args;
5898+
const releases = await listReleases(project_id, options);
5899+
return {
5900+
content: [{ type: "text", text: JSON.stringify(releases, null, 2) }],
5901+
};
5902+
}
5903+
5904+
case "get_release": {
5905+
const args = GetReleaseSchema.parse(request.params.arguments);
5906+
const release = await getRelease(
5907+
args.project_id,
5908+
args.tag_name,
5909+
args.include_html_description
5910+
);
5911+
return {
5912+
content: [{ type: "text", text: JSON.stringify(release, null, 2) }],
5913+
};
5914+
}
5915+
5916+
case "create_release": {
5917+
const args = CreateReleaseSchema.parse(request.params.arguments);
5918+
const { project_id, ...options } = args;
5919+
const release = await createRelease(project_id, options);
5920+
return {
5921+
content: [{ type: "text", text: JSON.stringify(release, null, 2) }],
5922+
};
5923+
}
5924+
5925+
case "update_release": {
5926+
const args = UpdateReleaseSchema.parse(request.params.arguments);
5927+
const { project_id, tag_name, ...options } = args;
5928+
const release = await updateRelease(project_id, tag_name, options);
5929+
return {
5930+
content: [{ type: "text", text: JSON.stringify(release, null, 2) }],
5931+
};
5932+
}
5933+
5934+
case "delete_release": {
5935+
const args = DeleteReleaseSchema.parse(request.params.arguments);
5936+
const release = await deleteRelease(args.project_id, args.tag_name);
5937+
return {
5938+
content: [
5939+
{
5940+
type: "text",
5941+
text: JSON.stringify(
5942+
{ status: "success", message: "Release deleted successfully", release },
5943+
null,
5944+
2
5945+
),
5946+
},
5947+
],
5948+
};
5949+
}
5950+
5951+
case "create_release_evidence": {
5952+
const args = CreateReleaseEvidenceSchema.parse(request.params.arguments);
5953+
await createReleaseEvidence(args.project_id, args.tag_name);
5954+
return {
5955+
content: [
5956+
{
5957+
type: "text",
5958+
text: JSON.stringify(
5959+
{ status: "success", message: "Release evidence created successfully" },
5960+
null,
5961+
2
5962+
),
5963+
},
5964+
],
5965+
};
5966+
}
5967+
5968+
case "download_release_asset": {
5969+
const args = DownloadReleaseAssetSchema.parse(request.params.arguments);
5970+
const assetContent = await downloadReleaseAsset(
5971+
args.project_id,
5972+
args.tag_name,
5973+
args.direct_asset_path
5974+
);
5975+
return {
5976+
content: [{ type: "text", text: assetContent }],
5977+
};
5978+
}
5979+
56545980
default:
56555981
throw new Error(`Unknown tool: ${request.params.name}`);
56565982
}
@@ -6165,4 +6491,4 @@ async function runServer() {
61656491
runServer().catch(error => {
61666492
logger.error("Fatal error in main():", error);
61676493
process.exit(1);
6168-
});
6494+
});

0 commit comments

Comments
 (0)