Commit 8f3abd9
cmd/gitlab/DESIGN.md
@@ -0,0 +1,685 @@
+# GitLab MCP Server Design Document
+
+**Project:** GitLab MCP Server
+**Repository:** `https://github.com/xlgmokha/mcp`
+**Target Path:** `cmd/gitlab/` and `pkg/gitlab/`
+**Language:** Go
+**Version:** 1.0.0
+
+---
+
+## Overview
+
+This document outlines the design for a comprehensive GitLab MCP server that provides AI assistants with access to GitLab APIs for issue management, user activity tracking, project operations, and workflow automation. The design is informed by the existing Jive Ruby tool and follows the established Go MCP server patterns.
+
+## Project Context
+
+### Inspiration from Jive Tool
+The Jive Ruby tool (`/Users/xlgmokha/src/gitlab.com/mokhax/gitlab`) demonstrates several key patterns:
+- **Local-first workflow**: Sync GitLab data to local Markdown files with YAML frontmatter
+- **Interactive issue management**: Browse, edit, and sync issues back to GitLab
+- **Template-driven creation**: Use ERB templates for consistent issue creation
+- **Author-specific filtering**: Focus on specific authors and labels
+- **Activity tracking**: Export user activity to YAML files
+
+### MCP Architecture Alignment
+Following the established Go MCP server patterns:
+- Stateless tools that communicate via JSON-RPC over stdio
+- Comprehensive input validation and error handling
+- Structured logging and debugging capabilities
+- Clean separation of concerns with `pkg/gitlab/` for reusable logic
+- Thread-safe operations with proper concurrency handling
+
+---
+
+## Architecture
+
+### Directory Structure
+```
+cmd/gitlab/
+โโโ main.go # Entry point and CLI setup
+โโโ README.md # Usage documentation
+
+pkg/gitlab/
+โโโ server.go # MCP server implementation
+โโโ server_test.go # Server tests
+โโโ client.go # GitLab API client
+โโโ client_test.go # Client tests
+โโโ types.go # GitLab API types and structures
+โโโ cache.go # Local caching implementation
+โโโ templates.go # Issue template handling
+โโโ activity.go # User activity tracking
+```
+
+### Core Components
+
+#### 1. GitLab API Client (`pkg/gitlab/client.go`)
+- HTTP client with retry logic and rate limiting
+- Authentication via Personal Access Token
+- Pagination support for large result sets
+- Comprehensive error handling and logging
+- Support for gitlab.com and self-hosted instances
+
+#### 2. MCP Server (`pkg/gitlab/server.go`)
+- JSON-RPC 2.0 over stdio communication
+- Tool registration and request routing
+- Input validation and sanitization
+- Structured error responses
+- Configurable GitLab instance and authentication
+
+#### 3. Local Cache System (`pkg/gitlab/cache.go`)
+- Optional local storage for offline access
+- Markdown files with YAML frontmatter (compatible with Jive)
+- Sync detection and conflict resolution
+- Cache invalidation strategies
+
+#### 4. Template System (`pkg/gitlab/templates.go`)
+- Issue and MR templates with Go text/template
+- Support for existing ERB templates from Jive
+- Variable substitution and validation
+- Template library management
+
+---
+
+## Tools Specification
+
+### Core GitLab Operations
+
+#### `gitlab_get_user_info`
+Get current authenticated user information.
+```json
+{
+ "name": "gitlab_get_user_info",
+ "description": "Get information about the authenticated GitLab user",
+ "inputSchema": {
+ "type": "object",
+ "properties": {}
+ }
+}
+```
+
+#### `gitlab_list_groups`
+List accessible GitLab groups with optional filtering.
+```json
+{
+ "name": "gitlab_list_groups",
+ "description": "List GitLab groups accessible to the authenticated user",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "search": {"type": "string", "description": "Search term for group names"},
+ "owned": {"type": "boolean", "description": "Only groups owned by user", "default": false},
+ "per_page": {"type": "integer", "description": "Results per page", "default": 20, "maximum": 100}
+ }
+ }
+}
+```
+
+#### `gitlab_list_projects`
+List projects with advanced filtering options.
+```json
+{
+ "name": "gitlab_list_projects",
+ "description": "List GitLab projects with filtering options",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "group_id": {"type": "integer", "description": "Limit to specific group"},
+ "search": {"type": "string", "description": "Search term for project names"},
+ "owned": {"type": "boolean", "description": "Only owned projects", "default": false},
+ "membership": {"type": "boolean", "description": "Projects user is member of", "default": true},
+ "archived": {"type": "boolean", "description": "Include archived projects", "default": false},
+ "visibility": {"type": "string", "enum": ["private", "internal", "public"], "description": "Project visibility"},
+ "per_page": {"type": "integer", "description": "Results per page", "default": 20, "maximum": 100}
+ }
+ }
+}
+```
+
+### Issue Management
+
+#### `gitlab_list_issues`
+List issues with comprehensive filtering (inspired by Jive patterns).
+```json
+{
+ "name": "gitlab_list_issues",
+ "description": "List GitLab issues with advanced filtering options",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "project_id": {"type": "integer", "description": "Project ID"},
+ "group_id": {"type": "integer", "description": "Group ID (alternative to project_id)"},
+ "state": {"type": "string", "enum": ["opened", "closed", "all"], "default": "opened"},
+ "author_username": {"type": "string", "description": "Filter by author username"},
+ "assignee_username": {"type": "string", "description": "Filter by assignee username"},
+ "labels": {"type": "array", "items": {"type": "string"}, "description": "Filter by labels"},
+ "milestone": {"type": "string", "description": "Filter by milestone title"},
+ "search": {"type": "string", "description": "Search in title and description"},
+ "created_after": {"type": "string", "format": "date-time", "description": "Created after date"},
+ "created_before": {"type": "string", "format": "date-time", "description": "Created before date"},
+ "updated_after": {"type": "string", "format": "date-time", "description": "Updated after date"},
+ "updated_before": {"type": "string", "format": "date-time", "description": "Updated before date"},
+ "sort": {"type": "string", "enum": ["created_at", "updated_at", "priority", "due_date"], "default": "updated_at"},
+ "order": {"type": "string", "enum": ["asc", "desc"], "default": "desc"},
+ "per_page": {"type": "integer", "description": "Results per page", "default": 20, "maximum": 100}
+ }
+ }
+}
+```
+
+#### `gitlab_get_issue`
+Get detailed issue information including comments and metadata.
+```json
+{
+ "name": "gitlab_get_issue",
+ "description": "Get detailed information about a specific GitLab issue",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "project_id": {"type": "integer", "description": "Project ID"},
+ "issue_iid": {"type": "integer", "description": "Issue internal ID"},
+ "include_comments": {"type": "boolean", "description": "Include issue comments", "default": true},
+ "include_system_notes": {"type": "boolean", "description": "Include system notes", "default": false}
+ },
+ "required": ["project_id", "issue_iid"]
+ }
+}
+```
+
+#### `gitlab_create_issue`
+Create new issues with template support.
+```json
+{
+ "name": "gitlab_create_issue",
+ "description": "Create a new GitLab issue with optional template",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "project_id": {"type": "integer", "description": "Project ID"},
+ "title": {"type": "string", "description": "Issue title"},
+ "description": {"type": "string", "description": "Issue description"},
+ "template_name": {"type": "string", "description": "Template to use for issue creation"},
+ "template_variables": {"type": "object", "description": "Variables for template substitution"},
+ "labels": {"type": "array", "items": {"type": "string"}, "description": "Issue labels"},
+ "assignee_usernames": {"type": "array", "items": {"type": "string"}, "description": "Assignee usernames"},
+ "milestone_id": {"type": "integer", "description": "Milestone ID"},
+ "due_date": {"type": "string", "format": "date", "description": "Due date (YYYY-MM-DD)"},
+ "weight": {"type": "integer", "description": "Issue weight", "minimum": 1},
+ "confidential": {"type": "boolean", "description": "Mark as confidential", "default": false}
+ },
+ "required": ["project_id", "title"]
+ }
+}
+```
+
+#### `gitlab_update_issue`
+Update existing issues with change tracking.
+```json
+{
+ "name": "gitlab_update_issue",
+ "description": "Update an existing GitLab issue",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "project_id": {"type": "integer", "description": "Project ID"},
+ "issue_iid": {"type": "integer", "description": "Issue internal ID"},
+ "title": {"type": "string", "description": "New issue title"},
+ "description": {"type": "string", "description": "New issue description"},
+ "state_event": {"type": "string", "enum": ["close", "reopen"], "description": "State change"},
+ "labels": {"type": "array", "items": {"type": "string"}, "description": "Replace labels"},
+ "add_labels": {"type": "array", "items": {"type": "string"}, "description": "Add labels"},
+ "remove_labels": {"type": "array", "items": {"type": "string"}, "description": "Remove labels"},
+ "assignee_usernames": {"type": "array", "items": {"type": "string"}, "description": "Replace assignees"},
+ "milestone_id": {"type": "integer", "description": "Milestone ID (null to unset)"},
+ "due_date": {"type": "string", "format": "date", "description": "Due date (null to unset)"},
+ "weight": {"type": "integer", "description": "Issue weight"},
+ "confidential": {"type": "boolean", "description": "Mark as confidential"}
+ },
+ "required": ["project_id", "issue_iid"]
+ }
+}
+```
+
+#### `gitlab_add_issue_comment`
+Add comments to issues with formatting support.
+```json
+{
+ "name": "gitlab_add_issue_comment",
+ "description": "Add a comment to a GitLab issue",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "project_id": {"type": "integer", "description": "Project ID"},
+ "issue_iid": {"type": "integer", "description": "Issue internal ID"},
+ "body": {"type": "string", "description": "Comment body (Markdown supported)"},
+ "confidential": {"type": "boolean", "description": "Mark comment as confidential", "default": false}
+ },
+ "required": ["project_id", "issue_iid", "body"]
+ }
+}
+```
+
+### Merge Request Operations
+
+#### `gitlab_list_merge_requests`
+List merge requests with filtering options.
+```json
+{
+ "name": "gitlab_list_merge_requests",
+ "description": "List GitLab merge requests with filtering options",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "project_id": {"type": "integer", "description": "Project ID"},
+ "group_id": {"type": "integer", "description": "Group ID (alternative to project_id)"},
+ "state": {"type": "string", "enum": ["opened", "closed", "merged", "all"], "default": "opened"},
+ "author_username": {"type": "string", "description": "Filter by author username"},
+ "assignee_username": {"type": "string", "description": "Filter by assignee username"},
+ "reviewer_username": {"type": "string", "description": "Filter by reviewer username"},
+ "labels": {"type": "array", "items": {"type": "string"}, "description": "Filter by labels"},
+ "milestone": {"type": "string", "description": "Filter by milestone title"},
+ "source_branch": {"type": "string", "description": "Filter by source branch"},
+ "target_branch": {"type": "string", "description": "Filter by target branch"},
+ "search": {"type": "string", "description": "Search in title and description"},
+ "draft": {"type": "boolean", "description": "Filter draft MRs"},
+ "sort": {"type": "string", "enum": ["created_at", "updated_at", "priority"], "default": "updated_at"},
+ "order": {"type": "string", "enum": ["asc", "desc"], "default": "desc"},
+ "per_page": {"type": "integer", "description": "Results per page", "default": 20, "maximum": 100}
+ }
+ }
+}
+```
+
+#### `gitlab_get_merge_request`
+Get detailed merge request information.
+```json
+{
+ "name": "gitlab_get_merge_request",
+ "description": "Get detailed information about a specific GitLab merge request",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "project_id": {"type": "integer", "description": "Project ID"},
+ "merge_request_iid": {"type": "integer", "description": "Merge request internal ID"},
+ "include_comments": {"type": "boolean", "description": "Include MR comments", "default": true},
+ "include_commits": {"type": "boolean", "description": "Include commit list", "default": false},
+ "include_changes": {"type": "boolean", "description": "Include file changes", "default": false}
+ },
+ "required": ["project_id", "merge_request_iid"]
+ }
+}
+```
+
+#### `gitlab_create_merge_request`
+Create new merge requests from branches.
+```json
+{
+ "name": "gitlab_create_merge_request",
+ "description": "Create a new GitLab merge request",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "project_id": {"type": "integer", "description": "Project ID"},
+ "title": {"type": "string", "description": "Merge request title"},
+ "description": {"type": "string", "description": "Merge request description"},
+ "source_branch": {"type": "string", "description": "Source branch name"},
+ "target_branch": {"type": "string", "description": "Target branch name", "default": "main"},
+ "template_name": {"type": "string", "description": "Template to use for MR creation"},
+ "template_variables": {"type": "object", "description": "Variables for template substitution"},
+ "labels": {"type": "array", "items": {"type": "string"}, "description": "MR labels"},
+ "assignee_usernames": {"type": "array", "items": {"type": "string"}, "description": "Assignee usernames"},
+ "reviewer_usernames": {"type": "array", "items": {"type": "string"}, "description": "Reviewer usernames"},
+ "milestone_id": {"type": "integer", "description": "Milestone ID"},
+ "remove_source_branch": {"type": "boolean", "description": "Remove source branch when merged", "default": false},
+ "squash": {"type": "boolean", "description": "Squash commits when merged", "default": false},
+ "allow_collaboration": {"type": "boolean", "description": "Allow collaboration", "default": false}
+ },
+ "required": ["project_id", "title", "source_branch", "target_branch"]
+ }
+}
+```
+
+### User Activity Tracking (Inspired by Jive)
+
+#### `gitlab_get_user_activity`
+Get comprehensive user activity with export capabilities.
+```json
+{
+ "name": "gitlab_get_user_activity",
+ "description": "Get GitLab user activity with optional export to YAML format",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "username": {"type": "string", "description": "GitLab username"},
+ "start_date": {"type": "string", "format": "date", "description": "Activity start date (YYYY-MM-DD)"},
+ "end_date": {"type": "string", "format": "date", "description": "Activity end date (YYYY-MM-DD)"},
+ "activity_types": {
+ "type": "array",
+ "items": {"type": "string", "enum": ["push", "merge", "issue", "comment", "approve", "join"]},
+ "description": "Filter by activity types"
+ },
+ "projects": {"type": "array", "items": {"type": "integer"}, "description": "Filter by project IDs"},
+ "export_format": {"type": "string", "enum": ["json", "yaml"], "default": "json"},
+ "save_to_cache": {"type": "boolean", "description": "Save to local cache", "default": false},
+ "per_page": {"type": "integer", "description": "Results per page", "default": 50, "maximum": 100}
+ },
+ "required": ["username"]
+ }
+}
+```
+
+#### `gitlab_analyze_user_contributions`
+Analyze user contributions across projects and time periods.
+```json
+{
+ "name": "gitlab_analyze_user_contributions",
+ "description": "Analyze user contributions with statistics and patterns",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "username": {"type": "string", "description": "GitLab username"},
+ "start_date": {"type": "string", "format": "date", "description": "Analysis start date"},
+ "end_date": {"type": "string", "format": "date", "description": "Analysis end date"},
+ "project_ids": {"type": "array", "items": {"type": "integer"}, "description": "Limit to specific projects"},
+ "include_statistics": {"type": "boolean", "description": "Include detailed statistics", "default": true},
+ "group_by": {"type": "string", "enum": ["day", "week", "month"], "default": "week"},
+ "export_format": {"type": "string", "enum": ["json", "csv", "yaml"], "default": "json"}
+ },
+ "required": ["username"]
+ }
+}
+```
+
+### Local Cache Management (Jive-Compatible)
+
+#### `gitlab_sync_to_cache`
+Sync GitLab data to local Markdown cache (Jive-compatible format).
+```json
+{
+ "name": "gitlab_sync_to_cache",
+ "description": "Sync GitLab data to local Markdown cache with YAML frontmatter",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "cache_path": {"type": "string", "description": "Local cache directory path"},
+ "sync_target": {"type": "string", "enum": ["issues", "merge_requests", "projects", "all"], "default": "issues"},
+ "project_ids": {"type": "array", "items": {"type": "integer"}, "description": "Specific projects to sync"},
+ "group_ids": {"type": "array", "items": {"type": "integer"}, "description": "Specific groups to sync"},
+ "filters": {
+ "type": "object",
+ "properties": {
+ "state": {"type": "string", "enum": ["opened", "closed", "all"], "default": "opened"},
+ "author_usernames": {"type": "array", "items": {"type": "string"}},
+ "labels": {"type": "array", "items": {"type": "string"}},
+ "updated_after": {"type": "string", "format": "date-time"}
+ },
+ "description": "Filters for syncing data"
+ },
+ "force_update": {"type": "boolean", "description": "Force update existing cache files", "default": false},
+ "preserve_local_changes": {"type": "boolean", "description": "Preserve local modifications", "default": true}
+ },
+ "required": ["cache_path"]
+ }
+}
+```
+
+#### `gitlab_cache_status`
+Check local cache status and sync state.
+```json
+{
+ "name": "gitlab_cache_status",
+ "description": "Check status of local GitLab cache",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "cache_path": {"type": "string", "description": "Local cache directory path"},
+ "check_sync": {"type": "boolean", "description": "Check sync status with GitLab", "default": true},
+ "detailed": {"type": "boolean", "description": "Include detailed file information", "default": false}
+ },
+ "required": ["cache_path"]
+ }
+}
+```
+
+### Template Management
+
+#### `gitlab_list_templates`
+List available issue and MR templates.
+```json
+{
+ "name": "gitlab_list_templates",
+ "description": "List available GitLab templates for issues and merge requests",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "template_type": {"type": "string", "enum": ["issue", "merge_request", "all"], "default": "all"},
+ "project_id": {"type": "integer", "description": "Project ID for project-specific templates"},
+ "include_system": {"type": "boolean", "description": "Include system templates", "default": true},
+ "include_custom": {"type": "boolean", "description": "Include custom templates", "default": true}
+ }
+ }
+}
+```
+
+#### `gitlab_apply_template`
+Apply template to issue or MR creation.
+```json
+{
+ "name": "gitlab_apply_template",
+ "description": "Apply template with variable substitution",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "template_name": {"type": "string", "description": "Template name"},
+ "template_type": {"type": "string", "enum": ["issue", "merge_request"], "description": "Template type"},
+ "variables": {"type": "object", "description": "Template variables for substitution"},
+ "project_id": {"type": "integer", "description": "Project ID for project-specific templates"}
+ },
+ "required": ["template_name", "template_type"]
+ }
+}
+```
+
+### Search and Discovery
+
+#### `gitlab_search`
+Global search across GitLab content.
+```json
+{
+ "name": "gitlab_search",
+ "description": "Search across GitLab projects, issues, merge requests, and code",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "query": {"type": "string", "description": "Search query"},
+ "scope": {
+ "type": "string",
+ "enum": ["projects", "issues", "merge_requests", "milestones", "users", "code", "commits", "blobs", "wiki_blobs"],
+ "description": "Search scope"
+ },
+ "project_ids": {"type": "array", "items": {"type": "integer"}, "description": "Limit search to specific projects"},
+ "group_ids": {"type": "array", "items": {"type": "integer"}, "description": "Limit search to specific groups"},
+ "filters": {
+ "type": "object",
+ "properties": {
+ "state": {"type": "string", "enum": ["opened", "closed", "merged", "all"]},
+ "confidential": {"type": "boolean", "description": "Include confidential items"},
+ "order_by": {"type": "string", "enum": ["created_at", "updated_at", "title"], "default": "updated_at"},
+ "sort": {"type": "string", "enum": ["asc", "desc"], "default": "desc"}
+ }
+ },
+ "per_page": {"type": "integer", "description": "Results per page", "default": 20, "maximum": 100}
+ },
+ "required": ["query", "scope"]
+ }
+}
+```
+
+---
+
+## Configuration
+
+### Environment Variables
+- `GITLAB_TOKEN`: Personal Access Token for authentication (required)
+- `GITLAB_URL`: GitLab instance URL (default: "https://gitlab.com")
+- `GITLAB_API_VERSION`: API version (default: "v4")
+- `GITLAB_CACHE_DIR`: Default cache directory for local storage
+- `GITLAB_TIMEOUT`: Request timeout in seconds (default: 30)
+- `GITLAB_RATE_LIMIT`: Requests per minute (default: 1000)
+
+### Command Line Arguments
+```bash
+mcp-gitlab [options]
+
+Options:
+ --gitlab-url string GitLab instance URL (default "https://gitlab.com")
+ --gitlab-token string Personal Access Token (overrides env var)
+ --cache-dir string Local cache directory
+ --template-dir string Custom template directory
+ --debug Enable debug logging
+ --config string Configuration file path
+```
+
+### Configuration File Support
+YAML configuration file for complex setups:
+```yaml
+gitlab:
+ url: "https://gitlab.company.com"
+ token: "${GITLAB_TOKEN}"
+ timeout: 30
+ rate_limit: 1000
+
+cache:
+ enabled: true
+ directory: "./gitlab-cache"
+ format: "markdown" # markdown (Jive-compatible) or json
+
+templates:
+ directory: "./templates"
+ default_issue_template: "bug_report"
+ default_mr_template: "feature"
+
+defaults:
+ group_id: 9970
+ project_ids: [278964, 66499760, 68877410, 69516684]
+ authors: ["xlgmokha", "mokhax"]
+ labels: ["group::authorization"]
+
+logging:
+ level: "info"
+ format: "json"
+```
+
+---
+
+## Implementation Details
+
+### Authentication and Security
+- Personal Access Token authentication
+- Support for token rotation and refresh
+- Secure token storage (environment variables, not in config files)
+- Rate limiting and retry logic with exponential backoff
+- Request timeout and circuit breaker patterns
+
+### Local Cache System
+- **Jive Compatibility**: Store issues as Markdown files with YAML frontmatter
+- **Sync Detection**: Compare timestamps and checksums for changes
+- **Conflict Resolution**: Preserve local changes with merge conflict markers
+- **Cache Structure**:
+ ```
+ cache/
+ โโโ groups/
+ โ โโโ {group_id}/
+ โโโ projects/
+ โ โโโ {project_id}/
+ โโโ issues/
+ โ โโโ {project_id}/
+ โ โโโ {issue_iid}.md
+ โโโ merge_requests/
+ โ โโโ {project_id}/
+ โ โโโ {mr_iid}.md
+ โโโ users/
+ โโโ {username}/
+ โโโ activity/
+ โโโ {date}.yml
+ ```
+
+### Template System
+- **Go text/template** engine for variable substitution
+- **ERB Compatibility**: Convert existing Jive ERB templates
+- **Variable Resolution**: Support environment variables and user input
+- **Template Libraries**: System and project-specific templates
+- **Validation**: Schema validation for template variables
+
+### Error Handling and Resilience
+- Comprehensive GitLab API error mapping
+- Graceful degradation for network issues
+- Request retry with exponential backoff
+- Circuit breaker for service availability
+- Detailed error messages with suggested actions
+
+### Performance Optimization
+- **Connection Pooling**: Reuse HTTP connections
+- **Caching**: Local cache for frequently accessed data
+- **Pagination**: Efficient handling of large result sets
+- **Concurrent Requests**: Parallel processing where safe
+- **Memory Management**: Streaming for large responses
+
+### Testing Strategy
+- **Unit Tests**: All tools and utility functions
+- **Integration Tests**: GitLab API interactions with test instance
+- **Mock Testing**: Offline testing with recorded responses
+- **Performance Tests**: Load testing and benchmarking
+- **Cache Tests**: Sync and conflict resolution scenarios
+
+---
+
+## Migration from Jive
+
+### Data Migration
+1. **Cache Format**: Convert existing Jive cache to MCP-compatible format
+2. **Templates**: Convert ERB templates to Go text/template format
+3. **Configuration**: Map Jive environment variables to MCP config
+4. **Scripts**: Provide migration utilities for seamless transition
+
+### Workflow Integration
+1. **Backward Compatibility**: Support existing Jive cache structure
+2. **Incremental Migration**: Allow gradual transition from Jive to MCP
+3. **Feature Parity**: Ensure all Jive features are available in MCP server
+4. **Enhanced Capabilities**: Leverage MCP for AI-assisted workflows
+
+---
+
+## Future Enhancements
+
+### Phase 2 Features
+- **GitLab CI/CD Integration**: Pipeline management and monitoring
+- **Wiki Management**: GitLab wiki content operations
+- **Repository Operations**: File and branch management
+- **Notification System**: Real-time updates and webhooks
+- **Advanced Analytics**: Project and team productivity metrics
+
+### AI Assistant Integration
+- **Smart Suggestions**: AI-powered issue and MR recommendations
+- **Automated Labeling**: Intelligent label assignment
+- **Code Review Assistant**: AI-enhanced code review workflows
+- **Progress Tracking**: Automated status updates and reporting
+- **Workflow Automation**: Custom automation based on patterns
+
+---
+
+## Success Metrics
+
+### Technical Metrics
+- **Response Time**: < 100ms for cached operations, < 2s for API calls
+- **Reliability**: 99.9% uptime with proper error handling
+- **Compatibility**: 100% feature parity with Jive tool
+- **Performance**: Handle 1000+ issues/MRs without memory issues
+
+### User Experience Metrics
+- **Migration Time**: < 30 minutes from Jive to MCP setup
+- **Learning Curve**: < 1 hour to become productive with AI assistant
+- **Workflow Efficiency**: 50% reduction in manual GitLab operations
+- **Error Recovery**: Clear error messages with actionable solutions
+
+This design provides a comprehensive foundation for building a GitLab MCP server that combines the proven patterns from the Jive tool with the modern MCP architecture, enabling powerful AI-assisted GitLab workflows.
\ No newline at end of file
cmd/gitlab/main.go
@@ -0,0 +1,88 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/xlgmokha/mcp/pkg/gitlab"
+)
+
+func main() {
+ var (
+ gitlabURL = flag.String("gitlab-url", "https://gitlab.com", "GitLab instance URL")
+ gitlabToken = flag.String("gitlab-token", "", "GitLab Personal Access Token (overrides GITLAB_TOKEN env var)")
+ help = flag.Bool("help", false, "Show help information")
+ )
+ flag.Parse()
+
+ if *help {
+ fmt.Printf(`GitLab MCP Server
+
+This server provides access to GitLab APIs for issue management, project tracking,
+and workflow automation designed for GitLab software engineers.
+
+Usage: %s [options]
+
+Options:
+`, os.Args[0])
+ flag.PrintDefaults()
+ fmt.Print(`
+Examples:
+ # Use GITLAB_TOKEN environment variable (recommended)
+ export-access-token && mcp-gitlab
+
+ # Specify token directly
+ mcp-gitlab --gitlab-token your_token_here
+
+ # Use with self-hosted GitLab instance
+ mcp-gitlab --gitlab-url https://gitlab.company.com
+
+Tools:
+ - gitlab_list_my_projects: List projects you have access to with activity info
+ - gitlab_list_my_issues: Issues assigned/authored/mentioned, prioritized by activity
+ - gitlab_get_issue_conversations: Full conversation threads with participants
+ - gitlab_find_similar_issues: Cross-project similarity search using AI
+ - gitlab_get_my_activity: Recent activity summary and triage assistance
+
+Environment Variables:
+ - GITLAB_TOKEN: Personal Access Token (use with export-access-token script)
+ - GITLAB_URL: GitLab instance URL (default: https://gitlab.com)
+
+For GitLab software engineers: This server integrates with your existing
+export-access-token workflow and provides AI-assisted organization of your
+GitLab work across multiple projects.
+
+For more information, visit: https://github.com/xlgmokha/mcp
+`)
+ return
+ }
+
+ // Get GitLab token from flag or environment variable
+ token := *gitlabToken
+ if token == "" {
+ token = os.Getenv("GITLAB_TOKEN")
+ if token == "" {
+ log.Fatal("GitLab token required. Set GITLAB_TOKEN environment variable or use --gitlab-token flag.\n" +
+ "For GitLab employees: run 'export-access-token' first, then start the server.")
+ }
+ }
+
+ // Get GitLab URL from flag or environment variable
+ url := *gitlabURL
+ if envURL := os.Getenv("GITLAB_URL"); envURL != "" {
+ url = envURL
+ }
+
+ server, err := gitlab.NewServer(url, token)
+ if err != nil {
+ log.Fatalf("Failed to create GitLab server: %v", err)
+ }
+
+ ctx := context.Background()
+ if err := server.Run(ctx); err != nil {
+ log.Fatalf("Server error: %v", err)
+ }
+}
\ No newline at end of file
cmd/gitlab/README.md
@@ -0,0 +1,287 @@
+# GitLab MCP Server
+
+A comprehensive GitLab MCP server designed specifically for GitLab software engineers to manage their daily workflow with AI assistance.
+
+## Overview
+
+This server provides AI-powered access to GitLab APIs for:
+- **Project Discovery**: Find and organize projects you have access to
+- **Issue Management**: Track assigned, authored, and mentioned issues
+- **Conversation Tracking**: Follow issue discussions and threads
+- **Cross-Project Search**: Find similar issues across all your projects
+- **Activity Triage**: Get intelligent summaries of your recent work
+
+Perfect for GitLab employees who need to stay organized across multiple projects and issue threads.
+
+## Installation
+
+### Build and Install
+
+```bash
+# Build the GitLab server
+make gitlab
+
+# Install to system (optional)
+sudo make install
+```
+
+### Setup Authentication
+
+The server integrates with your existing GitLab authentication workflow:
+
+```bash
+# For GitLab employees (recommended)
+export-access-token # Sets GITLAB_TOKEN from your pass database
+mcp-gitlab # Start the server
+
+# Or set token manually
+export GITLAB_TOKEN=your_personal_access_token
+mcp-gitlab
+
+# Or specify token as argument
+mcp-gitlab --gitlab-token your_personal_access_token
+```
+
+## Configuration
+
+### Environment Variables
+
+- **`GITLAB_TOKEN`**: Personal Access Token (required)
+- **`GITLAB_URL`**: GitLab instance URL (default: https://gitlab.com)
+
+### Command Line Options
+
+```bash
+mcp-gitlab [options]
+
+Options:
+ --gitlab-url string GitLab instance URL (default "https://gitlab.com")
+ --gitlab-token string Personal Access Token (overrides GITLAB_TOKEN)
+ --help Show help information
+```
+
+## Tools Reference
+
+### 1. `gitlab_list_my_projects`
+List projects you have access to with activity information.
+
+**Parameters:**
+- `limit` (string, optional): Number of projects to return (default: 20, max: 100)
+- `search` (string, optional): Search term for project names
+- `membership` (bool, optional): Only projects you're a member of
+- `archived` (bool, optional): Include archived projects
+
+**Example Output:**
+```
+Your GitLab Projects (15 found):
+
+**1. gitlab-org/gitlab**
+ ๐ https://gitlab.com/gitlab-org/gitlab
+ ๐ 342 open issues | โญ 23.1k stars | ๐ด 5.2k forks
+ ๐
Last activity: 2024-12-23 14:30
+ ๐ The GitLab DevOps Platform...
+```
+
+### 2. `gitlab_list_my_issues`
+List issues assigned, authored, or mentioning you.
+
+**Parameters:**
+- `limit` (string, optional): Number of issues to return (default: 20, max: 100)
+- `scope` (string, optional): "assigned_to_me", "authored_by_me", "all_involving_me" (default: "assigned_to_me")
+- `state` (string, optional): "opened", "closed", "all" (default: "opened")
+- `search` (string, optional): Search term in issue titles/descriptions
+
+**Example Output:**
+```
+Your GitLab Issues (assigned_to_me, 8 found):
+
+**1. Fix authentication bug in SAML flow** ๐ฏ
+ ๐ gitlab-org/gitlab #123456
+ ๐ https://gitlab.com/gitlab-org/gitlab/-/issues/123456
+ ๐ค Author: John Doe | ๐ฅ Assigned: You
+ ๐ท๏ธ bug, authentication, saml
+ ๐ฌ 12 comments | ๐
Updated: 2024-12-23 10:15
+```
+
+### 3. `gitlab_get_issue_conversations`
+Get detailed conversation thread for a specific issue.
+
+**Parameters:**
+- `project_id` (string, required): Project ID
+- `issue_iid` (string, required): Issue internal ID
+- `include_system_notes` (bool, optional): Include system notes (default: false)
+
+**Example Output:**
+```
+**Issue Conversation: Fix authentication bug in SAML flow**
+๐ gitlab-org/gitlab #123456 | ๐ https://gitlab.com/gitlab-org/gitlab/-/issues/123456
+
+**Original Issue** - John Doe (2024-12-20 09:30)
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+Users are experiencing authentication failures when using SAML...
+
+**Issue Details:**
+โข **State:** opened
+โข **Assignees:** You
+โข **Labels:** bug, authentication, saml
+โข **Total Comments:** 12
+
+**Conversation Thread (12 comments):**
+
+๐ฌ **Comment 1** - Jane Smith (2024-12-20 11:15)
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+I can reproduce this issue on staging...
+```
+
+### 4. `gitlab_find_similar_issues`
+Search for similar issues across all your accessible projects.
+
+**Parameters:**
+- `query` (string, required): Search query terms
+- `limit` (string, optional): Number of results (default: 10, max: 50)
+- `scope` (string, optional): Search scope (default: "issues")
+- `include_closed` (string, optional): Include closed issues ("true"/"false")
+
+**Example Output:**
+```
+**Similar Issues Found for: "authentication SAML"**
+Found 5 issues across 3 projects:
+
+**๐๏ธ Project 1: gitlab-org/gitlab** (3 issues)
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+1. ๐ข **SAML authentication timeout issues** opened
+ ๐ #123457 | ๐ค Alice Cooper | ๐ฅ Bob Wilson
+ ๐ท๏ธ authentication, saml, timeout
+ ๐ https://gitlab.com/gitlab-org/gitlab/-/issues/123457
+ ๐
Updated: 2024-12-22 16:45
+ ๐ Users experiencing timeouts during SAML auth...
+```
+
+### 5. `gitlab_get_my_activity`
+Get comprehensive activity summary with intelligent triage.
+
+**Parameters:**
+- `limit` (string, optional): Number of items to analyze (default: 20, max: 100)
+- `days` (string, optional): Days of history to analyze (default: 7, max: 30)
+
+**Example Output:**
+```
+**GitLab Activity Summary for Your Name**
+๐
Last 7 days | ๐ค @your-username
+
+**๐ Quick Summary:**
+โข Assigned Issues: 8 open
+โข Authored Issues: 3 open
+โข Recent Activity Events: 15
+
+**๐ฏ Items Needing Attention:**
+๐ฌ **Issues with Recent Comments:**
+ โข gitlab-org/gitlab #123456 - Fix auth bug (5 comments)
+ โข security/security #789 - Review MR permissions (2 comments)
+
+**๐ Assigned Issues (8):**
+1. **Fix authentication bug in SAML flow** - gitlab-org/gitlab #123456
+ ๐
Dec 23 | ๐ฌ 12 comments | ๐ท๏ธ bug, authentication, saml
+```
+
+## Usage Examples
+
+### Daily Workflow Startup
+```bash
+# 1. Set up your environment
+export-access-token
+
+# 2. Start the MCP server
+mcp-gitlab
+
+# 3. In Claude Code, you can now ask:
+# - "What issues are assigned to me?"
+# - "Show me the latest comments on issue #123456 in project 12345"
+# - "Find similar issues to 'authentication timeout'"
+# - "Give me my activity summary for this week"
+```
+
+### Integration with Claude Code
+
+Add to your Claude Code configuration (~/.claude.json):
+
+```json
+{
+ "mcpServers": {
+ "gitlab": {
+ "command": "/usr/local/bin/mcp-gitlab"
+ }
+ }
+}
+```
+
+Then you can use natural language with Claude:
+
+- **"What GitLab work do I need to focus on today?"** โ Uses `gitlab_get_my_activity`
+- **"Show me projects I'm working on"** โ Uses `gitlab_list_my_projects`
+- **"Find issues similar to this authentication bug"** โ Uses `gitlab_find_similar_issues`
+- **"What's the latest discussion on issue #123 in project 456?"** โ Uses `gitlab_get_issue_conversations`
+
+## Benefits for GitLab Engineers
+
+### ๐ฏ **Workflow Organization**
+- **Intelligent Triage**: Get AI-powered summaries of what needs attention
+- **Cross-Project Visibility**: See all your work across GitLab's many projects
+- **Priority Detection**: Automatically identify overdue issues and recent activity
+
+### ๐ **Discovery & Context**
+- **Similar Issue Finding**: Avoid duplicates and find related work
+- **Conversation Catching**: Never miss important discussions
+- **Project Discovery**: Find relevant projects and track activity
+
+### ๐ค **AI Integration**
+- **Natural Language Interface**: Ask Claude about your GitLab work
+- **Smart Summarization**: Get context-aware summaries of issues and conversations
+- **Pattern Recognition**: AI helps identify relationships and priorities
+
+### โก **Efficiency Gains**
+- **Single Interface**: Access all GitLab data through Claude Code
+- **Reduced Context Switching**: Stay in your development environment
+- **Automated Organization**: Let AI handle the triage and organization
+
+## Troubleshooting
+
+### Authentication Issues
+```bash
+# Check if token is set
+echo $GITLAB_TOKEN
+
+# Test token validity
+curl -H "Authorization: Bearer $GITLAB_TOKEN" https://gitlab.com/api/v4/user
+
+# For GitLab employees, ensure export-access-token works
+export-access-token
+echo $GITLAB_TOKEN # Should show your token
+```
+
+### Connection Issues
+```bash
+# Test with custom GitLab URL
+mcp-gitlab --gitlab-url https://your-gitlab-instance.com
+
+# Check GitLab API access
+curl -H "Authorization: Bearer $GITLAB_TOKEN" https://your-gitlab-instance.com/api/v4/projects
+```
+
+### Server Issues
+```bash
+# Test basic server functionality
+echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' | mcp-gitlab
+
+# Expected: List of 5 tools
+```
+
+## Contributing
+
+This GitLab MCP server is part of the larger MCP project. See the main project README for contribution guidelines.
+
+## Support
+
+For GitLab employees: This server is designed to integrate seamlessly with your existing `export-access-token` workflow and GitLab development environment.
+
+For issues or feature requests, please file an issue in the main project repository.
\ No newline at end of file
pkg/gitlab/server.go
@@ -0,0 +1,804 @@
+package gitlab
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/xlgmokha/mcp/pkg/mcp"
+)
+
+type Server struct {
+ *mcp.Server
+ mu sync.RWMutex
+ gitlabURL string
+ token string
+ client *http.Client
+}
+
+type GitLabProject struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ NameWithNamespace string `json:"name_with_namespace"`
+ Path string `json:"path"`
+ PathWithNamespace string `json:"path_with_namespace"`
+ WebURL string `json:"web_url"`
+ Description string `json:"description"`
+ Visibility string `json:"visibility"`
+ LastActivityAt time.Time `json:"last_activity_at"`
+ CreatedAt time.Time `json:"created_at"`
+ Namespace struct {
+ Name string `json:"name"`
+ Path string `json:"path"`
+ } `json:"namespace"`
+ OpenIssuesCount int `json:"open_issues_count"`
+ ForksCount int `json:"forks_count"`
+ StarCount int `json:"star_count"`
+}
+
+type GitLabIssue struct {
+ ID int `json:"id"`
+ IID int `json:"iid"`
+ ProjectID int `json:"project_id"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ State string `json:"state"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ WebURL string `json:"web_url"`
+ Author struct {
+ ID int `json:"id"`
+ Username string `json:"username"`
+ Name string `json:"name"`
+ } `json:"author"`
+ Assignees []struct {
+ ID int `json:"id"`
+ Username string `json:"username"`
+ Name string `json:"name"`
+ } `json:"assignees"`
+ Labels []string `json:"labels"`
+ UserNotesCount int `json:"user_notes_count"`
+ DueDate *string `json:"due_date"`
+ TimeStats struct {
+ TimeEstimate int `json:"time_estimate"`
+ TotalTimeSpent int `json:"total_time_spent"`
+ } `json:"time_stats"`
+}
+
+type GitLabNote struct {
+ ID int `json:"id"`
+ Body string `json:"body"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ Author struct {
+ ID int `json:"id"`
+ Username string `json:"username"`
+ Name string `json:"name"`
+ } `json:"author"`
+ System bool `json:"system"`
+ NoteableType string `json:"noteable_type"`
+ NoteableID int `json:"noteable_id"`
+}
+
+type GitLabUser struct {
+ ID int `json:"id"`
+ Username string `json:"username"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ State string `json:"state"`
+}
+
+func NewServer(gitlabURL, token string) (*Server, error) {
+ baseServer := mcp.NewServer("gitlab", "0.1.0")
+
+ server := &Server{
+ Server: baseServer,
+ gitlabURL: strings.TrimSuffix(gitlabURL, "/"),
+ token: token,
+ client: &http.Client{
+ Timeout: 30 * time.Second,
+ },
+ }
+
+ // Register tools for GitLab employee workflow
+ server.RegisterTool("gitlab_list_my_projects", server.handleListMyProjects)
+ server.RegisterTool("gitlab_list_my_issues", server.handleListMyIssues)
+ server.RegisterTool("gitlab_get_issue_conversations", server.handleGetIssueConversations)
+ server.RegisterTool("gitlab_find_similar_issues", server.handleFindSimilarIssues)
+ server.RegisterTool("gitlab_get_my_activity", server.handleGetMyActivity)
+
+ return server, nil
+}
+
+func (s *Server) makeRequest(method, endpoint string, params map[string]string) ([]byte, error) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ url := fmt.Sprintf("%s/api/v4%s", s.gitlabURL, endpoint)
+
+ req, err := http.NewRequest(method, url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("Authorization", "Bearer "+s.token)
+ req.Header.Set("Content-Type", "application/json")
+
+ // Add query parameters
+ if len(params) > 0 {
+ q := req.URL.Query()
+ for key, value := range params {
+ q.Add(key, value)
+ }
+ req.URL.RawQuery = q.Encode()
+ }
+
+ resp, err := s.client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("request failed: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 400 {
+ return nil, fmt.Errorf("GitLab API error: %s", resp.Status)
+ }
+
+ // Read response body
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+
+ return body, nil
+}
+
+func (s *Server) getCurrentUser() (*GitLabUser, error) {
+ body, err := s.makeRequest("GET", "/user", nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var user GitLabUser
+ if err := json.Unmarshal(body, &user); err != nil {
+ return nil, fmt.Errorf("failed to parse user response: %w", err)
+ }
+
+ return &user, nil
+}
+
+func (s *Server) handleListMyProjects(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ args := req.Arguments
+
+ // Parse optional parameters
+ limitStr, _ := args["limit"].(string)
+ limit := 20 // Default limit
+ if limitStr != "" {
+ if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
+ limit = l
+ }
+ }
+
+ searchTerm, _ := args["search"].(string)
+ membershipOnly, _ := args["membership"].(bool)
+ includeArchived, _ := args["archived"].(bool)
+
+ // Build API parameters
+ params := map[string]string{
+ "per_page": strconv.Itoa(limit),
+ "simple": "true",
+ "order_by": "last_activity_at",
+ "sort": "desc",
+ }
+
+ if searchTerm != "" {
+ params["search"] = searchTerm
+ }
+
+ if membershipOnly {
+ params["membership"] = "true"
+ }
+
+ if !includeArchived {
+ params["archived"] = "false"
+ }
+
+ // Make API request
+ body, err := s.makeRequest("GET", "/projects", params)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to fetch projects: %v", err)), nil
+ }
+
+ var projects []GitLabProject
+ if err := json.Unmarshal(body, &projects); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to parse projects response: %v", err)), nil
+ }
+
+ // Format response for user
+ result := fmt.Sprintf("Your GitLab Projects (%d found):\n\n", len(projects))
+
+ if len(projects) == 0 {
+ result += "No projects found matching your criteria.\n"
+ } else {
+ for i, project := range projects {
+ lastActivity := project.LastActivityAt.Format("2006-01-02 15:04")
+ result += fmt.Sprintf("**%d. %s**\n", i+1, project.NameWithNamespace)
+ result += fmt.Sprintf(" ๐ %s\n", project.WebURL)
+ result += fmt.Sprintf(" ๐ %d open issues | โญ %d stars | ๐ด %d forks\n",
+ project.OpenIssuesCount, project.StarCount, project.ForksCount)
+ result += fmt.Sprintf(" ๐
Last activity: %s\n", lastActivity)
+ if project.Description != "" {
+ // Truncate long descriptions
+ desc := project.Description
+ if len(desc) > 100 {
+ desc = desc[:97] + "..."
+ }
+ result += fmt.Sprintf(" ๐ %s\n", desc)
+ }
+ result += "\n"
+ }
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(result)), nil
+}
+
+func (s *Server) handleListMyIssues(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ args := req.Arguments
+
+ // Parse optional parameters
+ limitStr, _ := args["limit"].(string)
+ limit := 20 // Default limit
+ if limitStr != "" {
+ if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
+ limit = l
+ }
+ }
+
+ scope, _ := args["scope"].(string)
+ if scope == "" {
+ scope = "assigned_to_me" // Default: issues assigned to user
+ }
+
+ state, _ := args["state"].(string)
+ if state == "" {
+ state = "opened" // Default: only open issues
+ }
+
+ searchTerm, _ := args["search"].(string)
+
+ // Get current user for filtering
+ user, err := s.getCurrentUser()
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to get current user: %v", err)), nil
+ }
+
+ // Build API parameters based on scope
+ params := map[string]string{
+ "per_page": strconv.Itoa(limit),
+ "state": state,
+ "order_by": "updated_at",
+ "sort": "desc",
+ }
+
+ switch scope {
+ case "assigned_to_me":
+ params["assignee_username"] = user.Username
+ case "authored_by_me":
+ params["author_username"] = user.Username
+ case "all_involving_me":
+ // This will require multiple API calls or use a different endpoint
+ params["scope"] = "all"
+ default:
+ params["assignee_username"] = user.Username
+ }
+
+ if searchTerm != "" {
+ params["search"] = searchTerm
+ }
+
+ // Make API request
+ body, err := s.makeRequest("GET", "/issues", params)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to fetch issues: %v", err)), nil
+ }
+
+ var issues []GitLabIssue
+ if err := json.Unmarshal(body, &issues); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to parse issues response: %v", err)), nil
+ }
+
+ // Format response for user
+ result := fmt.Sprintf("Your GitLab Issues (%s, %d found):\n\n", scope, len(issues))
+
+ if len(issues) == 0 {
+ result += "No issues found matching your criteria.\n"
+ } else {
+ for i, issue := range issues {
+ updatedAt := issue.UpdatedAt.Format("2006-01-02 15:04")
+
+ // Get project name from web URL (quick extraction)
+ projectName := extractProjectFromURL(issue.WebURL)
+
+ result += fmt.Sprintf("**%d. %s** ๐ฏ\n", i+1, issue.Title)
+ result += fmt.Sprintf(" ๐ %s #%d\n", projectName, issue.IID)
+ result += fmt.Sprintf(" ๐ %s\n", issue.WebURL)
+ result += fmt.Sprintf(" ๐ค Author: %s", issue.Author.Name)
+
+ // Show assignees
+ if len(issue.Assignees) > 0 {
+ assigneeNames := make([]string, len(issue.Assignees))
+ for j, assignee := range issue.Assignees {
+ assigneeNames[j] = assignee.Name
+ }
+ result += fmt.Sprintf(" | ๐ฅ Assigned: %s", strings.Join(assigneeNames, ", "))
+ }
+ result += "\n"
+
+ // Show labels if any
+ if len(issue.Labels) > 0 {
+ result += fmt.Sprintf(" ๐ท๏ธ %s\n", strings.Join(issue.Labels, ", "))
+ }
+
+ result += fmt.Sprintf(" ๐ฌ %d comments | ๐
Updated: %s\n",
+ issue.UserNotesCount, updatedAt)
+
+ // Show due date if set
+ if issue.DueDate != nil && *issue.DueDate != "" {
+ result += fmt.Sprintf(" โฐ Due: %s\n", *issue.DueDate)
+ }
+
+ result += "\n"
+ }
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(result)), nil
+}
+
+// Helper function to extract project name from GitLab issue URL
+func extractProjectFromURL(webURL string) string {
+ // URL format: https://gitlab.com/namespace/project/-/issues/123
+ parts := strings.Split(webURL, "/")
+ if len(parts) >= 5 {
+ return fmt.Sprintf("%s/%s", parts[len(parts)-4], parts[len(parts)-3])
+ }
+ return "Unknown"
+}
+
+func (s *Server) handleGetIssueConversations(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ args := req.Arguments
+
+ projectIDStr, ok := args["project_id"].(string)
+ if !ok {
+ return mcp.NewToolError("project_id parameter is required"), nil
+ }
+
+ issueIIDStr, ok := args["issue_iid"].(string)
+ if !ok {
+ return mcp.NewToolError("issue_iid parameter is required"), nil
+ }
+
+ projectID, err := strconv.Atoi(projectIDStr)
+ if err != nil {
+ return mcp.NewToolError("project_id must be a valid integer"), nil
+ }
+
+ issueIID, err := strconv.Atoi(issueIIDStr)
+ if err != nil {
+ return mcp.NewToolError("issue_iid must be a valid integer"), nil
+ }
+
+ includeSystemNotes, _ := args["include_system_notes"].(bool)
+
+ // First, get the issue details
+ issueEndpoint := fmt.Sprintf("/projects/%d/issues/%d", projectID, issueIID)
+ issueBody, err := s.makeRequest("GET", issueEndpoint, nil)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to fetch issue: %v", err)), nil
+ }
+
+ var issue GitLabIssue
+ if err := json.Unmarshal(issueBody, &issue); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to parse issue response: %v", err)), nil
+ }
+
+ // Get issue notes (comments)
+ notesEndpoint := fmt.Sprintf("/projects/%d/issues/%d/notes", projectID, issueIID)
+ notesParams := map[string]string{
+ "order_by": "created_at",
+ "sort": "asc",
+ }
+
+ notesBody, err := s.makeRequest("GET", notesEndpoint, notesParams)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to fetch issue notes: %v", err)), nil
+ }
+
+ var notes []GitLabNote
+ if err := json.Unmarshal(notesBody, ¬es); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to parse notes response: %v", err)), nil
+ }
+
+ // Filter notes based on preferences
+ var filteredNotes []GitLabNote
+ for _, note := range notes {
+ if !includeSystemNotes && note.System {
+ continue
+ }
+ filteredNotes = append(filteredNotes, note)
+ }
+
+ // Format the conversation
+ projectName := extractProjectFromURL(issue.WebURL)
+ result := fmt.Sprintf("**Issue Conversation: %s**\n", issue.Title)
+ result += fmt.Sprintf("๐ %s #%d | ๐ %s\n\n", projectName, issue.IID, issue.WebURL)
+
+ // Issue description
+ result += fmt.Sprintf("**Original Issue** - %s (%s)\n",
+ issue.Author.Name, issue.CreatedAt.Format("2006-01-02 15:04"))
+ result += "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n"
+ if issue.Description != "" {
+ result += fmt.Sprintf("%s\n", issue.Description)
+ } else {
+ result += "_No description provided._\n"
+ }
+ result += "\n"
+
+ // Issue metadata
+ result += "**Issue Details:**\n"
+ result += fmt.Sprintf("โข **State:** %s\n", issue.State)
+ if len(issue.Assignees) > 0 {
+ assigneeNames := make([]string, len(issue.Assignees))
+ for i, assignee := range issue.Assignees {
+ assigneeNames[i] = assignee.Name
+ }
+ result += fmt.Sprintf("โข **Assignees:** %s\n", strings.Join(assigneeNames, ", "))
+ }
+ if len(issue.Labels) > 0 {
+ result += fmt.Sprintf("โข **Labels:** %s\n", strings.Join(issue.Labels, ", "))
+ }
+ if issue.DueDate != nil && *issue.DueDate != "" {
+ result += fmt.Sprintf("โข **Due Date:** %s\n", *issue.DueDate)
+ }
+ result += fmt.Sprintf("โข **Total Comments:** %d\n", len(filteredNotes))
+ result += "\n"
+
+ // Conversation thread
+ if len(filteredNotes) == 0 {
+ result += "**No comments yet.**\n"
+ } else {
+ result += fmt.Sprintf("**Conversation Thread (%d comments):**\n\n", len(filteredNotes))
+
+ for i, note := range filteredNotes {
+ commentType := "๐ฌ"
+ if note.System {
+ commentType = "๐ง"
+ }
+
+ result += fmt.Sprintf("%s **Comment %d** - %s (%s)\n",
+ commentType, i+1, note.Author.Name, note.CreatedAt.Format("2006-01-02 15:04"))
+ result += "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n"
+ result += fmt.Sprintf("%s\n\n", note.Body)
+ }
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(result)), nil
+}
+
+func (s *Server) handleFindSimilarIssues(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ args := req.Arguments
+
+ searchQuery, ok := args["query"].(string)
+ if !ok || searchQuery == "" {
+ return mcp.NewToolError("query parameter is required"), nil
+ }
+
+ limitStr, _ := args["limit"].(string)
+ limit := 10 // Default limit for similarity search
+ if limitStr != "" {
+ if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 {
+ limit = l
+ }
+ }
+
+ scope, _ := args["scope"].(string)
+ if scope == "" {
+ scope = "issues" // Default to issues
+ }
+
+ includeClosedStr, _ := args["include_closed"].(string)
+ includeClosed := false
+ if includeClosedStr == "true" {
+ includeClosed = true
+ }
+
+ // Build search parameters
+ params := map[string]string{
+ "search": searchQuery,
+ "scope": scope,
+ "per_page": strconv.Itoa(limit),
+ }
+
+ // Make search API request
+ body, err := s.makeRequest("GET", "/search", params)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to search issues: %v", err)), nil
+ }
+
+ var searchResults []GitLabIssue
+ if err := json.Unmarshal(body, &searchResults); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to parse search results: %v", err)), nil
+ }
+
+ // Filter results based on state preferences
+ var filteredResults []GitLabIssue
+ for _, issue := range searchResults {
+ if !includeClosed && issue.State != "opened" {
+ continue
+ }
+ filteredResults = append(filteredResults, issue)
+ }
+
+ // Group results by project for better organization
+ projectGroups := make(map[string][]GitLabIssue)
+ for _, issue := range filteredResults {
+ projectName := extractProjectFromURL(issue.WebURL)
+ projectGroups[projectName] = append(projectGroups[projectName], issue)
+ }
+
+ // Format response
+ result := fmt.Sprintf("**Similar Issues Found for: \"%s\"**\n", searchQuery)
+ result += fmt.Sprintf("Found %d issues across %d projects:\n\n", len(filteredResults), len(projectGroups))
+
+ if len(filteredResults) == 0 {
+ result += "No similar issues found. Try:\n"
+ result += "โข Different keywords or search terms\n"
+ result += "โข Including closed issues with include_closed=true\n"
+ result += "โข Broader search scope\n"
+ } else {
+ // Display results grouped by project
+ projectCount := 0
+ for projectName, issues := range projectGroups {
+ projectCount++
+ result += fmt.Sprintf("**๐๏ธ Project %d: %s** (%d issues)\n", projectCount, projectName, len(issues))
+ result += "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n"
+
+ for i, issue := range issues {
+ stateIcon := "๐ข"
+ if issue.State == "closed" {
+ stateIcon = "๐ด"
+ }
+
+ result += fmt.Sprintf("%d. %s **%s** %s\n", i+1, stateIcon, issue.Title, issue.State)
+ result += fmt.Sprintf(" ๐ #%d | ๐ค %s", issue.IID, issue.Author.Name)
+
+ if len(issue.Assignees) > 0 {
+ result += fmt.Sprintf(" | ๐ฅ %s", issue.Assignees[0].Name)
+ }
+ result += "\n"
+
+ if len(issue.Labels) > 0 {
+ result += fmt.Sprintf(" ๐ท๏ธ %s\n", strings.Join(issue.Labels, ", "))
+ }
+
+ result += fmt.Sprintf(" ๐ %s\n", issue.WebURL)
+ result += fmt.Sprintf(" ๐
Updated: %s\n", issue.UpdatedAt.Format("2006-01-02 15:04"))
+
+ // Show snippet of description if available
+ if issue.Description != "" {
+ desc := issue.Description
+ if len(desc) > 150 {
+ desc = desc[:147] + "..."
+ }
+ result += fmt.Sprintf(" ๐ %s\n", desc)
+ }
+ result += "\n"
+ }
+ result += "\n"
+ }
+
+ // Add similarity analysis tips
+ result += "**๐ก Similarity Analysis Tips:**\n"
+ result += "โข Look for common labels and patterns\n"
+ result += "โข Check if issues are linked or reference each other\n"
+ result += "โข Consider if these could be duplicate issues\n"
+ result += "โข Review assignees for domain expertise\n"
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(result)), nil
+}
+
+func (s *Server) handleGetMyActivity(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ args := req.Arguments
+
+ // Parse optional parameters
+ limitStr, _ := args["limit"].(string)
+ limit := 20 // Default limit
+ if limitStr != "" {
+ if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
+ limit = l
+ }
+ }
+
+ days, _ := args["days"].(string)
+ daysInt := 7 // Default: last 7 days
+ if days != "" {
+ if d, err := strconv.Atoi(days); err == nil && d > 0 && d <= 30 {
+ daysInt = d
+ }
+ }
+
+ // Get current user
+ user, err := s.getCurrentUser()
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to get current user: %v", err)), nil
+ }
+
+ // Calculate date range
+ since := time.Now().AddDate(0, 0, -daysInt).Format("2006-01-02T15:04:05Z")
+
+ // Get recent issues assigned to user
+ assignedParams := map[string]string{
+ "assignee_username": user.Username,
+ "state": "opened",
+ "order_by": "updated_at",
+ "sort": "desc",
+ "per_page": strconv.Itoa(limit / 2),
+ "updated_after": since,
+ }
+
+ assignedBody, err := s.makeRequest("GET", "/issues", assignedParams)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to fetch assigned issues: %v", err)), nil
+ }
+
+ var assignedIssues []GitLabIssue
+ json.Unmarshal(assignedBody, &assignedIssues)
+
+ // Get recent issues authored by user
+ authoredParams := map[string]string{
+ "author_username": user.Username,
+ "state": "opened",
+ "order_by": "updated_at",
+ "sort": "desc",
+ "per_page": strconv.Itoa(limit / 2),
+ "updated_after": since,
+ }
+
+ authoredBody, err := s.makeRequest("GET", "/issues", authoredParams)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to fetch authored issues: %v", err)), nil
+ }
+
+ var authoredIssues []GitLabIssue
+ json.Unmarshal(authoredBody, &authoredIssues)
+
+ // Get user's recent activity events
+ activityParams := map[string]string{
+ "per_page": "20",
+ }
+
+ activityBody, err := s.makeRequest("GET", fmt.Sprintf("/users/%d/events", user.ID), activityParams)
+ if err != nil {
+ // Activity endpoint might not be available, continue without it
+ activityBody = []byte("[]")
+ }
+
+ // Parse activity (basic structure)
+ var activities []map[string]interface{}
+ json.Unmarshal(activityBody, &activities)
+
+ // Format comprehensive activity summary
+ result := fmt.Sprintf("**GitLab Activity Summary for %s**\n", user.Name)
+ result += fmt.Sprintf("๐
Last %d days | ๐ค @%s\n\n", daysInt, user.Username)
+
+ // Summary statistics
+ result += "**๐ Quick Summary:**\n"
+ result += fmt.Sprintf("โข Assigned Issues: %d open\n", len(assignedIssues))
+ result += fmt.Sprintf("โข Authored Issues: %d open\n", len(authoredIssues))
+ result += fmt.Sprintf("โข Recent Activity Events: %d\n", len(activities))
+ result += "\n"
+
+ // Priority items that need attention
+ result += "**๐ฏ Items Needing Attention:**\n"
+ priorityCount := 0
+
+ // Check for overdue items
+ for _, issue := range assignedIssues {
+ if issue.DueDate != nil && *issue.DueDate != "" {
+ dueDate, err := time.Parse("2006-01-02", *issue.DueDate)
+ if err == nil && dueDate.Before(time.Now()) {
+ if priorityCount == 0 {
+ result += "โ ๏ธ **Overdue Issues:**\n"
+ }
+ projectName := extractProjectFromURL(issue.WebURL)
+ result += fmt.Sprintf(" โข %s #%d - %s (Due: %s)\n",
+ projectName, issue.IID, issue.Title, *issue.DueDate)
+ priorityCount++
+ }
+ }
+ }
+
+ // Check for issues with recent activity (comments)
+ recentActivityCount := 0
+ for _, issue := range assignedIssues {
+ if issue.UpdatedAt.After(time.Now().AddDate(0, 0, -2)) && issue.UserNotesCount > 0 {
+ if recentActivityCount == 0 && priorityCount > 0 {
+ result += "\n๐ฌ **Issues with Recent Comments:**\n"
+ } else if recentActivityCount == 0 {
+ result += "๐ฌ **Issues with Recent Comments:**\n"
+ }
+ projectName := extractProjectFromURL(issue.WebURL)
+ result += fmt.Sprintf(" โข %s #%d - %s (%d comments)\n",
+ projectName, issue.IID, issue.Title, issue.UserNotesCount)
+ recentActivityCount++
+ if recentActivityCount >= 5 {
+ break
+ }
+ }
+ }
+
+ if priorityCount == 0 && recentActivityCount == 0 {
+ result += "โ
No urgent items requiring immediate attention\n"
+ }
+ result += "\n"
+
+ // Assigned issues section
+ if len(assignedIssues) > 0 {
+ result += fmt.Sprintf("**๐ Assigned Issues (%d):**\n", len(assignedIssues))
+ for i, issue := range assignedIssues {
+ if i >= 10 {
+ break // Limit display
+ }
+ projectName := extractProjectFromURL(issue.WebURL)
+ updatedAt := issue.UpdatedAt.Format("Jan 2")
+
+ result += fmt.Sprintf("%d. **%s** - %s #%d\n", i+1, issue.Title, projectName, issue.IID)
+ result += fmt.Sprintf(" ๐
%s | ๐ฌ %d comments", updatedAt, issue.UserNotesCount)
+
+ if len(issue.Labels) > 0 {
+ result += fmt.Sprintf(" | ๐ท๏ธ %s", strings.Join(issue.Labels[:min(3, len(issue.Labels))], ", "))
+ }
+ result += "\n\n"
+ }
+ }
+
+ // Authored issues section
+ if len(authoredIssues) > 0 {
+ result += fmt.Sprintf("**โ๏ธ Issues You Created (%d active):**\n", len(authoredIssues))
+ for i, issue := range authoredIssues {
+ if i >= 5 {
+ break // Limit display
+ }
+ projectName := extractProjectFromURL(issue.WebURL)
+ updatedAt := issue.UpdatedAt.Format("Jan 2")
+
+ result += fmt.Sprintf("%d. **%s** - %s #%d\n", i+1, issue.Title, projectName, issue.IID)
+ result += fmt.Sprintf(" ๐
%s | ๐ฌ %d comments", updatedAt, issue.UserNotesCount)
+
+ if len(issue.Assignees) > 0 {
+ result += fmt.Sprintf(" | ๐ฅ %s", issue.Assignees[0].Name)
+ }
+ result += "\n\n"
+ }
+ }
+
+ // Productivity tips
+ result += "**๐ก Productivity Tips:**\n"
+ result += "โข Use `gitlab_get_issue_conversations` to catch up on specific discussions\n"
+ result += "โข Use `gitlab_find_similar_issues` to check for duplicates before creating new issues\n"
+ result += "โข Check labels and assignees to understand priority and ownership\n"
+
+ return mcp.NewToolResult(mcp.NewTextContent(result)), nil
+}
+
+// Helper function for min of two integers
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
\ No newline at end of file
Makefile
@@ -11,7 +11,7 @@ BINDIR = bin
INSTALLDIR = /usr/local/bin
# Server binaries
-SERVERS = git filesystem fetch memory sequential-thinking time maildir signal
+SERVERS = git filesystem fetch memory sequential-thinking time maildir signal gitlab
BINARIES = $(addprefix $(BINDIR)/mcp-,$(SERVERS))
# Build flags
@@ -107,6 +107,7 @@ sequential-thinking: $(BINDIR)/mcp-sequential-thinking ## Build sequential-think
time: $(BINDIR)/mcp-time ## Build time server only
maildir: $(BINDIR)/mcp-maildir ## Build maildir server only
signal: $(BINDIR)/mcp-signal ## Build signal server only
+gitlab: $(BINDIR)/mcp-gitlab ## Build gitlab server only
help: ## Show this help message
@echo "Go MCP Servers - Available targets:"
@@ -114,4 +115,4 @@ help: ## Show this help message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
@echo ""
@echo "Individual servers:"
- @echo " git, filesystem, fetch, memory, sequential-thinking, time, maildir, signal"
+ @echo " git, filesystem, fetch, memory, sequential-thinking, time, maildir, signal, gitlab"