@@ -57,7 +57,7 @@ def __init__(self):
57
57
58
58
# SSL Configuration options
59
59
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
61
61
62
62
# Build GitLab client configuration
63
63
gitlab_config = {
@@ -342,7 +342,25 @@ async def search_repositories(search_term: Optional[str] = None, visibility: str
342
342
# Filter results based on repository access permissions
343
343
accessible_projects = []
344
344
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 :
346
364
try :
347
365
repo_info = {
348
366
'id' : project .id ,
@@ -581,6 +599,108 @@ async def list_pipelines(request: PipelineListRequest) -> dict:
581
599
"error" : f"β Error listing pipelines: { str (e )} "
582
600
}
583
601
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
+
584
704
@mcp .tool ()
585
705
async def list_jobs (request : JobListRequest ) -> dict :
586
706
"""βοΈ List jobs in a pipeline - supports both project_id and project_name"""
@@ -808,40 +928,24 @@ async def list_latest_commits(request: CommitsRequest) -> dict:
808
928
809
929
def main ():
810
930
"""Main entry point for the FastMCP server."""
811
- # Get transport mode from environment
812
- transport_mode = os .getenv ("MCP_TRANSPORT" , "stdio" ).lower ()
813
-
814
931
print ("π Starting GitLab MCP Server (FastMCP - Read-Only)..." )
815
932
print ("π Available Tools:" )
816
933
print (" π search_repositories - Search/filter repositories" )
817
934
print (" π list_issues - List project issues" )
818
935
print (" π list_merge_requests - List merge requests" )
819
936
print (" ποΈ list_pipelines - List CI/CD pipelines" )
937
+ print (" π scan_group_failed_pipelines - Scan all group projects for failed pipelines" )
820
938
print (" βοΈ list_jobs - List pipeline jobs" )
821
939
print (" β check_latest_failed_jobs - Check failed jobs" )
822
940
print (" π list_latest_commits - List recent commits" )
823
941
print (f"\n π Security: Max { config .max_api_calls_per_hour } calls/hour" )
824
942
print (f"π Actions: { config .allowed_actions } " )
825
943
print (f"π GitLab: { config .gitlab_url } " )
826
944
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 ()
845
949
846
950
if __name__ == "__main__" :
847
- main ()
951
+ main ()
0 commit comments