Compare commits

...

9 Commits

Author SHA1 Message Date
f8b1444afd [main] fix: description null error handling
📝 Details:
- GitLab issues/milestones의 null description 처리
- schemas.ts에서 description을 nullable로 변경
2025-05-27 12:25:31 +09:00
06f9437329 Merge pull request #53 from zereight/fix/51-description-nullable
FIX: description null error
2025-05-27 12:20:39 +09:00
dc99f864ca FIX: description null error 2025-05-27 02:00:36 +09:00
8ba33986f3 [main] docs: update changelog for v1.0.45 pipeline tools
🚀 Breaking Changes:
- None

📝 Details:
- Added 5 new pipeline management tools
- Pipeline status monitoring and analysis support
2025-05-24 21:02:58 +09:00
64a936446e [release] feat: update version to 1.0.45
🚀 Breaking Changes:
- Version updated from 1.0.44 to 1.0.45
2025-05-24 20:57:15 +09:00
8ab0ac7145 Merge pull request #52 from vicendominguez/main
feat(release): 1.0.44  adds pipeline jobs tool
2025-05-24 20:55:55 +09:00
ea06c21f29 feat(release): 1.0.44 adds pipeline jobs tool 2025-05-24 13:31:47 +02:00
140620397b chore(release): 1.0.43 - get_repository_tree is added read_only_mode 2025-05-23 00:41:56 +09:00
3d7aa8035d docs: translate issue notes changelog from Korean to English 2025-05-22 21:28:34 +09:00
5 changed files with 451 additions and 27 deletions

View File

