From 69e217b3578ec89e7d38eca75b620f9b9c39d913 Mon Sep 17 00:00:00 2001 From: Admin Date: Mon, 17 Mar 2025 23:47:30 -0700 Subject: [PATCH 1/2] Fix GitLab fork function parameter handling - Made owner property optional in GitLabForkParentSchema - Made forked_from_project property optional in GitLabForkSchema - Improved error handling in fork_repository MCP handler --- index.ts | 21 ++++++++++++++++----- schemas.ts | 4 ++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/index.ts b/index.ts index 97a4b2c..32d3fec 100644 --- a/index.ts +++ b/index.ts @@ -894,11 +894,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case "fork_repository": { - const args = ForkRepositorySchema.parse(request.params.arguments); - const fork = await forkProject(args.project_id, args.namespace); - return { - content: [{ type: "text", text: JSON.stringify(fork, null, 2) }], - }; + const forkArgs = ForkRepositorySchema.parse(request.params.arguments); + try { + const forkedProject = await forkProject(forkArgs.project_id, forkArgs.namespace); + return { + content: [{ type: "text", text: JSON.stringify(forkedProject, null, 2) }], + }; + } catch (forkError) { + console.error("Error forking repository:", forkError); + let forkErrorMessage = "Failed to fork repository"; + if (forkError instanceof Error) { + forkErrorMessage = `${forkErrorMessage}: ${forkError.message}`; + } + return { + content: [{ type: "text", text: JSON.stringify({ error: forkErrorMessage }, null, 2) }], + }; + } } case "create_branch": { diff --git a/schemas.ts b/schemas.ts index f8bc29b..9874b8d 100644 --- a/schemas.ts +++ b/schemas.ts @@ -159,12 +159,12 @@ export const GitLabForkParentSchema = z.object({ username: z.string(), // Changed from login to match GitLab API id: z.number(), avatar_url: z.string(), - }), + }).optional(), // Made optional to handle cases where GitLab API doesn't include it web_url: z.string(), // Changed from html_url to match GitLab API }); export const GitLabForkSchema = GitLabRepositorySchema.extend({ - forked_from_project: GitLabForkParentSchema, // Changed from parent to match GitLab API + forked_from_project: GitLabForkParentSchema.optional(), // Made optional to handle cases where GitLab API doesn't include it }); // Issue related schemas From 29cc6fbc28eb66cc3d9d79d200044afc7f9a050c Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 18 Mar 2025 00:13:28 -0700 Subject: [PATCH 2/2] 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;