초기 설정 파일 추가: .gitignore, tsconfig.json, package.json 및 README.md 생성
Generated by Copilot
This commit is contained in:
539
build/index.js
Executable file
539
build/index.js
Executable 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
348
build/schemas.js
Normal 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"),
|
||||
});
|
Reference in New Issue
Block a user