@ -1,12 +1,38 @@
## [1.0.46] - 2025-05-27
### Fixed
- Fixed issue where GitLab issues and milestones with null descriptions would cause JSON-RPC errors
- Changed `description` field to be nullable with default empty string in schemas
- This allows proper handling of GitLab issues/milestones without descriptions
- See: [PR #53](https://github.com/zereight/gitlab-mcp/pull/53), [Issue #51](https://github.com/zereight/gitlab-mcp/issues/51)
---
## [1.0.45] - 2025-05-24
### Added
- 🔄 **Pipeline Management Tools**: Added GitLab pipeline status monitoring and management functionality
- `list_pipelines`: List project pipelines with various filtering options
- `get_pipeline`: Get detailed information about a specific pipeline
- `list_pipeline_jobs`: List all jobs in a specific pipeline
- `get_pipeline_job`: Get detailed information about a specific pipeline job
- `get_pipeline_job_output`: Get execution logs/output from pipeline jobs
- 📊 Pipeline status summary and analysis support
- Example: "How many of the last N pipelines are successful?"
- Example: "Can you make a summary of the output in the last pipeline?"
- See: [PR #52](https://github.com/zereight/gitlab-mcp/pull/52)
---
## [1.0.42] - 2025-05-22
### Added
- 이슈(issues)에 노트(note)를 생성하고 수정할 수 있는 기능이 추가되었습니다.
- 이제 버그나 할 일 같은 이슈에도 댓글(메모)을 달거나, 이미 단 댓글을 고칠 수 있습니다.
- 예시: "버그를 고쳤어요!"라는 댓글을 이슈에 달 수 있고, 필요하면 "버그를 완전히 고쳤어요!"로 바꿀 수 있습니다.
- 함수형 프로그래밍 원칙과 SOLID 원칙을 준수하여, 코드의 재사용성과 유지보수성이 높아졌습니다.
- 출처: [PR #47](https://github.com/zereight/gitlab-mcp/pull/47)
- Added support for creating and updating issue notes (comments) in GitLab.
- You can now add or edit comments on issues.
- See: [PR #47](https://github.com/zereight/gitlab-mcp/pull/47)
---

View File

@ -75,7 +75,6 @@ When using with the Claude App, you need to set up your API key and URLs directl
## Tools 🛠️
+<!-- TOOLS-START -->
1. `create_or_update_file` - Create or update a single file in a GitLab project
2. `search_repositories` - Search for GitLab projects
3. `create_repository` - Create a new GitLab project
@ -93,7 +92,7 @@ When using with the Claude App, you need to set up your API key and URLs directl
15. `mr_discussions` - List discussion items for a merge request
16. `update_merge_request_note` - Modify an existing merge request thread note
17. `create_merge_request_note` - Add a new note to an existing merge request thread
18. `update_issue_note` - Update the content of an existing issue note
18. `update_issue_note` - Modify an existing issue thread note
19. `create_issue_note` - Add a new note to an existing issue thread
20. `list_issues` - List issues in a GitLab project with filtering options
21. `get_issue` - Get details of a specific issue in a GitLab project
@ -121,4 +120,9 @@ When using with the Claude App, you need to set up your API key and URLs directl
43. `update_wiki_page` - Update an existing wiki page in a GitLab project
44. `delete_wiki_page` - Delete a wiki page from a GitLab project
45. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories)
46. `list_pipelines` - List pipelines in a GitLab project with filtering options
47. `get_pipeline` - Get details of a specific pipeline in a GitLab project
48. `list_pipeline_jobs` - List all jobs in a specific pipeline
49. `get_pipeline_job` - Get details of a GitLab pipeline job number
50. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number
<!-- TOOLS-END -->

310
index.ts
View File

@ -17,7 +17,6 @@ import { fileURLToPath } from "url";
import { dirname } from "path";
import fs from "fs";
import path from "path";
// Add type imports for proxy agents
import { Agent } from "http";
import { URL } from "url";
@ -84,6 +83,15 @@ import {
UpdateWikiPageSchema,
DeleteWikiPageSchema,
GitLabWikiPageSchema,
GetRepositoryTreeSchema,
GitLabTreeItemSchema,
GitLabPipelineSchema,
GetPipelineSchema,
ListPipelinesSchema,
ListPipelineJobsSchema,
// pipeline job schemas
GetPipelineJobOutputSchema,
GitLabPipelineJobSchema,
// Discussion Schemas
GitLabDiscussionNoteSchema, // Added
GitLabDiscussionSchema,
@ -108,6 +116,11 @@ import {
type GitLabNamespaceExistsResponse,
type GitLabProject,
type GitLabLabel,
type GitLabPipeline,
type ListPipelinesOptions,
type GetPipelineOptions,
type ListPipelineJobsOptions,
type GitLabPipelineJob,
// Discussion Types
type GitLabDiscussionNote, // Added
type GitLabDiscussion,
@ -117,8 +130,6 @@ import {
type UpdateWikiPageOptions,
type DeleteWikiPageOptions,
type GitLabWikiPage,
GitLabTreeItemSchema,
GetRepositoryTreeSchema,
type GitLabTreeItem,
type GetRepositoryTreeOptions,
UpdateIssueNoteSchema,
@ -430,6 +441,31 @@ const allTools = [
"Get the repository tree for a GitLab project (list files and directories)",
inputSchema: zodToJsonSchema(GetRepositoryTreeSchema),
},
{
name: "list_pipelines",
description: "List pipelines in a GitLab project with filtering options",
inputSchema: zodToJsonSchema(ListPipelinesSchema),
},
{
name: "get_pipeline",
description: "Get details of a specific pipeline in a GitLab project",
inputSchema: zodToJsonSchema(GetPipelineSchema),
},
{
name: "list_pipeline_jobs",
description: "List all jobs in a specific pipeline",
inputSchema: zodToJsonSchema(ListPipelineJobsSchema),
},
{
name: "get_pipeline_job",
description: "Get details of a GitLab pipeline job number",
inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema),
},
{
name: "get_pipeline_job_output",
description: "Get the output/trace of a GitLab pipeline job number",
inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema),
},
];
// Define which tools are read-only
@ -448,10 +484,16 @@ const readOnlyTools = [
"get_namespace",
"verify_namespace",
"get_project",
"get_pipeline",
"list_pipelines",
"list_pipeline_jobs",
"get_pipeline_job",
"get_pipeline_job_output",
"list_projects",
"list_labels",
"get_label",
"list_group_projects",
"get_repository_tree",
];
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
@ -1054,10 +1096,10 @@ async function listIssueDiscussions(
projectId: string,
issueIid: number,
options: {
page?: number,
per_page?: number,
sort?: "asc" | "desc",
order_by?: "created_at" | "updated_at"
page?: number;
per_page?: number;
sort?: "asc" | "desc";
order_by?: "created_at" | "updated_at";
} = {}
): Promise<GitLabDiscussion[]> {
projectId = decodeURIComponent(projectId); // Decode project ID
@ -2299,6 +2341,166 @@ async function deleteWikiPage(projectId: string, slug: string): Promise<void> {
await handleGitLabError(response);
}
/**
* List pipelines in a GitLab project
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {ListPipelinesOptions} options - Options for filtering pipelines
* @returns {Promise<GitLabPipeline[]>} List of pipelines
*/
async function listPipelines(
projectId: string,
options: Omit<ListPipelinesOptions, "project_id"> = {}
): Promise<GitLabPipeline[]> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines`
);
// Add all query parameters
Object.entries(options).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.append(key, value.toString());
}
});
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
await handleGitLabError(response);
const data = await response.json();
return z.array(GitLabPipelineSchema).parse(data);
}
/**
* Get details of a specific pipeline
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} pipelineId - The ID of the pipeline
* @returns {Promise<GitLabPipeline>} Pipeline details
*/
async function getPipeline(
projectId: string,
pipelineId: number
): Promise<GitLabPipeline> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}`
);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
if (response.status === 404) {
throw new Error(`Pipeline not found`);
}
await handleGitLabError(response);
const data = await response.json();
return GitLabPipelineSchema.parse(data);
}
/**
* List all jobs in a specific pipeline
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} pipelineId - The ID of the pipeline
* @param {Object} options - Options for filtering jobs
* @returns {Promise<GitLabPipelineJob[]>} List of pipeline jobs
*/
async function listPipelineJobs(
projectId: string,
pipelineId: number,
options: Omit<ListPipelineJobsOptions, "project_id" | "pipeline_id"> = {}
): Promise<GitLabPipelineJob[]> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/jobs`
);
// Add all 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, value.toString());
}
}
});
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
if (response.status === 404) {
throw new Error(`Pipeline not found`);
}
await handleGitLabError(response);
const data = await response.json();
return z.array(GitLabPipelineJobSchema).parse(data);
}
async function getPipelineJob(
projectId: string,
jobId: number
): Promise<GitLabPipelineJob> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/jobs/${jobId}`
);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
if (response.status === 404) {
throw new Error(`Job not found`);
}
await handleGitLabError(response);
const data = await response.json();
return GitLabPipelineJobSchema.parse(data);
}
/**
* Get the output/trace of a pipeline job
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} jobId - The ID of the job
* @returns {Promise<string>} The job output/trace
*/
async function getPipelineJobOutput(
projectId: string,
jobId: number
): Promise<string> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/jobs/${jobId}/trace`
);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
headers: {
...DEFAULT_HEADERS,
Accept: "text/plain", // Override Accept header to get plain text
},
});
if (response.status === 404) {
throw new Error(`Job trace not found or job is not finished yet`);
}
await handleGitLabError(response);
return await response.text();
}
/**
* Get the repository tree for a project
* @param {string} projectId - The ID or URL-encoded path of the project
@ -2549,9 +2751,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
case "update_issue_note": {
const args = UpdateIssueNoteSchema.parse(
request.params.arguments
);
const args = UpdateIssueNoteSchema.parse(request.params.arguments);
const note = await updateIssueNote(
args.project_id,
args.issue_iid,
@ -2565,9 +2765,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
case "create_issue_note": {
const args = CreateIssueNoteSchema.parse(
request.params.arguments
);
const args = CreateIssueNoteSchema.parse(request.params.arguments);
const note = await createIssueNote(
args.project_id,
args.issue_iid,
@ -2757,8 +2955,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
case "create_merge_request_thread": {
const args = CreateMergeRequestThreadSchema.parse(request.params.arguments);
const { project_id, merge_request_iid, body, position, created_at } = args;
const args = CreateMergeRequestThreadSchema.parse(
request.params.arguments
);
const { project_id, merge_request_iid, body, position, created_at } =
args;
const thread = await createMergeRequestThread(
project_id,
@ -2827,9 +3028,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const args = ListIssueDiscussionsSchema.parse(request.params.arguments);
const { project_id, issue_iid, ...options } = args;
const discussions = await listIssueDiscussions(project_id, issue_iid, options);
const discussions = await listIssueDiscussions(
project_id,
issue_iid,
options
);
return {
content: [{ type: "text", text: JSON.stringify(discussions, null, 2) }],
content: [
{ type: "text", text: JSON.stringify(discussions, null, 2) },
],
};
}
@ -3024,6 +3231,75 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
};
}
case "list_pipelines": {
const args = ListPipelinesSchema.parse(request.params.arguments);
const { project_id, ...options } = args;
const pipelines = await listPipelines(project_id, options);
return {
content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }],
};
}
case "get_pipeline": {
const { project_id, pipeline_id } = GetPipelineSchema.parse(
request.params.arguments
);
const pipeline = await getPipeline(project_id, pipeline_id);
return {
content: [
{
type: "text",
text: JSON.stringify(pipeline, null, 2),
},
],
};
}
case "list_pipeline_jobs": {
const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse(
request.params.arguments
);
const jobs = await listPipelineJobs(project_id, pipeline_id, options);
return {
content: [
{
type: "text",
text: JSON.stringify(jobs, null, 2),
},
],
};
}
case "get_pipeline_job": {
const { project_id, job_id } = GetPipelineJobOutputSchema.parse(
request.params.arguments
);
const jobDetails = await getPipelineJob(project_id, job_id);
return {
content: [
{
type: "text",
text: JSON.stringify(jobDetails, null, 2),
},
],
};
}
case "get_pipeline_job_output": {
const { project_id, job_id } = GetPipelineJobOutputSchema.parse(
request.params.arguments
);
const jobOutput = await getPipelineJobOutput(project_id, job_id);
return {
content: [
{
type: "text",
text: jobOutput,
},
],
};
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}

View File

@ -1,6 +1,6 @@
{
"name": "@zereight/mcp-gitlab",
"version": "1.0.42",
"version": "1.0.46",
"description": "MCP server for using the GitLab API",
"license": "MIT",
"author": "zereight",

View File

@ -7,6 +7,119 @@ export const GitLabAuthorSchema = z.object({
date: z.string(),
});
// Pipeline related schemas
export const GitLabPipelineSchema = z.object({
id: z.number(),
project_id: z.number(),
sha: z.string(),
ref: z.string(),
status: z.string(),
source: z.string().optional(),
created_at: z.string(),
updated_at: z.string(),
web_url: z.string(),
duration: z.number().nullable().optional(),
started_at: z.string().nullable().optional(),
finished_at: z.string().nullable().optional(),
coverage: z.number().nullable().optional(),
user: z.object({
id: z.number(),
name: z.string(),
username: z.string(),
avatar_url: z.string().nullable().optional(),
}).optional(),
detailed_status: z.object({
icon: z.string().optional(),
text: z.string().optional(),
label: z.string().optional(),
group: z.string().optional(),
tooltip: z.string().optional(),
has_details: z.boolean().optional(),
details_path: z.string().optional(),
illustration: z.object({
image: z.string().optional(),
size: z.string().optional(),
title: z.string().optional(),
}).optional(),
favicon: z.string().optional(),
}).optional(),
});
// Pipeline job related schemas
export const GitLabPipelineJobSchema = z.object({
id: z.number(),
status: z.string(),
stage: z.string(),
name: z.string(),
ref: z.string(),
tag: z.boolean(),
coverage: z.number().nullable().optional(),
created_at: z.string(),
started_at: z.string().nullable().optional(),
finished_at: z.string().nullable().optional(),
duration: z.number().nullable().optional(),
user: z.object({
id: z.number(),
name: z.string(),
username: z.string(),
avatar_url: z.string().nullable().optional(),
}).optional(),
commit: z.object({
id: z.string(),
short_id: z.string(),
title: z.string(),
author_name: z.string(),
author_email: z.string(),
}).optional(),
pipeline: z.object({
id: z.number(),
project_id: z.number(),
status: z.string(),
ref: z.string(),
sha: z.string(),
}).optional(),
web_url: z.string().optional(),
});
// Schema for listing pipelines
export const ListPipelinesSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"),
scope: z.enum(['running', 'pending', 'finished', 'branches', 'tags']).optional().describe("The scope of pipelines"),
status: z.enum(['created', 'waiting_for_resource', 'preparing', 'pending', 'running', 'success', 'failed', 'canceled', 'skipped', 'manual', 'scheduled']).optional().describe("The status of pipelines"),
ref: z.string().optional().describe("The ref of pipelines"),
sha: z.string().optional().describe("The SHA of pipelines"),
yaml_errors: z.boolean().optional().describe("Returns pipelines with invalid configurations"),
username: z.string().optional().describe("The username of the user who triggered pipelines"),
updated_after: z.string().optional().describe("Return pipelines updated after the specified date"),
updated_before: z.string().optional().describe("Return pipelines updated before the specified date"),
order_by: z.enum(['id', 'status', 'ref', 'updated_at', 'user_id']).optional().describe("Order pipelines by"),
sort: z.enum(['asc', 'desc']).optional().describe("Sort pipelines"),
page: z.number().optional().describe("Page number for pagination"),
per_page: z.number().optional().describe("Number of items per page (max 100)"),
});
// Schema for getting a specific pipeline
export const GetPipelineSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"),
pipeline_id: z.number().describe("The ID of the pipeline"),
});
// Schema for listing jobs in a pipeline
export const ListPipelineJobsSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"),
pipeline_id: z.number().describe("The ID of the pipeline"),
scope: z.enum(['created', 'pending', 'running', 'failed', 'success', 'canceled', 'skipped', 'manual']).optional().describe("The scope of jobs to show"),
include_retried: z.boolean().optional().describe("Whether to include retried jobs"),
page: z.number().optional().describe("Page number for pagination"),
per_page: z.number().optional().describe("Number of items per page (max 100)"),
});
// Schema for the input parameters for pipeline job operations
export const GetPipelineJobOutputSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"),
job_id: z.number().describe("The ID of the job"),
});
// Namespace related schemas
// Base schema for project-related operations
@ -294,7 +407,7 @@ export const GitLabMilestoneSchema = z.object({
id: z.number(),
iid: z.number(), // Added to match GitLab API
title: z.string(),
description: z.string(),
description: z.string().nullable().default(""),
state: z.string(),
web_url: z.string(), // Changed from html_url to match GitLab API
});
@ -304,7 +417,7 @@ export const GitLabIssueSchema = z.object({
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
description: z.string().nullable().default(""), // Changed from body to match GitLab API
state: z.string(),
author: GitLabUserSchema,
assignees: z.array(GitLabUserSchema),
@ -1120,3 +1233,8 @@ export type GetRepositoryTreeOptions = z.infer<typeof GetRepositoryTreeSchema>;
export type MergeRequestThreadPosition = z.infer<typeof MergeRequestThreadPositionSchema>;
export type CreateMergeRequestThreadOptions = z.infer<typeof CreateMergeRequestThreadSchema>;
export type CreateMergeRequestNoteOptions = z.infer<typeof CreateMergeRequestNoteSchema>;
export type GitLabPipelineJob = z.infer<typeof GitLabPipelineJobSchema>;
export type GitLabPipeline = z.infer<typeof GitLabPipelineSchema>;
export type ListPipelinesOptions = z.infer<typeof ListPipelinesSchema>;
export type GetPipelineOptions = z.infer<typeof GetPipelineSchema>;
export type ListPipelineJobsOptions = z.infer<typeof ListPipelineJobsSchema>;