Add GitLab Projects API support

This commit is contained in:
Admin
2025-03-18 00:27:10 -07:00
parent 29cc6fbc28
commit 82988bdf76
2 changed files with 171 additions and 0 deletions

View File

@ -46,6 +46,8 @@ import {
ListNamespacesSchema, ListNamespacesSchema,
GetNamespaceSchema, GetNamespaceSchema,
VerifyNamespaceSchema, VerifyNamespaceSchema,
GetProjectSchema,
ListProjectsSchema,
type GitLabFork, type GitLabFork,
type GitLabReference, type GitLabReference,
type GitLabRepository, type GitLabRepository,
@ -60,6 +62,7 @@ import {
type GitLabMergeRequestDiff, type GitLabMergeRequestDiff,
type GitLabNamespace, type GitLabNamespace,
type GitLabNamespaceExistsResponse, type GitLabNamespaceExistsResponse,
type GitLabProject,
CreateNoteSchema, CreateNoteSchema,
} from "./schemas.js"; } from "./schemas.js";
@ -902,6 +905,74 @@ async function verifyNamespaceExistence(
return GitLabNamespaceExistsResponseSchema.parse(data); return GitLabNamespaceExistsResponseSchema.parse(data);
} }
/**
* Get a single project
* 단일 프로젝트 조회
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {Object} options - Options for getting project details
* @param {boolean} [options.license] - Include project license data
* @param {boolean} [options.statistics] - Include project statistics
* @param {boolean} [options.with_custom_attributes] - Include custom attributes in response
* @returns {Promise<GitLabProject>} Project details
*/
async function getProject(
projectId: string,
options: {
license?: boolean;
statistics?: boolean;
with_custom_attributes?: boolean;
} = {}
): Promise<GitLabProject> {
const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`);
if (options.license) {
url.searchParams.append("license", "true");
}
if (options.statistics) {
url.searchParams.append("statistics", "true");
}
if (options.with_custom_attributes) {
url.searchParams.append("with_custom_attributes", "true");
}
const response = await fetch(url.toString(), {
headers: DEFAULT_HEADERS,
});
await handleGitLabError(response);
const data = await response.json();
return GitLabRepositorySchema.parse(data);
}
/**
* List projects
* 프로젝트 목록 조회
*
* @param {Object} options - Options for listing projects
* @returns {Promise<GitLabProject[]>} List of projects
*/
async function listProjects(options: z.infer<typeof ListProjectsSchema> = {}): Promise<GitLabProject[]> {
const url = new URL(`${GITLAB_API_URL}/projects`);
// Add all the query parameters from options
Object.entries(options).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.append(key, value.toString());
}
});
const response = await fetch(url.toString(), {
headers: DEFAULT_HEADERS,
});
await handleGitLabError(response);
const data = await response.json();
return z.array(GitLabRepositorySchema).parse(data);
}
server.setRequestHandler(ListToolsRequestSchema, async () => { server.setRequestHandler(ListToolsRequestSchema, async () => {
return { return {
tools: [ tools: [
@ -988,6 +1059,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
description: "Verify if a specified namespace already exists", description: "Verify if a specified namespace already exists",
inputSchema: zodToJsonSchema(VerifyNamespaceSchema), inputSchema: zodToJsonSchema(VerifyNamespaceSchema),
}, },
{
name: "get_project",
description: "Get details on a specified project",
inputSchema: zodToJsonSchema(GetProjectSchema),
},
{
name: "list_projects",
description: "List projects accessible by the current user",
inputSchema: zodToJsonSchema(ListProjectsSchema),
},
], ],
}; };
}); });
@ -1183,6 +1264,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}; };
} }
case "get_project": {
const args = GetProjectSchema.parse(request.params.arguments);
const { id, ...options } = args;
const project = await getProject(id, options);
return {
content: [{ type: "text", text: JSON.stringify(project, null, 2) }],
};
}
case "list_projects": {
const args = ListProjectsSchema.parse(request.params.arguments);
const projects = await listProjects(args);
return {
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
};
}
case "create_note": { case "create_note": {
const args = CreateNoteSchema.parse(request.params.arguments); const args = CreateNoteSchema.parse(request.params.arguments);
const { project_id, noteable_type, noteable_iid, body } = args; const { project_id, noteable_type, noteable_iid, body } = args;

View File

@ -58,6 +58,48 @@ export const GitLabRepositorySchema = z.object({
created_at: z.string().optional(), created_at: z.string().optional(),
last_activity_at: z.string().optional(), last_activity_at: z.string().optional(),
default_branch: z.string().optional(), default_branch: z.string().optional(),
namespace: z.object({
id: z.number(),
name: z.string(),
path: z.string(),
kind: z.string(),
full_path: z.string(),
avatar_url: z.string().nullable().optional(),
web_url: z.string().optional(),
}).optional(),
readme_url: z.string().optional().nullable(),
topics: z.array(z.string()).optional(),
tag_list: z.array(z.string()).optional(), // deprecated but still present
open_issues_count: z.number().optional(),
archived: z.boolean().optional(),
forks_count: z.number().optional(),
star_count: z.number().optional(),
permissions: z.object({
project_access: z.object({
access_level: z.number(),
notification_level: z.number().optional(),
}).optional().nullable(),
group_access: z.object({
access_level: z.number(),
notification_level: z.number().optional(),
}).optional().nullable(),
}).optional(),
container_registry_enabled: z.boolean().optional(),
container_registry_access_level: z.string().optional(),
issues_enabled: z.boolean().optional(),
merge_requests_enabled: z.boolean().optional(),
wiki_enabled: z.boolean().optional(),
jobs_enabled: z.boolean().optional(),
snippets_enabled: z.boolean().optional(),
can_create_merge_request_in: z.boolean().optional(),
resolve_outdated_diff_discussions: z.boolean().optional(),
shared_runners_enabled: z.boolean().optional(),
shared_with_groups: z.array(z.object({
group_id: z.number(),
group_name: z.string(),
group_full_path: z.string(),
group_access_level: z.number(),
})).optional(),
}); });
// File content schemas // File content schemas
@ -450,6 +492,36 @@ export const VerifyNamespaceSchema = z.object({
parent_id: z.number().optional().describe("ID of the parent namespace. If unspecified, only returns top-level namespaces"), parent_id: z.number().optional().describe("ID of the parent namespace. If unspecified, only returns top-level namespaces"),
}); });
// Project API operation schemas
export const GetProjectSchema = z.object({
id: z.string().describe("ID or URL-encoded path of the project"),
license: z.boolean().optional().describe("Include project license data"),
statistics: z.boolean().optional().describe("Include project statistics"),
with_custom_attributes: z.boolean().optional().describe("Include custom attributes in response"),
});
export const ListProjectsSchema = z.object({
archived: z.boolean().optional().describe("Limit by archived status"),
id_after: z.number().optional().describe("Limit results to projects with IDs greater than the specified ID"),
id_before: z.number().optional().describe("Limit results to projects with IDs less than the specified ID"),
membership: z.boolean().optional().describe("Limit by projects that the current user is a member of"),
min_access_level: z.number().optional().describe("Limit by minimum access level"),
order_by: z.enum(['id', 'name', 'path', 'created_at', 'updated_at', 'last_activity_at']).optional().describe("Return projects ordered by field"),
owned: z.boolean().optional().describe("Limit by projects explicitly owned by the current user"),
search: z.string().optional().describe("Return list of projects matching the search criteria"),
simple: z.boolean().optional().describe("Return only limited fields for each project"),
sort: z.enum(['asc', 'desc']).optional().describe("Return projects sorted in ascending or descending order"),
starred: z.boolean().optional().describe("Limit by projects starred by the current user"),
visibility: z.enum(['public', 'internal', 'private']).optional().describe("Limit by visibility"),
with_custom_attributes: z.boolean().optional().describe("Include custom attributes in response"),
with_issues_enabled: z.boolean().optional().describe("Limit by enabled issues feature"),
with_merge_requests_enabled: z.boolean().optional().describe("Limit by enabled merge requests feature"),
with_programming_language: z.string().optional().describe("Limit by projects which use the given programming language"),
with_shared: z.boolean().optional().describe("Include projects shared to this group"),
page: z.number().optional().describe("Page number for pagination"),
per_page: z.number().optional().describe("Number of items per page"),
});
// Export types // Export types
export type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>; export type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>;
export type GitLabFork = z.infer<typeof GitLabForkSchema>; export type GitLabFork = z.infer<typeof GitLabForkSchema>;
@ -483,3 +555,4 @@ export type GitLabMergeRequestDiff = z.infer<
export type CreateNoteOptions = z.infer<typeof CreateNoteSchema>; export type CreateNoteOptions = z.infer<typeof CreateNoteSchema>;
export type GitLabNamespace = z.infer<typeof GitLabNamespaceSchema>; export type GitLabNamespace = z.infer<typeof GitLabNamespaceSchema>;
export type GitLabNamespaceExistsResponse = z.infer<typeof GitLabNamespaceExistsResponseSchema>; export type GitLabNamespaceExistsResponse = z.infer<typeof GitLabNamespaceExistsResponseSchema>;
export type GitLabProject = z.infer<typeof GitLabRepositorySchema>;