초기 설정 파일 추가: .gitignore, tsconfig.json, package.json 및 README.md 생성

Generated by Copilot
This commit is contained in:
simple
2025-02-11 10:36:57 +09:00
commit d686f4e2d7
9 changed files with 2861 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
.DS_Store

54
README.md Normal file
View File

@ -0,0 +1,54 @@
# @zereight/mcp-gitlab
GitLab MCP(Model Context Protocol) Server.
## Installation and Execution
```bash
npx @zereight/mcp-gitlab
```
## Environment Variable Configuration
Before running the server, you need to set the following environment variables:
```bash
GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token
GITLAB_API_URL=your_gitlab_api_url # Default: https://gitlab.com/api/v4
```
## License
MIT License
## How to use
## Using with Claude App
When using with the Claude App, you need to set up your API key and URLs directly.
```json
{
"mcpServers": {
"GitLab communication server": {
"command": "npx @zereight/mcp-gitlab",
"args": [],
"env": {
"GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token",
"GITLAB_API_URL": "your_gitlab_api_url"
}
}
}
}
```
## Using with Cursor
When using with Cursor, you can set up environment variables and run the server as follows:
```bash
env GITLAB_PERSONAL_ACCESS_TOKEN=your_gitlab_token GITLAB_API_URL=your_gitlab_api_url npx @zereight/mcp-gitlab
```
- `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token.
- `GITLAB_API_URL`: Your GitLab API URL. (Default: `https://gitlab.com/api/v4`)

539
build/index.js Executable file
View File

