diff --git a/index.ts b/index.ts index fe405b9..44bac6d 100644 --- a/index.ts +++ b/index.ts @@ -46,6 +46,8 @@ import { ListNamespacesSchema, GetNamespaceSchema, VerifyNamespaceSchema, + GetProjectSchema, + ListProjectsSchema, type GitLabFork, type GitLabReference, type GitLabRepository, @@ -60,6 +62,7 @@ import { type GitLabMergeRequestDiff, type GitLabNamespace, type GitLabNamespaceExistsResponse, + type GitLabProject, CreateNoteSchema, } from "./schemas.js"; @@ -902,6 +905,74 @@ async function verifyNamespaceExistence( 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} Project details + */ +async function getProject( + projectId: string, + options: { + license?: boolean; + statistics?: boolean; + with_custom_attributes?: boolean; + } = {} +): Promise { + 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} List of projects + */ +async function listProjects(options: z.infer = {}): Promise { + 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 () => { return { tools: [ @@ -988,6 +1059,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "Verify if a specified namespace already exists", 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": { const args = CreateNoteSchema.parse(request.params.arguments); const { project_id, noteable_type, noteable_iid, body } = args; diff --git a/schemas.ts b/schemas.ts index 9eb0ac7..c785751 100644 --- a/schemas.ts +++ b/schemas.ts @@ -58,6 +58,48 @@ export const GitLabRepositorySchema = z.object({ created_at: z.string().optional(), last_activity_at: 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 @@ -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"), }); +// 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 type GitLabAuthor = z.infer; export type GitLabFork = z.infer; @@ -483,3 +555,4 @@ export type GitLabMergeRequestDiff = z.infer< export type CreateNoteOptions = z.infer; export type GitLabNamespace = z.infer; export type GitLabNamespaceExistsResponse = z.infer; +export type GitLabProject = z.infer;