Skip to content

Commit f1049b7

Browse files
author
Nisha Saini
committed
fixed check_repo_allowed
1 parent 8a80c0d commit f1049b7

File tree

5 files changed

+137
-44
lines changed

5 files changed

+137
-44
lines changed

β€ŽContainerfile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,5 @@ RUN pip install --no-cache-dir -r requirements.txt
1010
# Copy source code (uses .containerignore)
1111
COPY . .
1212

13-
# Expose port for HTTP transport (only used when MCP_TRANSPORT=http)
14-
EXPOSE 8085
15-
1613
# Entrypoint
1714
CMD ["python", "gitlab_mcp_server.py"]

β€ŽMakefile

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,12 @@ build:
1313
podman build -t $(IMG) .
1414

1515
# Notes:
16-
# - $(ENV_FILE) is expected to define the gitlab token & repos other vars are optional..
17-
# - The --tty option is used here since we might run this in a
18-
# terminal, but for the mcp.json version we don't use --tty.
19-
# - You can use Ctrl-D to quit nicely.
16+
# - $(ENV_FILE) is expected to define the gitlab token & repos other vars are optional.
17+
# - This server runs in STDIO mode for MCP clients like Cursor.
18+
# - Use Ctrl-C to quit the server.
2019
run:
21-
@podman run -i --tty --rm --env-file $(ENV_FILE) $(IMG)
22-
23-
run-http:
24-
@echo "🌐 Starting GitLab MCP server in HTTP mode"
25-
@podman run -i --rm --env-file $(ENV_FILE) -p 8085:8085 -e MCP_TRANSPORT=http $(IMG)
20+
@echo "πŸš€ Starting GitLab MCP server (STDIO mode)"
21+
@podman run -i --rm --env-file $(ENV_FILE) $(IMG)
2622

2723
clean:
2824
podman rmi -i $(IMG)

