From 6f400686fc6dca0b68272a68525e3ea938649f66 Mon Sep 17 00:00:00 2001 From: Bob Merkus Date: Wed, 9 Apr 2025 15:37:38 +0200 Subject: [PATCH 1/2] feat: list_repository_tree tool --- src/gitlab/index.ts | 48 +++++++++++++++++++++++++++++++++++++++++++ src/gitlab/schemas.ts | 22 +++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/gitlab/index.ts b/src/gitlab/index.ts index 06ac9d8675..dc3aca1112 100644 --- a/src/gitlab/index.ts +++ b/src/gitlab/index.ts @@ -18,6 +18,7 @@ import { GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabSearchResponseSchema, + GitLabTreeItemSchema, GitLabTreeSchema, GitLabCommitSchema, CreateRepositoryOptionsSchema, @@ -27,6 +28,7 @@ import { CreateOrUpdateFileSchema, SearchRepositoriesSchema, CreateRepositorySchema, + GetRepositoryTreeSchema, GetFileContentsSchema, PushFilesSchema, CreateIssueSchema, @@ -43,6 +45,7 @@ import { type GitLabSearchResponse, type GitLabTree, type GitLabCommit, + type GitLabTreeItem, type FileOperation, } from './schemas.js'; @@ -111,6 +114,40 @@ async function createBranch( return GitLabReferenceSchema.parse(await response.json()); } +async function getRepositoryTree( + projectId: string, + options: z.infer +): Promise { + const queryParams = new URLSearchParams(); + if (options.path) queryParams.append('path', options.path); + if (options.ref) queryParams.append('ref', options.ref); + if (options.recursive) queryParams.append('recursive', 'true'); + if (options.per_page) queryParams.append('per_page', options.per_page.toString()); + if (options.page_token) queryParams.append('page_token', options.page_token); + if (options.pagination) queryParams.append('pagination', options.pagination); + + const response = await fetch( + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/tree?${queryParams.toString()}`, + { + headers: { + 'Authorization': `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`, + 'Content-Type': 'application/json', + }, + } + ); + + if (response.status === 404) { + throw new Error('Repository or path not found'); + } + + if (!response.ok) { + throw new Error(`Failed to get repository tree: ${response.statusText}`); + } + + const data = await response.json(); + return z.array(GitLabTreeItemSchema).parse(data); +} + async function getFileContents( projectId: string, filePath: string, @@ -377,6 +414,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { description: "Create a new GitLab project", inputSchema: zodToJsonSchema(CreateRepositorySchema) }, + { + name: "list_repository_tree", + description: "Get the tree of a GitLab project at a specific path", + inputSchema: zodToJsonSchema(GetRepositoryTreeSchema) + }, { name: "get_file_contents", description: "Get the contents of a file or directory from a GitLab project", @@ -451,6 +493,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { return { content: [{ type: "text", text: JSON.stringify(repository, null, 2) }] }; } + case "list_repository_tree": { + const args = GetRepositoryTreeSchema.parse(request.params.arguments); + const tree = await getRepositoryTree(args.project_id, args); + return { content: [{ type: "text", text: JSON.stringify(tree, null, 2) }] }; + } + case "get_file_contents": { const args = GetFileContentsSchema.parse(request.params.arguments); const contents = await getFileContents(args.project_id, args.file_path, args.ref); diff --git a/src/gitlab/schemas.ts b/src/gitlab/schemas.ts index af93380dd0..6493a6275c 100644 --- a/src/gitlab/schemas.ts +++ b/src/gitlab/schemas.ts @@ -80,6 +80,25 @@ export const GitLabTreeSchema = z.object({ tree: z.array(GitLabTreeEntrySchema) }); +// Repository tree schemas +export const GitLabTreeItemSchema = z.object({ + id: z.string(), + name: z.string(), + type: z.enum(['tree', 'blob']), + path: z.string(), + mode: z.string() +}); + +export const GetRepositoryTreeSchema = z.object({ + project_id: z.string().describe("The ID or URL-encoded path of the project"), + path: z.string().optional().describe("The path inside the repository"), + ref: z.string().optional().describe("The name of a repository branch or tag. Defaults to the default branch."), + recursive: z.boolean().optional().describe("Boolean value to get a recursive tree"), + per_page: z.number().optional().describe("Number of results to show per page"), + page_token: z.string().optional().describe("The tree record ID for pagination"), + pagination: z.string().optional().describe("Pagination method (keyset)") +}); + export const GitLabCommitSchema = z.object({ id: z.string(), // Changed from sha to match GitLab API short_id: z.string(), // Added to match GitLab API @@ -322,4 +341,5 @@ 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 GitLabTreeItem = z.infer; \ No newline at end of file From 73a66b3c50acf9c92db7b7dc73badf2bb9754fd1 Mon Sep 17 00:00:00 2001 From: Bob Merkus Date: Wed, 9 Apr 2025 15:37:49 +0200 Subject: [PATCH 2/2] docs: add list_repository_tree --- src/gitlab/README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/gitlab/README.md b/src/gitlab/README.md index 2687f6ed37..5b33980664 100644 --- a/src/gitlab/README.md +++ b/src/gitlab/README.md @@ -38,7 +38,7 @@ MCP Server for the GitLab API, enabling project management, file operations, and - `search` (string): Search query - `page` (optional number): Page number for pagination - `per_page` (optional number): Results per page (default 20) - - Returns: Project search results + - Returns: Project search results with total count 4. `create_repository` - Create a new GitLab project @@ -49,15 +49,27 @@ MCP Server for the GitLab API, enabling project management, file operations, and - `initialize_with_readme` (optional boolean): Initialize with README - Returns: Created project details -5. `get_file_contents` +5. `list_repository_tree` + - List files and directories in a repository + - Inputs: + - `project_id` (string): Project ID or URL-encoded path + - `path` (optional string): Path to list contents from + - `ref` (optional string): Branch/tag/commit to list from + - `recursive` (optional boolean): List recursively + - `per_page` (optional number): Results per page + - `page_token` (optional string): Token for pagination + - `pagination` (optional string): Pagination type + - Returns: List of files and directories with their metadata + +6. `get_file_contents` - Get contents of a file or directory - Inputs: - `project_id` (string): Project ID or URL-encoded path - `file_path` (string): Path to file/directory - `ref` (optional string): Branch/tag/commit to get contents from - - Returns: File/directory contents + - Returns: File/directory contents with metadata -6. `create_issue` +7. `create_issue` - Create a new issue - Inputs: - `project_id` (string): Project ID or URL-encoded path @@ -68,7 +80,7 @@ MCP Server for the GitLab API, enabling project management, file operations, and - `milestone_id` (optional number): Milestone ID - Returns: Created issue details -7. `create_merge_request` +8. `create_merge_request` - Create a new merge request - Inputs: - `project_id` (string): Project ID or URL-encoded path @@ -80,14 +92,14 @@ MCP Server for the GitLab API, enabling project management, file operations, and - `allow_collaboration` (optional boolean): Allow commits from upstream members - Returns: Created merge request details -8. `fork_repository` +9. `fork_repository` - Fork a project - Inputs: - `project_id` (string): Project ID or URL-encoded path - `namespace` (optional string): Namespace to fork to - Returns: Forked project details -9. `create_branch` +10. `create_branch` - Create a new branch - Inputs: - `project_id` (string): Project ID or URL-encoded path