@ -0,0 +1,539 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import fetch from "node-fetch";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { GitLabForkSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabIssueSchema, GitLabMergeRequestSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabSearchResponseSchema, GitLabTreeSchema, GitLabCommitSchema, CreateOrUpdateFileSchema, SearchRepositoriesSchema, CreateRepositorySchema, GetFileContentsSchema, PushFilesSchema, CreateIssueSchema, CreateMergeRequestSchema, ForkRepositorySchema, CreateBranchSchema, GitLabMergeRequestDiffSchema, GetMergeRequestSchema, GetMergeRequestDiffsSchema, UpdateMergeRequestSchema, } from "./schemas.js";
const server = new Server({
name: "gitlab-mcp-server",
version: "0.0.1",
}, {
capabilities: {
tools: {},
},
});
const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
const GITLAB_API_URL = process.env.GITLAB_API_URL || "https://gitlab.com/api/v4";
if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
process.exit(1);
}
// GitLab API 공통 헤더
const DEFAULT_HEADERS = {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
};
// API 에러 처리를 위한 유틸리티 함수
async function handleGitLabError(response) {
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
}
}
// 프로젝트 포크 생성
async function forkProject(projectId, namespace) {
// API 엔드포인트 URL 생성
const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/fork`);
if (namespace) {
url.searchParams.append("namespace", namespace);
}
const response = await fetch(url.toString(), {
method: "POST",
headers: DEFAULT_HEADERS,
});
// 이미 존재하는 프로젝트인 경우 처리
if (response.status === 409) {
throw new Error("Project already exists in the target namespace");
}
await handleGitLabError(response);
const data = await response.json();
return GitLabForkSchema.parse(data);
}
// 새로운 브랜치 생성
async function createBranch(projectId, options) {
const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/branches`);
const response = await fetch(url.toString(), {
method: "POST",
headers: DEFAULT_HEADERS,
body: JSON.stringify({
branch: options.name,
ref: options.ref,
}),
});
await handleGitLabError(response);
return GitLabReferenceSchema.parse(await response.json());
}
// 프로젝트의 기본 브랜치 조회
async function getDefaultBranchRef(projectId) {
const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}`);
const response = await fetch(url.toString(), {
headers: DEFAULT_HEADERS,
});
await handleGitLabError(response);
const project = GitLabRepositorySchema.parse(await response.json());
return project.default_branch ?? "main";
}
// 파일 내용 조회
async function getFileContents(projectId, filePath, ref) {
const encodedPath = encodeURIComponent(filePath);
// ref가 없는 경우 default branch를 가져옴
if (!ref) {
ref = await getDefaultBranchRef(projectId);
}
const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
url.searchParams.append("ref", ref);
const response = await fetch(url.toString(), {
headers: DEFAULT_HEADERS,
});
// 파일을 찾을 수 없는 경우 처리
if (response.status === 404) {
throw new Error(`File not found: ${filePath}`);
}
await handleGitLabError(response);
const data = await response.json();
const parsedData = GitLabContentSchema.parse(data);
// Base64로 인코딩된 파일 내용을 UTF-8로 디코딩
if (!Array.isArray(parsedData) && parsedData.content) {
parsedData.content = Buffer.from(parsedData.content, "base64").toString("utf8");
parsedData.encoding = "utf8";
}
return parsedData;
}
// 이슈 생성
async function createIssue(projectId, options) {
const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/issues`);
const response = await fetch(url.toString(), {
method: "POST",
headers: DEFAULT_HEADERS,
body: JSON.stringify({
title: options.title,
description: options.description,
assignee_ids: options.assignee_ids,
milestone_id: options.milestone_id,
labels: options.labels?.join(","),
}),
});
// 잘못된 요청 처리
if (response.status === 400) {
const errorBody = await response.text();
throw new Error(`Invalid request: ${errorBody}`);
}
await handleGitLabError(response);
const data = await response.json();
return GitLabIssueSchema.parse(data);
}
async function createMergeRequest(projectId, options) {
const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests`);
const response = await fetch(url.toString(), {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
},
body: JSON.stringify({
title: options.title,
description: options.description,
source_branch: options.source_branch,
target_branch: options.target_branch,
allow_collaboration: options.allow_collaboration,
draft: options.draft,
}),
});
if (response.status === 400) {
const errorBody = await response.text();
throw new Error(`Invalid request: ${errorBody}`);
}
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
}
const data = await response.json();
return GitLabMergeRequestSchema.parse(data);
}
async function createOrUpdateFile(projectId, filePath, content, commitMessage, branch, previousPath) {
const encodedPath = encodeURIComponent(filePath);
const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`);
const body = {
branch,
content,
commit_message: commitMessage,
encoding: "text",
...(previousPath ? { previous_path: previousPath } : {}),
};
// Check if file exists
let method = "POST";
try {
await getFileContents(projectId, filePath, branch);
method = "PUT";
}
catch (error) {
if (!(error instanceof Error && error.message.includes("File not found"))) {
throw error;
}
// File doesn't exist, use POST
}
const response = await fetch(url.toString(), {
method,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
},
body: JSON.stringify(body),
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
}
const data = await response.json();
return GitLabCreateUpdateFileResponseSchema.parse(data);
}
async function createTree(projectId, files, ref) {
const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/tree`);
if (ref) {
url.searchParams.append("ref", ref);
}
const response = await fetch(url.toString(), {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
},
body: JSON.stringify({
files: files.map((file) => ({
file_path: file.path,
content: file.content,
encoding: "text",
})),
}),
});
if (response.status === 400) {
const errorBody = await response.text();
throw new Error(`Invalid request: ${errorBody}`);
}
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
}
const data = await response.json();
return GitLabTreeSchema.parse(data);
}
async function createCommit(projectId, message, branch, actions) {
const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/repository/commits`);
const response = await fetch(url.toString(), {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
},
body: JSON.stringify({
branch,
commit_message: message,
actions: actions.map((action) => ({
action: "create",
file_path: action.path,
content: action.content,
encoding: "text",
})),
}),
});
if (response.status === 400) {
const errorBody = await response.text();
throw new Error(`Invalid request: ${errorBody}`);
}
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
}
const data = await response.json();
return GitLabCommitSchema.parse(data);
}
async function searchProjects(query, page = 1, perPage = 20) {
const url = new URL(`${GITLAB_API_URL}/api/v4/projects`);
url.searchParams.append("search", query);
url.searchParams.append("page", page.toString());
url.searchParams.append("per_page", perPage.toString());
url.searchParams.append("order_by", "id");
url.searchParams.append("sort", "desc");
const response = await fetch(url.toString(), {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
},
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
}
const projects = (await response.json());
const totalCount = response.headers.get("x-total");
const totalPages = response.headers.get("x-total-pages");
// GitLab API doesn't return these headers for results > 10,000
const count = totalCount ? parseInt(totalCount) : projects.length;
return GitLabSearchResponseSchema.parse({
count,
total_pages: totalPages ? parseInt(totalPages) : Math.ceil(count / perPage),
current_page: page,
items: projects,
});
}
async function createRepository(options) {
const response = await fetch(`${GITLAB_API_URL}/api/v4/projects`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
},
body: JSON.stringify({
name: options.name,
description: options.description,
visibility: options.visibility,
initialize_with_readme: options.initialize_with_readme,
default_branch: "main",
path: options.name.toLowerCase().replace(/\s+/g, "-"),
}),
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
}
const data = await response.json();
return GitLabRepositorySchema.parse(data);
}
// MR 조회 함수
async function getMergeRequest(projectId, mergeRequestIid) {
const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
const response = await fetch(url.toString(), {
headers: DEFAULT_HEADERS,
});
await handleGitLabError(response);
return GitLabMergeRequestSchema.parse(await response.json());
}
// MR 변경사항 조회 함수
async function getMergeRequestDiffs(projectId, mergeRequestIid, view) {
const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/changes`);
if (view) {
url.searchParams.append("view", view);
}
const response = await fetch(url.toString(), {
headers: DEFAULT_HEADERS,
});
await handleGitLabError(response);
const data = (await response.json());
return z.array(GitLabMergeRequestDiffSchema).parse(data.changes);
}
// MR 업데이트 함수
async function updateMergeRequest(projectId, mergeRequestIid, options) {
const url = new URL(`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}`);
const response = await fetch(url.toString(), {
method: "PUT",
headers: DEFAULT_HEADERS,
body: JSON.stringify(options),
});
await handleGitLabError(response);
return GitLabMergeRequestSchema.parse(await response.json());
}
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "create_or_update_file",
description: "Create or update a single file in a GitLab project",
inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema),
},
{
name: "search_repositories",
description: "Search for GitLab projects",
inputSchema: zodToJsonSchema(SearchRepositoriesSchema),
},
{
name: "create_repository",
description: "Create a new GitLab project",
inputSchema: zodToJsonSchema(CreateRepositorySchema),
},
{
name: "get_file_contents",
description: "Get the contents of a file or directory from a GitLab project",
inputSchema: zodToJsonSchema(GetFileContentsSchema),
},
{
name: "push_files",
description: "Push multiple files to a GitLab project in a single commit",
inputSchema: zodToJsonSchema(PushFilesSchema),
},
{
name: "create_issue",
description: "Create a new issue in a GitLab project",
inputSchema: zodToJsonSchema(CreateIssueSchema),
},
{
name: "create_merge_request",
description: "Create a new merge request in a GitLab project",
inputSchema: zodToJsonSchema(CreateMergeRequestSchema),
},
{
name: "fork_repository",
description: "Fork a GitLab project to your account or specified namespace",
inputSchema: zodToJsonSchema(ForkRepositorySchema),
},
{
name: "create_branch",
description: "Create a new branch in a GitLab project",
inputSchema: zodToJsonSchema(CreateBranchSchema),
},
{
name: "get_merge_request",
description: "Get details of a merge request",
inputSchema: zodToJsonSchema(GetMergeRequestSchema),
},
{
name: "get_merge_request_diffs",
description: "Get the changes/diffs of a merge request",
inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema),
},
{
name: "update_merge_request",
description: "Update a merge request",
inputSchema: zodToJsonSchema(UpdateMergeRequestSchema),
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
if (!request.params.arguments) {
throw new Error("Arguments are required");
}
switch (request.params.name) {
case "fork_repository": {
const args = ForkRepositorySchema.parse(request.params.arguments);
const fork = await forkProject(args.project_id, args.namespace);
return {
content: [{ type: "text", text: JSON.stringify(fork, null, 2) }],
};
}
case "create_branch": {
const args = CreateBranchSchema.parse(request.params.arguments);
let ref = args.ref;
if (!ref) {
ref = await getDefaultBranchRef(args.project_id);
}
const branch = await createBranch(args.project_id, {
name: args.branch,
ref,
});
return {
content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
};
}
case "search_repositories": {
const args = SearchRepositoriesSchema.parse(request.params.arguments);
const results = await searchProjects(args.search, args.page, args.per_page);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
case "create_repository": {
const args = CreateRepositorySchema.parse(request.params.arguments);
const repository = await createRepository(args);
return {
content: [
{ type: "text", text: JSON.stringify(repository, 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);
return {
content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
};
}
case "create_or_update_file": {
const args = CreateOrUpdateFileSchema.parse(request.params.arguments);
const result = await createOrUpdateFile(args.project_id, args.file_path, args.content, args.commit_message, args.branch, args.previous_path);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "push_files": {
const args = PushFilesSchema.parse(request.params.arguments);
const result = await createCommit(args.project_id, args.commit_message, args.branch, args.files.map((f) => ({ path: f.file_path, content: f.content })));
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "create_issue": {
const args = CreateIssueSchema.parse(request.params.arguments);
const { project_id, ...options } = args;
const issue = await createIssue(project_id, options);
return {
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
};
}
case "create_merge_request": {
const args = CreateMergeRequestSchema.parse(request.params.arguments);
const { project_id, ...options } = args;
const mergeRequest = await createMergeRequest(project_id, options);
return {
content: [
{ type: "text", text: JSON.stringify(mergeRequest, null, 2) },
],
};
}
case "get_merge_request": {
const args = GetMergeRequestSchema.parse(request.params.arguments);
const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid);
return {
content: [
{ type: "text", text: JSON.stringify(mergeRequest, null, 2) },
],
};
}
case "get_merge_request_diffs": {
const args = GetMergeRequestDiffsSchema.parse(request.params.arguments);
const diffs = await getMergeRequestDiffs(args.project_id, args.merge_request_iid, args.view);
return {
content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }],
};
}
case "update_merge_request": {
const args = UpdateMergeRequestSchema.parse(request.params.arguments);
const { project_id, merge_request_iid, ...options } = args;
const mergeRequest = await updateMergeRequest(project_id, merge_request_iid, options);
return {
content: [
{ type: "text", text: JSON.stringify(mergeRequest, null, 2) },
],
};
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
}
catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid arguments: ${error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(", ")}`);
}
throw error;
}
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("GitLab MCP Server running on stdio");
}
runServer().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});

348
build/schemas.js Normal file
View File

@ -0,0 +1,348 @@
import { z } from "zod";
// Base schemas for common types
export const GitLabAuthorSchema = z.object({
name: z.string(),
email: z.string(),
date: z.string(),
});
// Repository related schemas
export const GitLabOwnerSchema = z.object({
username: z.string(), // Changed from login to match GitLab API
id: z.number(),
avatar_url: z.string(),
web_url: z.string(), // Changed from html_url to match GitLab API
name: z.string(), // Added as GitLab includes full name
state: z.string(), // Added as GitLab includes user state
});
export const GitLabRepositorySchema = z.object({
id: z.number(),
name: z.string(),
path_with_namespace: z.string(),
visibility: z.string().optional(),
owner: GitLabOwnerSchema.optional(),
web_url: z.string().optional(),
description: z.string().nullable(),
fork: z.boolean().optional(),
ssh_url_to_repo: z.string().optional(),
http_url_to_repo: z.string().optional(),
created_at: z.string().optional(),
last_activity_at: z.string().optional(),
default_branch: z.string().optional(),
});
// File content schemas
export const GitLabFileContentSchema = z.object({
file_name: z.string(), // Changed from name to match GitLab API
file_path: z.string(), // Changed from path to match GitLab API
size: z.number(),
encoding: z.string(),
content: z.string(),
content_sha256: z.string(), // Changed from sha to match GitLab API
ref: z.string(), // Added as GitLab requires branch reference
blob_id: z.string(), // Added to match GitLab API
last_commit_id: z.string(), // Added to match GitLab API
});
export const GitLabDirectoryContentSchema = z.object({
name: z.string(),
path: z.string(),
type: z.string(),
mode: z.string(),
id: z.string(), // Changed from sha to match GitLab API
web_url: z.string(), // Changed from html_url to match GitLab API
});
export const GitLabContentSchema = z.union([
GitLabFileContentSchema,
z.array(GitLabDirectoryContentSchema),
]);
// Operation schemas
export const FileOperationSchema = z.object({
path: z.string(),
content: z.string(),
});
// Tree and commit schemas
export const GitLabTreeEntrySchema = z.object({
id: z.string(), // Changed from sha to match GitLab API
name: z.string(),
type: z.enum(["blob", "tree"]),
path: z.string(),
mode: z.string(),
});
export const GitLabTreeSchema = z.object({
id: z.string(), // Changed from sha to match GitLab API
tree: z.array(GitLabTreeEntrySchema),
});
export const GitLabCommitSchema = z.object({
id: z.string(), // Changed from sha to match GitLab API
short_id: z.string(), // Added to match GitLab API
title: z.string(), // Changed from message to match GitLab API
author_name: z.string(),
author_email: z.string(),
authored_date: z.string(),
committer_name: z.string(),
committer_email: z.string(),
committed_date: z.string(),
web_url: z.string(), // Changed from html_url to match GitLab API
parent_ids: z.array(z.string()), // Changed from parents to match GitLab API
});
// Reference schema
export const GitLabReferenceSchema = z.object({
name: z.string(), // Changed from ref to match GitLab API
commit: z.object({
id: z.string(), // Changed from sha to match GitLab API
web_url: z.string(), // Changed from url to match GitLab API
}),
});
// Input schemas for operations
export const CreateRepositoryOptionsSchema = z.object({
name: z.string(),
description: z.string().optional(),
visibility: z.enum(["private", "internal", "public"]).optional(), // Changed from private to match GitLab API
initialize_with_readme: z.boolean().optional(), // Changed from auto_init to match GitLab API
});
export const CreateIssueOptionsSchema = z.object({
title: z.string(),
description: z.string().optional(), // Changed from body to match GitLab API
assignee_ids: z.array(z.number()).optional(), // Changed from assignees to match GitLab API
milestone_id: z.number().optional(), // Changed from milestone to match GitLab API
labels: z.array(z.string()).optional(),
});
export const CreateMergeRequestOptionsSchema = z.object({
// Changed from CreatePullRequestOptionsSchema
title: z.string(),
description: z.string().optional(), // Changed from body to match GitLab API
source_branch: z.string(), // Changed from head to match GitLab API
target_branch: z.string(), // Changed from base to match GitLab API
allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API
draft: z.boolean().optional(),
});
export const CreateBranchOptionsSchema = z.object({
name: z.string(), // Changed from ref to match GitLab API
ref: z.string(), // The source branch/commit for the new branch
});
// Response schemas for operations
export const GitLabCreateUpdateFileResponseSchema = z.object({
file_path: z.string(),
branch: z.string(),
commit_id: z.string(), // Changed from sha to match GitLab API
content: GitLabFileContentSchema.optional(),
});
export const GitLabSearchResponseSchema = z.object({
count: z.number().optional(),
total_pages: z.number().optional(),
current_page: z.number().optional(),
items: z.array(GitLabRepositorySchema),
});
// Fork related schemas
export const GitLabForkParentSchema = z.object({
name: z.string(),
path_with_namespace: z.string(), // Changed from full_name to match GitLab API
owner: z.object({
username: z.string(), // Changed from login to match GitLab API
id: z.number(),
avatar_url: z.string(),
}),
web_url: z.string(), // Changed from html_url to match GitLab API
});
export const GitLabForkSchema = GitLabRepositorySchema.extend({
forked_from_project: GitLabForkParentSchema, // Changed from parent to match GitLab API
});
// Issue related schemas
export const GitLabLabelSchema = z.object({
id: z.number(),
name: z.string(),
color: z.string(),
description: z.string().optional(),
});
export const GitLabUserSchema = z.object({
username: z.string(), // Changed from login to match GitLab API
id: z.number(),
name: z.string(),
avatar_url: z.string(),
web_url: z.string(), // Changed from html_url to match GitLab API
});
export const GitLabMilestoneSchema = z.object({
id: z.number(),
iid: z.number(), // Added to match GitLab API
title: z.string(),
description: z.string(),
state: z.string(),
web_url: z.string(), // Changed from html_url to match GitLab API
});
export const GitLabIssueSchema = z.object({
id: z.number(),
iid: z.number(), // Added to match GitLab API
project_id: z.number(), // Added to match GitLab API
title: z.string(),
description: z.string(), // Changed from body to match GitLab API
state: z.string(),
author: GitLabUserSchema,
assignees: z.array(GitLabUserSchema),
labels: z.array(GitLabLabelSchema),
milestone: GitLabMilestoneSchema.nullable(),
created_at: z.string(),
updated_at: z.string(),
closed_at: z.string().nullable(),
web_url: z.string(), // Changed from html_url to match GitLab API
});
// Merge Request related schemas (equivalent to Pull Request)
export const GitLabMergeRequestDiffRefSchema = z.object({
base_sha: z.string(),
head_sha: z.string(),
start_sha: z.string(),
});
export const GitLabMergeRequestSchema = z.object({
id: z.number(),
iid: z.number(),
project_id: z.number(),
title: z.string(),
description: z.string().nullable(),
state: z.string(),
merged: z.boolean().optional(),
draft: z.boolean().optional(),
author: GitLabUserSchema,
assignees: z.array(GitLabUserSchema).optional(),
source_branch: z.string(),
target_branch: z.string(),
diff_refs: GitLabMergeRequestDiffRefSchema.optional(),
web_url: z.string(),
created_at: z.string(),
updated_at: z.string(),
merged_at: z.string().nullable(),
closed_at: z.string().nullable(),
merge_commit_sha: z.string().nullable(),
detailed_merge_status: z.string().optional(),
merge_status: z.string().optional(),
merge_error: z.string().nullable().optional(),
work_in_progress: z.boolean().optional(),
blocking_discussions_resolved: z.boolean().optional(),
should_remove_source_branch: z.boolean().nullable().optional(),
force_remove_source_branch: z.boolean().optional(),
allow_collaboration: z.boolean().optional(),
allow_maintainer_to_push: z.boolean().optional(),
changes_count: z.string().optional(),
merge_when_pipeline_succeeds: z.boolean().optional(),
squash: z.boolean().optional(),
labels: z.array(z.string()).optional(),
});
// API Operation Parameter Schemas
const ProjectParamsSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), // Changed from owner/repo to match GitLab API
});
export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
file_path: z.string().describe("Path where to create/update the file"),
content: z.string().describe("Content of the file"),
commit_message: z.string().describe("Commit message"),
branch: z.string().describe("Branch to create/update the file in"),
previous_path: z
.string()
.optional()
.describe("Path of the file to move/rename"),
});
export const SearchRepositoriesSchema = z.object({
search: z.string().describe("Search query"), // Changed from query to match GitLab API
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 const CreateRepositorySchema = z.object({
name: z.string().describe("Repository name"),
description: z.string().optional().describe("Repository description"),
visibility: z
.enum(["private", "internal", "public"])
.optional()
.describe("Repository visibility level"),
initialize_with_readme: z
.boolean()
.optional()
.describe("Initialize with README.md"),
});
export const GetFileContentsSchema = ProjectParamsSchema.extend({
file_path: z.string().describe("Path to the file or directory"),
ref: z.string().optional().describe("Branch/tag/commit to get contents from"),
});
export const PushFilesSchema = ProjectParamsSchema.extend({
branch: z.string().describe("Branch to push to"),
files: z
.array(z.object({
file_path: z.string().describe("Path where to create the file"),
content: z.string().describe("Content of the file"),
}))
.describe("Array of files to push"),
commit_message: z.string().describe("Commit message"),
});
export const CreateIssueSchema = ProjectParamsSchema.extend({
title: z.string().describe("Issue title"),
description: z.string().optional().describe("Issue description"),
assignee_ids: z
.array(z.number())
.optional()
.describe("Array of user IDs to assign"),
labels: z.array(z.string()).optional().describe("Array of label names"),
milestone_id: z.number().optional().describe("Milestone ID to assign"),
});
export const CreateMergeRequestSchema = ProjectParamsSchema.extend({
title: z.string().describe("Merge request title"),
description: z.string().optional().describe("Merge request description"),
source_branch: z.string().describe("Branch containing changes"),
target_branch: z.string().describe("Branch to merge into"),
draft: z.boolean().optional().describe("Create as draft merge request"),
allow_collaboration: z
.boolean()
.optional()
.describe("Allow commits from upstream members"),
});
export const ForkRepositorySchema = ProjectParamsSchema.extend({
namespace: z.string().optional().describe("Namespace to fork to (full path)"),
});
export const CreateBranchSchema = ProjectParamsSchema.extend({
branch: z.string().describe("Name for the new branch"),
ref: z.string().optional().describe("Source branch/commit for new branch"),
});
export const GitLabMergeRequestDiffSchema = z.object({
old_path: z.string(),
new_path: z.string(),
a_mode: z.string(),
b_mode: z.string(),
diff: z.string(),
new_file: z.boolean(),
renamed_file: z.boolean(),
deleted_file: z.boolean(),
});
export const GetMergeRequestSchema = ProjectParamsSchema.extend({
merge_request_iid: z
.number()
.describe("The internal ID of the merge request"),
});
export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
title: z.string().optional().describe("The title of the merge request"),
description: z
.string()
.optional()
.describe("The description of the merge request"),
target_branch: z.string().optional().describe("The target branch"),
assignee_ids: z
.array(z.number())
.optional()
.describe("The ID of the users to assign the MR to"),
labels: z.array(z.string()).optional().describe("Labels for the MR"),
state_event: z
.enum(["close", "reopen"])
.optional()
.describe("New state (close/reopen) for the MR"),
remove_source_branch: z
.boolean()
.optional()
.describe("Flag indicating if the source branch should be removed"),
squash: z
.boolean()
.optional()
.describe("Squash commits into a single commit when merging"),
draft: z.boolean().optional().describe("Work in progress merge request"),
});
export const GetMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"),
});

