From 29cc6fbc28eb66cc3d9d79d200044afc7f9a050c Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 18 Mar 2025 00:13:28 -0700 Subject: [PATCH 1/3] Add GitLab Namespaces API support --- index.ts | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++ schemas.ts | 45 +++++++++++++++++++ 2 files changed, 175 insertions(+) diff --git a/index.ts b/index.ts index 32d3fec..fe405b9 100644 --- a/index.ts +++ b/index.ts @@ -24,6 +24,8 @@ import { GitLabSearchResponseSchema, GitLabTreeSchema, GitLabCommitSchema, + GitLabNamespaceSchema, + GitLabNamespaceExistsResponseSchema, CreateRepositoryOptionsSchema, CreateIssueOptionsSchema, CreateMergeRequestOptionsSchema, @@ -41,6 +43,9 @@ import { GetMergeRequestSchema, GetMergeRequestDiffsSchema, UpdateMergeRequestSchema, + ListNamespacesSchema, + GetNamespaceSchema, + VerifyNamespaceSchema, type GitLabFork, type GitLabReference, type GitLabRepository, @@ -53,6 +58,8 @@ import { type GitLabCommit, type FileOperation, type GitLabMergeRequestDiff, + type GitLabNamespace, + type GitLabNamespaceExistsResponse, CreateNoteSchema, } from "./schemas.js"; @@ -811,6 +818,90 @@ async function createNote( return await response.json(); } +/** + * List all namespaces + * 사용 가능한 모든 네임스페이스 목록 조회 + * + * @param {Object} options - Options for listing namespaces + * @param {string} [options.search] - Search query to filter namespaces + * @param {boolean} [options.owned_only] - Only return namespaces owned by the authenticated user + * @param {boolean} [options.top_level_only] - Only return top-level namespaces + * @returns {Promise} List of namespaces + */ +async function listNamespaces(options: { + search?: string; + owned_only?: boolean; + top_level_only?: boolean; +}): Promise { + const url = new URL(`${GITLAB_API_URL}/namespaces`); + + if (options.search) { + url.searchParams.append("search", options.search); + } + + if (options.owned_only) { + url.searchParams.append("owned_only", "true"); + } + + if (options.top_level_only) { + url.searchParams.append("top_level_only", "true"); + } + + const response = await fetch(url.toString(), { + headers: DEFAULT_HEADERS, + }); + + await handleGitLabError(response); + const data = await response.json(); + return z.array(GitLabNamespaceSchema).parse(data); +} + +/** + * Get details on a namespace + * 네임스페이스 상세 정보 조회 + * + * @param {string} id - The ID or URL-encoded path of the namespace + * @returns {Promise} The namespace details + */ +async function getNamespace(id: string): Promise { + const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(id)}`); + + const response = await fetch(url.toString(), { + headers: DEFAULT_HEADERS, + }); + + await handleGitLabError(response); + const data = await response.json(); + return GitLabNamespaceSchema.parse(data); +} + +/** + * Verify if a namespace exists + * 네임스페이스 존재 여부 확인 + * + * @param {string} namespacePath - The path of the namespace to check + * @param {number} [parentId] - The ID of the parent namespace + * @returns {Promise} The verification result + */ +async function verifyNamespaceExistence( + namespacePath: string, + parentId?: number +): Promise { + const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(namespacePath)}/exists`); + + if (parentId) { + url.searchParams.append("parent_id", parentId.toString()); + } + + const response = await fetch(url.toString(), { + headers: DEFAULT_HEADERS, + }); + + await handleGitLabError(response); + const data = await response.json(); + return GitLabNamespaceExistsResponseSchema.parse(data); +} + server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ @@ -882,6 +973,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "Create a new note (comment) to an issue or merge request", inputSchema: zodToJsonSchema(CreateNoteSchema), }, + { + name: "list_namespaces", + description: "List all namespaces available to the current user", + inputSchema: zodToJsonSchema(ListNamespacesSchema), + }, + { + name: "get_namespace", + description: "Get details on a specified namespace", + inputSchema: zodToJsonSchema(GetNamespaceSchema), + }, + { + name: "verify_namespace", + description: "Verify if a specified namespace already exists", + inputSchema: zodToJsonSchema(VerifyNamespaceSchema), + }, ], }; }); @@ -1053,6 +1159,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case "list_namespaces": { + const args = ListNamespacesSchema.parse(request.params.arguments); + const namespaces = await listNamespaces(args); + return { + content: [{ type: "text", text: JSON.stringify(namespaces, null, 2) }], + }; + } + + case "get_namespace": { + const args = GetNamespaceSchema.parse(request.params.arguments); + const namespace = await getNamespace(args.id); + return { + content: [{ type: "text", text: JSON.stringify(namespace, null, 2) }], + }; + } + + case "verify_namespace": { + const args = VerifyNamespaceSchema.parse(request.params.arguments); + const result = await verifyNamespaceExistence(args.namespace, args.parent_id); + return { + content: [{ type: "text", text: JSON.stringify(result, 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 9874b8d..9eb0ac7 100644 --- a/schemas.ts +++ b/schemas.ts @@ -7,6 +7,33 @@ export const GitLabAuthorSchema = z.object({ date: z.string(), }); +// Namespace related schemas +export const GitLabNamespaceSchema = z.object({ + id: z.number(), + name: z.string(), + path: z.string(), + kind: z.enum(["user", "group"]), + full_path: z.string(), + parent_id: z.number().nullable(), + avatar_url: z.string().nullable(), + web_url: z.string(), + members_count_with_descendants: z.number().optional(), + billable_members_count: z.number().optional(), + max_seats_used: z.number().optional(), + seats_in_use: z.number().optional(), + plan: z.string().optional(), + end_date: z.string().nullable().optional(), + trial_ends_on: z.string().nullable().optional(), + trial: z.boolean().optional(), + root_repository_size: z.number().optional(), + projects_count: z.number().optional(), +}); + +export const GitLabNamespaceExistsResponseSchema = z.object({ + exists: z.boolean(), + suggests: z.array(z.string()).optional(), +}); + // Repository related schemas export const GitLabOwnerSchema = z.object({ username: z.string(), // Changed from login to match GitLab API @@ -407,6 +434,22 @@ export const CreateNoteSchema = z.object({ body: z.string().describe("Note content"), }); +// Add namespace-related operation schemas +export const ListNamespacesSchema = z.object({ + search: z.string().optional().describe("Only returns namespaces accessible by the current user"), + owned_only: z.boolean().optional().describe("If true, only returns namespaces by the current user"), + top_level_only: z.boolean().optional().describe("In GitLab 16.8 and later, if true, only returns top-level namespaces"), +}); + +export const GetNamespaceSchema = z.object({ + id: z.string().describe("ID or URL-encoded path of the namespace"), +}); + +export const VerifyNamespaceSchema = z.object({ + namespace: z.string().describe("Path of the namespace"), + parent_id: z.number().optional().describe("ID of the parent namespace. If unspecified, only returns top-level namespaces"), +}); + // Export types export type GitLabAuthor = z.infer; export type GitLabFork = z.infer; @@ -438,3 +481,5 @@ export type GitLabMergeRequestDiff = z.infer< typeof GitLabMergeRequestDiffSchema >; export type CreateNoteOptions = z.infer; +export type GitLabNamespace = z.infer; +export type GitLabNamespaceExistsResponse = z.infer; From 82988bdf769dacab9913d91359f5402418d319b6 Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 18 Mar 2025 00:27:10 -0700 Subject: [PATCH 2/3] Add GitLab Projects API support --- index.ts | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ schemas.ts | 73 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) 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; From 08434df9e14948a3499b35873c6ea0e2a217c58a Mon Sep 17 00:00:00 2001 From: simple Date: Tue, 18 Mar 2025 17:53:21 +0900 Subject: [PATCH 3/3] version up --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d5cd55b..e3271df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.16", + "version": "1.0.17", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight",