From 61ee1244f431c591f199d93d683f2f9b573e48b6 Mon Sep 17 00:00:00 2001 From: simple Date: Wed, 2 Apr 2025 00:12:24 +0900 Subject: [PATCH] =?UTF-8?q?build:=20test-note.js=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/index.js | 1583 -------------------------------------------- build/schemas.js | 684 ------------------- build/test-note.js | 54 -- 3 files changed, 2321 deletions(-) delete mode 100755 build/index.js delete mode 100644 build/schemas.js delete mode 100644 build/test-note.js diff --git a/build/index.js b/build/index.js deleted file mode 100755 index db763e5..0000000 --- a/build/index.js +++ /dev/null @@ -1,1583 +0,0 @@ -#!/usr/bin/env node -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; -import fetch from "node-fetch"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; -import { fileURLToPath } from "url"; -import { dirname } from "path"; -import fs from "fs"; -import path from "path"; -import { GitLabForkSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabIssueSchema, GitLabMergeRequestSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabSearchResponseSchema, GitLabTreeSchema, GitLabCommitSchema, GitLabNamespaceSchema, GitLabNamespaceExistsResponseSchema, GitLabProjectSchema, CreateOrUpdateFileSchema, SearchRepositoriesSchema, CreateRepositorySchema, GetFileContentsSchema, PushFilesSchema, CreateIssueSchema, CreateMergeRequestSchema, ForkRepositorySchema, CreateBranchSchema, GitLabMergeRequestDiffSchema, GetMergeRequestSchema, GetMergeRequestDiffsSchema, UpdateMergeRequestSchema, ListIssuesSchema, GetIssueSchema, UpdateIssueSchema, DeleteIssueSchema, GitLabIssueLinkSchema, GitLabIssueWithLinkDetailsSchema, ListIssueLinksSchema, GetIssueLinkSchema, CreateIssueLinkSchema, DeleteIssueLinkSchema, ListNamespacesSchema, GetNamespaceSchema, VerifyNamespaceSchema, GetProjectSchema, ListProjectsSchema, ListLabelsSchema, GetLabelSchema, CreateLabelSchema, UpdateLabelSchema, DeleteLabelSchema, CreateNoteSchema, ListGroupProjectsSchema, -// Discussion Schemas -GitLabDiscussionNoteSchema, // Added -GitLabDiscussionSchema, UpdateMergeRequestNoteSchema, // Added -ListMergeRequestDiscussionsSchema, } from "./schemas.js"; -/** - * Read version from package.json - */ -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const packageJsonPath = path.resolve(__dirname, '../package.json'); -let SERVER_VERSION = "unknown"; -try { - if (fs.existsSync(packageJsonPath)) { - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - SERVER_VERSION = packageJson.version || SERVER_VERSION; - } -} -catch (error) { - console.error("Warning: Could not read version from package.json:", error); -} -const server = new Server({ - name: "better-gitlab-mcp-server", - version: SERVER_VERSION, -}, { - capabilities: { - tools: {}, - }, -}); -const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN; -/** - * Smart URL handling for GitLab API - * - * @param {string | undefined} url - Input GitLab API URL - * @returns {string} Normalized GitLab API URL with /api/v4 path - */ -function normalizeGitLabApiUrl(url) { - if (!url) { - return "https://gitlab.com/api/v4"; - } - // Remove trailing slash if present - let normalizedUrl = url.endsWith('/') ? url.slice(0, -1) : url; - // Check if URL already has /api/v4 - if (!normalizedUrl.endsWith('/api/v4') && !normalizedUrl.endsWith('/api/v4/')) { - // Append /api/v4 if not already present - normalizedUrl = `${normalizedUrl}/api/v4`; - } - return normalizedUrl; -} -// Use the normalizeGitLabApiUrl function to handle various URL formats -const GITLAB_API_URL = normalizeGitLabApiUrl(process.env.GITLAB_API_URL || ""); -if (!GITLAB_PERSONAL_ACCESS_TOKEN) { - console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set"); - process.exit(1); -} -/** - * Common headers for GitLab API requests - * GitLab API 공통 헤더 (Common headers for GitLab API) - */ -const DEFAULT_HEADERS = { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, -}; -/** - * Utility function for handling GitLab API errors - * API 에러 처리를 위한 유틸리티 함수 (Utility function for handling API errors) - * - * @param {import("node-fetch").Response} response - The response from GitLab API - * @throws {Error} Throws an error with response details if the request failed - */ -async function handleGitLabError(response) { - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); - } -} -/** - * Create a fork of a GitLab project - * 프로젝트 포크 생성 (Create a project fork) - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {string} [namespace] - The namespace to fork the project to - * @returns {Promise} The created fork - */ -async function forkProject(projectId, namespace) { - // API 엔드포인트 URL 생성 - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork`); - if (namespace) { - url.searchParams.append("namespace", namespace); - } - const response = await fetch(url.toString(), { - method: "POST", - headers: DEFAULT_HEADERS, - }); - // 이미 존재하는 프로젝트인 경우 처리 - if (response.status === 409) { - throw new Error("Project already exists in the target namespace"); - } - await handleGitLabError(response); - const data = await response.json(); - return GitLabForkSchema.parse(data); -} -/** - * Create a new branch in a GitLab project - * 새로운 브랜치 생성 (Create a new branch) - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {z.infer} options - Branch creation options - * @returns {Promise} The created branch reference - */ -async function createBranch(projectId, options) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/branches`); - const response = await fetch(url.toString(), { - method: "POST", - headers: DEFAULT_HEADERS, - body: JSON.stringify({ - branch: options.name, - ref: options.ref, - }), - }); - await handleGitLabError(response); - return GitLabReferenceSchema.parse(await response.json()); -} -/** - * Get the default branch for a GitLab project - * 프로젝트의 기본 브랜치 조회 (Get the default branch of a project) - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @returns {Promise} The name of the default branch - */ -async function getDefaultBranchRef(projectId) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`); - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); - const project = GitLabRepositorySchema.parse(await response.json()); - return project.default_branch ?? "main"; -} -/** - * Get the contents of a file from a GitLab project - * 파일 내용 조회 (Get file contents) - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {string} filePath - The path of the file to get - * @param {string} [ref] - The name of the branch, tag or commit - * @returns {Promise} The file content - */ -async function getFileContents(projectId, filePath, ref) { - const encodedPath = encodeURIComponent(filePath); - // ref가 없는 경우 default branch를 가져옴 - if (!ref) { - ref = await getDefaultBranchRef(projectId); - } - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`); - url.searchParams.append("ref", ref); - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - // 파일을 찾을 수 없는 경우 처리 - if (response.status === 404) { - throw new Error(`File not found: ${filePath}`); - } - await handleGitLabError(response); - const data = await response.json(); - const parsedData = GitLabContentSchema.parse(data); - // Base64로 인코딩된 파일 내용을 UTF-8로 디코딩 - if (!Array.isArray(parsedData) && parsedData.content) { - parsedData.content = Buffer.from(parsedData.content, "base64").toString("utf8"); - parsedData.encoding = "utf8"; - } - return parsedData; -} -/** - * Create a new issue in a GitLab project - * 이슈 생성 (Create an issue) - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {z.infer} options - Issue creation options - * @returns {Promise} The created issue - */ -async function createIssue(projectId, options) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`); - const response = await fetch(url.toString(), { - method: "POST", - headers: DEFAULT_HEADERS, - body: JSON.stringify({ - title: options.title, - description: options.description, - assignee_ids: options.assignee_ids, - milestone_id: options.milestone_id, - labels: options.labels?.join(","), - }), - }); - // 잘못된 요청 처리 - if (response.status === 400) { - const errorBody = await response.text(); - throw new Error(`Invalid request: ${errorBody}`); - } - await handleGitLabError(response); - const data = await response.json(); - return GitLabIssueSchema.parse(data); -} -/** - * List issues in a GitLab project - * 프로젝트의 이슈 목록 조회 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {Object} options - Options for listing issues - * @returns {Promise} List of issues - */ -async function listIssues(projectId, options = {}) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`); - // Add all query parameters - Object.entries(options).forEach(([key, value]) => { - if (value !== undefined) { - if (key === 'label_name' && Array.isArray(value)) { - // Handle array of labels - url.searchParams.append(key, value.join(',')); - } - else { - 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(GitLabIssueSchema).parse(data); -} -/** - * Get a single issue from a GitLab project - * 단일 이슈 조회 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {number} issueIid - The internal ID of the project issue - * @returns {Promise} The issue - */ -async function getIssue(projectId, issueIid) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`); - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); - const data = await response.json(); - return GitLabIssueSchema.parse(data); -} -/** - * Update an issue in a GitLab project - * 이슈 업데이트 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {number} issueIid - The internal ID of the project issue - * @param {Object} options - Update options for the issue - * @returns {Promise} The updated issue - */ -async function updateIssue(projectId, issueIid, options) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`); - // Convert labels array to comma-separated string if present - const body = { ...options }; - if (body.labels && Array.isArray(body.labels)) { - body.labels = body.labels.join(','); - } - const response = await fetch(url.toString(), { - method: "PUT", - headers: DEFAULT_HEADERS, - body: JSON.stringify(body), - }); - await handleGitLabError(response); - const data = await response.json(); - return GitLabIssueSchema.parse(data); -} -/** - * Delete an issue from a GitLab project - * 이슈 삭제 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {number} issueIid - The internal ID of the project issue - * @returns {Promise} - */ -async function deleteIssue(projectId, issueIid) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}`); - const response = await fetch(url.toString(), { - method: "DELETE", - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); -} -/** - * List all issue links for a specific issue - * 이슈 관계 목록 조회 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {number} issueIid - The internal ID of the project issue - * @returns {Promise} List of issues with link details - */ -async function listIssueLinks(projectId, issueIid) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links`); - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); - const data = await response.json(); - return z.array(GitLabIssueWithLinkDetailsSchema).parse(data); -} -/** - * Get a specific issue link - * 특정 이슈 관계 조회 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {number} issueIid - The internal ID of the project issue - * @param {number} issueLinkId - The ID of the issue link - * @returns {Promise} The issue link - */ -async function getIssueLink(projectId, issueIid, issueLinkId) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links/${issueLinkId}`); - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); - const data = await response.json(); - return GitLabIssueLinkSchema.parse(data); -} -/** - * Create an issue link between two issues - * 이슈 관계 생성 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {number} issueIid - The internal ID of the project issue - * @param {string} targetProjectId - The ID or URL-encoded path of the target project - * @param {number} targetIssueIid - The internal ID of the target project issue - * @param {string} linkType - The type of the relation (relates_to, blocks, is_blocked_by) - * @returns {Promise} The created issue link - */ -async function createIssueLink(projectId, issueIid, targetProjectId, targetIssueIid, linkType = 'relates_to') { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links`); - const response = await fetch(url.toString(), { - method: "POST", - headers: DEFAULT_HEADERS, - body: JSON.stringify({ - target_project_id: targetProjectId, - target_issue_iid: targetIssueIid, - link_type: linkType - }), - }); - await handleGitLabError(response); - const data = await response.json(); - return GitLabIssueLinkSchema.parse(data); -} -/** - * Delete an issue link - * 이슈 관계 삭제 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {number} issueIid - The internal ID of the project issue - * @param {number} issueLinkId - The ID of the issue link - * @returns {Promise} - */ -async function deleteIssueLink(projectId, issueIid, issueLinkId) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues/${issueIid}/links/${issueLinkId}`); - const response = await fetch(url.toString(), { - method: "DELETE", - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); -} -/** - * Create a new merge request in a GitLab project - * 병합 요청 생성 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {z.infer} options - Merge request creation options - * @returns {Promise} The created merge request - */ -async function createMergeRequest(projectId, options) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`); - const response = await fetch(url.toString(), { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, - }, - body: JSON.stringify({ - title: options.title, - description: options.description, - source_branch: options.source_branch, - target_branch: options.target_branch, - allow_collaboration: options.allow_collaboration, - draft: options.draft, - }), - }); - if (response.status === 400) { - const errorBody = await response.text(); - throw new Error(`Invalid request: ${errorBody}`); - } - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); - } - const data = await response.json(); - return GitLabMergeRequestSchema.parse(data); -} -/** - * List merge request discussion items - * 병합 요청 토론 목록 조회 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {number} mergeRequestIid - The IID of a merge request - * @returns {Promise} List of discussions - */ -async function listMergeRequestDiscussions(projectId, mergeRequestIid) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/discussions`); - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); - const data = await response.json(); - // Ensure the response is parsed as an array of discussions - return z.array(GitLabDiscussionSchema).parse(data); -} -/** - * Modify an existing merge request thread note - * 병합 요청 토론 노트 수정 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {number} mergeRequestIid - The IID of a merge request - * @param {string} discussionId - The ID of a thread - * @param {number} noteId - The ID of a thread note - * @param {string} body - The new content of the note - * @param {boolean} [resolved] - Resolve/unresolve state - * @returns {Promise} The updated note - */ -async function updateMergeRequestNote(projectId, mergeRequestIid, discussionId, noteId, body, resolved) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}`); - const payload = { body }; - if (resolved !== undefined) { - payload.resolved = resolved; - } - const response = await fetch(url.toString(), { - method: "PUT", - headers: DEFAULT_HEADERS, - body: JSON.stringify(payload), - }); - await handleGitLabError(response); - const data = await response.json(); - return GitLabDiscussionNoteSchema.parse(data); -} -/** - * Create or update a file in a GitLab project - * 파일 생성 또는 업데이트 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {string} filePath - The path of the file to create or update - * @param {string} content - The content of the file - * @param {string} commitMessage - The commit message - * @param {string} branch - The branch name - * @param {string} [previousPath] - The previous path of the file in case of rename - * @returns {Promise} The file update response - */ -async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath, last_commit_id, commit_id) { - const encodedPath = encodeURIComponent(filePath); - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`); - const body = { - branch, - content, - commit_message: commitMessage, - encoding: "text", - ...(previousPath ? { previous_path: previousPath } : {}), - }; - // Check if file exists - let method = "POST"; - try { - // Get file contents to check existence and retrieve commit IDs - const fileData = await getFileContents(projectId, filePath, branch); - method = "PUT"; - // If fileData is not an array, it's a file content object with commit IDs - if (!Array.isArray(fileData)) { - // Use commit IDs from the file data if not provided in parameters - if (!commit_id && fileData.commit_id) { - body.commit_id = fileData.commit_id; - } - else if (commit_id) { - body.commit_id = commit_id; - } - if (!last_commit_id && fileData.last_commit_id) { - body.last_commit_id = fileData.last_commit_id; - } - else if (last_commit_id) { - body.last_commit_id = last_commit_id; - } - } - } - catch (error) { - if (!(error instanceof Error && error.message.includes("File not found"))) { - throw error; - } - // File doesn't exist, use POST - no need for commit IDs for new files - // But still use any provided as parameters if they exist - if (commit_id) { - body.commit_id = commit_id; - } - if (last_commit_id) { - body.last_commit_id = last_commit_id; - } - } - const response = await fetch(url.toString(), { - method, - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, - }, - body: JSON.stringify(body), - }); - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); - } - const data = await response.json(); - return GitLabCreateUpdateFileResponseSchema.parse(data); -} -/** - * Create a tree structure in a GitLab project repository - * 저장소에 트리 구조 생성 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {FileOperation[]} files - Array of file operations - * @param {string} [ref] - The name of the branch, tag or commit - * @returns {Promise} The created tree - */ -async function createTree(projectId, files, ref) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/tree`); - if (ref) { - url.searchParams.append("ref", ref); - } - const response = await fetch(url.toString(), { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, - }, - body: JSON.stringify({ - files: files.map((file) => ({ - file_path: file.path, - content: file.content, - encoding: "text", - })), - }), - }); - if (response.status === 400) { - const errorBody = await response.text(); - throw new Error(`Invalid request: ${errorBody}`); - } - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); - } - const data = await response.json(); - return GitLabTreeSchema.parse(data); -} -/** - * Create a commit in a GitLab project repository - * 저장소에 커밋 생성 - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {string} message - The commit message - * @param {string} branch - The branch name - * @param {FileOperation[]} actions - Array of file operations for the commit - * @returns {Promise} The created commit - */ -async function createCommit(projectId, message, branch, actions) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits`); - const response = await fetch(url.toString(), { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, - }, - body: JSON.stringify({ - branch, - commit_message: message, - actions: actions.map((action) => ({ - action: "create", - file_path: action.path, - content: action.content, - encoding: "text", - })), - }), - }); - if (response.status === 400) { - const errorBody = await response.text(); - throw new Error(`Invalid request: ${errorBody}`); - } - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); - } - const data = await response.json(); - return GitLabCommitSchema.parse(data); -} -/** - * Search for GitLab projects - * 프로젝트 검색 - * - * @param {string} query - The search query - * @param {number} [page=1] - The page number - * @param {number} [perPage=20] - Number of items per page - * @returns {Promise} The search results - */ -async function searchProjects(query, page = 1, perPage = 20) { - const url = new URL(`${GITLAB_API_URL}/projects`); - url.searchParams.append("search", query); - url.searchParams.append("page", page.toString()); - url.searchParams.append("per_page", perPage.toString()); - url.searchParams.append("order_by", "id"); - url.searchParams.append("sort", "desc"); - const response = await fetch(url.toString(), { - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, - }, - }); - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); - } - const projects = (await response.json()); - const totalCount = response.headers.get("x-total"); - const totalPages = response.headers.get("x-total-pages"); - // GitLab API doesn't return these headers for results > 10,000 - const count = totalCount ? parseInt(totalCount) : projects.length; - return GitLabSearchResponseSchema.parse({ - count, - total_pages: totalPages ? parseInt(totalPages) : Math.ceil(count / perPage), - current_page: page, - items: projects, - }); -} -/** - * Create a new GitLab repository - * 새 저장소 생성 - * - * @param {z.infer} options - Repository creation options - * @returns {Promise} The created repository - */ -async function createRepository(options) { - const response = await fetch(`${GITLAB_API_URL}/projects`, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, - }, - body: JSON.stringify({ - name: options.name, - description: options.description, - visibility: options.visibility, - initialize_with_readme: options.initialize_with_readme, - default_branch: "main", - path: options.name.toLowerCase().replace(/\s+/g, "-"), - }), - }); - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); - } - const data = await response.json(); - return GitLabRepositorySchema.parse(data); -} -/** - * Get merge request details - * MR 조회 함수 (Function to retrieve merge request) - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {number} mergeRequestIid - The internal ID of the merge request - * @returns {Promise} The merge request details - */ -async function getMergeRequest(projectId, mergeRequestIid) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`); - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); - return GitLabMergeRequestSchema.parse(await response.json()); -} -/** - * Get merge request changes/diffs - * MR 변경사항 조회 함수 (Function to retrieve merge request changes) - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {number} mergeRequestIid - The internal ID of the merge request - * @param {string} [view] - The view type for the diff (inline or parallel) - * @returns {Promise} The merge request diffs - */ -async function getMergeRequestDiffs(projectId, mergeRequestIid, view) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/changes`); - if (view) { - url.searchParams.append("view", view); - } - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); - const data = (await response.json()); - return z.array(GitLabMergeRequestDiffSchema).parse(data.changes); -} -/** - * Update a merge request - * MR 업데이트 함수 (Function to update merge request) - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {number} mergeRequestIid - The internal ID of the merge request - * @param {Object} options - The update options - * @returns {Promise} The updated merge request - */ -async function updateMergeRequest(projectId, mergeRequestIid, options) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`); - const response = await fetch(url.toString(), { - method: "PUT", - headers: DEFAULT_HEADERS, - body: JSON.stringify(options), - }); - await handleGitLabError(response); - return GitLabMergeRequestSchema.parse(await response.json()); -} -/** - * Create a new note (comment) on an issue or merge request - * 📦 새로운 함수: createNote - 이슈 또는 병합 요청에 노트(댓글)를 추가하는 함수 - * (New function: createNote - Function to add a note (comment) to an issue or merge request) - * - * @param {string} projectId - The ID or URL-encoded path of the project - * @param {"issue" | "merge_request"} noteableType - The type of the item to add a note to (issue or merge_request) - * @param {number} noteableIid - The internal ID of the issue or merge request - * @param {string} body - The content of the note - * @returns {Promise} The created note - */ -async function createNote(projectId, noteableType, // 'issue' 또는 'merge_request' 타입 명시 -noteableIid, body) { - // ⚙️ 응답 타입은 GitLab API 문서에 따라 조정 가능 - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/${noteableType}s/${noteableIid}/notes` // Using plural form (issues/merge_requests) as per GitLab API documentation - ); - const response = await fetch(url.toString(), { - method: "POST", - headers: DEFAULT_HEADERS, - body: JSON.stringify({ body }), - }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorText}`); - } - 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) { - 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) { - 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, parentId) { - 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); -} -/** - * 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, options = {}) { - 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 = {}) { - // Construct the query parameters - const params = new URLSearchParams(); - for (const [key, value] of Object.entries(options)) { - if (value !== undefined && value !== null) { - if (typeof value === "boolean") { - params.append(key, value ? "true" : "false"); - } - else { - params.append(key, String(value)); - } - } - } - // Make the API request - const response = await fetch(`${GITLAB_API_URL}/projects?${params.toString()}`, { - method: "GET", - headers: DEFAULT_HEADERS, - }); - // Handle errors - await handleGitLabError(response); - // Parse and return the data - const data = await response.json(); - return z.array(GitLabProjectSchema).parse(data); -} -/** - * List labels for a project - * - * @param projectId The ID or URL-encoded path of the project - * @param options Optional parameters for listing labels - * @returns Array of GitLab labels - */ -async function listLabels(projectId, options = {}) { - // Construct the URL with project path - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`); - // Add query parameters - Object.entries(options).forEach(([key, value]) => { - if (value !== undefined) { - if (typeof value === "boolean") { - url.searchParams.append(key, value ? "true" : "false"); - } - else { - url.searchParams.append(key, String(value)); - } - } - }); - // Make the API request - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - // Handle errors - await handleGitLabError(response); - // Parse and return the data - const data = await response.json(); - return data; -} -/** - * Get a single label from a project - * - * @param projectId The ID or URL-encoded path of the project - * @param labelId The ID or name of the label - * @param includeAncestorGroups Whether to include ancestor groups - * @returns GitLab label - */ -async function getLabel(projectId, labelId, includeAncestorGroups) { - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`); - // Add query parameters - if (includeAncestorGroups !== undefined) { - url.searchParams.append("include_ancestor_groups", includeAncestorGroups ? "true" : "false"); - } - // Make the API request - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - // Handle errors - await handleGitLabError(response); - // Parse and return the data - const data = await response.json(); - return data; -} -/** - * Create a new label in a project - * - * @param projectId The ID or URL-encoded path of the project - * @param options Options for creating the label - * @returns Created GitLab label - */ -async function createLabel(projectId, options) { - // Make the API request - const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`, { - method: "POST", - headers: DEFAULT_HEADERS, - body: JSON.stringify(options), - }); - // Handle errors - await handleGitLabError(response); - // Parse and return the data - const data = await response.json(); - return data; -} -/** - * Update an existing label in a project - * - * @param projectId The ID or URL-encoded path of the project - * @param labelId The ID or name of the label to update - * @param options Options for updating the label - * @returns Updated GitLab label - */ -async function updateLabel(projectId, labelId, options) { - // Make the API request - const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`, { - method: "PUT", - headers: DEFAULT_HEADERS, - body: JSON.stringify(options), - }); - // Handle errors - await handleGitLabError(response); - // Parse and return the data - const data = await response.json(); - return data; -} -/** - * Delete a label from a project - * - * @param projectId The ID or URL-encoded path of the project - * @param labelId The ID or name of the label to delete - */ -async function deleteLabel(projectId, labelId) { - // Make the API request - const response = await fetch(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`, { - method: "DELETE", - headers: DEFAULT_HEADERS, - }); - // Handle errors - await handleGitLabError(response); -} -/** - * List all projects in a GitLab group - * - * @param {z.infer} options - Options for listing group projects - * @returns {Promise} Array of projects in the group - */ -async function listGroupProjects(options) { - const url = new URL(`${GITLAB_API_URL}/groups/${encodeURIComponent(options.group_id)}/projects`); - // Add optional parameters to URL - if (options.include_subgroups) - url.searchParams.append('include_subgroups', 'true'); - if (options.search) - url.searchParams.append('search', options.search); - if (options.order_by) - url.searchParams.append('order_by', options.order_by); - if (options.sort) - url.searchParams.append('sort', options.sort); - if (options.page) - url.searchParams.append('page', options.page.toString()); - if (options.per_page) - url.searchParams.append('per_page', options.per_page.toString()); - if (options.archived !== undefined) - url.searchParams.append('archived', options.archived.toString()); - if (options.visibility) - url.searchParams.append('visibility', options.visibility); - if (options.with_issues_enabled !== undefined) - url.searchParams.append('with_issues_enabled', options.with_issues_enabled.toString()); - if (options.with_merge_requests_enabled !== undefined) - url.searchParams.append('with_merge_requests_enabled', options.with_merge_requests_enabled.toString()); - if (options.min_access_level !== undefined) - url.searchParams.append('min_access_level', options.min_access_level.toString()); - if (options.with_programming_language) - url.searchParams.append('with_programming_language', options.with_programming_language); - if (options.starred !== undefined) - url.searchParams.append('starred', options.starred.toString()); - if (options.statistics !== undefined) - url.searchParams.append('statistics', options.statistics.toString()); - if (options.with_custom_attributes !== undefined) - url.searchParams.append('with_custom_attributes', options.with_custom_attributes.toString()); - if (options.with_security_reports !== undefined) - url.searchParams.append('with_security_reports', options.with_security_reports.toString()); - const response = await fetch(url.toString(), { - method: "GET", - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); - const projects = await response.json(); - return GitLabProjectSchema.array().parse(projects); -} -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: "create_or_update_file", - description: "Create or update a single file in a GitLab project", - inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema), - }, - { - name: "search_repositories", - description: "Search for GitLab projects", - inputSchema: zodToJsonSchema(SearchRepositoriesSchema), - }, - { - name: "create_repository", - description: "Create a new GitLab project", - inputSchema: zodToJsonSchema(CreateRepositorySchema), - }, - { - name: "get_file_contents", - description: "Get the contents of a file or directory from a GitLab project", - inputSchema: zodToJsonSchema(GetFileContentsSchema), - }, - { - name: "push_files", - description: "Push multiple files to a GitLab project in a single commit", - inputSchema: zodToJsonSchema(PushFilesSchema), - }, - { - name: "create_issue", - description: "Create a new issue in a GitLab project", - inputSchema: zodToJsonSchema(CreateIssueSchema), - }, - { - name: "create_merge_request", - description: "Create a new merge request in a GitLab project", - inputSchema: zodToJsonSchema(CreateMergeRequestSchema), - }, - { - name: "fork_repository", - description: "Fork a GitLab project to your account or specified namespace", - inputSchema: zodToJsonSchema(ForkRepositorySchema), - }, - { - name: "create_branch", - description: "Create a new branch in a GitLab project", - inputSchema: zodToJsonSchema(CreateBranchSchema), - }, - { - name: "get_merge_request", - description: "Get details of a merge request", - inputSchema: zodToJsonSchema(GetMergeRequestSchema), - }, - { - name: "get_merge_request_diffs", - description: "Get the changes/diffs of a merge request", - inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema), - }, - { - name: "update_merge_request", - description: "Update a merge request", - inputSchema: zodToJsonSchema(UpdateMergeRequestSchema), - }, - { - name: "create_note", - description: "Create a new note (comment) to an issue or merge request", - inputSchema: zodToJsonSchema(CreateNoteSchema), - }, - { - name: "list_merge_request_discussions", - description: "List discussion items for a merge request", - inputSchema: zodToJsonSchema(ListMergeRequestDiscussionsSchema), - }, - { - name: "update_merge_request_note", - description: "Modify an existing merge request thread note", - inputSchema: zodToJsonSchema(UpdateMergeRequestNoteSchema), - }, - { - name: "list_issues", - description: "List issues in a GitLab project with filtering options", - inputSchema: zodToJsonSchema(ListIssuesSchema), - }, - { - name: "get_issue", - description: "Get details of a specific issue in a GitLab project", - inputSchema: zodToJsonSchema(GetIssueSchema), - }, - { - name: "update_issue", - description: "Update an issue in a GitLab project", - inputSchema: zodToJsonSchema(UpdateIssueSchema), - }, - { - name: "delete_issue", - description: "Delete an issue from a GitLab project", - inputSchema: zodToJsonSchema(DeleteIssueSchema), - }, - { - name: "list_issue_links", - description: "List all issue links for a specific issue", - inputSchema: zodToJsonSchema(ListIssueLinksSchema), - }, - { - name: "get_issue_link", - description: "Get a specific issue link", - inputSchema: zodToJsonSchema(GetIssueLinkSchema), - }, - { - name: "create_issue_link", - description: "Create an issue link between two issues", - inputSchema: zodToJsonSchema(CreateIssueLinkSchema), - }, - { - name: "delete_issue_link", - description: "Delete an issue link", - inputSchema: zodToJsonSchema(DeleteIssueLinkSchema), - }, - { - name: "list_namespaces", - description: "List all namespaces available to the current user", - inputSchema: zodToJsonSchema(ListNamespacesSchema), - }, - { - name: "get_namespace", - description: "Get details of a namespace by ID or path", - inputSchema: zodToJsonSchema(GetNamespaceSchema), - }, - { - name: "verify_namespace", - description: "Verify if a namespace path exists", - inputSchema: zodToJsonSchema(VerifyNamespaceSchema), - }, - { - name: "get_project", - description: "Get details of a specific project", - inputSchema: zodToJsonSchema(GetProjectSchema), - }, - { - name: "list_projects", - description: "List projects accessible by the current user", - inputSchema: zodToJsonSchema(ListProjectsSchema), - }, - { - name: "list_labels", - description: "List labels for a project", - inputSchema: zodToJsonSchema(ListLabelsSchema), - }, - { - name: "get_label", - description: "Get a single label from a project", - inputSchema: zodToJsonSchema(GetLabelSchema), - }, - { - name: "create_label", - description: "Create a new label in a project", - inputSchema: zodToJsonSchema(CreateLabelSchema), - }, - { - name: "update_label", - description: "Update an existing label in a project", - inputSchema: zodToJsonSchema(UpdateLabelSchema), - }, - { - name: "delete_label", - description: "Delete a label from a project", - inputSchema: zodToJsonSchema(DeleteLabelSchema), - }, - { - name: "list_group_projects", - description: "List projects in a GitLab group with filtering options", - inputSchema: zodToJsonSchema(ListGroupProjectsSchema), - }, - ], - }; -}); -server.setRequestHandler(CallToolRequestSchema, async (request) => { - try { - if (!request.params.arguments) { - throw new Error("Arguments are required"); - } - switch (request.params.name) { - case "fork_repository": { - 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": { - const args = CreateBranchSchema.parse(request.params.arguments); - let ref = args.ref; - if (!ref) { - ref = await getDefaultBranchRef(args.project_id); - } - const branch = await createBranch(args.project_id, { - name: args.branch, - ref, - }); - return { - content: [{ type: "text", text: JSON.stringify(branch, null, 2) }], - }; - } - case "search_repositories": { - const args = SearchRepositoriesSchema.parse(request.params.arguments); - const results = await searchProjects(args.search, args.page, args.per_page); - return { - content: [{ type: "text", text: JSON.stringify(results, null, 2) }], - }; - } - case "create_repository": { - const args = CreateRepositorySchema.parse(request.params.arguments); - const repository = await createRepository(args); - return { - content: [ - { type: "text", text: JSON.stringify(repository, null, 2) }, - ], - }; - } - case "get_file_contents": { - const args = GetFileContentsSchema.parse(request.params.arguments); - const contents = await getFileContents(args.project_id, args.file_path, args.ref); - return { - content: [{ type: "text", text: JSON.stringify(contents, null, 2) }], - }; - } - case "create_or_update_file": { - const args = CreateOrUpdateFileSchema.parse(request.params.arguments); - const result = await createOrUpdateFile(args.project_id, args.file_path, args.content, args.commit_message, args.branch, args.previous_path, args.last_commit_id, args.commit_id); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - case "push_files": { - const args = PushFilesSchema.parse(request.params.arguments); - const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map((f) => ({ path: f.file_path, content: f.content }))); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } - case "create_issue": { - const args = CreateIssueSchema.parse(request.params.arguments); - const { project_id, ...options } = args; - const issue = await createIssue(project_id, options); - return { - content: [{ type: "text", text: JSON.stringify(issue, null, 2) }], - }; - } - case "create_merge_request": { - const args = CreateMergeRequestSchema.parse(request.params.arguments); - const { project_id, ...options } = args; - const mergeRequest = await createMergeRequest(project_id, options); - return { - content: [ - { type: "text", text: JSON.stringify(mergeRequest, null, 2) }, - ], - }; - } - case "update_merge_request_note": { - const args = UpdateMergeRequestNoteSchema.parse(request.params.arguments); - const note = await updateMergeRequestNote(args.project_id, args.merge_request_iid, args.discussion_id, args.note_id, args.body, args.resolved // Pass resolved if provided - ); - return { - content: [{ type: "text", text: JSON.stringify(note, null, 2) }], - }; - } - case "get_merge_request": { - const args = GetMergeRequestSchema.parse(request.params.arguments); - const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid); - return { - content: [ - { type: "text", text: JSON.stringify(mergeRequest, null, 2) }, - ], - }; - } - case "get_merge_request_diffs": { - const args = GetMergeRequestDiffsSchema.parse(request.params.arguments); - const diffs = await getMergeRequestDiffs(args.project_id, args.merge_request_iid, args.view); - return { - content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }], - }; - } - case "update_merge_request": { - const args = UpdateMergeRequestSchema.parse(request.params.arguments); - const { project_id, merge_request_iid, ...options } = args; - const mergeRequest = await updateMergeRequest(project_id, merge_request_iid, options); - return { - content: [ - { type: "text", text: JSON.stringify(mergeRequest, null, 2) }, - ], - }; - } - case "list_merge_request_discussions": { - const args = ListMergeRequestDiscussionsSchema.parse(request.params.arguments); - const discussions = await listMergeRequestDiscussions(args.project_id, args.merge_request_iid); - return { - content: [ - { type: "text", text: JSON.stringify(discussions, null, 2) }, - ], - }; - } - case "list_namespaces": { - const args = ListNamespacesSchema.parse(request.params.arguments); - const url = new URL(`${GITLAB_API_URL}/namespaces`); - if (args.search) { - url.searchParams.append("search", args.search); - } - if (args.page) { - url.searchParams.append("page", args.page.toString()); - } - if (args.per_page) { - url.searchParams.append("per_page", args.per_page.toString()); - } - if (args.owned) { - url.searchParams.append("owned", args.owned.toString()); - } - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); - const data = await response.json(); - const namespaces = z.array(GitLabNamespaceSchema).parse(data); - return { - content: [{ type: "text", text: JSON.stringify(namespaces, null, 2) }], - }; - } - case "get_namespace": { - const args = GetNamespaceSchema.parse(request.params.arguments); - const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.namespace_id)}`); - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); - const data = await response.json(); - const namespace = GitLabNamespaceSchema.parse(data); - return { - content: [{ type: "text", text: JSON.stringify(namespace, null, 2) }], - }; - } - case "verify_namespace": { - const args = VerifyNamespaceSchema.parse(request.params.arguments); - const url = new URL(`${GITLAB_API_URL}/namespaces/${encodeURIComponent(args.path)}/exists`); - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); - const data = await response.json(); - const namespaceExists = GitLabNamespaceExistsResponseSchema.parse(data); - return { - content: [{ type: "text", text: JSON.stringify(namespaceExists, null, 2) }], - }; - } - case "get_project": { - const args = GetProjectSchema.parse(request.params.arguments); - const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(args.project_id)}`); - const response = await fetch(url.toString(), { - headers: DEFAULT_HEADERS, - }); - await handleGitLabError(response); - const data = await response.json(); - const project = GitLabProjectSchema.parse(data); - 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; - const note = await createNote(project_id, noteable_type, noteable_iid, body); - return { - content: [{ type: "text", text: JSON.stringify(note, null, 2) }], - }; - } - case "list_issues": { - const args = ListIssuesSchema.parse(request.params.arguments); - const { project_id, ...options } = args; - const issues = await listIssues(project_id, options); - return { - content: [{ type: "text", text: JSON.stringify(issues, null, 2) }], - }; - } - case "get_issue": { - const args = GetIssueSchema.parse(request.params.arguments); - const issue = await getIssue(args.project_id, args.issue_iid); - return { - content: [{ type: "text", text: JSON.stringify(issue, null, 2) }], - }; - } - case "update_issue": { - const args = UpdateIssueSchema.parse(request.params.arguments); - const { project_id, issue_iid, ...options } = args; - const issue = await updateIssue(project_id, issue_iid, options); - return { - content: [{ type: "text", text: JSON.stringify(issue, null, 2) }], - }; - } - case "delete_issue": { - const args = DeleteIssueSchema.parse(request.params.arguments); - await deleteIssue(args.project_id, args.issue_iid); - return { - content: [{ type: "text", text: JSON.stringify({ status: "success", message: "Issue deleted successfully" }, null, 2) }], - }; - } - case "list_issue_links": { - const args = ListIssueLinksSchema.parse(request.params.arguments); - const links = await listIssueLinks(args.project_id, args.issue_iid); - return { - content: [{ type: "text", text: JSON.stringify(links, null, 2) }], - }; - } - case "get_issue_link": { - const args = GetIssueLinkSchema.parse(request.params.arguments); - const link = await getIssueLink(args.project_id, args.issue_iid, args.issue_link_id); - return { - content: [{ type: "text", text: JSON.stringify(link, null, 2) }], - }; - } - case "create_issue_link": { - const args = CreateIssueLinkSchema.parse(request.params.arguments); - const link = await createIssueLink(args.project_id, args.issue_iid, args.target_project_id, args.target_issue_iid, args.link_type); - return { - content: [{ type: "text", text: JSON.stringify(link, null, 2) }], - }; - } - case "delete_issue_link": { - const args = DeleteIssueLinkSchema.parse(request.params.arguments); - await deleteIssueLink(args.project_id, args.issue_iid, args.issue_link_id); - return { - content: [{ type: "text", text: JSON.stringify({ status: "success", message: "Issue link deleted successfully" }, null, 2) }], - }; - } - case "list_labels": { - const args = ListLabelsSchema.parse(request.params.arguments); - const labels = await listLabels(args.project_id, args); - return { - content: [{ type: "text", text: JSON.stringify(labels, null, 2) }], - }; - } - case "get_label": { - const args = GetLabelSchema.parse(request.params.arguments); - const label = await getLabel(args.project_id, args.label_id, args.include_ancestor_groups); - return { - content: [{ type: "text", text: JSON.stringify(label, null, 2) }], - }; - } - case "create_label": { - const args = CreateLabelSchema.parse(request.params.arguments); - const label = await createLabel(args.project_id, args); - return { - content: [{ type: "text", text: JSON.stringify(label, null, 2) }], - }; - } - case "update_label": { - const args = UpdateLabelSchema.parse(request.params.arguments); - const { project_id, label_id, ...options } = args; - const label = await updateLabel(project_id, label_id, options); - return { - content: [{ type: "text", text: JSON.stringify(label, null, 2) }], - }; - } - case "delete_label": { - const args = DeleteLabelSchema.parse(request.params.arguments); - await deleteLabel(args.project_id, args.label_id); - return { - content: [{ type: "text", text: JSON.stringify({ status: "success", message: "Label deleted successfully" }, null, 2) }], - }; - } - case "list_group_projects": { - const args = ListGroupProjectsSchema.parse(request.params.arguments); - const projects = await listGroupProjects(args); - return { - content: [{ type: "text", text: JSON.stringify(projects, null, 2) }], - }; - } - default: - throw new Error(`Unknown tool: ${request.params.name}`); - } - } - catch (error) { - if (error instanceof z.ZodError) { - throw new Error(`Invalid arguments: ${error.errors - .map((e) => `${e.path.join(".")}: ${e.message}`) - .join(", ")}`); - } - throw error; - } -}); -/** - * Initialize and run the server - * 서버 초기화 및 실행 - */ -async function runServer() { - try { - console.error("========================"); - console.error(`GitLab MCP Server v${SERVER_VERSION}`); - console.error(`API URL: ${GITLAB_API_URL}`); - console.error("========================"); - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("GitLab MCP Server running on stdio"); - } - catch (error) { - console.error("Error initializing server:", error); - process.exit(1); - } -} -runServer().catch((error) => { - console.error("Fatal error in main():", error); - process.exit(1); -}); diff --git a/build/schemas.js b/build/schemas.js deleted file mode 100644 index 1754513..0000000 --- a/build/schemas.js +++ /dev/null @@ -1,684 +0,0 @@ -import { z } from "zod"; -// Base schemas for common types -export const GitLabAuthorSchema = z.object({ - name: z.string(), - email: z.string(), - date: z.string(), -}); -// Namespace related schemas -// Base schema for project-related operations -const ProjectParamsSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), // Changed from owner/repo to match GitLab API -}); -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 - id: z.number(), - avatar_url: z.string(), - web_url: z.string(), // Changed from html_url to match GitLab API - name: z.string(), // Added as GitLab includes full name - state: z.string(), // Added as GitLab includes user state -}); -export const GitLabRepositorySchema = z.object({ - id: z.number(), - name: z.string(), - path_with_namespace: z.string(), - visibility: z.string().optional(), - owner: GitLabOwnerSchema.optional(), - web_url: z.string().optional(), - description: z.string().nullable(), - fork: z.boolean().optional(), - ssh_url_to_repo: z.string().optional(), - http_url_to_repo: z.string().optional(), - 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(), -}); -// Project schema (extended from repository schema) -export const GitLabProjectSchema = GitLabRepositorySchema; -// File content schemas -export const GitLabFileContentSchema = z.object({ - file_name: z.string(), // Changed from name to match GitLab API - file_path: z.string(), // Changed from path to match GitLab API - size: z.number(), - encoding: z.string(), - content: z.string(), - content_sha256: z.string(), // Changed from sha to match GitLab API - ref: z.string(), // Added as GitLab requires branch reference - blob_id: z.string(), // Added to match GitLab API - commit_id: z.string(), // ID of the current file version - last_commit_id: z.string(), // Added to match GitLab API - execute_filemode: z.boolean().optional(), // Added to match GitLab API -}); -export const GitLabDirectoryContentSchema = z.object({ - name: z.string(), - path: z.string(), - type: z.string(), - mode: z.string(), - id: z.string(), // Changed from sha to match GitLab API - web_url: z.string(), // Changed from html_url to match GitLab API -}); -export const GitLabContentSchema = z.union([ - GitLabFileContentSchema, - z.array(GitLabDirectoryContentSchema), -]); -// Operation schemas -export const FileOperationSchema = z.object({ - path: z.string(), - content: z.string(), -}); -// Tree and commit schemas -export const GitLabTreeEntrySchema = z.object({ - id: z.string(), // Changed from sha to match GitLab API - name: z.string(), - type: z.enum(["blob", "tree"]), - path: z.string(), - mode: z.string(), -}); -export const GitLabTreeSchema = z.object({ - id: z.string(), // Changed from sha to match GitLab API - tree: z.array(GitLabTreeEntrySchema), -}); -export const GitLabCommitSchema = z.object({ - id: z.string(), // Changed from sha to match GitLab API - short_id: z.string(), // Added to match GitLab API - title: z.string(), // Changed from message to match GitLab API - author_name: z.string(), - author_email: z.string(), - authored_date: z.string(), - committer_name: z.string(), - committer_email: z.string(), - committed_date: z.string(), - web_url: z.string(), // Changed from html_url to match GitLab API - parent_ids: z.array(z.string()), // Changed from parents to match GitLab API -}); -// Reference schema -export const GitLabReferenceSchema = z.object({ - name: z.string(), // Changed from ref to match GitLab API - commit: z.object({ - id: z.string(), // Changed from sha to match GitLab API - web_url: z.string(), // Changed from url to match GitLab API - }), -}); -// Input schemas for operations -export const CreateRepositoryOptionsSchema = z.object({ - name: z.string(), - description: z.string().optional(), - visibility: z.enum(["private", "internal", "public"]).optional(), // Changed from private to match GitLab API - initialize_with_readme: z.boolean().optional(), // Changed from auto_init to match GitLab API -}); -export const CreateIssueOptionsSchema = z.object({ - title: z.string(), - description: z.string().optional(), // Changed from body to match GitLab API - assignee_ids: z.array(z.number()).optional(), // Changed from assignees to match GitLab API - milestone_id: z.number().optional(), // Changed from milestone to match GitLab API - labels: z.array(z.string()).optional(), -}); -export const CreateMergeRequestOptionsSchema = z.object({ - // Changed from CreatePullRequestOptionsSchema - title: z.string(), - description: z.string().optional(), // Changed from body to match GitLab API - source_branch: z.string(), // Changed from head to match GitLab API - target_branch: z.string(), // Changed from base to match GitLab API - allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API - draft: z.boolean().optional(), -}); -export const CreateBranchOptionsSchema = z.object({ - name: z.string(), // Changed from ref to match GitLab API - ref: z.string(), // The source branch/commit for the new branch -}); -// Response schemas for operations -export const GitLabCreateUpdateFileResponseSchema = z.object({ - file_path: z.string(), - branch: z.string(), - commit_id: z.string().optional(), // Optional since it's not always returned by the API - content: GitLabFileContentSchema.optional(), -}); -export const GitLabSearchResponseSchema = z.object({ - count: z.number().optional(), - total_pages: z.number().optional(), - current_page: z.number().optional(), - items: z.array(GitLabRepositorySchema), -}); -// Issue related schemas -export const GitLabLabelSchema = z.object({ - id: z.number(), - name: z.string(), - color: z.string(), - text_color: z.string(), - description: z.string().nullable(), - description_html: z.string().nullable(), - open_issues_count: z.number().optional(), - closed_issues_count: z.number().optional(), - open_merge_requests_count: z.number().optional(), - subscribed: z.boolean().optional(), - priority: z.number().nullable().optional(), - is_project_label: z.boolean().optional(), -}); -export const GitLabUserSchema = z.object({ - username: z.string(), // Changed from login to match GitLab API - id: z.number(), - name: z.string(), - avatar_url: z.string(), - web_url: z.string(), // Changed from html_url to match GitLab API -}); -export const GitLabMilestoneSchema = z.object({ - id: z.number(), - iid: z.number(), // Added to match GitLab API - title: z.string(), - description: z.string(), - state: z.string(), - web_url: z.string(), // Changed from html_url to match GitLab API -}); -export const GitLabIssueSchema = z.object({ - id: z.number(), - iid: z.number(), // Added to match GitLab API - project_id: z.number(), // Added to match GitLab API - title: z.string(), - description: z.string(), // Changed from body to match GitLab API - state: z.string(), - author: GitLabUserSchema, - assignees: z.array(GitLabUserSchema), - labels: z.array(GitLabLabelSchema).or(z.array(z.string())), // Support both label objects and strings - milestone: GitLabMilestoneSchema.nullable(), - created_at: z.string(), - updated_at: z.string(), - closed_at: z.string().nullable(), - web_url: z.string(), // Changed from html_url to match GitLab API - references: z.object({ - short: z.string(), - relative: z.string(), - full: z.string(), - }).optional(), - time_stats: z.object({ - time_estimate: z.number(), - total_time_spent: z.number(), - human_time_estimate: z.string().nullable(), - human_total_time_spent: z.string().nullable(), - }).optional(), - confidential: z.boolean().optional(), - due_date: z.string().nullable().optional(), - discussion_locked: z.boolean().nullable().optional(), - weight: z.number().nullable().optional(), -}); -// NEW SCHEMA: For issue with link details (used in listing issue links) -export const GitLabIssueWithLinkDetailsSchema = GitLabIssueSchema.extend({ - issue_link_id: z.number(), - link_type: z.enum(['relates_to', 'blocks', 'is_blocked_by']), - link_created_at: z.string(), - link_updated_at: z.string(), -}); -// Fork related schemas -export const GitLabForkParentSchema = z.object({ - name: z.string(), - path_with_namespace: z.string(), // Changed from full_name to match GitLab API - owner: 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.optional(), // Made optional to handle cases where GitLab API doesn't include it -}); -// Merge Request related schemas (equivalent to Pull Request) -export const GitLabMergeRequestDiffRefSchema = z.object({ - base_sha: z.string(), - head_sha: z.string(), - start_sha: z.string(), -}); -export const GitLabMergeRequestSchema = z.object({ - id: z.number(), - iid: z.number(), - project_id: z.number(), - title: z.string(), - description: z.string().nullable(), - state: z.string(), - merged: z.boolean().optional(), - draft: z.boolean().optional(), - author: GitLabUserSchema, - assignees: z.array(GitLabUserSchema).optional(), - source_branch: z.string(), - target_branch: z.string(), - diff_refs: GitLabMergeRequestDiffRefSchema.nullable().optional(), - web_url: z.string(), - created_at: z.string(), - updated_at: z.string(), - merged_at: z.string().nullable(), - closed_at: z.string().nullable(), - merge_commit_sha: z.string().nullable(), - detailed_merge_status: z.string().optional(), - merge_status: z.string().optional(), - merge_error: z.string().nullable().optional(), - work_in_progress: z.boolean().optional(), - blocking_discussions_resolved: z.boolean().optional(), - should_remove_source_branch: z.boolean().nullable().optional(), - force_remove_source_branch: z.boolean().nullable().optional(), - allow_collaboration: z.boolean().optional(), - allow_maintainer_to_push: z.boolean().optional(), - changes_count: z.string().nullable().optional(), - merge_when_pipeline_succeeds: z.boolean().optional(), - squash: z.boolean().optional(), - labels: z.array(z.string()).optional(), -}); -// Discussion related schemas -export const GitLabDiscussionNoteSchema = z.object({ - id: z.number(), - type: z.enum(["DiscussionNote", "DiffNote", "Note"]).nullable(), // Allow null type for regular notes - body: z.string(), - attachment: z.any().nullable(), // Can be string or object, handle appropriately - author: GitLabUserSchema, - created_at: z.string(), - updated_at: z.string(), - system: z.boolean(), - noteable_id: z.number(), - noteable_type: z.enum(["Issue", "MergeRequest", "Snippet", "Commit", "Epic"]), - project_id: z.number().optional(), // Optional for group-level discussions like Epics - noteable_iid: z.number().nullable(), - resolvable: z.boolean().optional(), - resolved: z.boolean().optional(), - resolved_by: GitLabUserSchema.nullable().optional(), - resolved_at: z.string().nullable().optional(), - position: z.object({ - base_sha: z.string(), - start_sha: z.string(), - head_sha: z.string(), - old_path: z.string(), - new_path: z.string(), - position_type: z.enum(["text", "image", "file"]), - old_line: z.number().nullable(), - new_line: z.number().nullable(), - line_range: z.object({ - start: z.object({ - line_code: z.string(), - type: z.enum(["new", "old"]), - old_line: z.number().nullable(), - new_line: z.number().nullable(), - }), - end: z.object({ - line_code: z.string(), - type: z.enum(["new", "old"]), - old_line: z.number().nullable(), - new_line: z.number().nullable(), - }), - }).nullable().optional(), // For multi-line diff notes - width: z.number().optional(), // For image diff notes - height: z.number().optional(), // For image diff notes - x: z.number().optional(), // For image diff notes - y: z.number().optional(), // For image diff notes - }).optional(), -}); -export const GitLabDiscussionSchema = z.object({ - id: z.string(), - individual_note: z.boolean(), - notes: z.array(GitLabDiscussionNoteSchema), -}); -// Input schema for listing merge request discussions -export const ListMergeRequestDiscussionsSchema = ProjectParamsSchema.extend({ - merge_request_iid: z.number().describe("The IID of a merge request"), -}); -// Input schema for updating a merge request discussion note -export const UpdateMergeRequestNoteSchema = ProjectParamsSchema.extend({ - merge_request_iid: z.number().describe("The IID of a merge request"), - discussion_id: z.string().describe("The ID of a thread"), - note_id: z.number().describe("The ID of a thread note"), - body: z.string().describe("The content of the note or reply"), - resolved: z.boolean().optional().describe("Resolve or unresolve the note"), // Optional based on API docs -}); -// API Operation Parameter Schemas -export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({ - file_path: z.string().describe("Path where to create/update the file"), - content: z.string().describe("Content of the file"), - commit_message: z.string().describe("Commit message"), - branch: z.string().describe("Branch to create/update the file in"), - previous_path: z - .string() - .optional() - .describe("Path of the file to move/rename"), - last_commit_id: z - .string() - .optional() - .describe("Last known file commit ID"), - commit_id: z - .string() - .optional() - .describe("Current file commit ID (for update operations)"), -}); -export const SearchRepositoriesSchema = z.object({ - search: z.string().describe("Search query"), // Changed from query to match GitLab API - page: z - .number() - .optional() - .describe("Page number for pagination (default: 1)"), - per_page: z - .number() - .optional() - .describe("Number of results per page (default: 20)"), -}); -export const CreateRepositorySchema = z.object({ - name: z.string().describe("Repository name"), - description: z.string().optional().describe("Repository description"), - visibility: z - .enum(["private", "internal", "public"]) - .optional() - .describe("Repository visibility level"), - initialize_with_readme: z - .boolean() - .optional() - .describe("Initialize with README.md"), -}); -export const GetFileContentsSchema = ProjectParamsSchema.extend({ - file_path: z.string().describe("Path to the file or directory"), - ref: z.string().optional().describe("Branch/tag/commit to get contents from"), -}); -export const PushFilesSchema = ProjectParamsSchema.extend({ - branch: z.string().describe("Branch to push to"), - files: z - .array(z.object({ - file_path: z.string().describe("Path where to create the file"), - content: z.string().describe("Content of the file"), - })) - .describe("Array of files to push"), - commit_message: z.string().describe("Commit message"), -}); -export const CreateIssueSchema = ProjectParamsSchema.extend({ - title: z.string().describe("Issue title"), - description: z.string().optional().describe("Issue description"), - assignee_ids: z - .array(z.number()) - .optional() - .describe("Array of user IDs to assign"), - labels: z.array(z.string()).optional().describe("Array of label names"), - milestone_id: z.number().optional().describe("Milestone ID to assign"), -}); -export const CreateMergeRequestSchema = ProjectParamsSchema.extend({ - title: z.string().describe("Merge request title"), - description: z.string().optional().describe("Merge request description"), - source_branch: z.string().describe("Branch containing changes"), - target_branch: z.string().describe("Branch to merge into"), - draft: z.boolean().optional().describe("Create as draft merge request"), - allow_collaboration: z - .boolean() - .optional() - .describe("Allow commits from upstream members"), -}); -export const ForkRepositorySchema = ProjectParamsSchema.extend({ - namespace: z.string().optional().describe("Namespace to fork to (full path)"), -}); -export const CreateBranchSchema = ProjectParamsSchema.extend({ - branch: z.string().describe("Name for the new branch"), - ref: z.string().optional().describe("Source branch/commit for new branch"), -}); -export const GitLabMergeRequestDiffSchema = z.object({ - old_path: z.string(), - new_path: z.string(), - a_mode: z.string(), - b_mode: z.string(), - diff: z.string(), - new_file: z.boolean(), - renamed_file: z.boolean(), - deleted_file: z.boolean(), -}); -export const GetMergeRequestSchema = ProjectParamsSchema.extend({ - merge_request_iid: z - .number() - .describe("The internal ID of the merge request"), -}); -export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({ - title: z.string().optional().describe("The title of the merge request"), - description: z - .string() - .optional() - .describe("The description of the merge request"), - target_branch: z.string().optional().describe("The target branch"), - assignee_ids: z - .array(z.number()) - .optional() - .describe("The ID of the users to assign the MR to"), - labels: z.array(z.string()).optional().describe("Labels for the MR"), - state_event: z - .enum(["close", "reopen"]) - .optional() - .describe("New state (close/reopen) for the MR"), - remove_source_branch: z - .boolean() - .optional() - .describe("Flag indicating if the source branch should be removed"), - squash: z - .boolean() - .optional() - .describe("Squash commits into a single commit when merging"), - draft: z.boolean().optional().describe("Work in progress merge request"), -}); -export const GetMergeRequestDiffsSchema = GetMergeRequestSchema.extend({ - view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"), -}); -export const CreateNoteSchema = z.object({ - project_id: z.string().describe("Project ID or namespace/project_path"), - noteable_type: z - .enum(["issue", "merge_request"]) - .describe("Type of noteable (issue or merge_request)"), - noteable_iid: z.number().describe("IID of the issue or merge request"), - body: z.string().describe("Note content"), -}); -// Issues API operation schemas -export const ListIssuesSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - assignee_id: z.number().optional().describe("Return issues assigned to the given user ID"), - assignee_username: z.string().optional().describe("Return issues assigned to the given username"), - author_id: z.number().optional().describe("Return issues created by the given user ID"), - author_username: z.string().optional().describe("Return issues created by the given username"), - confidential: z.boolean().optional().describe("Filter confidential or public issues"), - created_after: z.string().optional().describe("Return issues created after the given time"), - created_before: z.string().optional().describe("Return issues created before the given time"), - due_date: z.string().optional().describe("Return issues that have the due date"), - label_name: z.array(z.string()).optional().describe("Array of label names"), - milestone: z.string().optional().describe("Milestone title"), - scope: z.enum(['created-by-me', 'assigned-to-me', 'all']).optional().describe("Return issues from a specific scope"), - search: z.string().optional().describe("Search for specific terms"), - state: z.enum(['opened', 'closed', 'all']).optional().describe("Return issues with a specific state"), - updated_after: z.string().optional().describe("Return issues updated after the given time"), - updated_before: z.string().optional().describe("Return issues updated before the given time"), - with_labels_details: z.boolean().optional().describe("Return more details for each label"), - page: z.number().optional().describe("Page number for pagination"), - per_page: z.number().optional().describe("Number of items per page"), -}); -export const GetIssueSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - issue_iid: z.number().describe("The internal ID of the project issue"), -}); -export const UpdateIssueSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - issue_iid: z.number().describe("The internal ID of the project issue"), - title: z.string().optional().describe("The title of the issue"), - description: z.string().optional().describe("The description of the issue"), - assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign issue to"), - confidential: z.boolean().optional().describe("Set the issue to be confidential"), - discussion_locked: z.boolean().optional().describe("Flag to lock discussions"), - due_date: z.string().optional().describe("Date the issue is due (YYYY-MM-DD)"), - labels: z.array(z.string()).optional().describe("Array of label names"), - milestone_id: z.number().optional().describe("Milestone ID to assign"), - state_event: z.enum(['close', 'reopen']).optional().describe("Update issue state (close/reopen)"), - weight: z.number().optional().describe("Weight of the issue (0-9)"), -}); -export const DeleteIssueSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - issue_iid: z.number().describe("The internal ID of the project issue"), -}); -// Issue links related schemas -export const GitLabIssueLinkSchema = z.object({ - source_issue: GitLabIssueSchema, - target_issue: GitLabIssueSchema, - link_type: z.enum(['relates_to', 'blocks', 'is_blocked_by']), -}); -export const ListIssueLinksSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - issue_iid: z.number().describe("The internal ID of a project's issue"), -}); -export const GetIssueLinkSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - issue_iid: z.number().describe("The internal ID of a project's issue"), - issue_link_id: z.number().describe("ID of an issue relationship"), -}); -export const CreateIssueLinkSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - issue_iid: z.number().describe("The internal ID of a project's issue"), - target_project_id: z.string().describe("The ID or URL-encoded path of a target project"), - target_issue_iid: z.number().describe("The internal ID of a target project's issue"), - link_type: z.enum(['relates_to', 'blocks', 'is_blocked_by']).optional().describe("The type of the relation, defaults to relates_to"), -}); -export const DeleteIssueLinkSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - issue_iid: z.number().describe("The internal ID of a project's issue"), - issue_link_id: z.number().describe("The ID of an issue relationship"), -}); -// Namespace API operation schemas -export const ListNamespacesSchema = z.object({ - search: z.string().optional().describe("Search term for namespaces"), - page: z.number().optional().describe("Page number for pagination"), - per_page: z.number().optional().describe("Number of items per page"), - owned: z.boolean().optional().describe("Filter for namespaces owned by current user"), -}); -export const GetNamespaceSchema = z.object({ - namespace_id: z.string().describe("Namespace ID or full path"), -}); -export const VerifyNamespaceSchema = z.object({ - path: z.string().describe("Namespace path to verify"), -}); -// Project API operation schemas -export const GetProjectSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), -}); -export const ListProjectsSchema = z.object({ - search: z.string().optional().describe("Search term for projects"), - page: z.number().optional().describe("Page number for pagination"), - per_page: z.number().optional().describe("Number of items per page"), - owned: z.boolean().optional().describe("Filter for projects owned by current user"), - membership: z.boolean().optional().describe("Filter for projects where current user is a member"), - simple: z.boolean().optional().describe("Return only limited fields"), - archived: z.boolean().optional().describe("Filter for archived projects"), - visibility: z.enum(["public", "internal", "private"]).optional().describe("Filter by project visibility"), - order_by: z.enum(["id", "name", "path", "created_at", "updated_at", "last_activity_at"]).optional().describe("Return projects ordered by field"), - sort: z.enum(["asc", "desc"]).optional().describe("Return projects sorted in ascending or descending order"), - with_issues_enabled: z.boolean().optional().describe("Filter projects with issues feature enabled"), - with_merge_requests_enabled: z.boolean().optional().describe("Filter projects with merge requests feature enabled"), - min_access_level: z.number().optional().describe("Filter by minimum access level"), -}); -// Label operation schemas -export const ListLabelsSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - with_counts: z.boolean().optional().describe("Whether or not to include issue and merge request counts"), - include_ancestor_groups: z.boolean().optional().describe("Include ancestor groups"), - search: z.string().optional().describe("Keyword to filter labels by"), -}); -export const GetLabelSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - label_id: z.union([z.number(), z.string()]).describe("The ID or title of a project's label"), - include_ancestor_groups: z.boolean().optional().describe("Include ancestor groups"), -}); -export const CreateLabelSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - name: z.string().describe("The name of the label"), - color: z.string().describe("The color of the label given in 6-digit hex notation with leading '#' sign"), - description: z.string().optional().describe("The description of the label"), - priority: z.number().nullable().optional().describe("The priority of the label"), -}); -export const UpdateLabelSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - label_id: z.union([z.number(), z.string()]).describe("The ID or title of a project's label"), - new_name: z.string().optional().describe("The new name of the label"), - color: z.string().optional().describe("The color of the label given in 6-digit hex notation with leading '#' sign"), - description: z.string().optional().describe("The new description of the label"), - priority: z.number().nullable().optional().describe("The new priority of the label"), -}); -export const DeleteLabelSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), - label_id: z.union([z.number(), z.string()]).describe("The ID or title of a project's label"), -}); -// Group projects schema -export const ListGroupProjectsSchema = z.object({ - group_id: z.string().describe("Group ID or path"), - include_subgroups: z.boolean().optional().describe("Include projects from subgroups"), - search: z.string().optional().describe("Search term to filter projects"), - order_by: z.enum(['name', 'path', 'created_at', 'updated_at', 'last_activity_at']).optional().describe("Field to sort by"), - sort: z.enum(['asc', 'desc']).optional().describe("Sort direction"), - page: z.number().optional().describe("Page number"), - per_page: z.number().optional().describe("Number of results per page"), - archived: z.boolean().optional().describe("Filter for archived projects"), - visibility: z.enum(["public", "internal", "private"]).optional().describe("Filter by project visibility"), - with_issues_enabled: z.boolean().optional().describe("Filter projects with issues feature enabled"), - with_merge_requests_enabled: z.boolean().optional().describe("Filter projects with merge requests feature enabled"), - min_access_level: z.number().optional().describe("Filter by minimum access level"), - with_programming_language: z.string().optional().describe("Filter by programming language"), - starred: z.boolean().optional().describe("Filter by starred projects"), - statistics: z.boolean().optional().describe("Include project statistics"), - with_custom_attributes: z.boolean().optional().describe("Include custom attributes"), - with_security_reports: z.boolean().optional().describe("Include security reports") -}); diff --git a/build/test-note.js b/build/test-note.js deleted file mode 100644 index a66909f..0000000 --- a/build/test-note.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * This test file verifies that the createNote function works correctly - * with the fixed endpoint URL construction that uses plural resource names - * (issues instead of issue, merge_requests instead of merge_request). - */ -import fetch from "node-fetch"; -// GitLab API configuration (replace with actual values when testing) -const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com"; -const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_TOKEN || ""; -const PROJECT_ID = process.env.PROJECT_ID || "your/project"; -const ISSUE_IID = Number(process.env.ISSUE_IID || "1"); -async function testCreateIssueNote() { - try { - // Using plural form "issues" in the URL - const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(PROJECT_ID)}/issues/${ISSUE_IID}/notes`); - const response = await fetch(url.toString(), { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, - }, - body: JSON.stringify({ body: "Test note from API - with plural endpoint" }), - }); - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`); - } - const data = await response.json(); - console.log("Successfully created note:"); - console.log(JSON.stringify(data, null, 2)); - return true; - } - catch (error) { - console.error("Error creating note:", error); - return false; - } -} -// Only run the test if executed directly -if (require.main === module) { - console.log("Testing note creation with plural 'issues' endpoint..."); - testCreateIssueNote().then(success => { - if (success) { - console.log("✅ Test successful!"); - process.exit(0); - } - else { - console.log("❌ Test failed!"); - process.exit(1); - } - }); -} -// Export for use in other tests -export { testCreateIssueNote };