diff --git a/build/index.js b/build/index.js index e793ffd..547a261 100755 --- a/build/index.js +++ b/build/index.js @@ -9,7 +9,11 @@ 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, } from "./schemas.js"; +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 */ @@ -417,6 +421,51 @@ async function createMergeRequest(projectId, options) { 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 * 파일 생성 또는 업데이트 @@ -1077,6 +1126,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { 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", @@ -1269,6 +1328,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { ], }; } + 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); @@ -1295,6 +1362,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { ], }; } + 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`); diff --git a/build/schemas.js b/build/schemas.js index 7f6bb40..1754513 100644 --- a/build/schemas.js +++ b/build/schemas.js @@ -6,6 +6,10 @@ export const GitLabAuthorSchema = z.object({ 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(), @@ -324,10 +328,71 @@ export const GitLabMergeRequestSchema = z.object({ squash: z.boolean().optional(), labels: z.array(z.string()).optional(), }); -// API Operation Parameter Schemas -const ProjectParamsSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), // Changed from owner/repo to match GitLab API +// 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"), diff --git a/index.ts b/index.ts index b58dceb..d141fd7 100644 --- a/index.ts +++ b/index.ts @@ -67,6 +67,11 @@ import { DeleteLabelSchema, CreateNoteSchema, ListGroupProjectsSchema, + // Discussion Schemas + GitLabDiscussionNoteSchema, // Added + GitLabDiscussionSchema, + UpdateMergeRequestNoteSchema, // Added + ListMergeRequestDiscussionsSchema, type GitLabFork, type GitLabReference, type GitLabRepository, @@ -85,6 +90,9 @@ import { type GitLabNamespaceExistsResponse, type GitLabProject, type GitLabLabel, + // Discussion Types + type GitLabDiscussionNote, // Added + type GitLabDiscussion, } from "./schemas.js"; /** @@ -650,6 +658,76 @@ async function createMergeRequest( 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: string, + mergeRequestIid: number +): Promise { + 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: string, + mergeRequestIid: number, + discussionId: string, + noteId: number, + body: string, + resolved?: boolean +): Promise { + const url = new URL( + `${GITLAB_API_URL}/projects/${encodeURIComponent( + projectId + )}/merge_requests/${mergeRequestIid}/discussions/${discussionId}/notes/${noteId}` + ); + + const payload: { body: string; resolved?: boolean } = { 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 * 파일 생성 또는 업데이트 @@ -1507,6 +1585,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { 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", @@ -1733,6 +1821,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + 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( @@ -1773,6 +1876,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + 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`); diff --git a/schemas.ts b/schemas.ts index 96bfce5..7f557f8 100644 --- a/schemas.ts +++ b/schemas.ts @@ -8,6 +8,11 @@ export const GitLabAuthorSchema = z.object({ }); // 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(), @@ -354,10 +359,77 @@ export const GitLabMergeRequestSchema = z.object({ labels: z.array(z.string()).optional(), }); -// API Operation Parameter Schemas -const ProjectParamsSchema = z.object({ - project_id: z.string().describe("Project ID or URL-encoded path"), // Changed from owner/repo to match GitLab API +// 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({ // Only present for DiffNote + 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 type GitLabDiscussionNote = z.infer; + +export const GitLabDiscussionSchema = z.object({ + id: z.string(), + individual_note: z.boolean(), + notes: z.array(GitLabDiscussionNoteSchema), +}); +export type GitLabDiscussion = z.infer; + +// 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"),