824
index.ts Normal file
View File

@ -0,0 +1,824 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fetch from "node-fetch";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { fileURLToPath } from "url";
import { dirname, resolve } from "path";
import {
GitLabForkSchema,
GitLabReferenceSchema,
GitLabRepositorySchema,
GitLabIssueSchema,
GitLabMergeRequestSchema,
GitLabContentSchema,
GitLabCreateUpdateFileResponseSchema,
GitLabSearchResponseSchema,
GitLabTreeSchema,
GitLabCommitSchema,
CreateRepositoryOptionsSchema,
CreateIssueOptionsSchema,
CreateMergeRequestOptionsSchema,
CreateBranchOptionsSchema,
CreateOrUpdateFileSchema,
SearchRepositoriesSchema,
CreateRepositorySchema,
GetFileContentsSchema,
PushFilesSchema,
CreateIssueSchema,
CreateMergeRequestSchema,
ForkRepositorySchema,
CreateBranchSchema,
GitLabMergeRequestDiffSchema,
GetMergeRequestSchema,
GetMergeRequestDiffsSchema,
UpdateMergeRequestSchema,
type GitLabFork,
type GitLabReference,
type GitLabRepository,
type GitLabIssue,
type GitLabMergeRequest,
type GitLabContent,
type GitLabCreateUpdateFileResponse,
type GitLabSearchResponse,
type GitLabTree,
type GitLabCommit,
type FileOperation,
type GitLabMergeRequestDiff,
} from "./schemas.js";
const server = new Server(
{
name: "gitlab-mcp-server",
version: "0.0.1",
},
{
capabilities: {
tools: {},
},
}
);
const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
const GITLAB_API_URL =
process.env.GITLAB_API_URL || "https://gitlab.com/api/v4";
if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
process.exit(1);
}
// GitLab API 공통 헤더
const DEFAULT_HEADERS = {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
};
// API 에러 처리를 위한 유틸리티 함수
async function handleGitLabError(
response: import("node-fetch").Response
): Promise<void> {
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
);
}
}
// 프로젝트 포크 생성
async function forkProject(
projectId: string,
namespace?: string
): Promise<GitLabFork> {
// API 엔드포인트 URL 생성
const url = new URL(
`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/fork`
);
if (namespace) {
url.searchParams.append("namespace", namespace);
}
const response = await fetch(url.toString(), {
method: "POST",
headers: DEFAULT_HEADERS,
});
// 이미 존재하는 프로젝트인 경우 처리
if (response.status === 409) {
throw new Error("Project already exists in the target namespace");
}
await handleGitLabError(response);
const data = await response.json();
return GitLabForkSchema.parse(data);
}
// 새로운 브랜치 생성
async function createBranch(
projectId: string,
options: z.infer<typeof CreateBranchOptionsSchema>
): Promise<GitLabReference> {
const url = new URL(
`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
projectId
)}/repository/branches`
);
const response = await fetch(url.toString(), {
method: "POST",
headers: DEFAULT_HEADERS,
body: JSON.stringify({
branch: options.name,
ref: options.ref,
}),
});
await handleGitLabError(response);
return GitLabReferenceSchema.parse(await response.json());
}
// 프로젝트의 기본 브랜치 조회
async function getDefaultBranchRef(projectId: string): Promise<string> {
const url = new URL(
`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}`
);
const response = await fetch(url.toString(), {
headers: DEFAULT_HEADERS,
});
await handleGitLabError(response);
const project = GitLabRepositorySchema.parse(await response.json());
return project.default_branch ?? "main";
}
// 파일 내용 조회
async function getFileContents(
projectId: string,
filePath: string,
ref?: string
): Promise<GitLabContent> {
const encodedPath = encodeURIComponent(filePath);
// ref가 없는 경우 default branch를 가져옴
if (!ref) {
ref = await getDefaultBranchRef(projectId);
}
const url = new URL(
`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
projectId
)}/repository/files/${encodedPath}`
);
url.searchParams.append("ref", ref);
const response = await fetch(url.toString(), {
headers: DEFAULT_HEADERS,
});
// 파일을 찾을 수 없는 경우 처리
if (response.status === 404) {
throw new Error(`File not found: ${filePath}`);
}
await handleGitLabError(response);
const data = await response.json();
const parsedData = GitLabContentSchema.parse(data);
// Base64로 인코딩된 파일 내용을 UTF-8로 디코딩
if (!Array.isArray(parsedData) && parsedData.content) {
parsedData.content = Buffer.from(parsedData.content, "base64").toString(
"utf8"
);
parsedData.encoding = "utf8";
}
return parsedData;
}
// 이슈 생성
async function createIssue(
projectId: string,
options: z.infer<typeof CreateIssueOptionsSchema>
): Promise<GitLabIssue> {
const url = new URL(
`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/issues`
);
const response = await fetch(url.toString(), {
method: "POST",
headers: DEFAULT_HEADERS,
body: JSON.stringify({
title: options.title,
description: options.description,
assignee_ids: options.assignee_ids,
milestone_id: options.milestone_id,
labels: options.labels?.join(","),
}),
});
// 잘못된 요청 처리
if (response.status === 400) {
const errorBody = await response.text();
throw new Error(`Invalid request: ${errorBody}`);
}
await handleGitLabError(response);
const data = await response.json();
return GitLabIssueSchema.parse(data);
}
async function createMergeRequest(
projectId: string,
options: z.infer<typeof CreateMergeRequestOptionsSchema>
): Promise<GitLabMergeRequest> {
const url = new URL(
`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
projectId
)}/merge_requests`
);
const response = await fetch(url.toString(), {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
},
body: JSON.stringify({
title: options.title,
description: options.description,
source_branch: options.source_branch,
target_branch: options.target_branch,
allow_collaboration: options.allow_collaboration,
draft: options.draft,
}),
});
if (response.status === 400) {
const errorBody = await response.text();
throw new Error(`Invalid request: ${errorBody}`);
}
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
);
}
const data = await response.json();
return GitLabMergeRequestSchema.parse(data);
}
async function createOrUpdateFile(
projectId: string,
filePath: string,
content: string,
commitMessage: string,
branch: string,
previousPath?: string
): Promise<GitLabCreateUpdateFileResponse> {
const encodedPath = encodeURIComponent(filePath);
const url = new URL(
`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
projectId
)}/repository/files/${encodedPath}`
);
const body = {
branch,
content,
commit_message: commitMessage,
encoding: "text",
...(previousPath ? { previous_path: previousPath } : {}),
};
// Check if file exists
let method = "POST";
try {
await getFileContents(projectId, filePath, branch);
method = "PUT";
} catch (error) {
if (!(error instanceof Error && error.message.includes("File not found"))) {
throw error;
}
// File doesn't exist, use POST
}
const response = await fetch(url.toString(), {
method,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
},
body: JSON.stringify(body),
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
);
}
const data = await response.json();
return GitLabCreateUpdateFileResponseSchema.parse(data);
}
async function createTree(
projectId: string,
files: FileOperation[],
ref?: string
): Promise<GitLabTree> {
const url = new URL(
`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
projectId
)}/repository/tree`
);
if (ref) {
url.searchParams.append("ref", ref);
}
const response = await fetch(url.toString(), {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
},
body: JSON.stringify({
files: files.map((file) => ({
file_path: file.path,
content: file.content,
encoding: "text",
})),
}),
});
if (response.status === 400) {
const errorBody = await response.text();
throw new Error(`Invalid request: ${errorBody}`);
}
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
);
}
const data = await response.json();
return GitLabTreeSchema.parse(data);
}
async function createCommit(
projectId: string,
message: string,
branch: string,
actions: FileOperation[]
): Promise<GitLabCommit> {
const url = new URL(
`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
projectId
)}/repository/commits`
);
const response = await fetch(url.toString(), {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
},
body: JSON.stringify({
branch,
commit_message: message,
actions: actions.map((action) => ({
action: "create",
file_path: action.path,
content: action.content,
encoding: "text",
})),
}),
});
if (response.status === 400) {
const errorBody = await response.text();
throw new Error(`Invalid request: ${errorBody}`);
}
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
);
}
const data = await response.json();
return GitLabCommitSchema.parse(data);
}
async function searchProjects(
query: string,
page: number = 1,
perPage: number = 20
): Promise<GitLabSearchResponse> {
const url = new URL(`${GITLAB_API_URL}/api/v4/projects`);
url.searchParams.append("search", query);
url.searchParams.append("page", page.toString());
url.searchParams.append("per_page", perPage.toString());
url.searchParams.append("order_by", "id");
url.searchParams.append("sort", "desc");
const response = await fetch(url.toString(), {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
},
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
);
}
const projects = (await response.json()) as GitLabRepository[];
const totalCount = response.headers.get("x-total");
const totalPages = response.headers.get("x-total-pages");
// GitLab API doesn't return these headers for results > 10,000
const count = totalCount ? parseInt(totalCount) : projects.length;
return GitLabSearchResponseSchema.parse({
count,
total_pages: totalPages ? parseInt(totalPages) : Math.ceil(count / perPage),
current_page: page,
items: projects,
});
}
async function createRepository(
options: z.infer<typeof CreateRepositoryOptionsSchema>
): Promise<GitLabRepository> {
const response = await fetch(`${GITLAB_API_URL}/api/v4/projects`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
},
body: JSON.stringify({
name: options.name,
description: options.description,
visibility: options.visibility,
initialize_with_readme: options.initialize_with_readme,
default_branch: "main",
path: options.name.toLowerCase().replace(/\s+/g, "-"),
}),
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`
);
}
const data = await response.json();
return GitLabRepositorySchema.parse(data);
}
// MR 조회 함수
async function getMergeRequest(
projectId: string,
mergeRequestIid: number
): Promise<GitLabMergeRequest> {
const url = new URL(
`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
projectId
)}/merge_requests/${mergeRequestIid}`
);
const response = await fetch(url.toString(), {
headers: DEFAULT_HEADERS,
});
await handleGitLabError(response);
return GitLabMergeRequestSchema.parse(await response.json());
}
// MR 변경사항 조회 함수
async function getMergeRequestDiffs(
projectId: string,
mergeRequestIid: number,
view?: "inline" | "parallel"
): Promise<GitLabMergeRequestDiff[]> {
const url = new URL(
`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
projectId
)}/merge_requests/${mergeRequestIid}/changes`
);
if (view) {
url.searchParams.append("view", view);
}
const response = await fetch(url.toString(), {
headers: DEFAULT_HEADERS,
});
await handleGitLabError(response);
const data = (await response.json()) as { changes: unknown };
return z.array(GitLabMergeRequestDiffSchema).parse(data.changes);
}
// MR 업데이트 함수
async function updateMergeRequest(
projectId: string,
mergeRequestIid: number,
options: Omit<
z.infer<typeof UpdateMergeRequestSchema>,
"project_id" | "merge_request_iid"
>
): Promise<GitLabMergeRequest> {
const url = new URL(
`${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(
projectId
)}/merge_requests/${mergeRequestIid}`
);
const response = await fetch(url.toString(), {
method: "PUT",
headers: DEFAULT_HEADERS,
body: JSON.stringify(options),
});
await handleGitLabError(response);
return GitLabMergeRequestSchema.parse(await response.json());
}
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "create_or_update_file",
description: "Create or update a single file in a GitLab project",
inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema),
},
{
name: "search_repositories",
description: "Search for GitLab projects",
inputSchema: zodToJsonSchema(SearchRepositoriesSchema),
},
{
name: "create_repository",
description: "Create a new GitLab project",
inputSchema: zodToJsonSchema(CreateRepositorySchema),
},
{
name: "get_file_contents",
description:
"Get the contents of a file or directory from a GitLab project",
inputSchema: zodToJsonSchema(GetFileContentsSchema),
},
{
name: "push_files",
description:
"Push multiple files to a GitLab project in a single commit",
inputSchema: zodToJsonSchema(PushFilesSchema),
},
{
name: "create_issue",
description: "Create a new issue in a GitLab project",
inputSchema: zodToJsonSchema(CreateIssueSchema),
},
{
name: "create_merge_request",
description: "Create a new merge request in a GitLab project",
inputSchema: zodToJsonSchema(CreateMergeRequestSchema),
},
{
name: "fork_repository",
description:
"Fork a GitLab project to your account or specified namespace",
inputSchema: zodToJsonSchema(ForkRepositorySchema),
},
{
name: "create_branch",
description: "Create a new branch in a GitLab project",
inputSchema: zodToJsonSchema(CreateBranchSchema),
},
{
name: "get_merge_request",
description: "Get details of a merge request",
inputSchema: zodToJsonSchema(GetMergeRequestSchema),
},
{
name: "get_merge_request_diffs",
description: "Get the changes/diffs of a merge request",
inputSchema: zodToJsonSchema(GetMergeRequestDiffsSchema),
},
{
name: "update_merge_request",
description: "Update a merge request",
inputSchema: zodToJsonSchema(UpdateMergeRequestSchema),
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
if (!request.params.arguments) {
throw new Error("Arguments are required");
}
switch (request.params.name) {
case "fork_repository": {
const args = ForkRepositorySchema.parse(request.params.arguments);
const fork = await forkProject(args.project_id, args.namespace);
return {
content: [{ type: "text", text: JSON.stringify(fork, null, 2) }],
};
}
case "create_branch": {
const args = CreateBranchSchema.parse(request.params.arguments);
let ref = args.ref;
if (!ref) {
ref = await getDefaultBranchRef(args.project_id);
}
const branch = await createBranch(args.project_id, {
name: args.branch,
ref,
});
return {
content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
};
}
case "search_repositories": {
const args = SearchRepositoriesSchema.parse(request.params.arguments);
const results = await searchProjects(
args.search,
args.page,
args.per_page
);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
case "create_repository": {
const args = CreateRepositorySchema.parse(request.params.arguments);
const repository = await createRepository(args);
return {
content: [
{ type: "text", text: JSON.stringify(repository, 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
);
return {
content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
};
}
case "create_or_update_file": {
const args = CreateOrUpdateFileSchema.parse(request.params.arguments);
const result = await createOrUpdateFile(
args.project_id,
args.file_path,
args.content,
args.commit_message,
args.branch,
args.previous_path
);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "push_files": {
const args = PushFilesSchema.parse(request.params.arguments);
const result = await createCommit(
args.project_id,
args.commit_message,
args.branch,
args.files.map((f) => ({ path: f.file_path, content: f.content }))
);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
case "create_issue": {
const args = CreateIssueSchema.parse(request.params.arguments);
const { project_id, ...options } = args;
const issue = await createIssue(project_id, options);
return {
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
};
}
case "create_merge_request": {
const args = CreateMergeRequestSchema.parse(request.params.arguments);
const { project_id, ...options } = args;
const mergeRequest = await createMergeRequest(project_id, options);
return {
content: [
{ type: "text", text: JSON.stringify(mergeRequest, null, 2) },
],
};
}
case "get_merge_request": {
const args = GetMergeRequestSchema.parse(request.params.arguments);
const mergeRequest = await getMergeRequest(
args.project_id,
args.merge_request_iid
);
return {
content: [
{ type: "text", text: JSON.stringify(mergeRequest, null, 2) },
],
};
}
case "get_merge_request_diffs": {
const args = GetMergeRequestDiffsSchema.parse(request.params.arguments);
const diffs = await getMergeRequestDiffs(
args.project_id,
args.merge_request_iid,
args.view
);
return {
content: [{ type: "text", text: JSON.stringify(diffs, null, 2) }],
};
}
case "update_merge_request": {
const args = UpdateMergeRequestSchema.parse(request.params.arguments);
const { project_id, merge_request_iid, ...options } = args;
const mergeRequest = await updateMergeRequest(
project_id,
merge_request_iid,
options
);
return {
content: [
{ type: "text", text: JSON.stringify(mergeRequest, null, 2) },
],
};
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Invalid arguments: ${error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(", ")}`
);
}
throw error;
}
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("GitLab MCP Server running on stdio");
}
runServer().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});

