Compare commits

...

17 Commits

Author SHA1 Message Date
181f1e943c [main] feat: update milestone management tools and improve code formatting
🚀 Breaking Changes:
- Updated version from 1.0.48 to 1.0.50
- Refactored code for better readability and consistency

📝 Details:
- Improved descriptions and formatting in index.ts
- Ensured consistent use of URL encoding in API calls
2025-05-29 22:30:51 +09:00
2a80988a02 [main] chore: v1.0.48 버전 업데이트
📝 Details:
- Milestone Management Tools 추가 (PR #59)
- Docker Image Push Script 추가 (PR #60)
- package.json 버전 업데이트
- CHANGELOG.md 업데이트
2025-05-29 19:56:37 +09:00
5762b32a69 feat: add milestone management commands to README
🚀 Breaking Changes:
- Introduced new commands for milestone management in GitLab.

📝 Details:
- Added commands: list_milestones, get_milestone, create_milestone, edit_milestone, delete_milestone, get_milestone_issue, get_milestone_merge_requests, promote_milestone, get_milestone_burndown_events.
2025-05-29 19:53:19 +09:00
55e7ca3100 Merge pull request #59 from VinceCYLiao/feat/add-tools-for-milestones
feat: add tools for milestones
2025-05-29 19:52:36 +09:00
953f748e0d Merge pull request #60 from zereight/feat/docker_image_push
FEAT: docker image push script
2025-05-29 19:51:48 +09:00
0b876ebff6 FEAT: docker image push script 2025-05-29 16:46:53 +09:00
fd1c8b9704 feat: add tools for milestones 2025-05-29 15:10:12 +08:00
a2c2ac185a [main] release: v1.0.47
📝 Details:
- 버전을 1.0.47로 업데이트
- CHANGELOG에 새로운 기능과 버그 수정 사항 추가
  - list_merge_requests 기능 추가 (#56)
  - GitLabUserSchema의 avatar_url nullable 처리 (#55)
  - GitLabPipelineSchema의 illustration nullable 처리 (#58)
2025-05-29 09:13:43 +09:00
2462168697 Merge pull request #58 from zereight/fix/illustration-nullable
fix(schemas): make illustration nullable in GitLabPipelineSchema
2025-05-29 09:09:16 +09:00
88af65fcd0 Merge pull request #56 from jwang-sue/feature/list-merge-requests
feat: implement list_merge_requests functionality
2025-05-29 09:09:05 +09:00
0b55cc3cee Merge pull request #55 from svengt/fix/avatar-url-nullable
fix(schemas): make avatar_url nullable in GitLabUserSchema
2025-05-29 09:08:41 +09:00
40e39d7b36 fix(schemas): make illustration nullable in GitLabPipelineSchema 2025-05-29 04:35:29 +09:00
cc847772f1 feat: implement list_merge_requests functionality
- Add ListMergeRequestsSchema with comprehensive filtering options
- Implement listMergeRequests function following GitLab API
- Add tool definition and switch case handler
- Include in readOnlyTools array
- Update README.md with new tool documentation
2025-05-28 17:22:40 +02:00
ab571d211d fix(schemas): make avatar_url nullable in GitLabUserSchema
Users without profile pictures have null avatar_url values in GitLab API responses.
This change prevents errors when calling get_issue tool.
2025-05-28 15:44:15 +02:00
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
7 changed files with 789 additions and 16 deletions

View File

@ -1,3 +1,58 @@
## [1.0.48] - 2025-05-29
### Added
- 🎯 **Milestone Management Tools**: Added comprehensive milestone management functionality
- `create_milestone`: Create new milestones for GitLab projects
- `update_milestone`: Update existing milestone properties (title, description, dates, state)
- `delete_milestone`: Delete milestones from projects
- `list_milestones`: List and filter project milestones
- `get_milestone`: Get detailed information about specific milestones
- See: [PR #59](https://github.com/zereight/gitlab-mcp/pull/59)
### Fixed
- 🐳 **Docker Image Push Script**: Added automated Docker image push script for easier deployment
- Simplifies the Docker image build and push process
- See: [PR #60](https://github.com/zereight/gitlab-mcp/pull/60)
---
## [1.0.47] - 2025-05-29
### Added
- 🔄 **List Merge Requests Tool**: Added functionality to list and filter merge requests in GitLab projects
- `list_merge_requests`: List merge requests with comprehensive filtering options
- Supports filtering by state, scope, author, assignee, reviewer, labels, and more
- Includes pagination support for large result sets
- See: [PR #56](https://github.com/zereight/gitlab-mcp/pull/56)
### Fixed
- Fixed issue where GitLab users without profile pictures would cause JSON-RPC errors
- Changed `avatar_url` field to be nullable in GitLabUserSchema
- This allows proper handling of users without avatars in GitLab API responses
- See: [PR #55](https://github.com/zereight/gitlab-mcp/pull/55)
- Fixed issue where GitLab pipelines without illustrations would cause JSON-RPC errors
- Changed `illustration` field to be nullable in GitLabPipelineSchema
- This allows proper handling of pipelines without illustrations
- See: [PR #58](https://github.com/zereight/gitlab-mcp/pull/58), [Issue #57](https://github.com/zereight/gitlab-mcp/issues/57)
---
## [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 ## [1.0.45] - 2025-05-24
### Added ### Added

View File

@ -52,7 +52,7 @@ When using with the Claude App, you need to set up your API key and URLs directl
"GITLAB_READ_ONLY_MODE", "GITLAB_READ_ONLY_MODE",
"-e", "-e",
"USE_GITLAB_WIKI", "USE_GITLAB_WIKI",
"nkwd/gitlab-mcp" "iwakitakuma/gitlab-mcp"
], ],
"env": { "env": {
"GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token", "GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token",
@ -65,6 +65,12 @@ When using with the Claude App, you need to set up your API key and URLs directl
} }
``` ```
#### Docker Image Push
```shell
$ sh scripts/image_push.sh docker_user_name
```
### Environment Variables ### Environment Variables
- `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token. - `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token.
@ -125,4 +131,14 @@ When using with the Claude App, you need to set up your API key and URLs directl
48. `list_pipeline_jobs` - List all jobs in a specific pipeline 48. `list_pipeline_jobs` - List all jobs in a specific pipeline
49. `get_pipeline_job` - Get details of a GitLab pipeline job number 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 50. `get_pipeline_job_output` - Get the output/trace of a GitLab pipeline job number
51. `list_merge_requests` - List merge requests in a GitLab project with filtering options
52. `list_milestones` - List milestones in a GitLab project with filtering options
53. `get_milestone` - Get details of a specific milestone
54. `create_milestone` - Create a new milestone in a GitLab project
55. `edit_milestone ` - Edit an existing milestone in a GitLab project
56. `delete_milestone` - Delete a milestone from a GitLab project
57. `get_milestone_issue` - Get issues associated with a specific milestone
58. `get_milestone_merge_requests` - Get merge requests associated with a specific milestone
59. `promote_milestone` - Promote a milestone to the next stage
60. `get_milestone_burndown_events` - Get burndown events for a specific milestone
<!-- TOOLS-END --> <!-- TOOLS-END -->

535
index.ts
View File

@ -121,6 +121,16 @@ import {
type GetPipelineOptions, type GetPipelineOptions,
type ListPipelineJobsOptions, type ListPipelineJobsOptions,
type GitLabPipelineJob, type GitLabPipelineJob,
type GitLabMilestones,
type ListProjectMilestonesOptions,
type GetProjectMilestoneOptions,
type CreateProjectMilestoneOptions,
type EditProjectMilestoneOptions,
type DeleteProjectMilestoneOptions,
type GetMilestoneIssuesOptions,
type GetMilestoneMergeRequestsOptions,
type PromoteProjectMilestoneOptions,
type GetMilestoneBurndownEventsOptions,
// Discussion Types // Discussion Types
type GitLabDiscussionNote, // Added type GitLabDiscussionNote, // Added
type GitLabDiscussion, type GitLabDiscussion,
@ -134,6 +144,17 @@ import {
type GetRepositoryTreeOptions, type GetRepositoryTreeOptions,
UpdateIssueNoteSchema, UpdateIssueNoteSchema,
CreateIssueNoteSchema, CreateIssueNoteSchema,
ListMergeRequestsSchema,
GitLabMilestonesSchema,
ListProjectMilestonesSchema,
GetProjectMilestoneSchema,
CreateProjectMilestoneSchema,
EditProjectMilestoneSchema,
DeleteProjectMilestoneSchema,
GetMilestoneIssuesSchema,
GetMilestoneMergeRequestsSchema,
PromoteProjectMilestoneSchema,
GetMilestoneBurndownEventsSchema,
} from "./schemas.js"; } from "./schemas.js";
/** /**
@ -466,6 +487,57 @@ const allTools = [
description: "Get the output/trace of a GitLab pipeline job number", description: "Get the output/trace of a GitLab pipeline job number",
inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema), inputSchema: zodToJsonSchema(GetPipelineJobOutputSchema),
}, },
{
name: "list_merge_requests",
description:
"List merge requests in a GitLab project with filtering options",
inputSchema: zodToJsonSchema(ListMergeRequestsSchema),
},
{
name: "list_milestones",
description: "List milestones in a GitLab project with filtering options",
inputSchema: zodToJsonSchema(ListProjectMilestonesSchema),
},
{
name: "get_milestone",
description: "Get details of a specific milestone",
inputSchema: zodToJsonSchema(GetProjectMilestoneSchema),
},
{
name: "create_milestone",
description: "Create a new milestone in a GitLab project",
inputSchema: zodToJsonSchema(CreateProjectMilestoneSchema),
},
{
name: "edit_milestone",
description: "Edit an existing milestone in a GitLab project",
inputSchema: zodToJsonSchema(EditProjectMilestoneSchema),
},
{
name: "delete_milestone",
description: "Delete a milestone from a GitLab project",
inputSchema: zodToJsonSchema(DeleteProjectMilestoneSchema),
},
{
name: "get_milestone_issue",
description: "Get issues associated with a specific milestone",
inputSchema: zodToJsonSchema(GetMilestoneIssuesSchema),
},
{
name: "get_milestone_merge_requests",
description: "Get merge requests associated with a specific milestone",
inputSchema: zodToJsonSchema(GetMilestoneMergeRequestsSchema),
},
{
name: "promote_milestone",
description: "Promote a milestone to the next stage",
inputSchema: zodToJsonSchema(PromoteProjectMilestoneSchema),
},
{
name: "get_milestone_burndown_events",
description: "Get burndown events for a specific milestone",
inputSchema: zodToJsonSchema(GetMilestoneBurndownEventsSchema),
},
]; ];
// Define which tools are read-only // Define which tools are read-only
@ -476,6 +548,7 @@ const readOnlyTools = [
"get_merge_request_diffs", "get_merge_request_diffs",
"mr_discussions", "mr_discussions",
"list_issues", "list_issues",
"list_merge_requests",
"get_issue", "get_issue",
"list_issue_links", "list_issue_links",
"list_issue_discussions", "list_issue_discussions",
@ -494,6 +567,11 @@ const readOnlyTools = [
"get_label", "get_label",
"list_group_projects", "list_group_projects",
"get_repository_tree", "get_repository_tree",
"list_milestones",
"get_milestone",
"get_milestone_issue",
"get_milestone_merge_requests",
"get_milestone_burndown_events",
]; ];
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI // Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
@ -791,6 +869,43 @@ async function listIssues(
return z.array(GitLabIssueSchema).parse(data); return z.array(GitLabIssueSchema).parse(data);
} }
/**
* List merge requests in a GitLab project with optional filtering
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {Object} options - Optional filtering parameters
* @returns {Promise<GitLabMergeRequest[]>} List of merge requests
*/
async function listMergeRequests(
projectId: string,
options: Omit<z.infer<typeof ListMergeRequestsSchema>, "project_id"> = {}
): Promise<GitLabMergeRequest[]> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`
);
// Add all query parameters
Object.entries(options).forEach(([key, value]) => {
if (value !== undefined) {
if (key === "labels" && 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(), {
...DEFAULT_FETCH_CONFIG,
});
await handleGitLabError(response);
const data = await response.json();
return z.array(GitLabMergeRequestSchema).parse(data);
}
/** /**
* Get a single issue from a GitLab project * Get a single issue from a GitLab project
* 단일 이슈 조회 * 단일 이슈 조회
@ -2386,7 +2501,9 @@ async function getPipeline(
): Promise<GitLabPipeline> { ): Promise<GitLabPipeline> {
projectId = decodeURIComponent(projectId); // Decode project ID projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL( const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}` `${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/pipelines/${pipelineId}`
); );
const response = await fetch(url.toString(), { const response = await fetch(url.toString(), {
@ -2417,7 +2534,9 @@ async function listPipelineJobs(
): Promise<GitLabPipelineJob[]> { ): Promise<GitLabPipelineJob[]> {
projectId = decodeURIComponent(projectId); // Decode project ID projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL( const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/pipelines/${pipelineId}/jobs` `${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/pipelines/${pipelineId}/jobs`
); );
// Add all query parameters // Add all query parameters
@ -2449,9 +2568,7 @@ async function getPipelineJob(
): Promise<GitLabPipelineJob> { ): Promise<GitLabPipelineJob> {
projectId = decodeURIComponent(projectId); // Decode project ID projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL( const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent( `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/jobs/${jobId}`
projectId
)}/jobs/${jobId}`
); );
const response = await fetch(url.toString(), { const response = await fetch(url.toString(), {
@ -2544,6 +2661,248 @@ async function getRepositoryTree(
return z.array(GitLabTreeItemSchema).parse(data); return z.array(GitLabTreeItemSchema).parse(data);
} }
/**
* List project milestones in a GitLab project
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {Object} options - Options for listing milestones
* @returns {Promise<GitLabMilestones[]>} List of milestones
*/
async function listProjectMilestones(
projectId: string,
options: Omit<z.infer<typeof ListProjectMilestonesSchema>, "project_id">
): Promise<GitLabMilestones[]> {
projectId = decodeURIComponent(projectId);
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones`
);
Object.entries(options).forEach(([key, value]) => {
if (value !== undefined) {
if (key === "iids" && Array.isArray(value) && value.length > 0) {
value.forEach((iid) => {
url.searchParams.append("iids[]", iid.toString());
});
} else 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(GitLabMilestonesSchema).parse(data);
}
/**
* Get a single milestone in a GitLab project
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} milestoneId - The ID of the milestone
* @returns {Promise<GitLabMilestones>} Milestone details
*/
async function getProjectMilestone(
projectId: string,
milestoneId: number
): Promise<GitLabMilestones> {
projectId = decodeURIComponent(projectId);
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/milestones/${milestoneId}`
);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
await handleGitLabError(response);
const data = await response.json();
return GitLabMilestonesSchema.parse(data);
}
/**
* Create a new milestone in a GitLab project
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {Object} options - Options for creating a milestone
* @returns {Promise<GitLabMilestones>} Created milestone
*/
async function createProjectMilestone(
projectId: string,
options: Omit<z.infer<typeof CreateProjectMilestoneSchema>, "project_id">
): Promise<GitLabMilestones> {
projectId = decodeURIComponent(projectId);
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/milestones`
);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
method: "POST",
body: JSON.stringify(options),
});
await handleGitLabError(response);
const data = await response.json();
return GitLabMilestonesSchema.parse(data);
}
/**
* Edit an existing milestone in a GitLab project
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} milestoneId - The ID of the milestone
* @param {Object} options - Options for editing a milestone
* @returns {Promise<GitLabMilestones>} Updated milestone
*/
async function editProjectMilestone(
projectId: string,
milestoneId: number,
options: Omit<
z.infer<typeof EditProjectMilestoneSchema>,
"project_id" | "milestone_id"
>
): Promise<GitLabMilestones> {
projectId = decodeURIComponent(projectId);
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/milestones/${milestoneId}`
);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
method: "PUT",
body: JSON.stringify(options),
});
await handleGitLabError(response);
const data = await response.json();
return GitLabMilestonesSchema.parse(data);
}
/**
* Delete a milestone from a GitLab project
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} milestoneId - The ID of the milestone
* @returns {Promise<void>}
*/
async function deleteProjectMilestone(
projectId: string,
milestoneId: number
): Promise<void> {
projectId = decodeURIComponent(projectId);
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/milestones/${milestoneId}`
);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
method: "DELETE",
});
await handleGitLabError(response);
}
/**
* Get all issues assigned to a single milestone
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} milestoneId - The ID of the milestone
* @returns {Promise<GitLabIssue[]>} List of issues
*/
async function getMilestoneIssues(
projectId: string,
milestoneId: number
): Promise<GitLabIssue[]> {
projectId = decodeURIComponent(projectId);
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/milestones/${milestoneId}/issues`
);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
await handleGitLabError(response);
const data = await response.json();
return z.array(GitLabIssueSchema).parse(data);
}
/**
* Get all merge requests assigned to a single milestone
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} milestoneId - The ID of the milestone
* @returns {Promise<GitLabMergeRequest[]>} List of merge requests
*/
async function getMilestoneMergeRequests(
projectId: string,
milestoneId: number
): Promise<GitLabMergeRequest[]> {
projectId = decodeURIComponent(projectId);
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/milestones/${milestoneId}/merge_requests`
);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
await handleGitLabError(response);
const data = await response.json();
return z.array(GitLabMergeRequestSchema).parse(data);
}
/**
* Promote a project milestone to a group milestone
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} milestoneId - The ID of the milestone
* @returns {Promise<GitLabMilestones>} Promoted milestone
*/
async function promoteProjectMilestone(
projectId: string,
milestoneId: number
): Promise<GitLabMilestones> {
projectId = decodeURIComponent(projectId);
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/milestones/${milestoneId}/promote`
);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
method: "POST",
});
await handleGitLabError(response);
const data = await response.json();
return GitLabMilestonesSchema.parse(data);
}
/**
* Get all burndown chart events for a single milestone
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} milestoneId - The ID of the milestone
* @returns {Promise<any[]>} Burndown chart events
*/
async function getMilestoneBurndownEvents(
projectId: string,
milestoneId: number
): Promise<any[]> {
projectId = decodeURIComponent(projectId);
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/milestones/${milestoneId}/burndown_events`
);
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
await handleGitLabError(response);
const data = await response.json();
return data as any[];
}
server.setRequestHandler(ListToolsRequestSchema, async () => { server.setRequestHandler(ListToolsRequestSchema, async () => {
// Apply read-only filter first // Apply read-only filter first
const tools0 = GITLAB_READ_ONLY_MODE const tools0 = GITLAB_READ_ONLY_MODE
@ -3256,9 +3615,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
} }
case "list_pipeline_jobs": { case "list_pipeline_jobs": {
const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse( const { project_id, pipeline_id, ...options } =
request.params.arguments ListPipelineJobsSchema.parse(request.params.arguments);
);
const jobs = await listPipelineJobs(project_id, pipeline_id, options); const jobs = await listPipelineJobs(project_id, pipeline_id, options);
return { return {
content: [ content: [
@ -3300,6 +3658,167 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}; };
} }
case "list_merge_requests": {
const args = ListMergeRequestsSchema.parse(request.params.arguments);
const mergeRequests = await listMergeRequests(args.project_id, args);
return {
content: [
{ type: "text", text: JSON.stringify(mergeRequests, null, 2) },
],
};
}
case "list_milestones": {
const { project_id, ...options } = ListProjectMilestonesSchema.parse(
request.params.arguments
);
const milestones = await listProjectMilestones(project_id, options);
return {
content: [
{
type: "text",
text: JSON.stringify(milestones, null, 2),
},
],
};
}
case "get_milestone": {
const { project_id, milestone_id } = GetProjectMilestoneSchema.parse(
request.params.arguments
);
const milestone = await getProjectMilestone(project_id, milestone_id);
return {
content: [
{
type: "text",
text: JSON.stringify(milestone, null, 2),
},
],
};
}
case "create_milestone": {
const { project_id, ...options } = CreateProjectMilestoneSchema.parse(
request.params.arguments
);
const milestone = await createProjectMilestone(project_id, options);
return {
content: [
{
type: "text",
text: JSON.stringify(milestone, null, 2),
},
],
};
}
case "edit_milestone": {
const { project_id, milestone_id, ...options } =
EditProjectMilestoneSchema.parse(request.params.arguments);
const milestone = await editProjectMilestone(
project_id,
milestone_id,
options
);
return {
content: [
{
type: "text",
text: JSON.stringify(milestone, null, 2),
},
],
};
}
case "delete_milestone": {
const { project_id, milestone_id } = DeleteProjectMilestoneSchema.parse(
request.params.arguments
);
await deleteProjectMilestone(project_id, milestone_id);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
status: "success",
message: "Milestone deleted successfully",
},
null,
2
),
},
],
};
}
case "get_milestone_issue": {
const { project_id, milestone_id } = GetMilestoneIssuesSchema.parse(
request.params.arguments
);
const issues = await getMilestoneIssues(project_id, milestone_id);
return {
content: [
{
type: "text",
text: JSON.stringify(issues, null, 2),
},
],
};
}
case "get_milestone_merge_requests": {
const { project_id, milestone_id } =
GetMilestoneMergeRequestsSchema.parse(request.params.arguments);
const mergeRequests = await getMilestoneMergeRequests(
project_id,
milestone_id
);
return {
content: [
{
type: "text",
text: JSON.stringify(mergeRequests, null, 2),
},
],
};
}
case "promote_milestone": {
const { project_id, milestone_id } =
PromoteProjectMilestoneSchema.parse(request.params.arguments);
const milestone = await promoteProjectMilestone(
project_id,
milestone_id
);
return {
content: [
{
type: "text",
text: JSON.stringify(milestone, null, 2),
},
],
};
}
case "get_milestone_burndown_events": {
const { project_id, milestone_id } =
GetMilestoneBurndownEventsSchema.parse(request.params.arguments);
const events = await getMilestoneBurndownEvents(
project_id,
milestone_id
);
return {
content: [
{
type: "text",
text: JSON.stringify(events, null, 2),
},
],
};
}
default: default:
throw new Error(`Unknown tool: ${request.params.name}`); throw new Error(`Unknown tool: ${request.params.name}`);
} }

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@zereight/mcp-gitlab", "name": "@zereight/mcp-gitlab",
"version": "1.0.38", "version": "1.0.46",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@zereight/mcp-gitlab", "name": "@zereight/mcp-gitlab",
"version": "1.0.38", "version": "1.0.46",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "1.8.0", "@modelcontextprotocol/sdk": "1.8.0",

