Compare commits

...

23 Commits

Author SHA1 Message Date
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
25be1947b9 chore(release): 1.0.42 - issue note 기능 추가 (#47) 2025-05-22 21:24:29 +09:00
864ee77ae6 Merge pull request #47 from svengt/feat/add-issue-notes-support
feat: add support for creating and updating issue notes
2025-05-22 21:22:04 +09:00
dc6cc59434 feat: add support for creating and updating issue notes
- Added create_issue_note to add a new note to an existing issue thread
- Added update_issue_note to modify an existing issue thread note
- Similar to existing merge request note functions but for issues
2025-05-22 13:25:31 +02:00
5924fd3ed4 Merge pull request #45 from vlucaswang/fix/docs
fix: fix README
2025-05-21 14:28:08 +09:00
f4b265bf2e fix: fix README 2025-05-21 14:35:37 +09:30
b326f4c3c3 docs: update release notes for v1.0.40 (2025-05-21) 2025-05-21 03:40:02 +09:00
1350a024b5 Merge pull request #44 from huerlisi/feat/add-issue-notes-support
feat: add issue discussions support
2025-05-21 03:36:33 +09:00
4c57c37888 feat: add issue discussions support
Added `list_issue_discussions` tool to support GitLab issue discussions
similar to merge request discussions.
2025-05-20 15:45:23 +02:00
e4a28a9a47 버전 1.0.39로 업데이트 2025-05-20 18:34:05 +09:00
9f1e7b5bca Merge pull request #42 from vlucaswang/feat/add-docker
feat: add docker image and push to dockerhub
2025-05-20 18:32:19 +09:00
f37e210ee8 Merge pull request #41 from kamibayashi/fixed_resolve_outdated_diff_discussions_nullable
fixed resolve_outdated_diff_discussions nullable
2025-05-20 18:28:08 +09:00
6f789692be feat: add docker image and push to dockerhub 2025-05-20 16:04:37 +09:30
1bb70dccb9 fixed resolve_outdated_diff_discussions nullable 2025-05-19 17:18:01 +09:00
676bbcd4dd docs: add release-notes.md 2025-05-17 15:41:14 +09:00
0bb59a3217 Bump version 2025-05-17 15:38:18 +09:00
b908f03801 Merge pull request #40 from huerlisi/fix/discussion-enum
fix: add `expanded` to `start` and `end` for GitLabDiscussionNoteSchema
2025-05-17 15:36:11 +09:00
5024a2a5af fix: add expanded to start and end for GitLabDiscussionNoteSchema 2025-05-16 21:36:21 +02:00
8 changed files with 752 additions and 44 deletions

View File

@ -1,5 +1,35 @@
## [Released] - 2025-05-13 ## [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
- 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)
---
## [1.0.38] - 2025-05-17
### Fixed ### Fixed
- **GitLab MCP Server:** Modified GitLab API helper functions to decode the `project_id` using `decodeURIComponent()` before processing. This resolves API call failures caused by differences in project ID encoding between Gemini and other AI models. API requests are now handled consistently regardless of the model. - Added `expanded` property to `start` and `end` in `GitLabDiscussionNoteSchema`
Now you can expand or collapse more information at the start and end of discussion notes.
Example: In code review, you can choose to show or hide specific parts of the discussion.
(See: [PR #40](https://github.com/zereight/gitlab-mcp/pull/40))

24
Dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM node:22.15-alpine AS builder
COPY . /app
COPY tsconfig.json /tsconfig.json
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm npm install
RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
FROM node:22.12-alpine AS release
WORKDIR /app
COPY --from=builder /app/build /app/build
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/package-lock.json /app/package-lock.json
ENV NODE_ENV=production
RUN npm ci --ignore-scripts --omit-dev
ENTRYPOINT ["node", "build/index.js"]

View File

@ -14,6 +14,8 @@ GitLab MCP(Model Context Protocol) Server. **Includes bug fixes and improvements
When using with the Claude App, you need to set up your API key and URLs directly. When using with the Claude App, you need to set up your API key and URLs directly.
#### npx
```json ```json
{ {
"mcpServers": { "mcpServers": {
@ -31,8 +33,39 @@ When using with the Claude App, you need to set up your API key and URLs directl
} }
``` ```
### Environment Variables #### Docker
```json
{
"mcpServers": {
"GitLab communication server": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITLAB_PERSONAL_ACCESS_TOKEN",
"-e",
"GITLAB_API_URL",
"-e",
"GITLAB_READ_ONLY_MODE",
"-e",
"USE_GITLAB_WIKI",
"nkwd/gitlab-mcp"
],
"env": {
"GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token",
"GITLAB_API_URL": "https://gitlab.com/api/v4", // Optional, for self-hosted GitLab
"GITLAB_READ_ONLY_MODE": "false",
"USE_GITLAB_WIKI": "true"
}
}
}
}
```
### Environment Variables
- `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token. - `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token.
- `GITLAB_API_URL`: Your GitLab API URL. (Default: `https://gitlab.com/api/v4`) - `GITLAB_API_URL`: Your GitLab API URL. (Default: `https://gitlab.com/api/v4`)
@ -59,29 +92,37 @@ 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 15. `mr_discussions` - List discussion items for a merge request
16. `update_merge_request_note` - Modify an existing merge request thread note 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 17. `create_merge_request_note` - Add a new note to an existing merge request thread
18. `list_issues` - List issues in a GitLab project with filtering options 18. `update_issue_note` - Modify an existing issue thread note
19. `get_issue` - Get details of a specific issue in a GitLab project 19. `create_issue_note` - Add a new note to an existing issue thread
20. `update_issue` - Update an issue in a GitLab project 20. `list_issues` - List issues in a GitLab project with filtering options
21. `delete_issue` - Delete an issue from a GitLab project 21. `get_issue` - Get details of a specific issue in a GitLab project
22. `list_issue_links` - List all issue links for a specific issue 22. `update_issue` - Update an issue in a GitLab project
23. `get_issue_link` - Get a specific issue link 23. `delete_issue` - Delete an issue from a GitLab project
24. `create_issue_link` - Create an issue link between two issues 24. `list_issue_links` - List all issue links for a specific issue
25. `delete_issue_link` - Delete an issue link 25. `list_issue_discussions` - List discussions for an issue in a GitLab project
26. `list_namespaces` - List all namespaces available to the current user 26. `get_issue_link` - Get a specific issue link
27. `get_namespace` - Get details of a namespace by ID or path 27. `create_issue_link` - Create an issue link between two issues
28. `verify_namespace` - Verify if a namespace path exists 28. `delete_issue_link` - Delete an issue link
29. `get_project` - Get details of a specific project 29. `list_namespaces` - List all namespaces available to the current user
30. `list_projects` - List projects accessible by the current user 30. `get_namespace` - Get details of a namespace by ID or path
31. `list_labels` - List labels for a project 31. `verify_namespace` - Verify if a namespace path exists
32. `get_label` - Get a single label from a project 32. `get_project` - Get details of a specific project
33. `create_label` - Create a new label in a project 33. `list_projects` - List projects accessible by the current user
34. `update_label` - Update an existing label in a project 34. `list_labels` - List labels for a project
35. `delete_label` - Delete a label from a project 35. `get_label` - Get a single label from a project
36. `list_group_projects` - List projects in a GitLab group with filtering options 36. `create_label` - Create a new label in a project
37. `list_wiki_pages` - List wiki pages in a GitLab project 37. `update_label` - Update an existing label in a project
38. `get_wiki_page` - Get details of a specific wiki page 38. `delete_label` - Delete a label from a project
39. `create_wiki_page` - Create a new wiki page in a GitLab project 39. `list_group_projects` - List projects in a GitLab group with filtering options
40. `update_wiki_page` - Update an existing wiki page in a GitLab project 40. `list_wiki_pages` - List wiki pages in a GitLab project
41. `delete_wiki_page` - Delete a wiki page from a GitLab project 41. `get_wiki_page` - Get details of a specific wiki page
42. `get_repository_tree` - Get the repository tree for a GitLab project (list files and directories) 42. `create_wiki_page` - Create a new wiki page in a GitLab project
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 --> <!-- TOOLS-END -->

472
index.ts
View File

@ -17,7 +17,6 @@ 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";
// Add type imports for proxy agents // Add type imports for proxy agents
import { Agent } from "http"; import { Agent } from "http";
import { URL } from "url"; import { URL } from "url";
@ -61,6 +60,7 @@ import {
GitLabIssueLinkSchema, GitLabIssueLinkSchema,
GitLabIssueWithLinkDetailsSchema, GitLabIssueWithLinkDetailsSchema,
ListIssueLinksSchema, ListIssueLinksSchema,
ListIssueDiscussionsSchema,
GetIssueLinkSchema, GetIssueLinkSchema,
CreateIssueLinkSchema, CreateIssueLinkSchema,
DeleteIssueLinkSchema, DeleteIssueLinkSchema,
@ -83,6 +83,15 @@ import {
UpdateWikiPageSchema, UpdateWikiPageSchema,
DeleteWikiPageSchema, DeleteWikiPageSchema,
GitLabWikiPageSchema, GitLabWikiPageSchema,
GetRepositoryTreeSchema,
GitLabTreeItemSchema,
GitLabPipelineSchema,
GetPipelineSchema,
ListPipelinesSchema,
ListPipelineJobsSchema,
// pipeline job schemas
GetPipelineJobOutputSchema,
GitLabPipelineJobSchema,
// Discussion Schemas // Discussion Schemas
GitLabDiscussionNoteSchema, // Added GitLabDiscussionNoteSchema, // Added
GitLabDiscussionSchema, GitLabDiscussionSchema,
@ -107,6 +116,11 @@ import {
type GitLabNamespaceExistsResponse, type GitLabNamespaceExistsResponse,
type GitLabProject, type GitLabProject,
type GitLabLabel, type GitLabLabel,
type GitLabPipeline,
type ListPipelinesOptions,
type GetPipelineOptions,
type ListPipelineJobsOptions,
type GitLabPipelineJob,
// Discussion Types // Discussion Types
type GitLabDiscussionNote, // Added type GitLabDiscussionNote, // Added
type GitLabDiscussion, type GitLabDiscussion,
@ -116,10 +130,10 @@ import {
type UpdateWikiPageOptions, type UpdateWikiPageOptions,
type DeleteWikiPageOptions, type DeleteWikiPageOptions,
type GitLabWikiPage, type GitLabWikiPage,
GitLabTreeItemSchema,
GetRepositoryTreeSchema,
type GitLabTreeItem, type GitLabTreeItem,
type GetRepositoryTreeOptions, type GetRepositoryTreeOptions,
UpdateIssueNoteSchema,
CreateIssueNoteSchema,
} from "./schemas.js"; } from "./schemas.js";
/** /**
@ -286,6 +300,16 @@ const allTools = [
description: "Add a new note to an existing merge request thread", description: "Add a new note to an existing merge request thread",
inputSchema: zodToJsonSchema(CreateMergeRequestNoteSchema), inputSchema: zodToJsonSchema(CreateMergeRequestNoteSchema),
}, },
{
name: "update_issue_note",
description: "Modify an existing issue thread note",
inputSchema: zodToJsonSchema(UpdateIssueNoteSchema),
},
{
name: "create_issue_note",
description: "Add a new note to an existing issue thread",
inputSchema: zodToJsonSchema(CreateIssueNoteSchema),
},
{ {
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",
@ -311,6 +335,11 @@ const allTools = [
description: "List all issue links for a specific issue", description: "List all issue links for a specific issue",
inputSchema: zodToJsonSchema(ListIssueLinksSchema), inputSchema: zodToJsonSchema(ListIssueLinksSchema),
}, },
{
name: "list_issue_discussions",
description: "List discussions for an issue in a GitLab project",
inputSchema: zodToJsonSchema(ListIssueDiscussionsSchema),
},
{ {
name: "get_issue_link", name: "get_issue_link",
description: "Get a specific issue link", description: "Get a specific issue link",
@ -412,6 +441,31 @@ const allTools = [
"Get the repository tree for a GitLab project (list files and directories)", "Get the repository tree for a GitLab project (list files and directories)",
inputSchema: zodToJsonSchema(GetRepositoryTreeSchema), 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 // Define which tools are read-only
@ -424,15 +478,22 @@ const readOnlyTools = [
"list_issues", "list_issues",
"get_issue", "get_issue",
"list_issue_links", "list_issue_links",
"list_issue_discussions",
"get_issue_link", "get_issue_link",
"list_namespaces", "list_namespaces",
"get_namespace", "get_namespace",
"verify_namespace", "verify_namespace",
"get_project", "get_project",
"get_pipeline",
"list_pipelines",
"list_pipeline_jobs",
"get_pipeline_job",
"get_pipeline_job_output",
"list_projects", "list_projects",
"list_labels", "list_labels",
"get_label", "get_label",
"list_group_projects", "list_group_projects",
"get_repository_tree",
]; ];
// 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
@ -1023,6 +1084,56 @@ async function listMergeRequestDiscussions(
return z.array(GitLabDiscussionSchema).parse(data); return z.array(GitLabDiscussionSchema).parse(data);
} }
/**
* List discussions for an issue
*
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} issueIid - The internal ID of the project issue
* @param {Object} options - Pagination and sorting options
* @returns {Promise<GitLabDiscussion[]>} List of issue discussions
*/
async function listIssueDiscussions(
projectId: string,
issueIid: number,
options: {
page?: number;
per_page?: number;
sort?: "asc" | "desc";
order_by?: "created_at" | "updated_at";
} = {}
): Promise<GitLabDiscussion[]> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/issues/${issueIid}/discussions`
);
// Add query parameters for pagination and sorting
if (options.page) {
url.searchParams.append("page", options.page.toString());
}
if (options.per_page) {
url.searchParams.append("per_page", options.per_page.toString());
}
if (options.sort) {
url.searchParams.append("sort", options.sort);
}
if (options.order_by) {
url.searchParams.append("order_by", options.order_by);
}
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
});
await handleGitLabError(response);
const data = await response.json();
// Parse the response as an array of discussions
return z.array(GitLabDiscussionSchema).parse(data);
}
/** /**
* Modify an existing merge request thread note * Modify an existing merge request thread note
* 병합 요청 토론 노트 수정 * 병합 요청 토론 노트 수정
@ -1069,6 +1180,81 @@ async function updateMergeRequestNote(
return GitLabDiscussionNoteSchema.parse(data); return GitLabDiscussionNoteSchema.parse(data);
} }
/**
* Update an issue discussion note
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} issueIid - The IID of an issue
* @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
* @returns {Promise<GitLabDiscussionNote>} The updated note
*/
async function updateIssueNote(
projectId: string,
issueIid: number,
discussionId: string,
noteId: number,
body: string
): Promise<GitLabDiscussionNote> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/issues/${issueIid}/discussions/${discussionId}/notes/${noteId}`
);
const payload = { body };
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
method: "PUT",
body: JSON.stringify(payload),
});
await handleGitLabError(response);
const data = await response.json();
return GitLabDiscussionNoteSchema.parse(data);
}
/**
* Create a note in an issue discussion
* @param {string} projectId - The ID or URL-encoded path of the project
* @param {number} issueIid - The IID of an issue
* @param {string} discussionId - The ID of a thread
* @param {string} body - The content of the new note
* @param {string} [createdAt] - The creation date of the note (ISO 8601 format)
* @returns {Promise<GitLabDiscussionNote>} The created note
*/
async function createIssueNote(
projectId: string,
issueIid: number,
discussionId: string,
body: string,
createdAt?: string
): Promise<GitLabDiscussionNote> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
)}/issues/${issueIid}/discussions/${discussionId}/notes`
);
const payload: { body: string; created_at?: string } = { body };
if (createdAt) {
payload.created_at = createdAt;
}
const response = await fetch(url.toString(), {
...DEFAULT_FETCH_CONFIG,
method: "POST",
body: JSON.stringify(payload),
});
await handleGitLabError(response);
const data = await response.json();
return GitLabDiscussionNoteSchema.parse(data);
}
/** /**
* Add a new note to an existing merge request thread * Add a new note to an existing merge request thread
* 기존 병합 요청 스레드에 새 노트 추가 * 기존 병합 요청 스레드에 새 노트 추가
@ -2155,6 +2341,166 @@ async function deleteWikiPage(projectId: string, slug: string): Promise<void> {
await handleGitLabError(response); 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 * Get the repository tree for a project
* @param {string} projectId - The ID or URL-encoded path of the project * @param {string} projectId - The ID or URL-encoded path of the project
@ -2404,6 +2750,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}; };
} }
case "update_issue_note": {
const args = UpdateIssueNoteSchema.parse(request.params.arguments);
const note = await updateIssueNote(
args.project_id,
args.issue_iid,
args.discussion_id,
args.note_id,
args.body
);
return {
content: [{ type: "text", text: JSON.stringify(note, null, 2) }],
};
}
case "create_issue_note": {
const args = CreateIssueNoteSchema.parse(request.params.arguments);
const note = await createIssueNote(
args.project_id,
args.issue_iid,
args.discussion_id,
args.body,
args.created_at
);
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(
@ -2581,8 +2955,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
} }
case "create_merge_request_thread": { case "create_merge_request_thread": {
const args = CreateMergeRequestThreadSchema.parse(request.params.arguments); const args = CreateMergeRequestThreadSchema.parse(
const { project_id, merge_request_iid, body, position, created_at } = args; request.params.arguments
);
const { project_id, merge_request_iid, body, position, created_at } =
args;
const thread = await createMergeRequestThread( const thread = await createMergeRequestThread(
project_id, project_id,
@ -2647,6 +3024,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}; };
} }
case "list_issue_discussions": {
const args = ListIssueDiscussionsSchema.parse(request.params.arguments);
const { project_id, issue_iid, ...options } = args;
const discussions = await listIssueDiscussions(
project_id,
issue_iid,
options
);
return {
content: [
{ type: "text", text: JSON.stringify(discussions, null, 2) },
],
};
}
case "get_issue_link": { case "get_issue_link": {
const args = GetIssueLinkSchema.parse(request.params.arguments); const args = GetIssueLinkSchema.parse(request.params.arguments);
const link = await getIssueLink( const link = await getIssueLink(
@ -2838,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: 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.36", "version": "1.0.38",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@zereight/mcp-gitlab", "name": "@zereight/mcp-gitlab",
"version": "1.0.36", "version": "1.0.38",
"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.37", "version": "1.0.45",
"description": "MCP server for using the GitLab API", "description": "MCP server for using the GitLab API",
"license": "MIT", "license": "MIT",
"author": "zereight", "author": "zereight",

5
release-notes.md Normal file
View File

@ -0,0 +1,5 @@
### 1.0.40 (2025-05-21)
- Added support for listing discussions (comments/notes) on GitLab issues.
- Example: You can now easily fetch all conversations (comments) attached to an issue via the API.
- Related PR: [#44](https://github.com/zereight/gitlab-mcp/pull/44)

View File

@ -7,6 +7,119 @@ export const GitLabAuthorSchema = z.object({
date: z.string(), 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 // Namespace related schemas
// Base schema for project-related operations // Base schema for project-related operations
@ -107,7 +220,7 @@ export const GitLabRepositorySchema = z.object({
jobs_enabled: z.boolean().optional(), jobs_enabled: z.boolean().optional(),
snippets_enabled: z.boolean().optional(), snippets_enabled: z.boolean().optional(),
can_create_merge_request_in: z.boolean().optional(), can_create_merge_request_in: z.boolean().optional(),
resolve_outdated_diff_discussions: z.boolean().optional(), resolve_outdated_diff_discussions: z.boolean().nullable().optional(),
shared_runners_enabled: z.boolean().optional(), shared_runners_enabled: z.boolean().optional(),
shared_with_groups: z shared_with_groups: z
.array( .array(
@ -436,13 +549,13 @@ export const GitLabDiscussionNoteSchema = z.object({
.object({ .object({
start: z.object({ start: z.object({
line_code: z.string(), line_code: z.string(),
type: z.enum(["new", "old"]), type: z.enum(["new", "old", "expanded"]),
old_line: z.number().nullable(), old_line: z.number().nullable(),
new_line: z.number().nullable(), new_line: z.number().nullable(),
}), }),
end: z.object({ end: z.object({
line_code: z.string(), line_code: z.string(),
type: z.enum(["new", "old"]), type: z.enum(["new", "old", "expanded"]),
old_line: z.number().nullable(), old_line: z.number().nullable(),
new_line: z.number().nullable(), new_line: z.number().nullable(),
}), }),
@ -491,6 +604,22 @@ export const CreateMergeRequestNoteSchema = ProjectParamsSchema.extend({
created_at: z.string().optional().describe("Date the note was created at (ISO 8601 format)"), created_at: z.string().optional().describe("Date the note was created at (ISO 8601 format)"),
}); });
// Input schema for updating an issue discussion note
export const UpdateIssueNoteSchema = ProjectParamsSchema.extend({
issue_iid: z.number().describe("The IID of an issue"),
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"),
});
// Input schema for adding a note to an existing issue discussion
export const CreateIssueNoteSchema = ProjectParamsSchema.extend({
issue_iid: z.number().describe("The IID of an issue"),
discussion_id: z.string().describe("The ID of a thread"),
body: z.string().describe("The content of the note or reply"),
created_at: z.string().optional().describe("Date the note was created at (ISO 8601 format)"),
});
// API Operation Parameter Schemas // API Operation Parameter Schemas
export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({ export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
@ -757,6 +886,15 @@ export const ListIssueLinksSchema = z.object({
issue_iid: z.number().describe("The internal ID of a project's issue"), issue_iid: z.number().describe("The internal ID of a project's issue"),
}); });
export const ListIssueDiscussionsSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"),
issue_iid: z.number().describe("The internal ID of the project issue"),
page: z.number().optional().describe("Page number for pagination"),
per_page: z.number().optional().describe("Number of items per page"),
sort: z.enum(["asc", "desc"]).optional().describe("Return issue discussions sorted in ascending or descending order"),
order_by: z.enum(["created_at", "updated_at"]).optional().describe("Return issue discussions ordered by created_at or updated_at fields"),
});
export const GetIssueLinkSchema = z.object({ export const GetIssueLinkSchema = 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 a project's issue"), issue_iid: z.number().describe("The internal ID of a project's issue"),
@ -1075,6 +1213,9 @@ export type GitLabMergeRequestDiff = z.infer<
>; >;
export type CreateNoteOptions = z.infer<typeof CreateNoteSchema>; export type CreateNoteOptions = z.infer<typeof CreateNoteSchema>;
export type GitLabIssueLink = z.infer<typeof GitLabIssueLinkSchema>; export type GitLabIssueLink = z.infer<typeof GitLabIssueLinkSchema>;
export type ListIssueDiscussionsOptions = z.infer<typeof ListIssueDiscussionsSchema>;
export type UpdateIssueNoteOptions = z.infer<typeof UpdateIssueNoteSchema>;
export type CreateIssueNoteOptions = z.infer<typeof CreateIssueNoteSchema>;
export type GitLabNamespace = z.infer<typeof GitLabNamespaceSchema>; export type GitLabNamespace = z.infer<typeof GitLabNamespaceSchema>;
export type GitLabNamespaceExistsResponse = z.infer< export type GitLabNamespaceExistsResponse = z.infer<
typeof GitLabNamespaceExistsResponseSchema typeof GitLabNamespaceExistsResponseSchema
@ -1092,3 +1233,8 @@ export type GetRepositoryTreeOptions = z.infer<typeof GetRepositoryTreeSchema>;
export type MergeRequestThreadPosition = z.infer<typeof MergeRequestThreadPositionSchema>; export type MergeRequestThreadPosition = z.infer<typeof MergeRequestThreadPositionSchema>;
export type CreateMergeRequestThreadOptions = z.infer<typeof CreateMergeRequestThreadSchema>; export type CreateMergeRequestThreadOptions = z.infer<typeof CreateMergeRequestThreadSchema>;
export type CreateMergeRequestNoteOptions = z.infer<typeof CreateMergeRequestNoteSchema>; 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>;