diff --git a/src/gitlab/README.md b/src/gitlab/README.md index 2687f6ed3..35c4a14ee 100644 --- a/src/gitlab/README.md +++ b/src/gitlab/README.md @@ -95,6 +95,15 @@ MCP Server for the GitLab API, enabling project management, file operations, and - `ref` (optional string): Source branch/commit for new branch - Returns: Created branch reference +10. `list_members` + - List all members of a project + - Inputs: + - `project_id` (string): Project ID or URL-encoded path + - `include_inheritance` (optional boolean): Include inherited members + - `page` (optional number): Page number for pagination (default: 1) + - `per_page` (optional number): Results per page (default: 20) + - Returns: List of members with their access levels and details + ## Setup ### Personal Access Token diff --git a/src/gitlab/index.ts b/src/gitlab/index.ts index 06ac9d867..3783b9c32 100644 --- a/src/gitlab/index.ts +++ b/src/gitlab/index.ts @@ -20,6 +20,7 @@ import { GitLabSearchResponseSchema, GitLabTreeSchema, GitLabCommitSchema, + GitLabMembersResponseSchema, CreateRepositoryOptionsSchema, CreateIssueOptionsSchema, CreateMergeRequestOptionsSchema, @@ -33,6 +34,7 @@ import { CreateMergeRequestSchema, ForkRepositorySchema, CreateBranchSchema, + ListMembersSchema, type GitLabFork, type GitLabReference, type GitLabRepository, @@ -44,6 +46,7 @@ import { type GitLabTree, type GitLabCommit, type FileOperation, + type GitLabMembersResponse, } from './schemas.js'; const server = new Server({ @@ -359,6 +362,35 @@ async function createRepository( return GitLabRepositorySchema.parse(await response.json()); } +async function listMembers( + projectId: string, + includeInheritance: boolean = false, + page: number = 1, + perPage: number = 20 +): Promise { + // Build URL for project members, with or without inheritance + const url = includeInheritance + ? `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/members/all` + : `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/members`; + + const urlObj = new URL(url); + urlObj.searchParams.append("page", page.toString()); + urlObj.searchParams.append("per_page", perPage.toString()); + + const response = await fetch(urlObj.toString(), { + headers: { + "Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}` + } + }); + + if (!response.ok) { + throw new Error(`GitLab API error: ${response.statusText} (${response.status})`); + } + + const members = await response.json(); + return GitLabMembersResponseSchema.parse(members); +} + server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ @@ -406,6 +438,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { name: "create_branch", description: "Create a new branch in a GitLab project", inputSchema: zodToJsonSchema(CreateBranchSchema) + }, + { + name: "list_members", + description: "List members of a GitLab project", + inputSchema: zodToJsonSchema(ListMembersSchema) } ] }; @@ -495,6 +532,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }] }; } + case "list_members": { + const args = ListMembersSchema.parse(request.params.arguments); + const members = await listMembers( + args.project_id, + args.include_inheritance, + args.page, + args.per_page + ); + return { content: [{ type: "text", text: JSON.stringify(members, null, 2) }] }; + } + default: throw new Error(`Unknown tool: ${request.params.name}`); } diff --git a/src/gitlab/schemas.ts b/src/gitlab/schemas.ts index af93380dd..86f7dacc6 100644 --- a/src/gitlab/schemas.ts +++ b/src/gitlab/schemas.ts @@ -7,6 +7,26 @@ export const GitLabAuthorSchema = z.object({ date: z.string() }); +// Member schema +export const GitLabMemberSchema = z.object({ + id: z.number(), + username: z.string(), + name: z.string(), + state: z.string(), + avatar_url: z.string(), + web_url: z.string(), + access_level: z.number(), + email: z.string().optional(), + group_saml_identity: z.object({ + extern_uid: z.string(), + provider: z.string(), + saml_provider_id: z.number() + }).nullable().optional(), + override: z.boolean().optional() +}); + +export const GitLabMembersResponseSchema = z.array(GitLabMemberSchema); + // Repository related schemas export const GitLabOwnerSchema = z.object({ username: z.string(), // Changed from login to match GitLab API @@ -304,6 +324,13 @@ export const CreateBranchSchema = ProjectParamsSchema.extend({ .describe("Source branch/commit for new branch") }); +export const ListMembersSchema = ProjectParamsSchema.extend({ + include_inheritance: z.boolean().optional() + .describe("Include members inherited from parent groups (use 'all' endpoint)"), + page: z.number().optional().describe("Page number for pagination (default: 1)"), + per_page: z.number().optional().describe("Number of results per page (default: 20)") +}); + // Export types export type GitLabAuthor = z.infer; export type GitLabFork = z.infer; @@ -322,4 +349,6 @@ export type CreateIssueOptions = z.infer; export type CreateMergeRequestOptions = z.infer; export type CreateBranchOptions = z.infer; export type GitLabCreateUpdateFileResponse = z.infer; -export type GitLabSearchResponse = z.infer; \ No newline at end of file +export type GitLabSearchResponse = z.infer; +export type GitLabMember = z.infer; +export type GitLabMembersResponse = z.infer; \ No newline at end of file