View File

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

View File

@ -40,7 +40,7 @@ export const GitLabPipelineSchema = z.object({
image: z.string().optional(), image: z.string().optional(),
size: z.string().optional(), size: z.string().optional(),
title: z.string().optional(), title: z.string().optional(),
}).optional(), }).nullable().optional(),
favicon: z.string().optional(), favicon: z.string().optional(),
}).optional(), }).optional(),
}); });
@ -333,6 +333,22 @@ export const GitLabReferenceSchema = z.object({
}), }),
}); });
// Milestones rest api output schemas
export const GitLabMilestonesSchema = z.object({
id: z.number(),
iid: z.number(),
project_id: z.number(),
title: z.string(),
description: z.string().nullable(),
due_date: z.string().nullable(),
start_date: z.string().nullable(),
state: z.string(),
updated_at: z.string(),
created_at: z.string(),
expired: z.boolean(),
web_url: z.string().optional()
});
// Input schemas for operations // Input schemas for operations
export const CreateRepositoryOptionsSchema = z.object({ export const CreateRepositoryOptionsSchema = z.object({
name: z.string(), name: z.string(),
@ -399,7 +415,7 @@ export const GitLabUserSchema = z.object({
username: z.string(), // Changed from login to match GitLab API username: z.string(), // Changed from login to match GitLab API
id: z.number(), id: z.number(),
name: z.string(), name: z.string(),
avatar_url: z.string(), avatar_url: z.string().nullable(),
web_url: z.string(), // Changed from html_url to match GitLab API web_url: z.string(), // Changed from html_url to match GitLab API
}); });
@ -407,7 +423,7 @@ export const GitLabMilestoneSchema = z.object({
id: z.number(), id: z.number(),
iid: z.number(), // Added to match GitLab API iid: z.number(), // Added to match GitLab API
title: z.string(), title: z.string(),
description: z.string(), description: z.string().nullable().default(""),
state: z.string(), state: z.string(),
web_url: z.string(), // Changed from html_url to match GitLab API web_url: z.string(), // Changed from html_url to match GitLab API
}); });
@ -417,7 +433,7 @@ export const GitLabIssueSchema = z.object({
iid: z.number(), // Added to match GitLab API iid: z.number(), // Added to match GitLab API
project_id: z.number(), // Added to match GitLab API project_id: z.number(), // Added to match GitLab API
title: z.string(), 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(), state: z.string(),
author: GitLabUserSchema, author: GitLabUserSchema,
assignees: z.array(GitLabUserSchema), assignees: z.array(GitLabUserSchema),
@ -834,6 +850,88 @@ export const ListIssuesSchema = z.object({
per_page: z.number().optional().describe("Number of items per page"), per_page: z.number().optional().describe("Number of items per page"),
}); });
// Merge Requests API operation schemas
export const ListMergeRequestsSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"),
assignee_id: z
.number()
.optional()
.describe("Returns merge requests assigned to the given user ID"),
assignee_username: z
.string()
.optional()
.describe("Returns merge requests assigned to the given username"),
author_id: z
.number()
.optional()
.describe("Returns merge requests created by the given user ID"),
author_username: z
.string()
.optional()
.describe("Returns merge requests created by the given username"),
reviewer_id: z
.number()
.optional()
.describe("Returns merge requests which have the user as a reviewer"),
reviewer_username: z
.string()
.optional()
.describe("Returns merge requests which have the user as a reviewer"),
created_after: z
.string()
.optional()
.describe("Return merge requests created after the given time"),
created_before: z
.string()
.optional()
.describe("Return merge requests created before the given time"),
updated_after: z
.string()
.optional()
.describe("Return merge requests updated after the given time"),
updated_before: z
.string()
.optional()
.describe("Return merge requests updated before the given time"),
labels: 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 merge requests from a specific scope"),
search: z.string().optional().describe("Search for specific terms"),
state: z
.enum(["opened", "closed", "locked", "merged", "all"])
.optional()
.describe("Return merge requests with a specific state"),
order_by: z
.enum(["created_at", "updated_at", "priority", "label_priority", "milestone_due", "popularity"])
.optional()
.describe("Return merge requests ordered by the given field"),
sort: z
.enum(["asc", "desc"])
.optional()
.describe("Return merge requests sorted in ascending or descending order"),
target_branch: z
.string()
.optional()
.describe("Return merge requests targeting a specific branch"),
source_branch: z
.string()
.optional()
.describe("Return merge requests from a specific source branch"),
wip: z
.enum(["yes", "no"])
.optional()
.describe("Filter merge requests against their wip status"),
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({ export const GetIssueSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), project_id: z.string().describe("Project ID or URL-encoded path"),
issue_iid: z.number().describe("The internal ID of the project issue"), issue_iid: z.number().describe("The internal ID of the project issue"),
@ -1178,6 +1276,63 @@ export const CreateMergeRequestThreadSchema = ProjectParamsSchema.extend({
created_at: z.string().optional().describe("Date the thread was created at (ISO 8601 format)"), created_at: z.string().optional().describe("Date the thread was created at (ISO 8601 format)"),
}); });
// Milestone related schemas
// Schema for listing project milestones
export const ListProjectMilestonesSchema = ProjectParamsSchema.extend({
iids: z.array(z.number()).optional().describe("Return only the milestones having the given iid"),
state: z.enum(["active", "closed"]).optional().describe("Return only active or closed milestones"),
title: z.string().optional().describe("Return only milestones with a title matching the provided string"),
search: z.string().optional().describe("Return only milestones with a title or description matching the provided string"),
include_ancestors: z.boolean().optional().describe("Include ancestor groups"),
updated_before: z.string().optional().describe("Return milestones updated before the specified date (ISO 8601 format)"),
updated_after: z.string().optional().describe("Return milestones updated after the specified date (ISO 8601 format)"),
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 single milestone
export const GetProjectMilestoneSchema = ProjectParamsSchema.extend({
milestone_id: z.number().describe("The ID of a project milestone"),
});
// Schema for creating a new milestone
export const CreateProjectMilestoneSchema = ProjectParamsSchema.extend({
title: z.string().describe("The title of the milestone"),
description: z.string().optional().describe("The description of the milestone"),
due_date: z.string().optional().describe("The due date of the milestone (YYYY-MM-DD)"),
start_date: z.string().optional().describe("The start date of the milestone (YYYY-MM-DD)"),
});
// Schema for editing a milestone
export const EditProjectMilestoneSchema = GetProjectMilestoneSchema.extend({
title: z.string().optional().describe("The title of the milestone"),
description: z.string().optional().describe("The description of the milestone"),
due_date: z.string().optional().describe("The due date of the milestone (YYYY-MM-DD)"),
start_date: z.string().optional().describe("The start date of the milestone (YYYY-MM-DD)"),
state_event: z.enum(["close", "activate"]).optional().describe("The state event of the milestone"),
});
// Schema for deleting a milestone
export const DeleteProjectMilestoneSchema = GetProjectMilestoneSchema;
// Schema for getting issues assigned to a milestone
export const GetMilestoneIssuesSchema = GetProjectMilestoneSchema;
// Schema for getting merge requests assigned to a milestone
export const GetMilestoneMergeRequestsSchema = GetProjectMilestoneSchema.extend({
page: z.number().optional().describe("Page number for pagination"),
per_page: z.number().optional().describe("Number of items per page (max 100)"),
});
// Schema for promoting a project milestone to a group milestone
export const PromoteProjectMilestoneSchema = GetProjectMilestoneSchema;
// Schema for getting burndown chart events for a milestone
export const GetMilestoneBurndownEventsSchema = GetProjectMilestoneSchema.extend({
page: z.number().optional().describe("Page number for pagination"),
per_page: z.number().optional().describe("Number of items per page (max 100)"),
});
// Export types // Export types
export type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>; export type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>;
export type GitLabFork = z.infer<typeof GitLabForkSchema>; export type GitLabFork = z.infer<typeof GitLabForkSchema>;
@ -1238,3 +1393,13 @@ export type GitLabPipeline = z.infer<typeof GitLabPipelineSchema>;
export type ListPipelinesOptions = z.infer<typeof ListPipelinesSchema>; export type ListPipelinesOptions = z.infer<typeof ListPipelinesSchema>;
export type GetPipelineOptions = z.infer<typeof GetPipelineSchema>; export type GetPipelineOptions = z.infer<typeof GetPipelineSchema>;
export type ListPipelineJobsOptions = z.infer<typeof ListPipelineJobsSchema>; export type ListPipelineJobsOptions = z.infer<typeof ListPipelineJobsSchema>;
export type GitLabMilestones = z.infer<typeof GitLabMilestonesSchema>;
export type ListProjectMilestonesOptions = z.infer<typeof ListProjectMilestonesSchema>;
export type GetProjectMilestoneOptions = z.infer<typeof GetProjectMilestoneSchema>;
export type CreateProjectMilestoneOptions = z.infer<typeof CreateProjectMilestoneSchema>;
export type EditProjectMilestoneOptions = z.infer<typeof EditProjectMilestoneSchema>;
export type DeleteProjectMilestoneOptions = z.infer<typeof DeleteProjectMilestoneSchema>;
export type GetMilestoneIssuesOptions = z.infer<typeof GetMilestoneIssuesSchema>;
export type GetMilestoneMergeRequestsOptions = z.infer<typeof GetMilestoneMergeRequestsSchema>;
export type PromoteProjectMilestoneOptions = z.infer<typeof PromoteProjectMilestoneSchema>;
export type GetMilestoneBurndownEventsOptions = z.infer<typeof GetMilestoneBurndownEventsSchema>;

18
scripts/image_push.sh Normal file
View File

@ -0,0 +1,18 @@
#!/bin/bash
if [ -z "$1" ]; then
echo "Error: docker user name required."
exit 1
fi
DOCKER_USER=$1
IMAGE_NAME=gitlab-mcp
IMAGE_VERSION=$(jq -r '.version' package.json)
echo "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}"
docker build --platform=linux/arm64 -t "${DOCKER_USER}/${IMAGE_NAME}:latest" .
docker tag "${DOCKER_USER}/${IMAGE_NAME}:latest" "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}"
docker push "${DOCKER_USER}/${IMAGE_NAME}:latest"
docker push "${DOCKER_USER}/${IMAGE_NAME}:${IMAGE_VERSION}"