Skip to content

Commit 7124315

Browse files
Merge pull request #211 from inspired-geek/main
feat(tags): Implement GitLab tags API functionality
2 parents ca89101 + afaebf8 commit 7124315

File tree

3 files changed

+279
-0
lines changed

3 files changed

+279
-0
lines changed

src/gitlabhandler.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ import {
113113
CreateProjectMilestoneSchema,
114114
EditProjectMilestoneSchema,
115115
CreateMergeRequestSchema,
116+
// Tags
117+
ListTagsSchema,
118+
GetTagSchema,
119+
CreateTagSchema,
120+
DeleteTagSchema,
121+
GetTagSignatureSchema,
122+
GitLabTagSchema,
123+
GitLabTagSignatureSchema,
124+
type GitLabTag,
125+
type GitLabTagSignature,
116126
} from "./schemas.js";
117127
import {GitlabSession} from "./gitlabsession.js";
118128

@@ -2551,4 +2561,117 @@ const url = new URL(`${config.GITLAB_API_URL}/namespaces`);
25512561

25522562
return savePath;
25532563
}
2564+
2565+
/**
2566+
* List project repository tags
2567+
*/
2568+
async listTags(
2569+
projectId: string,
2570+
options: Omit<z.infer<typeof ListTagsSchema>, "project_id"> = {}
2571+
): Promise<GitLabTag[]> {
2572+
projectId = decodeURIComponent(projectId);
2573+
const effectiveProjectId = this.getEffectiveProjectId(projectId);
2574+
const url = new URL(
2575+
`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags`
2576+
);
2577+
2578+
Object.entries(options).forEach(([key, value]) => {
2579+
if (value !== undefined && value !== null) {
2580+
url.searchParams.append(key, String(value));
2581+
}
2582+
});
2583+
2584+
const response = await this.fetch(url.toString(), {});
2585+
await this.handleGitLabError(response);
2586+
const data = await response.json();
2587+
return z.array(GitLabTagSchema).parse(data);
2588+
}
2589+
2590+
/**
2591+
* Get a single repository tag by name
2592+
*/
2593+
async getTag(projectId: string, tagName: string): Promise<GitLabTag> {
2594+
projectId = decodeURIComponent(projectId);
2595+
const effectiveProjectId = this.getEffectiveProjectId(projectId);
2596+
const url = new URL(
2597+
`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags/${encodeURIComponent(tagName)}`
2598+
);
2599+
2600+
const response = await this.fetch(url.toString(), {});
2601+
await this.handleGitLabError(response);
2602+
const data = await response.json();
2603+
return GitLabTagSchema.parse(data);
2604+
}
2605+
2606+
/**
2607+
* Create a new repository tag
2608+
*/
2609+
async createTag(
2610+
projectId: string,
2611+
options: Omit<z.infer<typeof CreateTagSchema>, "project_id">
2612+
): Promise<GitLabTag> {
2613+
projectId = decodeURIComponent(projectId);
2614+
const effectiveProjectId = this.getEffectiveProjectId(projectId);
2615+
const url = new URL(
2616+
`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags`
2617+
);
2618+
2619+
const body: any = {
2620+
tag_name: options.tag_name,
2621+
ref: options.ref,
2622+
};
2623+
2624+
if (options.message) {
2625+
body.message = options.message;
2626+
}
2627+
2628+
if (options.release_description) {
2629+
body.release_description = options.release_description;
2630+
}
2631+
2632+
const response = await this.fetch(url.toString(), {
2633+
method: "POST",
2634+
headers: {
2635+
"Content-Type": "application/json",
2636+
},
2637+
body: JSON.stringify(body),
2638+
});
2639+
2640+
await this.handleGitLabError(response);
2641+
const data = await response.json();
2642+
return GitLabTagSchema.parse(data);
2643+
}
2644+
2645+
/**
2646+
* Delete a repository tag
2647+
*/
2648+
async deleteTag(projectId: string, tagName: string): Promise<void> {
2649+
projectId = decodeURIComponent(projectId);
2650+
const effectiveProjectId = this.getEffectiveProjectId(projectId);
2651+
const url = new URL(
2652+
`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags/${encodeURIComponent(tagName)}`
2653+
);
2654+
2655+
const response = await this.fetch(url.toString(), {
2656+
method: "DELETE",
2657+
});
2658+
2659+
await this.handleGitLabError(response);
2660+
}
2661+
2662+
/**
2663+
* Get the X.509 signature of a tag
2664+
*/
2665+
async getTagSignature(projectId: string, tagName: string): Promise<GitLabTagSignature> {
2666+
projectId = decodeURIComponent(projectId);
2667+
const effectiveProjectId = this.getEffectiveProjectId(projectId);
2668+
const url = new URL(
2669+
`${config.GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/repository/tags/${encodeURIComponent(tagName)}/signature`
2670+
);
2671+
2672+
const response = await this.fetch(url.toString(), {});
2673+
await this.handleGitLabError(response);
2674+
const data = await response.json();
2675+
return GitLabTagSignatureSchema.parse(data);
2676+
}
25542677
}

