Merge pull request #26 from zereight/feature/fix-note

Add schemas for GitLab discussion notes and merge request discussions
This commit is contained in:
bbang-dduck
2025-03-31 19:11:35 +09:00
committed by GitHub
4 changed files with 336 additions and 7 deletions

View File

@ -9,7 +9,11 @@ import { fileURLToPath } from "url";
import { dirname } from "path"; import { dirname } from "path";
import fs from "fs"; import fs from "fs";
import path from "path"; 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 * Read version from package.json
*/ */
@ -417,6 +421,51 @@ async function createMergeRequest(projectId, options) {
const data = await response.json(); const data = await response.json();
return GitLabMergeRequestSchema.parse(data); 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<GitLabDiscussion[]>} 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<GitLabDiscussionNote>} 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 * 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", description: "Create a new note (comment) to an issue or merge request",
inputSchema: zodToJsonSchema(CreateNoteSchema), 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", name: "list_issues",
description: "List issues in a GitLab project with filtering options", 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": { case "get_merge_request": {
const args = GetMergeRequestSchema.parse(request.params.arguments); const args = GetMergeRequestSchema.parse(request.params.arguments);
const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid); 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": { case "list_namespaces": {
const args = ListNamespacesSchema.parse(request.params.arguments); const args = ListNamespacesSchema.parse(request.params.arguments);
const url = new URL(`${GITLAB_API_URL}/namespaces`); const url = new URL(`${GITLAB_API_URL}/namespaces`);

View File

@ -6,6 +6,10 @@ export const GitLabAuthorSchema = z.object({
date: z.string(), date: z.string(),
}); });
// Namespace related schemas // 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({ export const GitLabNamespaceSchema = z.object({
id: z.number(), id: z.number(),
name: z.string(), name: z.string(),
@ -324,10 +328,71 @@ export const GitLabMergeRequestSchema = z.object({
squash: z.boolean().optional(), squash: z.boolean().optional(),
labels: z.array(z.string()).optional(), labels: z.array(z.string()).optional(),
}); });
// API Operation Parameter Schemas // Discussion related schemas
const ProjectParamsSchema = z.object({ export const GitLabDiscussionNoteSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), // Changed from owner/repo to match GitLab API 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({ export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
file_path: z.string().describe("Path where to create/update the file"), file_path: z.string().describe("Path where to create/update the file"),
content: z.string().describe("Content of the file"), content: z.string().describe("Content of the file"),

116
index.ts
View File

@ -67,6 +67,11 @@ import {
DeleteLabelSchema, DeleteLabelSchema,
CreateNoteSchema, CreateNoteSchema,
ListGroupProjectsSchema, ListGroupProjectsSchema,
// Discussion Schemas
GitLabDiscussionNoteSchema, // Added
GitLabDiscussionSchema,
UpdateMergeRequestNoteSchema, // Added
ListMergeRequestDiscussionsSchema,
type GitLabFork, type GitLabFork,
type GitLabReference, type GitLabReference,
type GitLabRepository, type GitLabRepository,
@ -85,6 +90,9 @@ import {
type GitLabNamespaceExistsResponse, type GitLabNamespaceExistsResponse,
type GitLabProject, type GitLabProject,
type GitLabLabel, type GitLabLabel,
// Discussion Types
type GitLabDiscussionNote, // Added
type GitLabDiscussion,
} from "./schemas.js"; } from "./schemas.js";
/** /**
@ -650,6 +658,76 @@ async function createMergeRequest(
return GitLabMergeRequestSchema.parse(data); 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<GitLabDiscussion[]>} List of discussions
*/
async function listMergeRequestDiscussions(
projectId: string,
mergeRequestIid: number
): Promise<GitLabDiscussion[]> {
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<GitLabDiscussionNote>} The updated note
*/
async function updateMergeRequestNote(
projectId: string,
mergeRequestIid: number,
discussionId: string,
noteId: number,
body: string,
resolved?: boolean
): Promise<GitLabDiscussionNote> {
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 * 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", description: "Create a new note (comment) to an issue or merge request",
inputSchema: zodToJsonSchema(CreateNoteSchema), 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", name: "list_issues",
description: "List issues in a GitLab project with filtering options", 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": { case "get_merge_request": {
const args = GetMergeRequestSchema.parse(request.params.arguments); const args = GetMergeRequestSchema.parse(request.params.arguments);
const mergeRequest = await getMergeRequest( 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": { case "list_namespaces": {
const args = ListNamespacesSchema.parse(request.params.arguments); const args = ListNamespacesSchema.parse(request.params.arguments);
const url = new URL(`${GITLAB_API_URL}/namespaces`); const url = new URL(`${GITLAB_API_URL}/namespaces`);

View File

@ -8,6 +8,11 @@ export const GitLabAuthorSchema = z.object({
}); });
// Namespace related schemas // 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({ export const GitLabNamespaceSchema = z.object({
id: z.number(), id: z.number(),
name: z.string(), name: z.string(),
@ -354,10 +359,77 @@ export const GitLabMergeRequestSchema = z.object({
labels: z.array(z.string()).optional(), labels: z.array(z.string()).optional(),
}); });
// API Operation Parameter Schemas // Discussion related schemas
const ProjectParamsSchema = z.object({ export const GitLabDiscussionNoteSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), // Changed from owner/repo to match GitLab API 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<typeof GitLabDiscussionNoteSchema>;
export const GitLabDiscussionSchema = z.object({
id: z.string(),
individual_note: z.boolean(),
notes: z.array(GitLabDiscussionNoteSchema),
});
export type GitLabDiscussion = z.infer<typeof GitLabDiscussionSchema>;
// 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({ export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
file_path: z.string().describe("Path where to create/update the file"), file_path: z.string().describe("Path where to create/update the file"),