diff --git a/README.md b/README.md index 1bcc076..bd28a4f 100644 --- a/README.md +++ b/README.md @@ -40,36 +40,146 @@ env GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token GITLAB_API_URL=your_gitlab_ap - `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token. - `GITLAB_API_URL`: Your GitLab API URL. (Default: `https://gitlab.com/api/v4`) -## Tools Reference 🛠️ +## Tools 🛠️ + +1. `create_or_update_file` + + - Create or update a single file in a GitLab project. 📝 + - Inputs: + - `project_id` (string): Project ID or namespace/project_path + - `file_path` (string): Path to create/update the file + - `content` (string): File content + - `commit_message` (string): Commit message + - `branch` (string): Branch to create/update the file in + - `previous_path` (optional string): Previous file path when renaming a file + - Returns: File content and commit details + +2. `push_files` + + - Push multiple files in a single commit. 📤 + - Inputs: + - `project_id` (string): Project ID or namespace/project_path + - `branch` (string): Branch to push to + - `files` (array): Array of files to push, each with `file_path` and `content` properties + - `commit_message` (string): Commit message + - Returns: Updated branch reference + +3. `search_repositories` + + - Search for GitLab projects. 🔍 + - Inputs: + - `search` (string): Search query + - `page` (optional number): Page number (default: 1) + - `per_page` (optional number): Results per page (default: 20, max: 100) + - Returns: Project search results + +4. `create_repository` + + - Create a new GitLab project. ➕ + - Inputs: + - `name` (string): Project name + - `description` (optional string): Project description + - `visibility` (optional string): Project visibility level (public, private, internal) + - `initialize_with_readme` (optional boolean): Initialize with README + - Returns: Details of the created project + +5. `get_file_contents` + + - Get the contents of a file or directory. 📂 + - Inputs: + - `project_id` (string): Project ID or namespace/project_path + - `file_path` (string): Path to the file/directory + - `ref` (optional string): Branch, tag, or commit SHA (default: default branch) + - Returns: File/directory content + +6. `create_issue` + + - Create a new issue. 🐛 + - Inputs: + - `project_id` (string): Project ID or namespace/project_path + - `title` (string): Issue title + - `description` (string): Issue description + - `assignee_ids` (optional number[]): Array of assignee IDs + - `milestone_id` (optional number): Milestone ID + - `labels` (optional string[]): Array of labels + - Returns: Details of the created issue + +7. `create_merge_request` + + - Create a new merge request. 🚀 + - Inputs: + - `project_id` (string): Project ID or namespace/project_path + - `title` (string): Merge request title + - `description` (string): Merge request description + - `source_branch` (string): Branch with changes + - `target_branch` (string): Branch to merge into + - `allow_collaboration` (optional boolean): Allow collaborators to push commits to the source branch + - `draft` (optional boolean): Create as a draft merge request + - Returns: Details of the created merge request + +8. `fork_repository` + + - Fork a project. 🍴 + - Inputs: + - `project_id` (string): Project ID or namespace/project_path to fork + - `namespace` (optional string): Namespace to fork into (default: user namespace) + - Returns: Details of the forked project + +9. `create_branch` + + - Create a new branch. 🌿 + - Inputs: + - `project_id` (string): Project ID or namespace/project_path + - `name` (string): New branch name + - `ref` (optional string): Ref to create the branch from (branch, tag, commit SHA, default: default branch) + - Returns: Created branch reference + +10. `get_merge_request` + + - Get details of a merge request. ℹ️ + - Inputs: + - `project_id` (string): Project ID or namespace/project_path + - `merge_request_iid` (number): Merge request IID + - Returns: Merge request details + +11. `get_merge_request_diffs` + + - Get changes (diffs) of a merge request. diff + - Inputs: + - `project_id` (string): Project ID or namespace/project_path + - `merge_request_iid` (number): Merge request IID + - `view` (optional string): Diff view type ('inline' or 'parallel') + - Returns: Array of merge request diff information + +12. `update_merge_request` + + - Update a merge request. 🔄 + - Inputs: + - `project_id` (string): Project ID or namespace/project_path + - `merge_request_iid` (number): Merge request IID + - `title` (optional string): New title + - `description` (string): New description + - `target_branch` (optional string): New target branch + - `state_event` (optional string): Merge request state change event ('close', 'reopen') + - `remove_source_branch` (optional boolean): Remove source branch after merge + - `allow_collaboration` (optional boolean): Allow collaborators to push commits to the source branch + - Returns: Updated merge request details + +13. `create_note` + - Create a new note (comment) to an issue or merge request. 💬 + - Inputs: + - `project_id` (string): Project ID or namespace/project_path + - `noteable_type` (string): Type of noteable ("issue" or "merge_request") + - `noteable_iid` (number): IID of the issue or merge request + - `body` (string): Note content + - Returns: Details of the created note -| Tool | Description | Parameters | Returns | -|------|-------------|------------|---------| -| **`create_or_update_file`** | Create or update a single file in a GitLab project 📝 | • `project_id` (string): Project ID or path
• `file_path` (string): Path to create/update
• `content` (string): File content
• `commit_message` (string): Commit message
• `branch` (string): Target branch
• `previous_path` (optional): Previous path when renaming | File content and commit details | -| **`push_files`** | Push multiple files in a single commit 📤 (internally creates a tree and commit) | • `project_id` (string): Project ID or path
• `branch` (string): Target branch
• `files` (array): Array of files with `file_path` and `content`
• `commit_message` (string): Commit message | Updated branch reference | -| **`search_repositories`** | Search for GitLab projects 🔍 | • `search` (string): Search query
• `page` (optional): Page number (default: 1)
• `per_page` (optional): Results per page (default: 20) | Project search results | -| **`create_repository`** | Create a new GitLab project ➕ | • `name` (string): Project name
• `description` (optional): Project description
• `visibility` (optional): Visibility level
• `initialize_with_readme` (optional): Initialize with README | Created project details | -| **`get_file_contents`** | Get the contents of a file or directory 📂 | • `project_id` (string): Project ID or path
• `file_path` (string): Path to file/directory
• `ref` (optional): Branch, tag, or commit SHA | File/directory content | -| **`create_issue`** | Create a new issue 🐛 | • `project_id` (string): Project ID or path
• `title` (string): Issue title
• `description` (string): Issue description
• `assignee_ids` (optional): Array of assignee IDs
• `milestone_id` (optional): Milestone ID
• `labels` (optional): Array of labels | Created issue details | -| **`list_issues`** | List issues in a project with comprehensive filtering options 📋 | • `project_id` (string): Project ID or path
• Optional filters: `assignee_id`, `assignee_username`, `author_id`, `author_username`, `confidential`, `created_after/before`, `due_date`, `label_name`, `milestone`, `scope`, `search`, `state`, `updated_after/before`
• Pagination: `page`, `per_page` | Array of issues | -| **`get_issue`** | Get details of a specific issue | • `project_id` (string): Project ID or path
• `issue_iid` (number): Issue IID | Issue details | -| **`update_issue`** | Update an existing issue ✏️ | • `project_id` (string): Project ID or path
• `issue_iid` (number): Issue IID
• Editable fields: `title`, `description`, `assignee_ids`, `labels`, `milestone_id`, `state_event` (close/reopen), `confidential`, `discussion_locked`, `due_date`, `weight` | Updated issue details | -| **`delete_issue`** | Delete an issue | • `project_id` (string): Project ID or path
• `issue_iid` (number): Issue IID | Success message | -| **`list_issue_links`** | List all links for a specific issue | • `project_id` (string): Project ID or path
• `issue_iid` (number): Issue IID | Array of linked issues | -| **`get_issue_link`** | Get details of a specific issue link | • `project_id` (string): Project ID or path
• `issue_iid` (number): Issue IID
• `issue_link_id` (number): Link ID | Issue link details | -| **`create_issue_link`** | Create a link between two issues | • `project_id` (string): Project ID or path
• `issue_iid` (number): Source issue IID
• `target_project_id` (string): Target project ID
• `target_issue_iid` (number): Target issue IID
• `link_type` (optional): Relationship type | Created link details | -| **`delete_issue_link`** | Delete an issue link | • `project_id` (string): Project ID or path
• `issue_iid` (number): Issue IID
• `issue_link_id` (number): Link ID | Success message | -| **`create_merge_request`** | Create a new merge request 🚀 | • `project_id` (string): Project ID or path
• `title` (string): MR title
• `description` (string): MR description
• `source_branch` (string): Branch with changes
• `target_branch` (string): Branch to merge into
• `allow_collaboration` (optional): Allow collaborators
• `draft` (optional): Create as draft | Created merge request details | -| **`fork_repository`** | Fork a project 🍴 | • `project_id` (string): Project ID or path to fork
• `namespace` (optional): Namespace to fork into | Forked project details | -| **`create_branch`** | Create a new branch 🌿 | • `project_id` (string): Project ID or path
• `branch` (string): New branch name
• `ref` (optional): Reference to create from | Created branch reference | -| **`get_merge_request`** | Get details of a merge request ℹ️ | • `project_id` (string): Project ID or path
• `merge_request_iid` (number): MR IID | Merge request details | -| **`get_merge_request_diffs`** | Get changes of a merge request | • `project_id` (string): Project ID or path
• `merge_request_iid` (number): MR IID
• `view` (optional): Diff view type | Array of merge request diffs | -| **`update_merge_request`** | Update a merge request 🔄 | • `project_id` (string): Project ID or path
• `merge_request_iid` (number): MR IID
• Editable fields: `title`, `description`, `target_branch`, `assignee_ids`, `labels`, `state_event` (close/reopen), `remove_source_branch`, `squash`, `draft` | Updated merge request details | -| **`create_note`** | Create a comment on an issue or MR 💬 | • `project_id` (string): Project ID or path
• `noteable_type` (string): "issue" or "merge_request"
• `noteable_iid` (number): IID of the issue or MR
• `body` (string): Comment content | Created note details | -| **`list_namespaces`** | List available namespaces | • `search` (optional): Search term
• `page` (optional): Page number
• `per_page` (optional): Results per page
• `owned` (optional): Filter by ownership | Array of namespaces | -| **`get_namespace`** | Get details of a namespace | • `namespace_id` (string): Namespace ID or path | Namespace details | -| **`verify_namespace`** | Check if a namespace exists | • `path` (string): Namespace path to verify | Verification result | -| **`get_project`** | Get details of a specific project | • `project_id` (string): Project ID or path | Project details | | **`list_projects`** | List accessible projects with rich filtering options 📊 | • Search/filtering: `search`, `owned`, `membership`, `archived`, `visibility`
• Features filtering: `with_issues_enabled`, `with_merge_requests_enabled`
• Sorting: `order_by`, `sort`
• Access control: `min_access_level`
• Pagination: `page`, `per_page`, `simple` | Array of projects | +| **`list_labels`** | List all labels for a project with filtering options 🏷️ | • `project_id` (string): Project ID or path
• `with_counts` (optional): Include issue and merge request counts
• `include_ancestor_groups` (optional): Include ancestor groups
• `search` (optional): Filter labels by keyword | Array of labels | +| **`get_label`** | Get a single label from a project 🏷️ | • `project_id` (string): Project ID or path
• `label_id` (number/string): Label ID or name
• `include_ancestor_groups` (optional): Include ancestor groups | Label details | +| **`create_label`** | Create a new label in a project 🏷️➕ | • `project_id` (string): Project ID or path
• `name` (string): Label name
• `color` (string): Color in hex format (e.g., "#FF0000")
• `description` (optional): Label description
• `priority` (optional): Label priority | Created label details | +| **`update_label`** | Update an existing label in a project 🏷️✏️ | • `project_id` (string): Project ID or path
• `label_id` (number/string): Label ID or name
• `new_name` (optional): New label name
• `color` (optional): New color in hex format
• `description` (optional): New description
• `priority` (optional): New priority | Updated label details | +| **`delete_label`** | Delete a label from a project 🏷️❌ | • `project_id` (string): Project ID or path
• `label_id` (number/string): Label ID or name | Success message | ## Environment Variable Configuration diff --git a/index.ts b/index.ts index 322583f..9b9c9d4 100644 --- a/index.ts +++ b/index.ts @@ -27,6 +27,7 @@ import { GitLabNamespaceSchema, GitLabNamespaceExistsResponseSchema, GitLabProjectSchema, + GitLabLabelSchema, CreateRepositoryOptionsSchema, CreateIssueOptionsSchema, CreateMergeRequestOptionsSchema, @@ -59,6 +60,11 @@ import { VerifyNamespaceSchema, GetProjectSchema, ListProjectsSchema, + ListLabelsSchema, + GetLabelSchema, + CreateLabelSchema, + UpdateLabelSchema, + DeleteLabelSchema, CreateNoteSchema, type GitLabFork, type GitLabReference, @@ -77,6 +83,7 @@ import { type GitLabNamespace, type GitLabNamespaceExistsResponse, type GitLabProject, + type GitLabLabel, } from "./schemas.js"; /** @@ -1204,22 +1211,187 @@ async function getProject( * @returns {Promise} List of projects */ async function listProjects(options: z.infer = {}): Promise { - const url = new URL(`${GITLAB_API_URL}/projects`); + // Construct the query parameters + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(options)) { + if (value !== undefined && value !== null) { + if (typeof value === "boolean") { + params.append(key, value ? "true" : "false"); + } else { + params.append(key, String(value)); + } + } + } - // Add all the query parameters from options + // Make the API request + const response = await fetch( + `${GITLAB_API_URL}/projects?${params.toString()}`, + { + method: "GET", + headers: DEFAULT_HEADERS, + } + ); + + // Handle errors + await handleGitLabError(response); + + // Parse and return the data + const data = await response.json(); + return z.array(GitLabProjectSchema).parse(data); +} + +/** + * List labels for a project + * + * @param projectId The ID or URL-encoded path of the project + * @param options Optional parameters for listing labels + * @returns Array of GitLab labels + */ +async function listLabels( + projectId: string, + options: Omit, "project_id"> = {} +): Promise { + // Construct the URL with project path + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`); + + // Add query parameters Object.entries(options).forEach(([key, value]) => { if (value !== undefined) { - url.searchParams.append(key, value.toString()); + if (typeof value === "boolean") { + url.searchParams.append(key, value ? "true" : "false"); + } else { + url.searchParams.append(key, String(value)); + } } }); + // Make the API request const response = await fetch(url.toString(), { headers: DEFAULT_HEADERS, }); + // Handle errors await handleGitLabError(response); + + // Parse and return the data const data = await response.json(); - return z.array(GitLabRepositorySchema).parse(data); + return data as GitLabLabel[]; +} + +/** + * Get a single label from a project + * + * @param projectId The ID or URL-encoded path of the project + * @param labelId The ID or name of the label + * @param includeAncestorGroups Whether to include ancestor groups + * @returns GitLab label + */ +async function getLabel( + projectId: string, + labelId: number | string, + includeAncestorGroups?: boolean +): Promise { + const url = new URL(`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`); + + // Add query parameters + if (includeAncestorGroups !== undefined) { + url.searchParams.append("include_ancestor_groups", includeAncestorGroups ? "true" : "false"); + } + + // Make the API request + const response = await fetch(url.toString(), { + headers: DEFAULT_HEADERS, + }); + + // Handle errors + await handleGitLabError(response); + + // Parse and return the data + const data = await response.json(); + return data as GitLabLabel; +} + +/** + * Create a new label in a project + * + * @param projectId The ID or URL-encoded path of the project + * @param options Options for creating the label + * @returns Created GitLab label + */ +async function createLabel( + projectId: string, + options: Omit, "project_id"> +): Promise { + // Make the API request + const response = await fetch( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`, + { + method: "POST", + headers: DEFAULT_HEADERS, + body: JSON.stringify(options), + } + ); + + // Handle errors + await handleGitLabError(response); + + // Parse and return the data + const data = await response.json(); + return data as GitLabLabel; +} + +/** + * Update an existing label in a project + * + * @param projectId The ID or URL-encoded path of the project + * @param labelId The ID or name of the label to update + * @param options Options for updating the label + * @returns Updated GitLab label + */ +async function updateLabel( + projectId: string, + labelId: number | string, + options: Omit, "project_id" | "label_id"> +): Promise { + // Make the API request + const response = await fetch( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`, + { + method: "PUT", + headers: DEFAULT_HEADERS, + body: JSON.stringify(options), + } + ); + + // Handle errors + await handleGitLabError(response); + + // Parse and return the data + const data = await response.json(); + return data as GitLabLabel; +} + +/** + * Delete a label from a project + * + * @param projectId The ID or URL-encoded path of the project + * @param labelId The ID or name of the label to delete + */ +async function deleteLabel( + projectId: string, + labelId: number | string +): Promise { + // Make the API request + const response = await fetch( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels/${encodeURIComponent(String(labelId))}`, + { + method: "DELETE", + headers: DEFAULT_HEADERS, + } + ); + + // Handle errors + await handleGitLabError(response); } server.setRequestHandler(ListToolsRequestSchema, async () => { @@ -1358,6 +1530,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "List projects accessible by the current user", inputSchema: zodToJsonSchema(ListProjectsSchema), }, + { + name: "list_labels", + description: "List labels for a project", + inputSchema: zodToJsonSchema(ListLabelsSchema), + }, + { + name: "get_label", + description: "Get a single label from a project", + inputSchema: zodToJsonSchema(GetLabelSchema), + }, + { + name: "create_label", + description: "Create a new label in a project", + inputSchema: zodToJsonSchema(CreateLabelSchema), + }, + { + name: "update_label", + description: "Update an existing label in a project", + inputSchema: zodToJsonSchema(UpdateLabelSchema), + }, + { + name: "delete_label", + description: "Delete a label from a project", + inputSchema: zodToJsonSchema(DeleteLabelSchema), + }, ], }; }); @@ -1613,6 +1810,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { case "list_projects": { const args = ListProjectsSchema.parse(request.params.arguments); const projects = await listProjects(args); + return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }], }; @@ -1699,6 +1897,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }; } + case "list_labels": { + const args = ListLabelsSchema.parse(request.params.arguments); + const labels = await listLabels(args.project_id, args); + return { + content: [{ type: "text", text: JSON.stringify(labels, null, 2) }], + }; + } + + case "get_label": { + const args = GetLabelSchema.parse(request.params.arguments); + const label = await getLabel(args.project_id, args.label_id, args.include_ancestor_groups); + return { + content: [{ type: "text", text: JSON.stringify(label, null, 2) }], + }; + } + + case "create_label": { + const args = CreateLabelSchema.parse(request.params.arguments); + const label = await createLabel(args.project_id, args); + return { + content: [{ type: "text", text: JSON.stringify(label, null, 2) }], + }; + } + + case "update_label": { + const args = UpdateLabelSchema.parse(request.params.arguments); + const { project_id, label_id, ...options } = args; + const label = await updateLabel(project_id, label_id, options); + return { + content: [{ type: "text", text: JSON.stringify(label, null, 2) }], + }; + } + + case "delete_label": { + const args = DeleteLabelSchema.parse(request.params.arguments); + await deleteLabel(args.project_id, args.label_id); + return { + content: [{ type: "text", text: JSON.stringify({ status: "success", message: "Label deleted successfully" }, null, 2) }], + }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/package.json b/package.json index 14bd4f3..f98c4b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zereight/mcp-gitlab", - "version": "1.0.18", + "version": "1.0.19", "description": "MCP server for using the GitLab API", "license": "MIT", "author": "zereight", diff --git a/schemas.ts b/schemas.ts index a3fd19c..19a7904 100644 --- a/schemas.ts +++ b/schemas.ts @@ -228,7 +228,15 @@ export const GitLabLabelSchema = z.object({ id: z.number(), name: z.string(), color: z.string(), - description: z.string().optional(), + text_color: z.string(), + description: z.string().nullable(), + description_html: z.string().nullable(), + open_issues_count: z.number().optional(), + closed_issues_count: z.number().optional(), + open_merge_requests_count: z.number().optional(), + subscribed: z.boolean().optional(), + priority: z.number().nullable().optional(), + is_project_label: z.boolean().optional(), }); export const GitLabUserSchema = z.object({ @@ -619,6 +627,42 @@ export const ListProjectsSchema = z.object({ min_access_level: z.number().optional().describe("Filter by minimum access level"), }); +// Label operation schemas +export const ListLabelsSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + with_counts: z.boolean().optional().describe("Whether or not to include issue and merge request counts"), + include_ancestor_groups: z.boolean().optional().describe("Include ancestor groups"), + search: z.string().optional().describe("Keyword to filter labels by"), +}); + +export const GetLabelSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + label_id: z.union([z.number(), z.string()]).describe("The ID or title of a project's label"), + include_ancestor_groups: z.boolean().optional().describe("Include ancestor groups"), +}); + +export const CreateLabelSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + name: z.string().describe("The name of the label"), + color: z.string().describe("The color of the label given in 6-digit hex notation with leading '#' sign"), + description: z.string().optional().describe("The description of the label"), + priority: z.number().nullable().optional().describe("The priority of the label"), +}); + +export const UpdateLabelSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + label_id: z.union([z.number(), z.string()]).describe("The ID or title of a project's label"), + new_name: z.string().optional().describe("The new name of the label"), + color: z.string().optional().describe("The color of the label given in 6-digit hex notation with leading '#' sign"), + description: z.string().optional().describe("The new description of the label"), + priority: z.number().nullable().optional().describe("The new priority of the label"), +}); + +export const DeleteLabelSchema = z.object({ + project_id: z.string().describe("Project ID or URL-encoded path"), + label_id: z.union([z.number(), z.string()]).describe("The ID or title of a project's label"), +}); + // Export types export type GitLabAuthor = z.infer; export type GitLabFork = z.infer; @@ -645,3 +689,4 @@ export type GitLabIssueLink = z.infer; export type GitLabNamespace = z.infer; export type GitLabNamespaceExistsResponse = z.infer; export type GitLabProject = z.infer; +export type GitLabLabel = z.infer;