src/mcpserver.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ import {
9494
ListProjectMembersSchema,
9595
MarkdownUploadSchema,
9696
DownloadAttachmentSchema,
97+
// Tags
98+
ListTagsSchema,
99+
GetTagSchema,
100+
CreateTagSchema,
101+
DeleteTagSchema,
102+
GetTagSignatureSchema,
97103
} from "./schemas.js";
98104
import { createCookieJar } from "./authhelpers.js";
99105
import { CookieJar } from "tough-cookie";
@@ -334,6 +340,31 @@ const allTools = [
334340
description: "Delete a label from a project",
335341
inputSchema: zodToJsonSchema(DeleteLabelSchema),
336342
},
343+
{
344+
name: "list_tags",
345+
description: "List repository tags for a GitLab project",
346+
inputSchema: zodToJsonSchema(ListTagsSchema),
347+
},
348+
{
349+
name: "get_tag",
350+
description: "Get a single repository tag by name",
351+
inputSchema: zodToJsonSchema(GetTagSchema),
352+
},
353+
{
354+
name: "create_tag",
355+
description: "Create a new repository tag",
356+
inputSchema: zodToJsonSchema(CreateTagSchema),
357+
},
358+
{
359+
name: "delete_tag",
360+
description: "Delete a repository tag",
361+
inputSchema: zodToJsonSchema(DeleteTagSchema),
362+
},
363+
{
364+
name: "get_tag_signature",
365+
description: "Get the X.509 signature of a tag (404 if unsigned)",
366+
inputSchema: zodToJsonSchema(GetTagSignatureSchema),
367+
},
337368
{
338369
name: "list_group_projects",
339370
description: "List projects in a GitLab group with filtering options",
@@ -587,6 +618,10 @@ const readOnlyTools = [
587618
"get_commit_diff",
588619
"get_current_user",
589620
"download_attachment",
621+
// tags
622+
"list_tags",
623+
"get_tag",
624+
"get_tag_signature",
590625
];
591626

592627
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
@@ -1185,6 +1220,55 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
11851220
};
11861221
}
11871222