β€Žexample.env

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,8 @@ MCP_ALLOWED_REPOS=redhat/edge/ci-cd/pipe-x/*
1414
# Optional: Custom GitLab URL (defaults to gitlab.com)
1515
GITLAB_URL=https://gitlab.com
1616

17-
# Optional: Transport mode (stdio or http)
18-
MCP_TRANSPORT=stdio
19-
20-
# HTTP Transport Configuration (only used when MCP_TRANSPORT=http)
17+
# HTTP Transport Configuration (HTTP-only mode)
2118
MCP_HOST=127.0.0.1
22-
MCP_PORT=8085
2319

2420
# Recommended ports for multiple MCP servers:
2521
# GitLab MCP: 8086, Jira MCP: 8087, Other MCP: 8088, 8089, etc.

β€Žexample.mcp.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
{
22
"mcpServers": {
3-
"gitlabMCP": {
3+
"gitlabMCP_STDIO": {
44
"command": "podman",
55
"args": [
66
"run",
77
"-i",
88
"--rm",
99
"--env-file",
1010
"~/.rh-gitlab-mcp.env",
11-
"localhost/gitlab_mcp_server:latest"
11+
"localhost/gitlab-mcp:latest"
1212
],
13-
"description": "A containerized MCP server to query gitlab projects"
13+
"description": "A containerized GitLab MCP server using STDIO transport"
1414
}
1515
}
1616
}

β€Žgitlab_mcp_server.py

Lines changed: 128 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def __init__(self):
5757

5858
# SSL Configuration options
5959
ssl_verify = os.getenv("MCP_SSL_VERIFY", "true").lower() == "true"
60-
ssl_ca_bundle = os.getenv("SSL_CERT_FILE") # Path to CA bundle file
60+
ssl_ca_bundle = os.getenv("MCP_SSL_CA_BUNDLE") # Path to CA bundle file
6161

6262
# Build GitLab client configuration
6363
gitlab_config = {
@@ -342,7 +342,25 @@ async def search_repositories(search_term: Optional[str] = None, visibility: str
342342
# Filter results based on repository access permissions
343343
accessible_projects = []
344344
for project in projects:
345-
if check_repo_allowed(str(project.id)):
345+
# Check repo pattern directly without additional API call
346+
project_path = project.path_with_namespace
347+
is_allowed = False
348+
349+
# Check against each allowed pattern
350+
for allowed_pattern in config.allowed_repos:
351+
allowed_pattern = allowed_pattern.strip()
352+
353+
# Check if project ID matches (for backward compatibility)
354+
if str(project.id) == allowed_pattern:
355+
is_allowed = True
356+
break
357+
358+
# Check if project path matches the pattern
359+
if match_repo_pattern(project_path, allowed_pattern):
360+
is_allowed = True
361+
break
362+
363+
if is_allowed:
346364
try:
347365
repo_info = {
348366
'id': project.id,
@@ -581,6 +599,108 @@ async def list_pipelines(request: PipelineListRequest) -> dict:
581599
"error": f"❌ Error listing pipelines: {str(e)}"
582600
}
583601

602+
@mcp.tool()
603+
async def scan_group_failed_pipelines(limit: Optional[int] = 5) -> dict:
604+
"""πŸ” Scan all projects in allowed groups for failed pipelines, sorted by timestamp"""
605+
try:
606+
# Security checks
607+
security_error = security_check("read")
608+
if security_error:
609+
raise ValueError(security_error)
610+
611+
all_failed_pipelines = []
612+
scanned_projects = []
613+
614+
# Get all accessible projects from search
615+
try:
616+
accessible_projects = config.gl.projects.list(all=True, per_page=100)
617+
except Exception as e:
618+
logger.warning(f"⚠️ Could not list all projects, trying owned projects: {e}")
619+
accessible_projects = config.gl.projects.list(owned=True, per_page=100)
620+
621+
# Filter projects that match allowed repo patterns
622+
for project in accessible_projects:
623+
# Check repo pattern directly without additional API call
624+
project_path = project.path_with_namespace
625+
is_allowed = False
626+
627+
# Check against each allowed pattern
628+
for allowed_pattern in config.allowed_repos:
629+
allowed_pattern = allowed_pattern.strip()
630+
631+
# Check if project ID matches (for backward compatibility)
632+
if str(project.id) == allowed_pattern:
633+
is_allowed = True
634+
break
635+
636+
# Check if project path matches the pattern
637+
if match_repo_pattern(project_path, allowed_pattern):
638+
is_allowed = True
639+
break
640+
641+
if is_allowed:
642+
scanned_projects.append({
643+
'id': project.id,
644+
'name': project.name,
645+
'path_with_namespace': project.path_with_namespace
646+
})
647+
648+
try:
649+
# Get failed pipelines from this project
650+
failed_pipelines = project.pipelines.list(
651+
status='failed',
652+
per_page=limit,
653+
order_by='updated_at',
654+
sort='desc'
655+
)
656+
657+
for pipeline in failed_pipelines:
658+
all_failed_pipelines.append({
659+
'project_id': project.id,
660+
'project_name': project.name,
661+
'project_path': project.path_with_namespace,
662+
'pipeline_id': pipeline.id,
663+
'status': pipeline.status,
664+
'ref': pipeline.ref,
665+
'sha': pipeline.sha,
666+
'source': getattr(pipeline, 'source', 'unknown'),
667+
'created_at': pipeline.created_at,
668+
'updated_at': pipeline.updated_at,
669+
'duration': pipeline.duration,
670+
'web_url': pipeline.web_url,
671+
'user': pipeline.user['name'] if hasattr(pipeline, 'user') and pipeline.user else 'System'
672+
})
673+
674+
except Exception as e:
675+
logger.warning(f"⚠️ Could not get pipelines for {project.path_with_namespace}: {e}")
676+
continue
677+
678+
# Sort all failed pipelines by updated_at (most recent first)
679+
all_failed_pipelines.sort(key=lambda x: x['updated_at'], reverse=True)
680+
681+
# Take only the requested limit
682+
limited_pipelines = all_failed_pipelines[:limit]
683+
684+
message = f"πŸ” Scanned {len(scanned_projects)} projects, found {len(limited_pipelines)} recent failed pipelines"
685+
686+
logger.info(f"βœ… Scanned {len(scanned_projects)} projects for failed pipelines")
687+
return {
688+
"success": True,
689+
"message": message,
690+
"scanned_projects_count": len(scanned_projects),
691+
"scanned_projects": scanned_projects,
692+
"failed_pipelines_count": len(limited_pipelines),
693+
"failed_pipelines": limited_pipelines,
694+
"total_failed_found": len(all_failed_pipelines)
695+
}
696+
697+
except Exception as e:
698+
logger.error(f"❌ Error scanning group failed pipelines: {e}")
699+
return {
700+
"success": False,
701+
"error": f"❌ Error scanning group failed pipelines: {str(e)}"
702+
}
703+
584704
@mcp.tool()
585705
async def list_jobs(request: JobListRequest) -> dict:
586706
"""βš™οΈ List jobs in a pipeline - supports both project_id and project_name"""
@@ -808,40 +928,24 @@ async def list_latest_commits(request: CommitsRequest) -> dict:
808928

809929
def main():
810930
"""Main entry point for the FastMCP server."""
811-
# Get transport mode from environment
812-
transport_mode = os.getenv("MCP_TRANSPORT", "stdio").lower()
813-
814931
print("πŸš€ Starting GitLab MCP Server (FastMCP - Read-Only)...")
815932
print("πŸ“‹ Available Tools:")
816933
print(" πŸ” search_repositories - Search/filter repositories")
817934
print(" πŸ“‹ list_issues - List project issues")
818935
print(" πŸ”€ list_merge_requests - List merge requests")
819936
print(" πŸ—οΈ list_pipelines - List CI/CD pipelines")
937+
print(" πŸ” scan_group_failed_pipelines - Scan all group projects for failed pipelines")
820938
print(" βš™οΈ list_jobs - List pipeline jobs")
821939
print(" ❌ check_latest_failed_jobs - Check failed jobs")
822940
print(" πŸ“ list_latest_commits - List recent commits")
823941
print(f"\nπŸ”’ Security: Max {config.max_api_calls_per_hour} calls/hour")
824942
print(f"πŸ”’ Actions: {config.allowed_actions}")
825943
print(f"πŸ”— GitLab: {config.gitlab_url}")
826944
print("πŸ“– Query Support: Both project_id and project_name supported for flexible access")
827-
print(f"πŸš€ Transport Mode: {transport_mode.upper()}")
828-
829-
if transport_mode == "http":
830-
# HTTP Transport configuration
831-
host = os.getenv("MCP_HOST", "127.0.0.1")
832-
port = int(os.getenv("MCP_PORT", "8085"))
833-
834-
print(f"🌐 Starting HTTP server on {host}:{port}")
835-
print("\nβœ… FastMCP HTTP Server ready for connections!")
836-
837-
# Run with HTTP transport
838-
mcp.run(transport="http", host=host, port=port)
839-
else:
840-
# Default STDIO transport
841-
print("\nβœ… FastMCP STDIO Server ready for connections!")
842-
843-
# Run with STDIO transport (default)
844-
mcp.run()
945+
print("\nβœ… FastMCP Read-Only Server ready for connections!")
946+
947+
# Run the FastMCP server
948+
mcp.run()
845949

846950
if __name__ == "__main__":
847-
main()
951+
main()

0 commit comments

Comments
Β (0)