627
package-lock.json generated Normal file
View File

@ -0,0 +1,627 @@
{
"name": "@modelcontextprotocol/server-gitlab",
"version": "0.6.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@modelcontextprotocol/server-gitlab",
"version": "0.6.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "1.0.1",
"@types/node-fetch": "^2.6.12",
"dotenv": "^16.4.7",
"node-fetch": "^3.3.2",
"zod-to-json-schema": "^3.23.5"
},
"bin": {
"mcp-server-gitlab": "build/index.js"
},
"devDependencies": {
"shx": "^0.3.4",
"typescript": "^5.6.2"
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.0.1.tgz",
"integrity": "sha512-slLdFaxQJ9AlRg+hw28iiTtGvShAOgOKXcD0F91nUcRYiOMuS9ZBYjcdNZRXW9G5JQ511GRTdUy1zQVZDpJ+4w==",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
"raw-body": "^3.0.0",
"zod": "^3.23.8"
}
},
"node_modules/@types/node": {
"version": "22.13.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
"integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC"
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/interpret": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/raw-body": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.6.3",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/rechoir": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
"integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
"dev": true,
"dependencies": {
"resolve": "^1.1.6"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/shelljs": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
"integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"glob": "^7.0.0",
"interpret": "^1.0.0",
"rechoir": "^0.6.2"
},
"bin": {
"shjs": "bin/shjs"
},
"engines": {
"node": ">=4"
}
},
"node_modules/shx": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz",
"integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.3",
"shelljs": "^0.8.5"
},
"bin": {
"shx": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"license": "MIT"
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/zod": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
"integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
"license": "ISC",
"peerDependencies": {
"zod": "^3.24.1"
}
}
}
}

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "@zereight/mcp-gitlab",
"version": "1.0.2",
"description": "MCP server for using the GitLab API",
"license": "MIT",
"author": "zereight",
"type": "module",
"bin": "./build/index.js",
"files": [
"build"
],
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=14"
},
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"prepare": "npm run build",
"watch": "tsc --watch"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.0.1",
"@types/node-fetch": "^2.6.12",
"node-fetch": "^3.3.2",
"zod-to-json-schema": "^3.23.5"
},
"devDependencies": {
"typescript": "^5.6.2"
}
}

