gitlab-mcp-server
by yoda-digital
Verified
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
CallToolRequest,
CallToolRequestSchema,
ListToolsRequestSchema,
ListToolsResult,
ServerCapabilities,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import {
CreateOrUpdateFileSchema,
SearchRepositoriesSchema,
CreateRepositorySchema,
GetFileContentsSchema,
PushFilesSchema,
CreateIssueSchema,
CreateMergeRequestSchema,
ForkRepositorySchema,
CreateBranchSchema,
ListGroupProjectsSchema,
GetProjectEventsSchema,
ListCommitsSchema,
ListIssuesSchema,
ListMergeRequestsSchema,
ListProjectWikiPagesSchema,
GetProjectWikiPageSchema,
CreateProjectWikiPageSchema,
EditProjectWikiPageSchema,
DeleteProjectWikiPageSchema,
UploadProjectWikiAttachmentSchema,
ListGroupWikiPagesSchema,
GetGroupWikiPageSchema,
CreateGroupWikiPageSchema,
EditGroupWikiPageSchema,
DeleteGroupWikiPageSchema,
UploadGroupWikiAttachmentSchema,
ListProjectMembersSchema,
ListGroupMembersSchema,
FileOperationSchema,
} from './schemas.js';
import { GitLabApi } from './gitlab-api.js';
import { setupTransport } from './transport.js';
import {
formatEventsResponse,
formatCommitsResponse,
formatIssuesResponse,
formatMergeRequestsResponse,
formatWikiPagesResponse,
formatWikiPageResponse,
formatWikiAttachmentResponse,
} from './formatters.js';
import { isValidISODate } from './utils.js';
// Configuration
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';
const PORT = parseInt(process.env.PORT || '3000', 10);
const USE_SSE = process.env.USE_SSE === 'true';
if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
process.exit(1);
}
// Server capabilities
const serverCapabilities: ServerCapabilities = {
tools: {}
};
// Create server
const server = new Server({
name: "gitlab-mcp-server",
version: "0.1.0",
}, {
capabilities: serverCapabilities
});
// Create GitLab API client
const gitlabApi = new GitLabApi({
apiUrl: GITLAB_API_URL,
token: GITLAB_PERSONAL_ACCESS_TOKEN
});
// Helper function to convert Zod schema to JSON schema with proper type
function createJsonSchema(schema: z.ZodType<any>) {
// Convert the schema using zodToJsonSchema
const jsonSchema = zodToJsonSchema(schema);
// Ensure we return an object with the expected structure
return {
type: "object" as const,
properties: (jsonSchema as any).properties || {}
};
}
// Register tool handlers
server.setRequestHandler(ListToolsRequestSchema, async (): Promise<ListToolsResult> => {
return {
tools: [
{
name: "create_or_update_file",
description: "Create or update a single file in a GitLab project",
inputSchema: createJsonSchema(CreateOrUpdateFileSchema)
},
{
name: "search_repositories",
description: "Search for GitLab projects",
inputSchema: createJsonSchema(SearchRepositoriesSchema)
},
{
name: "create_repository",
description: "Create a new GitLab project",
inputSchema: createJsonSchema(CreateRepositorySchema)
},
{
name: "get_file_contents",
description: "Get the contents of a file or directory from a GitLab project",
inputSchema: createJsonSchema(GetFileContentsSchema)
},
{
name: "push_files",
description: "Push multiple files to a GitLab project in a single commit",
inputSchema: createJsonSchema(PushFilesSchema)
},
{
name: "create_issue",
description: "Create a new issue in a GitLab project",
inputSchema: createJsonSchema(CreateIssueSchema)
},
{
name: "create_merge_request",
description: "Create a new merge request in a GitLab project",
inputSchema: createJsonSchema(CreateMergeRequestSchema)
},
{
name: "fork_repository",
description: "Fork a GitLab project to your account or specified namespace",
inputSchema: createJsonSchema(ForkRepositorySchema)
},
{
name: "create_branch",
description: "Create a new branch in a GitLab project",
inputSchema: createJsonSchema(CreateBranchSchema)
},
{
name: "list_group_projects",
description: "List all projects (repositories) within a specific GitLab group",
inputSchema: createJsonSchema(ListGroupProjectsSchema)
},
{
name: "get_project_events",
description: "Get recent events/activities for a GitLab project",
inputSchema: createJsonSchema(GetProjectEventsSchema)
},
{
name: "list_commits",
description: "Get commit history for a GitLab project",
inputSchema: createJsonSchema(ListCommitsSchema)
},
{
name: "list_issues",
description: "Get issues for a GitLab project",
inputSchema: createJsonSchema(ListIssuesSchema)
},
{
name: "list_merge_requests",
description: "Get merge requests for a GitLab project",
inputSchema: createJsonSchema(ListMergeRequestsSchema)
},
// Project Wiki Tools
{
name: "list_project_wiki_pages",
description: "List all wiki pages for a GitLab project",
inputSchema: createJsonSchema(ListProjectWikiPagesSchema)
},
{
name: "get_project_wiki_page",
description: "Get a specific wiki page for a GitLab project",
inputSchema: createJsonSchema(GetProjectWikiPageSchema)
},
{
name: "create_project_wiki_page",
description: "Create a new wiki page for a GitLab project",
inputSchema: createJsonSchema(CreateProjectWikiPageSchema)
},
{
name: "edit_project_wiki_page",
description: "Edit an existing wiki page for a GitLab project",
inputSchema: createJsonSchema(EditProjectWikiPageSchema)
},
{
name: "delete_project_wiki_page",
description: "Delete a wiki page from a GitLab project",
inputSchema: createJsonSchema(DeleteProjectWikiPageSchema)
},
{
name: "upload_project_wiki_attachment",
description: "Upload an attachment to a GitLab project wiki",
inputSchema: createJsonSchema(UploadProjectWikiAttachmentSchema)
},
// Group Wiki Tools
{
name: "list_group_wiki_pages",
description: "List all wiki pages for a GitLab group",
inputSchema: createJsonSchema(ListGroupWikiPagesSchema)
},
{
name: "get_group_wiki_page",
description: "Get a specific wiki page for a GitLab group",
inputSchema: createJsonSchema(GetGroupWikiPageSchema)
},
{
name: "create_group_wiki_page",
description: "Create a new wiki page for a GitLab group",
inputSchema: createJsonSchema(CreateGroupWikiPageSchema)
},
{
name: "edit_group_wiki_page",
description: "Edit an existing wiki page for a GitLab group",
inputSchema: createJsonSchema(EditGroupWikiPageSchema)
},
{
name: "delete_group_wiki_page",
description: "Delete a wiki page from a GitLab group",
inputSchema: createJsonSchema(DeleteGroupWikiPageSchema)
},
{
name: "upload_group_wiki_attachment",
description: "Upload an attachment to a GitLab group wiki",
inputSchema: createJsonSchema(UploadGroupWikiAttachmentSchema)
},
// Member Tools
{
name: "list_project_members",
description: "List all members of a GitLab project (including inherited members)",
inputSchema: createJsonSchema(ListProjectMembersSchema)
},
{
name: "list_group_members",
description: "List all members of a GitLab group (including inherited members)",
inputSchema: createJsonSchema(ListGroupMembersSchema)
},
]
};
});
server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
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 gitlabApi.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 gitlabApi.getDefaultBranchRef(args.project_id);
}
const branch = await gitlabApi.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 gitlabApi.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 gitlabApi.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 gitlabApi.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 gitlabApi.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);
// Use individual file creation for each file instead of batch commit
const results = [];
for (const file of args.files) {
try {
const result = await gitlabApi.createOrUpdateFile(
args.project_id,
file.path,
file.content,
args.commit_message,
args.branch
);
results.push(result);
} catch (error) {
console.error(`Error creating/updating file ${file.path}:`, error);
throw error;
}
}
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
}
case "create_issue": {
const args = CreateIssueSchema.parse(request.params.arguments);
const { project_id, ...options } = args;
const issue = await gitlabApi.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 gitlabApi.createMergeRequest(project_id, options);
return { content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }] };
}
case "list_group_projects": {
const args = ListGroupProjectsSchema.parse(request.params.arguments);
const { group_id, ...options } = args;
const results = await gitlabApi.listGroupProjects(group_id, options);
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
}
case "get_project_events": {
// Parse and validate the arguments
const args = GetProjectEventsSchema.parse(request.params.arguments);
// Additional validation for pagination parameters
if (args.per_page && (args.per_page < 1 || args.per_page > 100)) {
throw new Error("per_page must be between 1 and 100");
}
if (args.page && args.page < 1) {
throw new Error("page must be greater than 0");
}
// Extract project_id and options
const { project_id, ...options } = args;
// Call the API function
const events = await gitlabApi.getProjectEvents(project_id, options);
// Format and return the response
return formatEventsResponse(events);
}
case "list_commits": {
// Parse and validate the arguments
const args = ListCommitsSchema.parse(request.params.arguments);
// Additional validation for pagination parameters
if (args.per_page && (args.per_page < 1 || args.per_page > 100)) {
throw new Error("per_page must be between 1 and 100");
}
if (args.page && args.page < 1) {
throw new Error("page must be greater than 0");
}
// Validate date formats if provided
if (args.since && !isValidISODate(args.since)) {
throw new Error(
"since must be a valid ISO 8601 date (YYYY-MM-DDTHH:MM:SSZ)"
);
}
if (args.until && !isValidISODate(args.until)) {
throw new Error(
"until must be a valid ISO 8601 date (YYYY-MM-DDTHH:MM:SSZ)"
);
}
// Extract project_id and options
const { project_id, ...options } = args;
// Call the API function
const commits = await gitlabApi.listCommits(project_id, options);
// Format and return the response
return formatCommitsResponse(commits);
}
case "list_issues": {
// Parse and validate the arguments
const args = ListIssuesSchema.parse(request.params.arguments);
// Additional validation for pagination parameters
if (args.per_page && (args.per_page < 1 || args.per_page > 100)) {
throw new Error("per_page must be between 1 and 100");
}
if (args.page && args.page < 1) {
throw new Error("page must be greater than 0");
}
// Validate date formats if provided
const dateFields = [
"created_after",
"created_before",
"updated_after",
"updated_before",
];
dateFields.forEach((field) => {
const value = args[field as keyof typeof args];
if (
typeof value === 'string' &&
!isValidISODate(value)
) {
throw new Error(
`${field} must be a valid ISO 8601 date (YYYY-MM-DDTHH:MM:SSZ)`
);
}
});
// Extract project_id and options
const { project_id, ...options } = args;
// Call the API function
const issues = await gitlabApi.listIssues(project_id, options);
// Format and return the response
return formatIssuesResponse(issues);
}
case "list_merge_requests": {
// Parse and validate the arguments
const args = ListMergeRequestsSchema.parse(request.params.arguments);
// Additional validation for pagination parameters
if (args.per_page && (args.per_page < 1 || args.per_page > 100)) {
throw new Error("per_page must be between 1 and 100");
}
if (args.page && args.page < 1) {
throw new Error("page must be greater than 0");
}
// Validate date formats if provided
const dateFields = [
"created_after",
"created_before",
"updated_after",
"updated_before",
];
dateFields.forEach((field) => {
const value = args[field as keyof typeof args];
if (
typeof value === 'string' &&
!isValidISODate(value)
) {
throw new Error(
`${field} must be a valid ISO 8601 date (YYYY-MM-DDTHH:MM:SSZ)`
);
}
});
// Extract project_id and options
const { project_id, ...options } = args;
// Call the API function
const mergeRequests = await gitlabApi.listMergeRequests(project_id, options);
// Format and return the response
return formatMergeRequestsResponse(mergeRequests);
}
// Project Wiki Tools
case "list_project_wiki_pages": {
const args = ListProjectWikiPagesSchema.parse(request.params.arguments);
const wikiPages = await gitlabApi.listProjectWikiPages(args.project_id, {
with_content: args.with_content
});
return formatWikiPagesResponse(wikiPages);
}
case "get_project_wiki_page": {
const args = GetProjectWikiPageSchema.parse(request.params.arguments);
const wikiPage = await gitlabApi.getProjectWikiPage(args.project_id, args.slug, {
render_html: args.render_html,
version: args.version
});
return formatWikiPageResponse(wikiPage);
}
case "create_project_wiki_page": {
const args = CreateProjectWikiPageSchema.parse(request.params.arguments);
const wikiPage = await gitlabApi.createProjectWikiPage(args.project_id, {
title: args.title,
content: args.content,
format: args.format
});
return formatWikiPageResponse(wikiPage);
}
case "edit_project_wiki_page": {
const args = EditProjectWikiPageSchema.parse(request.params.arguments);
const wikiPage = await gitlabApi.editProjectWikiPage(args.project_id, args.slug, {
title: args.title,
content: args.content,
format: args.format
});
return formatWikiPageResponse(wikiPage);
}
case "delete_project_wiki_page": {
const args = DeleteProjectWikiPageSchema.parse(request.params.arguments);
await gitlabApi.deleteProjectWikiPage(args.project_id, args.slug);
return { content: [{ type: "text", text: `Wiki page '${args.slug}' has been deleted.` }] };
}
case "upload_project_wiki_attachment": {
const args = UploadProjectWikiAttachmentSchema.parse(request.params.arguments);
const attachment = await gitlabApi.uploadProjectWikiAttachment(args.project_id, {
file_path: args.file_path,
content: args.content,
branch: args.branch
});
return formatWikiAttachmentResponse(attachment);
}
// Group Wiki Tools
case "list_group_wiki_pages": {
const args = ListGroupWikiPagesSchema.parse(request.params.arguments);
const wikiPages = await gitlabApi.listGroupWikiPages(args.group_id, {
with_content: args.with_content
});
return formatWikiPagesResponse(wikiPages);
}
case "get_group_wiki_page": {
const args = GetGroupWikiPageSchema.parse(request.params.arguments);
const wikiPage = await gitlabApi.getGroupWikiPage(args.group_id, args.slug, {
render_html: args.render_html,
version: args.version
});
return formatWikiPageResponse(wikiPage);
}
case "create_group_wiki_page": {
const args = CreateGroupWikiPageSchema.parse(request.params.arguments);
const wikiPage = await gitlabApi.createGroupWikiPage(args.group_id, {
title: args.title,
content: args.content,
format: args.format
});
return formatWikiPageResponse(wikiPage);
}
case "edit_group_wiki_page": {
const args = EditGroupWikiPageSchema.parse(request.params.arguments);
const wikiPage = await gitlabApi.editGroupWikiPage(args.group_id, args.slug, {
title: args.title,
content: args.content,
format: args.format
});
return formatWikiPageResponse(wikiPage);
}
case "delete_group_wiki_page": {
const args = DeleteGroupWikiPageSchema.parse(request.params.arguments);
await gitlabApi.deleteGroupWikiPage(args.group_id, args.slug);
return { content: [{ type: "text", text: `Wiki page '${args.slug}' has been deleted.` }] };
}
case "upload_group_wiki_attachment": {
const args = UploadGroupWikiAttachmentSchema.parse(request.params.arguments);
const attachment = await gitlabApi.uploadGroupWikiAttachment(args.group_id, {
file_path: args.file_path,
content: args.content,
branch: args.branch
});
return formatWikiAttachmentResponse(attachment);
}
case "list_project_members": {
const args = ListProjectMembersSchema.parse(request.params.arguments);
const { project_id, ...options } = args;
const members = await gitlabApi.listProjectMembers(project_id, options);
return { content: [{ type: "text", text: JSON.stringify(members, null, 2) }] };
}
case "list_group_members": {
const args = ListGroupMembersSchema.parse(request.params.arguments);
const { group_id, ...options } = args;
const members = await gitlabApi.listGroupMembers(group_id, options);
return { content: [{ type: "text", text: JSON.stringify(members, null, 2) }] };
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessage = `Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`;
throw new Error(errorMessage);
}
throw error;
}
});
// Start the server
async function runServer() {
try {
await setupTransport(server, { port: PORT, useSSE: USE_SSE });
console.error(`GitLab MCP Server running with ${USE_SSE ? 'SSE' : 'stdio'} transport`);
} catch (error) {
console.error("Error starting server:", error);
process.exit(1);
}
}
runServer();