1223+
case "list_tags": {
1224+
const args = ListTagsSchema.parse(request.params.arguments);
1225+
const tags = await gitlabSession.listTags(args.project_id, args);
1226+
return {
1227+
content: [{ type: "text", text: JSON.stringify(tags, null, 2) }],
1228+
};
1229+
}
1230+
1231+
case "get_tag": {
1232+
const args = GetTagSchema.parse(request.params.arguments);
1233+
const tag = await gitlabSession.getTag(args.project_id, args.tag_name);
1234+
return {
1235+
content: [{ type: "text", text: JSON.stringify(tag, null, 2) }],
1236+
};
1237+
}
1238+
1239+
case "create_tag": {
1240+
const args = CreateTagSchema.parse(request.params.arguments);
1241+
const tag = await gitlabSession.createTag(args.project_id, args);
1242+
return {
1243+
content: [{ type: "text", text: JSON.stringify(tag, null, 2) }],
1244+
};
1245+
}
1246+
1247+
case "delete_tag": {
1248+
const args = DeleteTagSchema.parse(request.params.arguments);
1249+
await gitlabSession.deleteTag(args.project_id, args.tag_name);
1250+
return {
1251+
content: [
1252+
{
1253+
type: "text",
1254+
text: JSON.stringify(
1255+
{ status: "success", message: "Tag deleted successfully" },
1256+
null,
1257+
2
1258+
),
1259+
},
1260+
],
1261+
};
1262+
}
1263+
1264+
case "get_tag_signature": {
1265+
const args = GetTagSignatureSchema.parse(request.params.arguments);
1266+
const signature = await gitlabSession.getTagSignature(args.project_id, args.tag_name);
1267+
return {
1268+
content: [{ type: "text", text: JSON.stringify(signature, null, 2) }],
1269+
};
1270+
}
1271+
11881272
case "list_group_projects": {
11891273
const args = ListGroupProjectsSchema.parse(request.params.arguments);
11901274
const projects = await gitlabSession.listGroupProjects(args);

src/schemas.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,74 @@ export const DeleteLabelSchema = z.object({
14261426
label_id: z.coerce.string().describe("The ID or title of a project's label"),
14271427
});
14281428

1429+
// Tag operation schemas
1430+
export const ListTagsSchema = z
1431+
.object({
1432+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1433+
order_by: z
1434+
.enum(["name", "updated", "version"]) // Default is updated
1435+
.optional()
1436+
.describe("Return tags ordered by name, updated, or version. Default is updated."),
1437+
sort: z.enum(["asc", "desc"]).optional().describe("Sort direction"),
1438+
search: z.string().optional().describe("Keyword to filter tags by"),
1439+
})
1440+
.merge(PaginationOptionsSchema);
1441+
1442+
export const GetTagSchema = z.object({
1443+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1444+
tag_name: z.string().describe("The name of the tag"),
1445+
});
1446+
1447+
export const CreateTagSchema = z.object({
1448+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1449+
tag_name: z.string().describe("The name of the tag"),
1450+
ref: z.string().describe("Create tag using commit SHA, another tag name, or branch name"),
1451+
message: z.string().optional().describe("Create annotated tag with message"),
1452+
release_description: z.string().optional().describe("Release notes for the tag"),
1453+
});
1454+
1455+
export const DeleteTagSchema = z.object({
1456+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1457+
tag_name: z.string().describe("The name of the tag"),
1458+
});
1459+
1460+
export const GetTagSignatureSchema = z.object({
1461+
project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
1462+
tag_name: z.string().describe("The name of the tag"),
1463+
});
1464+
1465+
// Tag data schemas
1466+
export const GitLabTagSchema = z.object({
1467+
name: z.string(),
1468+
message: z.string().nullable(),
1469+
target: z.string(),
1470+
commit: z.object({
1471+
id: z.string(),
1472+
short_id: z.string(),
1473+
title: z.string(),
1474+
created_at: z.string(),
1475+
parent_ids: z.array(z.string()),
1476+
message: z.string(),
1477+
author_name: z.string(),
1478+
author_email: z.string(),
1479+
authored_date: z.string(),
1480+
committer_name: z.string(),
1481+
committer_email: z.string(),
1482+
committed_date: z.string(),
1483+
}),
1484+
release: z.object({
1485+
tag_name: z.string(),
1486+
description: z.string(),
1487+
}).nullable(),
1488+
protected: z.boolean(),
1489+
});
1490+
1491+
export const GitLabTagSignatureSchema = z.object({
1492+
signature_type: z.string(),
1493+
signature: z.string(),
1494+
public_key: z.string(),
1495+
});
1496+
14291497
// Group projects schema
14301498
export const ListGroupProjectsSchema = z
14311499
.object({
@@ -1989,3 +2057,7 @@ export type PublishDraftNoteOptions = z.infer<typeof PublishDraftNoteSchema>;
19892057
export type BulkPublishDraftNotesOptions = z.infer<typeof BulkPublishDraftNotesSchema>;
19902058
export type GitLabMarkdownUpload = z.infer<typeof GitLabMarkdownUploadSchema>;
19912059
export type MarkdownUploadOptions = z.infer<typeof MarkdownUploadSchema>;
2060+
2061+
// Tag type exports
2062+
export type GitLabTag = z.infer<typeof GitLabTagSchema>;
2063+
export type GitLabTagSignature = z.infer<typeof GitLabTagSignatureSchema>;

0 commit comments

Comments
 (0)