420
schemas.ts Normal file
View File

@ -0,0 +1,420 @@
import { z } from "zod";
// Base schemas for common types
export const GitLabAuthorSchema = z.object({
name: z.string(),
email: z.string(),
date: z.string(),
});
// Repository related schemas
export const GitLabOwnerSchema = z.object({
username: z.string(), // Changed from login to match GitLab API
id: z.number(),
avatar_url: z.string(),
web_url: z.string(), // Changed from html_url to match GitLab API
name: z.string(), // Added as GitLab includes full name
state: z.string(), // Added as GitLab includes user state
});
export const GitLabRepositorySchema = z.object({
id: z.number(),
name: z.string(),
path_with_namespace: z.string(),
visibility: z.string().optional(),
owner: GitLabOwnerSchema.optional(),
web_url: z.string().optional(),
description: z.string().nullable(),
fork: z.boolean().optional(),
ssh_url_to_repo: z.string().optional(),
http_url_to_repo: z.string().optional(),
created_at: z.string().optional(),
last_activity_at: z.string().optional(),
default_branch: z.string().optional(),
});
// File content schemas
export const GitLabFileContentSchema = z.object({
file_name: z.string(), // Changed from name to match GitLab API
file_path: z.string(), // Changed from path to match GitLab API
size: z.number(),
encoding: z.string(),
content: z.string(),
content_sha256: z.string(), // Changed from sha to match GitLab API
ref: z.string(), // Added as GitLab requires branch reference
blob_id: z.string(), // Added to match GitLab API
last_commit_id: z.string(), // Added to match GitLab API
});
export const GitLabDirectoryContentSchema = z.object({
name: z.string(),
path: z.string(),
type: z.string(),
mode: z.string(),
id: z.string(), // Changed from sha to match GitLab API
web_url: z.string(), // Changed from html_url to match GitLab API
});
export const GitLabContentSchema = z.union([
GitLabFileContentSchema,
z.array(GitLabDirectoryContentSchema),
]);
// Operation schemas
export const FileOperationSchema = z.object({
path: z.string(),
content: z.string(),
});
// Tree and commit schemas
export const GitLabTreeEntrySchema = z.object({
id: z.string(), // Changed from sha to match GitLab API
name: z.string(),
type: z.enum(["blob", "tree"]),
path: z.string(),
mode: z.string(),
});
export const GitLabTreeSchema = z.object({
id: z.string(), // Changed from sha to match GitLab API
tree: z.array(GitLabTreeEntrySchema),
});
export const GitLabCommitSchema = z.object({
id: z.string(), // Changed from sha to match GitLab API
short_id: z.string(), // Added to match GitLab API
title: z.string(), // Changed from message to match GitLab API
author_name: z.string(),
author_email: z.string(),
authored_date: z.string(),
committer_name: z.string(),
committer_email: z.string(),
committed_date: z.string(),
web_url: z.string(), // Changed from html_url to match GitLab API
parent_ids: z.array(z.string()), // Changed from parents to match GitLab API
});
// Reference schema
export const GitLabReferenceSchema = z.object({
name: z.string(), // Changed from ref to match GitLab API
commit: z.object({
id: z.string(), // Changed from sha to match GitLab API
web_url: z.string(), // Changed from url to match GitLab API
}),
});
// Input schemas for operations
export const CreateRepositoryOptionsSchema = z.object({
name: z.string(),
description: z.string().optional(),
visibility: z.enum(["private", "internal", "public"]).optional(), // Changed from private to match GitLab API
initialize_with_readme: z.boolean().optional(), // Changed from auto_init to match GitLab API
});
export const CreateIssueOptionsSchema = z.object({
title: z.string(),
description: z.string().optional(), // Changed from body to match GitLab API
assignee_ids: z.array(z.number()).optional(), // Changed from assignees to match GitLab API
milestone_id: z.number().optional(), // Changed from milestone to match GitLab API
labels: z.array(z.string()).optional(),
});
export const CreateMergeRequestOptionsSchema = z.object({
// Changed from CreatePullRequestOptionsSchema
title: z.string(),
description: z.string().optional(), // Changed from body to match GitLab API
source_branch: z.string(), // Changed from head to match GitLab API
target_branch: z.string(), // Changed from base to match GitLab API
allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API
draft: z.boolean().optional(),
});
export const CreateBranchOptionsSchema = z.object({
name: z.string(), // Changed from ref to match GitLab API
ref: z.string(), // The source branch/commit for the new branch
});
// Response schemas for operations
export const GitLabCreateUpdateFileResponseSchema = z.object({
file_path: z.string(),
branch: z.string(),
commit_id: z.string(), // Changed from sha to match GitLab API
content: GitLabFileContentSchema.optional(),
});
export const GitLabSearchResponseSchema = z.object({
count: z.number().optional(),
total_pages: z.number().optional(),
current_page: z.number().optional(),
items: z.array(GitLabRepositorySchema),
});
// Fork related schemas
export const GitLabForkParentSchema = z.object({
name: z.string(),
path_with_namespace: z.string(), // Changed from full_name to match GitLab API
owner: z.object({
username: z.string(), // Changed from login to match GitLab API
id: z.number(),
avatar_url: z.string(),
}),
web_url: z.string(), // Changed from html_url to match GitLab API
});
export const GitLabForkSchema = GitLabRepositorySchema.extend({
forked_from_project: GitLabForkParentSchema, // Changed from parent to match GitLab API
});
// Issue related schemas
export const GitLabLabelSchema = z.object({
id: z.number(),
name: z.string(),
color: z.string(),
description: z.string().optional(),
});
export const GitLabUserSchema = z.object({
username: z.string(), // Changed from login to match GitLab API
id: z.number(),
name: z.string(),
avatar_url: z.string(),
web_url: z.string(), // Changed from html_url to match GitLab API
});
export const GitLabMilestoneSchema = z.object({
id: z.number(),
iid: z.number(), // Added to match GitLab API
title: z.string(),
description: z.string(),
state: z.string(),
web_url: z.string(), // Changed from html_url to match GitLab API
});
export const GitLabIssueSchema = z.object({
id: z.number(),
iid: z.number(), // Added to match GitLab API
project_id: z.number(), // Added to match GitLab API
title: z.string(),
description: z.string(), // Changed from body to match GitLab API
state: z.string(),
author: GitLabUserSchema,
assignees: z.array(GitLabUserSchema),
labels: z.array(GitLabLabelSchema),
milestone: GitLabMilestoneSchema.nullable(),
created_at: z.string(),
updated_at: z.string(),
closed_at: z.string().nullable(),
web_url: z.string(), // Changed from html_url to match GitLab API
});
// Merge Request related schemas (equivalent to Pull Request)
export const GitLabMergeRequestDiffRefSchema = z.object({
base_sha: z.string(),
head_sha: z.string(),
start_sha: z.string(),
});
export const GitLabMergeRequestSchema = z.object({
id: z.number(),
iid: z.number(),
project_id: z.number(),
title: z.string(),
description: z.string().nullable(),
state: z.string(),
merged: z.boolean().optional(),
draft: z.boolean().optional(),
author: GitLabUserSchema,
assignees: z.array(GitLabUserSchema).optional(),
source_branch: z.string(),
target_branch: z.string(),
diff_refs: GitLabMergeRequestDiffRefSchema.optional(),
web_url: z.string(),
created_at: z.string(),
updated_at: z.string(),
merged_at: z.string().nullable(),
closed_at: z.string().nullable(),
merge_commit_sha: z.string().nullable(),
detailed_merge_status: z.string().optional(),
merge_status: z.string().optional(),
merge_error: z.string().nullable().optional(),
work_in_progress: z.boolean().optional(),
blocking_discussions_resolved: z.boolean().optional(),
should_remove_source_branch: z.boolean().nullable().optional(),
force_remove_source_branch: z.boolean().optional(),
allow_collaboration: z.boolean().optional(),
allow_maintainer_to_push: z.boolean().optional(),
changes_count: z.string().optional(),
merge_when_pipeline_succeeds: z.boolean().optional(),
squash: z.boolean().optional(),
labels: z.array(z.string()).optional(),
});
// API Operation Parameter Schemas
const ProjectParamsSchema = z.object({
project_id: z.string().describe("Project ID or URL-encoded path"), // Changed from owner/repo to match GitLab API
});
export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
file_path: z.string().describe("Path where to create/update the file"),
content: z.string().describe("Content of the file"),
commit_message: z.string().describe("Commit message"),
branch: z.string().describe("Branch to create/update the file in"),
previous_path: z
.string()
.optional()
.describe("Path of the file to move/rename"),
});
export const SearchRepositoriesSchema = z.object({
search: z.string().describe("Search query"), // Changed from query to match GitLab API
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 const CreateRepositorySchema = z.object({
name: z.string().describe("Repository name"),
description: z.string().optional().describe("Repository description"),
visibility: z
.enum(["private", "internal", "public"])
.optional()
.describe("Repository visibility level"),
initialize_with_readme: z
.boolean()
.optional()
.describe("Initialize with README.md"),
});
export const GetFileContentsSchema = ProjectParamsSchema.extend({
file_path: z.string().describe("Path to the file or directory"),
ref: z.string().optional().describe("Branch/tag/commit to get contents from"),
});
export const PushFilesSchema = ProjectParamsSchema.extend({
branch: z.string().describe("Branch to push to"),
files: z
.array(
z.object({
file_path: z.string().describe("Path where to create the file"),
content: z.string().describe("Content of the file"),
})
)
.describe("Array of files to push"),
commit_message: z.string().describe("Commit message"),
});
export const CreateIssueSchema = ProjectParamsSchema.extend({
title: z.string().describe("Issue title"),
description: z.string().optional().describe("Issue description"),
assignee_ids: z
.array(z.number())
.optional()
.describe("Array of user IDs to assign"),
labels: z.array(z.string()).optional().describe("Array of label names"),
milestone_id: z.number().optional().describe("Milestone ID to assign"),
});
export const CreateMergeRequestSchema = ProjectParamsSchema.extend({
title: z.string().describe("Merge request title"),
description: z.string().optional().describe("Merge request description"),
source_branch: z.string().describe("Branch containing changes"),
target_branch: z.string().describe("Branch to merge into"),
draft: z.boolean().optional().describe("Create as draft merge request"),
allow_collaboration: z
.boolean()
.optional()
.describe("Allow commits from upstream members"),
});
export const ForkRepositorySchema = ProjectParamsSchema.extend({
namespace: z.string().optional().describe("Namespace to fork to (full path)"),
});
export const CreateBranchSchema = ProjectParamsSchema.extend({
branch: z.string().describe("Name for the new branch"),
ref: z.string().optional().describe("Source branch/commit for new branch"),
});
export const GitLabMergeRequestDiffSchema = z.object({
old_path: z.string(),
new_path: z.string(),
a_mode: z.string(),
b_mode: z.string(),
diff: z.string(),
new_file: z.boolean(),
renamed_file: z.boolean(),
deleted_file: z.boolean(),
});
export const GetMergeRequestSchema = ProjectParamsSchema.extend({
merge_request_iid: z
.number()
.describe("The internal ID of the merge request"),
});
export const UpdateMergeRequestSchema = GetMergeRequestSchema.extend({
title: z.string().optional().describe("The title of the merge request"),
description: z
.string()
.optional()
.describe("The description of the merge request"),
target_branch: z.string().optional().describe("The target branch"),
assignee_ids: z
.array(z.number())
.optional()
.describe("The ID of the users to assign the MR to"),
labels: z.array(z.string()).optional().describe("Labels for the MR"),
state_event: z
.enum(["close", "reopen"])
.optional()
.describe("New state (close/reopen) for the MR"),
remove_source_branch: z
.boolean()
.optional()
.describe("Flag indicating if the source branch should be removed"),
squash: z
.boolean()
.optional()
.describe("Squash commits into a single commit when merging"),
draft: z.boolean().optional().describe("Work in progress merge request"),
});
export const GetMergeRequestDiffsSchema = GetMergeRequestSchema.extend({
view: z.enum(["inline", "parallel"]).optional().describe("Diff view type"),
});
// Export types
export type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>;
export type GitLabFork = z.infer<typeof GitLabForkSchema>;
export type GitLabIssue = z.infer<typeof GitLabIssueSchema>;
export type GitLabMergeRequest = z.infer<typeof GitLabMergeRequestSchema>;
export type GitLabRepository = z.infer<typeof GitLabRepositorySchema>;
export type GitLabFileContent = z.infer<typeof GitLabFileContentSchema>;
export type GitLabDirectoryContent = z.infer<
typeof GitLabDirectoryContentSchema
>;
export type GitLabContent = z.infer<typeof GitLabContentSchema>;
export type FileOperation = z.infer<typeof FileOperationSchema>;
export type GitLabTree = z.infer<typeof GitLabTreeSchema>;
export type GitLabCommit = z.infer<typeof GitLabCommitSchema>;
export type GitLabReference = z.infer<typeof GitLabReferenceSchema>;
export type CreateRepositoryOptions = z.infer<
typeof CreateRepositoryOptionsSchema
>;
export type CreateIssueOptions = z.infer<typeof CreateIssueOptionsSchema>;
export type CreateMergeRequestOptions = z.infer<
typeof CreateMergeRequestOptionsSchema
>;
export type CreateBranchOptions = z.infer<typeof CreateBranchOptionsSchema>;
export type GitLabCreateUpdateFileResponse = z.infer<
typeof GitLabCreateUpdateFileResponseSchema
>;
export type GitLabSearchResponse = z.infer<typeof GitLabSearchResponseSchema>;
export type GitLabMergeRequestDiff = z.infer<
typeof GitLabMergeRequestDiffSchema
>;

15
tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["./**/*"],
"exclude": ["node_modules", "build"]
}