This is page 1 of 3. Use http://codebase.md/nguyenvanduocit/jira-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .claude │ └── commands │ ├── speckit.analyze.md │ ├── speckit.checklist.md │ ├── speckit.clarify.md │ ├── speckit.constitution.md │ ├── speckit.implement.md │ ├── speckit.plan.md │ ├── speckit.specify.md │ └── speckit.tasks.md ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows │ ├── gitleaks.yaml │ ├── release.yaml │ └── scan.yaml ├── .gitignore ├── .specify │ ├── memory │ │ └── constitution.md │ ├── scripts │ │ └── bash │ │ ├── check-prerequisites.sh │ │ ├── common.sh │ │ ├── create-new-feature.sh │ │ ├── setup-plan.sh │ │ └── update-agent-context.sh │ └── templates │ ├── agent-file-template.md │ ├── checklist-template.md │ ├── plan-template.md │ ├── spec-template.md │ └── tasks-template.md ├── CHANGELOG.md ├── CLAUDE.md ├── Dockerfile ├── go.mod ├── go.sum ├── LICENSE ├── main.go ├── prompts │ └── jira_prompts.go ├── README.md ├── services │ ├── atlassian.go │ ├── httpclient.go │ └── jira.go ├── specs │ └── 001-i-want-to │ ├── api-research.md │ ├── checklists │ │ ├── implementation-readiness.md │ │ └── requirements.md │ ├── contracts │ │ └── mcp-tool-contract.json │ ├── data-model.md │ ├── plan.md │ ├── quickstart.md │ ├── research.md │ ├── spec.md │ ├── tasks.md │ ├── test-dev-api.sh │ └── test-get-build.sh ├── tools │ ├── jira_comment.go │ ├── jira_development.go │ ├── jira_history.go │ ├── jira_issue.go │ ├── jira_relationship.go │ ├── jira_search.go │ ├── jira_sprint.go │ ├── jira_status.go │ ├── jira_transition.go │ ├── jira_version.go │ └── jira_worklog.go └── util ├── jira_formatter.go └── README.md ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | 20 | # Environment variables 21 | .env 22 | 23 | # IDE specific files 24 | .idea/ 25 | .vscode/ 26 | *.swp 27 | *.swo 28 | 29 | # OS specific files 30 | .DS_Store 31 | Thumbs.db 32 | 33 | # Build directory 34 | /build/ 35 | /dist/ 36 | jira-mcp 37 | ``` -------------------------------------------------------------------------------- /util/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Jira Formatter Utilities 2 | 3 | This package provides utility functions for formatting Jira issues from golang structs to human-readable string representations. 4 | 5 | ## Functions 6 | 7 | ### `FormatJiraIssue(issue *models.IssueScheme) string` 8 | 9 | Converts a complete Jira issue struct to a detailed, formatted string representation. This function handles all available fields from the `IssueFieldsSchemeV2` struct and related schemas. 10 | 11 | **Features:** 12 | - Basic issue information (Key, ID, URL) 13 | - Complete field information (Summary, Description, Type, Status, Priority, etc.) 14 | - People information (Reporter, Assignee, Creator) with email addresses when available 15 | - Date fields (Created, Updated, Last Viewed, etc.) 16 | - Project and parent issue information 17 | - Work-related fields (Work Ratio, Story Points) 18 | - Collections (Labels, Components, Fix Versions, Affected Versions) 19 | - Relationships (Subtasks, Issue Links) 20 | - Activity summaries (Watchers, Votes, Comments count, Worklogs count) 21 | - Available transitions 22 | - Security information 23 | 24 | **Usage:** 25 | ```go 26 | import "github.com/nguyenvanduocit/jira-mcp/util" 27 | 28 | // Get a Jira issue from the API 29 | issue, _, _ := client.Issue.Get(ctx, "PROJ-123", nil, []string{"transitions", "changelog"}) 30 | 31 | // Format it for display 32 | formattedOutput := util.FormatJiraIssue(issue) 33 | fmt.Println(formattedOutput) 34 | ``` 35 | 36 | ### `FormatJiraIssueCompact(issue *models.IssueSchemeV2) string` 37 | 38 | Returns a compact, single-line representation of a Jira issue suitable for lists or search results. 39 | 40 | **Features:** 41 | - Key, Summary, Status, Assignee, Priority 42 | - Pipe-separated format for easy scanning 43 | - Null-safe handling 44 | 45 | **Usage:** 46 | ```go 47 | // For search results or lists 48 | for _, issue := range searchResults.Issues { 49 | compactLine := util.FormatJiraIssueCompact(issue) 50 | fmt.Println(compactLine) 51 | } 52 | ``` 53 | 54 | **Example Output:** 55 | ``` 56 | Key: PROJ-123 | Summary: Fix login bug | Status: In Progress | Assignee: John Doe | Priority: High 57 | ``` 58 | 59 | ## Refactoring Benefits 60 | 61 | These utility functions were extracted from duplicate formatting logic in: 62 | - `tools/jira_issue.go` - Get Issue handler 63 | - `tools/jira_search.go` - Search Issues handler 64 | 65 | **Benefits:** 66 | 1. **DRY Principle**: Eliminates code duplication 67 | 2. **Comprehensive**: Handles all fields from the Jira issue struct 68 | 3. **Consistent**: Ensures uniform formatting across all tools 69 | 4. **Maintainable**: Single location for formatting logic updates 70 | 5. **Flexible**: Provides both detailed and compact formatting options 71 | 6. **Robust**: Includes null-safe handling for all optional fields 72 | 73 | ## Implementation Notes 74 | 75 | - All fields are handled with null-safe checks to prevent panics 76 | - Optional fields gracefully show "None", "Unassigned", or are omitted when empty 77 | - Collections (arrays/slices) are formatted as lists when present 78 | - Story point estimates are extracted from changelog history when available 79 | - Email addresses are shown in parentheses when available for user fields ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Jira MCP 2 | 3 | An opinionated Jira MCP server built from years of real-world software development experience. 4 | 5 | Unlike generic Jira integrations, this MCP is crafted from the daily workflows of engineers and automation QC teams. You'll find sophisticated tools designed for actual development needs—like retrieving all pull requests linked to an issue, managing complex sprint transitions, or tracking development information across your entire workflow. 6 | 7 | This isn't just another API wrapper. It's a reflection of how professionals actually use Jira: managing sprints, tracking development work, coordinating releases, and maintaining visibility across teams. Every tool is designed to solve real problems that arise in modern software development. 8 | 9 | ## Available tools 10 | 11 | ### Issue Management 12 | - **jira_get_issue** - Retrieve detailed information about a specific issue including status, assignee, description, subtasks, and available transitions 13 | - **jira_create_issue** - Create a new issue with specified details (returns key, ID, and URL) 14 | - **jira_create_child_issue** - Create a child issue (sub-task) linked to a parent issue 15 | - **jira_update_issue** - Modify an existing issue's details (supports partial updates) 16 | - **jira_list_issue_types** - List all available issue types in a project with their IDs, names, and descriptions 17 | 18 | ### Search 19 | - **jira_search_issue** - Search for issues using JQL (Jira Query Language) with customizable fields and expand options 20 | 21 | ### Sprint Management 22 | - **jira_list_sprints** - List all active and future sprints for a specific board or project 23 | - **jira_get_sprint** - Retrieve detailed information about a specific sprint by its ID 24 | - **jira_get_active_sprint** - Get the currently active sprint for a given board or project 25 | - **jira_search_sprint_by_name** - Search for sprints by name with exact or partial matching 26 | 27 | ### Status & Transitions 28 | - **jira_list_statuses** - Retrieve all available issue status IDs and their names for a project 29 | - **jira_transition_issue** - Transition an issue through its workflow using a valid transition ID 30 | 31 | ### Comments 32 | - **jira_add_comment** - Add a comment to an issue (uses Atlassian Document Format) 33 | - **jira_get_comments** - Retrieve all comments from an issue 34 | 35 | ### Worklogs 36 | - **jira_add_worklog** - Add a worklog entry to track time spent on an issue 37 | 38 | ### History & Audit 39 | - **jira_get_issue_history** - Retrieve the complete change history of an issue 40 | 41 | ### Issue Relationships 42 | - **jira_get_related_issues** - Retrieve issues that have a relationship (blocks, is blocked by, relates to, etc.) 43 | - **jira_link_issues** - Create a link between two issues, defining their relationship 44 | 45 | ### Version Management 46 | - **jira_get_version** - Retrieve detailed information about a specific project version 47 | - **jira_list_project_versions** - List all versions in a project with their details 48 | 49 | ### Development Information 50 | - **jira_get_development_information** - Retrieve branches, pull requests, and commits linked to an issue via development tool integrations (GitHub, GitLab, Bitbucket) 51 | 52 | 53 | 54 | ## Installation 55 | 56 | Copy this prompt to your AI assistant: 57 | 58 | ``` 59 | Install the Jira MCP server (https://github.com/nguyenvanduocit/jira-mcp) for my Claude Desktop or Cursor IDE. Read the MCP documentation carefully and guide me through the installation step by step. 60 | ``` 61 | 62 | If your AI assistant cannot help with this installation, it indicates either a misconfiguration or an ineffective AI tool. A capable AI assistant should be able to guide you through MCP installation. 63 | 64 | ## License 65 | MIT — see `LICENSE`. 66 | 67 | ## FOR AI 68 | 69 | > THIS SECTION IS FOR AI ONLY 70 | 71 | When working with this codebase, read these files to understand the project structure: 72 | 73 | 1. **CLAUDE.md** - Comprehensive project documentation including architecture, development commands, and coding conventions 74 | 2. **main.go** - Entry point that shows how the MCP server is initialized and tools are registered 75 | 3. **services/jira_client.go** - Singleton Jira client initialization and authentication 76 | 4. **tools/** - Individual tool implementations following consistent patterns 77 | 5. **docs/** - Detailed documentation (see structure below) 78 | 79 | Key concepts: 80 | - This is a Go-based MCP server that connects AI assistants to Jira 81 | - Each tool follows a registration + handler pattern with typed input validation 82 | - Tools are organized by category (issues, sprints, comments, worklogs, etc.) 83 | - All Jira operations use the `github.com/ctreminiom/go-atlassian` client library 84 | - Development principles documented in `.specify/memory/constitution.md` 85 | 86 | Before making changes, review: 87 | - **CLAUDE.md** for architecture patterns and development commands 88 | - **.specify/memory/constitution.md** for governance principles 89 | 90 | 91 | ## Quick start 92 | 93 | ### 1) Get an API token 94 | Create one at `https://id.atlassian.com/manage-profile/security/api-tokens`. 95 | 96 | ### 2) Add to Cursor 97 | Use Docker or a local binary (STDIO; no ports needed). 98 | 99 | #### Docker 100 | ```json 101 | { 102 | "mcpServers": { 103 | "jira": { 104 | "command": "docker", 105 | "args": [ 106 | "run", "--rm", "-i", 107 | "-e", "ATLASSIAN_HOST=https://your-company.atlassian.net", 108 | "-e", "[email protected]", 109 | "-e", "ATLASSIAN_TOKEN=your-api-token", 110 | "ghcr.io/nguyenvanduocit/jira-mcp:latest" 111 | ] 112 | } 113 | } 114 | } 115 | ``` 116 | 117 | #### Binary 118 | ```json 119 | { 120 | "mcpServers": { 121 | "jira": { 122 | "command": "/usr/local/bin/jira-mcp", 123 | "env": { 124 | "ATLASSIAN_HOST": "https://your-company.atlassian.net", 125 | "ATLASSIAN_EMAIL": "[email protected]", 126 | "ATLASSIAN_TOKEN": "your-api-token" 127 | } 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | ### 3) Try it in Cursor 134 | - “Show my issues assigned to me” 135 | - “What’s in the current sprint for ABC?” 136 | - “Create a bug in ABC: Login fails on Safari” 137 | 138 | ## Configuration 139 | - **ATLASSIAN_HOST**: `https://your-company.atlassian.net` 140 | - **ATLASSIAN_EMAIL**: your Atlassian email 141 | - **ATLASSIAN_TOKEN**: API token 142 | 143 | Optional `.env` (if running locally): 144 | ```bash 145 | ATLASSIAN_HOST=https://your-company.atlassian.net 146 | [email protected] 147 | ATLASSIAN_TOKEN=your-api-token 148 | ``` 149 | 150 | HTTP mode (optional, for debugging): 151 | ```bash 152 | jira-mcp -env .env -http_port 3000 153 | ``` 154 | Cursor config (HTTP mode): 155 | ```json 156 | { "mcpServers": { "jira": { "url": "http://localhost:3000/mcp" } } } 157 | ``` ``` -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- ```markdown 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | This is a Jira MCP (Model Control Protocol) connector written in Go that enables AI assistants like Claude to interact with Atlassian Jira. The project provides a comprehensive set of tools for managing Jira issues, sprints, comments, worklogs, and more through structured MCP tool calls. 8 | 9 | ## Development Commands 10 | 11 | ```bash 12 | # Build the project 13 | go build -o jira-mcp . 14 | 15 | # Run in development mode with HTTP server (requires .env file) 16 | go run . --env .env --http_port 3002 17 | 18 | # Run tests 19 | go test ./... 20 | 21 | # Install locally 22 | go install 23 | 24 | # Use go doc to understand packages and types 25 | go doc <pkg> 26 | go doc <sym>[.<methodOrField>] 27 | ``` 28 | 29 | ## Architecture Overview 30 | 31 | ### Core Structure 32 | - **main.go** - Entry point that initializes the MCP server, validates environment variables, and registers all tools 33 | - **services/** - Service layer containing Jira client setup and authentication 34 | - **tools/** - Tool implementations organized by functionality (issues, sprints, comments, etc.) 35 | - **util/** - Utility functions for error handling and response formatting 36 | 37 | ### Key Dependencies 38 | - `github.com/ctreminiom/go-atlassian` - Go client library for Atlassian APIs 39 | - `github.com/mark3labs/mcp-go` - Go implementation of Model Control Protocol 40 | - `github.com/joho/godotenv` - Environment variable loading 41 | 42 | ### Tool Implementation Pattern 43 | 44 | Each Jira operation follows this consistent pattern using **typed handlers**: 45 | 46 | 1. **Input Struct** - Define typed input with validation tags 47 | 2. **Registration Function** (`RegisterJira<Category>Tool`) - Creates tool definitions and registers them with the MCP server 48 | 3. **Typed Handler Function** - Processes tool calls with compile-time type safety 49 | 50 | Example tool structure: 51 | ```go 52 | // 1. Define input struct with validation 53 | type GetIssueInput struct { 54 | IssueKey string `json:"issue_key" validate:"required"` 55 | Fields string `json:"fields,omitempty"` 56 | Expand string `json:"expand,omitempty"` 57 | } 58 | 59 | // 2. Registration function 60 | func RegisterJiraIssueTool(s *server.MCPServer) { 61 | tool := mcp.NewTool("jira_get_issue", 62 | mcp.WithDescription("..."), 63 | mcp.WithString("issue_key", mcp.Required(), mcp.Description("...")), 64 | mcp.WithString("fields", mcp.Description("...")), 65 | ) 66 | s.AddTool(tool, mcp.NewTypedToolHandler(jiraGetIssueHandler)) 67 | } 68 | 69 | // 3. Typed handler with automatic validation 70 | func jiraGetIssueHandler(ctx context.Context, request mcp.CallToolRequest, input GetIssueInput) (*mcp.CallToolResult, error) { 71 | client := services.JiraClient() 72 | // Direct access to validated parameters - no type assertions needed 73 | issue, response, err := client.Issue.Get(ctx, input.IssueKey, fields, expand) 74 | if err != nil { 75 | return nil, fmt.Errorf("failed to get issue: %v", err) 76 | } 77 | return mcp.NewToolResultText(util.FormatIssue(issue)), nil 78 | } 79 | ``` 80 | 81 | ### Available Tool Categories 82 | - **Issue Management** - Create, read, update issues and subtasks 83 | - **Search** - JQL-based issue searching 84 | - **Sprint Management** - List sprints, move issues between sprints 85 | - **Status & Transitions** - Get available statuses and transition issues 86 | - **Comments** - Add and retrieve issue comments (uses Atlassian Document Format) 87 | - **Worklogs** - Time tracking functionality 88 | - **History** - Issue change history and audit logs 89 | - **Relationships** - Link and relate issues 90 | - **Versions** - Project version management 91 | - **Development Information** - Retrieve branches, pull requests, and commits linked to issues 92 | 93 | ## Configuration 94 | 95 | The application requires these environment variables: 96 | - `ATLASSIAN_HOST` - Your Atlassian instance URL (e.g., https://company.atlassian.net) 97 | - `ATLASSIAN_EMAIL` - Your Atlassian account email 98 | - `ATLASSIAN_TOKEN` - API token from Atlassian 99 | 100 | Environment variables can be loaded from a `.env` file using the `--env` flag. 101 | 102 | ## Service Architecture 103 | 104 | ### Jira Client Initialization 105 | The `services.JiraClient()` function uses `sync.OnceValue` to create a singleton Jira client instance with basic authentication. This ensures efficient connection reuse across all tool calls. 106 | 107 | ### HTTP vs STDIO Modes 108 | The server can run in two modes: 109 | - **STDIO mode** (default) - Standard MCP protocol over stdin/stdout 110 | - **HTTP mode** (`--http_port` flag) - HTTP server for development and testing 111 | 112 | ## Testing and Deployment 113 | 114 | The project includes: 115 | - Docker support with multi-stage builds 116 | - GitHub Actions for automated releases 117 | - Binary releases for multiple platforms (macOS, Linux, Windows) 118 | 119 | ## Code Conventions 120 | 121 | - Use structured input types for tool parameters with JSON tags and validation 122 | - All tool handlers should return `*mcp.CallToolResult` with formatted text or JSON 123 | - Client initialization should use the singleton pattern from services package 124 | - Response formatting should be human-readable for AI consumption 125 | - Comments MUST use Atlassian Document Format (ADF) with proper structure: 126 | ```go 127 | // ADF structure for comments 128 | Body: &models.CommentNodeScheme{ 129 | Version: 1, 130 | Type: "doc", 131 | Content: []*models.CommentNodeScheme{ 132 | { 133 | Type: "paragraph", 134 | Content: []*models.CommentNodeScheme{ 135 | {Type: "text", Text: "comment text"}, 136 | }, 137 | }, 138 | }, 139 | } 140 | ``` 141 | 142 | ## Governance 143 | 144 | This project follows strict governance principles documented in `.specify/memory/constitution.md`. Key principles include: 145 | 146 | - **MCP Protocol Compliance** - All functionality MUST be exposed as MCP tools 147 | - **AI-First Output Design** - Responses formatted for LLM consumption 148 | - **Simplicity Over Abstraction** - Avoid unnecessary helper functions and layers 149 | - **Type Safety & Validation** - Use typed handlers with input structs 150 | - **Resource Efficiency** - Singleton pattern for client connections 151 | - **Error Transparency** - Include endpoint context in error messages 152 | 153 | Before implementing new features or making changes, consult the constitution for detailed requirements and patterns. 154 | ``` -------------------------------------------------------------------------------- /.github/workflows/scan.yaml: -------------------------------------------------------------------------------- ```yaml 1 | name: Security and Licence Scan 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: Secret Scanning 15 | uses: trufflesecurity/trufflehog@main 16 | with: 17 | extra_args: --results=verified,unknown ``` -------------------------------------------------------------------------------- /.github/workflows/gitleaks.yaml: -------------------------------------------------------------------------------- ```yaml 1 | name: gitleaks 2 | on: 3 | pull_request: 4 | push: 5 | workflow_dispatch: 6 | schedule: 7 | - cron: "0 4 * * *" 8 | jobs: 9 | scan: 10 | name: gitleaks 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - uses: gitleaks/gitleaks-action@v2 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | FROM golang:1.23-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.sum ./ 6 | 7 | RUN go mod download 8 | 9 | COPY . . 10 | 11 | # Use build arguments for cross-compilation 12 | ARG TARGETOS 13 | ARG TARGETARCH 14 | 15 | RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o jira-mcp . 16 | 17 | FROM alpine:latest 18 | 19 | WORKDIR /app 20 | 21 | COPY --from=builder /app/jira-mcp . 22 | 23 | # Expose port for SSE server (optional) 24 | EXPOSE 8080 25 | 26 | ENTRYPOINT ["/app/jira-mcp"] 27 | 28 | CMD [] ``` -------------------------------------------------------------------------------- /.specify/templates/agent-file-template.md: -------------------------------------------------------------------------------- ```markdown 1 | # [PROJECT NAME] Development Guidelines 2 | 3 | Auto-generated from all feature plans. Last updated: [DATE] 4 | 5 | ## Active Technologies 6 | [EXTRACTED FROM ALL PLAN.MD FILES] 7 | 8 | ## Project Structure 9 | ``` 10 | [ACTUAL STRUCTURE FROM PLANS] 11 | ``` 12 | 13 | ## Commands 14 | [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] 15 | 16 | ## Code Style 17 | [LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE] 18 | 19 | ## Recent Changes 20 | [LAST 3 FEATURES AND WHAT THEY ADDED] 21 | 22 | <!-- MANUAL ADDITIONS START --> 23 | <!-- MANUAL ADDITIONS END --> ``` -------------------------------------------------------------------------------- /services/httpclient.go: -------------------------------------------------------------------------------- ```go 1 | package services 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "sync" 10 | ) 11 | 12 | var DefaultHttpClient = sync.OnceValue(func() *http.Client { 13 | transport := &http.Transport{} 14 | 15 | proxyURL := os.Getenv("PROXY_URL") 16 | if proxyURL != "" { 17 | proxy, err := url.Parse(proxyURL) 18 | if err != nil { 19 | panic(fmt.Sprintf("Failed to parse PROXY_URL: %v", err)) 20 | } 21 | transport.Proxy = http.ProxyURL(proxy) 22 | transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 23 | } 24 | 25 | return &http.Client{Transport: transport} 26 | }) 27 | ``` -------------------------------------------------------------------------------- /services/jira.go: -------------------------------------------------------------------------------- ```go 1 | package services 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | 7 | jira "github.com/ctreminiom/go-atlassian/jira/v3" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | var JiraClient = sync.OnceValue[*jira.Client](func() *jira.Client { 12 | host, mail, token := loadAtlassianCredentials() 13 | 14 | if host == "" || mail == "" || token == "" { 15 | log.Fatal("ATLASSIAN_HOST, ATLASSIAN_EMAIL, ATLASSIAN_TOKEN are required") 16 | } 17 | 18 | instance, err := jira.New(nil, host) 19 | if err != nil { 20 | log.Fatal(errors.WithMessage(err, "failed to create jira client")) 21 | } 22 | 23 | instance.Auth.SetBasicAuth(mail, token) 24 | 25 | return instance 26 | }) ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | ``` -------------------------------------------------------------------------------- /services/atlassian.go: -------------------------------------------------------------------------------- ```go 1 | package services 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "sync" 7 | 8 | "github.com/ctreminiom/go-atlassian/jira/agile" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | func loadAtlassianCredentials() (host, mail, token string) { 13 | host = os.Getenv("ATLASSIAN_HOST") 14 | mail = os.Getenv("ATLASSIAN_EMAIL") 15 | token = os.Getenv("ATLASSIAN_TOKEN") 16 | 17 | if host == "" || mail == "" || token == "" { 18 | log.Fatal("ATLASSIAN_HOST, ATLASSIAN_EMAIL, ATLASSIAN_TOKEN are required, please set it in MCP Config") 19 | } 20 | 21 | return host, mail, token 22 | } 23 | 24 | var AgileClient = sync.OnceValue[*agile.Client](func() *agile.Client { 25 | host, mail, token := loadAtlassianCredentials() 26 | 27 | instance, err := agile.New(nil, host) 28 | if err != nil { 29 | log.Fatal(errors.WithMessage(err, "failed to create agile client")) 30 | } 31 | 32 | instance.Auth.SetBasicAuth(mail, token) 33 | 34 | return instance 35 | }) 36 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | ``` -------------------------------------------------------------------------------- /.specify/templates/checklist-template.md: -------------------------------------------------------------------------------- ```markdown 1 | # [CHECKLIST TYPE] Checklist: [FEATURE NAME] 2 | 3 | **Purpose**: [Brief description of what this checklist covers] 4 | **Created**: [DATE] 5 | **Feature**: [Link to spec.md or relevant documentation] 6 | 7 | **Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements. 8 | 9 | <!-- 10 | ============================================================================ 11 | IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only. 12 | 13 | The /speckit.checklist command MUST replace these with actual items based on: 14 | - User's specific checklist request 15 | - Feature requirements from spec.md 16 | - Technical context from plan.md 17 | - Implementation details from tasks.md 18 | 19 | DO NOT keep these sample items in the generated checklist file. 20 | ============================================================================ 21 | --> 22 | 23 | ## [Category 1] 24 | 25 | - [ ] CHK001 First checklist item with clear action 26 | - [ ] CHK002 Second checklist item 27 | - [ ] CHK003 Third checklist item 28 | 29 | ## [Category 2] 30 | 31 | - [ ] CHK004 Another category item 32 | - [ ] CHK005 Item with specific criteria 33 | - [ ] CHK006 Final item in this category 34 | 35 | ## Notes 36 | 37 | - Check items off as completed: `[x]` 38 | - Add comments or findings inline 39 | - Link to relevant resources or documentation 40 | - Items are numbered sequentially for easy reference 41 | ``` -------------------------------------------------------------------------------- /specs/001-i-want-to/checklists/requirements.md: -------------------------------------------------------------------------------- ```markdown 1 | # Specification Quality Checklist: Retrieve Development Information from Jira Issue 2 | 3 | **Purpose**: Validate specification completeness and quality before proceeding to planning 4 | **Created**: 2025-10-07 5 | **Feature**: [spec.md](../spec.md) 6 | 7 | ## Content Quality 8 | 9 | - [x] No implementation details (languages, frameworks, APIs) 10 | - [x] Focused on user value and business needs 11 | - [x] Written for non-technical stakeholders 12 | - [x] All mandatory sections completed 13 | 14 | ## Requirement Completeness 15 | 16 | - [x] No [NEEDS CLARIFICATION] markers remain 17 | - [x] Requirements are testable and unambiguous 18 | - [x] Success criteria are measurable 19 | - [x] Success criteria are technology-agnostic (no implementation details) 20 | - [x] All acceptance scenarios are defined 21 | - [x] Edge cases are identified 22 | - [x] Scope is clearly bounded 23 | - [x] Dependencies and assumptions identified 24 | 25 | ## Feature Readiness 26 | 27 | - [x] All functional requirements have clear acceptance criteria 28 | - [x] User scenarios cover primary flows 29 | - [x] Feature meets measurable outcomes defined in Success Criteria 30 | - [x] No implementation details leak into specification 31 | 32 | ## Notes 33 | 34 | **Clarification Resolved**: FR-011 now specifies using Jira's standard `/rest/dev-status/1.0/issue/detail` endpoint via the go-atlassian client. 35 | 36 | **Validation Status**: ✅ All checklist items pass. Specification is ready for `/speckit.plan` phase. 37 | ``` -------------------------------------------------------------------------------- /specs/001-i-want-to/test-get-build.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Load environment variables from .env file 4 | set -a 5 | source "$(dirname "$0")/../../.env" 6 | set +a 7 | 8 | JIRA_HOST="${ATLASSIAN_HOST}" 9 | JIRA_EMAIL="${ATLASSIAN_EMAIL}" 10 | JIRA_TOKEN="${ATLASSIAN_TOKEN}" 11 | ISSUE_KEY="SHTP-6050" 12 | 13 | echo "=========================================" 14 | echo "Step 1: Getting numeric issue ID for ${ISSUE_KEY}" 15 | echo "=========================================" 16 | 17 | ISSUE_RESPONSE=$(curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 18 | -H "Accept: application/json" \ 19 | "${JIRA_HOST}/rest/api/3/issue/${ISSUE_KEY}?fields=id") 20 | 21 | echo "$ISSUE_RESPONSE" | jq '.' 22 | 23 | ISSUE_ID=$(echo "$ISSUE_RESPONSE" | jq -r '.id') 24 | 25 | if [ -z "$ISSUE_ID" ] || [ "$ISSUE_ID" = "null" ]; then 26 | echo "ERROR: Could not get issue ID" 27 | exit 1 28 | fi 29 | 30 | echo "" 31 | echo "Issue ID: ${ISSUE_ID}" 32 | echo "" 33 | 34 | echo "=========================================" 35 | echo "Step 2: Getting development summary" 36 | echo "=========================================" 37 | 38 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 39 | -H "Accept: application/json" \ 40 | "${JIRA_HOST}/rest/dev-status/latest/issue/summary?issueId=${ISSUE_ID}" | jq '.' 41 | 42 | echo "" 43 | echo "=========================================" 44 | echo "Step 8: Getting build information - cloud-providers" 45 | echo "=========================================" 46 | 47 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 48 | -H "Accept: application/json" \ 49 | "${JIRA_HOST}/rest/dev-status/latest/issue/detail?issueId=${ISSUE_ID}&applicationType=cloud-providers&dataType=build" | jq '.' 50 | ``` -------------------------------------------------------------------------------- /.specify/scripts/bash/setup-plan.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Parse command line arguments 6 | JSON_MODE=false 7 | ARGS=() 8 | 9 | for arg in "$@"; do 10 | case "$arg" in 11 | --json) 12 | JSON_MODE=true 13 | ;; 14 | --help|-h) 15 | echo "Usage: $0 [--json]" 16 | echo " --json Output results in JSON format" 17 | echo " --help Show this help message" 18 | exit 0 19 | ;; 20 | *) 21 | ARGS+=("$arg") 22 | ;; 23 | esac 24 | done 25 | 26 | # Get script directory and load common functions 27 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 28 | source "$SCRIPT_DIR/common.sh" 29 | 30 | # Get all paths and variables from common functions 31 | eval $(get_feature_paths) 32 | 33 | # Check if we're on a proper feature branch (only for git repos) 34 | check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 35 | 36 | # Ensure the feature directory exists 37 | mkdir -p "$FEATURE_DIR" 38 | 39 | # Copy plan template if it exists 40 | TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md" 41 | if [[ -f "$TEMPLATE" ]]; then 42 | cp "$TEMPLATE" "$IMPL_PLAN" 43 | echo "Copied plan template to $IMPL_PLAN" 44 | else 45 | echo "Warning: Plan template not found at $TEMPLATE" 46 | # Create a basic plan file if template doesn't exist 47 | touch "$IMPL_PLAN" 48 | fi 49 | 50 | # Output results 51 | if $JSON_MODE; then 52 | printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \ 53 | "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT" 54 | else 55 | echo "FEATURE_SPEC: $FEATURE_SPEC" 56 | echo "IMPL_PLAN: $IMPL_PLAN" 57 | echo "SPECS_DIR: $FEATURE_DIR" 58 | echo "BRANCH: $CURRENT_BRANCH" 59 | echo "HAS_GIT: $HAS_GIT" 60 | fi 61 | ``` -------------------------------------------------------------------------------- /tools/jira_status.go: -------------------------------------------------------------------------------- ```go 1 | package tools 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/mark3labs/mcp-go/mcp" 9 | "github.com/mark3labs/mcp-go/server" 10 | "github.com/nguyenvanduocit/jira-mcp/services" 11 | ) 12 | 13 | // Input types for typed tools 14 | type ListStatusesInput struct { 15 | ProjectKey string `json:"project_key" validate:"required"` 16 | } 17 | 18 | func RegisterJiraStatusTool(s *server.MCPServer) { 19 | jiraStatusListTool := mcp.NewTool("jira_list_statuses", 20 | mcp.WithDescription("Retrieve all available issue status IDs and their names for a specific Jira project"), 21 | mcp.WithString("project_key", mcp.Required(), mcp.Description("Project identifier (e.g., KP, PROJ)")), 22 | ) 23 | s.AddTool(jiraStatusListTool, mcp.NewTypedToolHandler(jiraGetStatusesHandler)) 24 | } 25 | 26 | func jiraGetStatusesHandler(ctx context.Context, request mcp.CallToolRequest, input ListStatusesInput) (*mcp.CallToolResult, error) { 27 | client := services.JiraClient() 28 | 29 | issueTypes, response, err := client.Project.Statuses(ctx, input.ProjectKey) 30 | if err != nil { 31 | if response != nil { 32 | return nil, fmt.Errorf("failed to get statuses: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint) 33 | } 34 | return nil, fmt.Errorf("failed to get statuses: %v", err) 35 | } 36 | 37 | if len(issueTypes) == 0 { 38 | return mcp.NewToolResultText("No issue types found for this project."), nil 39 | } 40 | 41 | var result strings.Builder 42 | result.WriteString("Available Statuses:\n") 43 | for _, issueType := range issueTypes { 44 | result.WriteString(fmt.Sprintf("\nIssue Type: %s\n", issueType.Name)) 45 | for _, status := range issueType.Statuses { 46 | result.WriteString(fmt.Sprintf(" - %s: %s\n", status.Name, status.ID)) 47 | } 48 | } 49 | 50 | return mcp.NewToolResultText(result.String()), nil 51 | } 52 | ``` -------------------------------------------------------------------------------- /tools/jira_transition.go: -------------------------------------------------------------------------------- ```go 1 | package tools 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/ctreminiom/go-atlassian/pkg/infra/models" 8 | "github.com/mark3labs/mcp-go/mcp" 9 | "github.com/mark3labs/mcp-go/server" 10 | "github.com/nguyenvanduocit/jira-mcp/services" 11 | ) 12 | 13 | // Input types for typed tools 14 | type TransitionIssueInput struct { 15 | IssueKey string `json:"issue_key" validate:"required"` 16 | TransitionID string `json:"transition_id" validate:"required"` 17 | Comment string `json:"comment,omitempty"` 18 | } 19 | 20 | func RegisterJiraTransitionTool(s *server.MCPServer) { 21 | jiraTransitionTool := mcp.NewTool("jira_transition_issue", 22 | mcp.WithDescription("Transition an issue through its workflow using a valid transition ID. Get available transitions from jira_get_issue"), 23 | mcp.WithString("issue_key", mcp.Required(), mcp.Description("The issue to transition (e.g., KP-123)")), 24 | mcp.WithString("transition_id", mcp.Required(), mcp.Description("Transition ID from available transitions list")), 25 | mcp.WithString("comment", mcp.Description("Optional comment to add with transition")), 26 | ) 27 | s.AddTool(jiraTransitionTool, mcp.NewTypedToolHandler(jiraTransitionIssueHandler)) 28 | } 29 | 30 | func jiraTransitionIssueHandler(ctx context.Context, request mcp.CallToolRequest, input TransitionIssueInput) (*mcp.CallToolResult, error) { 31 | client := services.JiraClient() 32 | 33 | var options *models.IssueMoveOptionsV3 34 | if input.Comment != "" { 35 | options = &models.IssueMoveOptionsV3{ 36 | Fields: &models.IssueScheme{}, 37 | } 38 | } 39 | 40 | response, err := client.Issue.Move(ctx, input.IssueKey, input.TransitionID, options) 41 | if err != nil { 42 | if response != nil { 43 | return nil, fmt.Errorf("transition failed: %s (endpoint: %s)", 44 | response.Bytes.String(), 45 | response.Endpoint) 46 | } 47 | return nil, fmt.Errorf("transition failed: %v", err) 48 | } 49 | 50 | return mcp.NewToolResultText("Issue transition completed successfully"), nil 51 | } 52 | ``` -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- ```yaml 1 | name: Release Please and GoReleaser 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | packages: write 12 | 13 | jobs: 14 | 15 | release-please: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | release_created: ${{ steps.release.outputs.release_created }} 19 | tag_name: ${{ steps.release.outputs.tag_name }} 20 | steps: 21 | - uses: googleapis/release-please-action@v4 22 | id: release 23 | with: 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | release-type: go 26 | 27 | goreleaser: 28 | needs: release-please 29 | if: ${{ needs.release-please.outputs.release_created }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 0 36 | - name: Set up Go 37 | uses: actions/setup-go@v5 38 | - name: Run GoReleaser 39 | uses: goreleaser/goreleaser-action@v6 40 | with: 41 | distribution: goreleaser 42 | version: latest 43 | args: release --clean 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | docker: 48 | needs: release-please 49 | if: ${{ needs.release-please.outputs.release_created }} 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | 55 | - name: Set up Docker Buildx 56 | uses: docker/setup-buildx-action@v3 57 | 58 | - name: Login to GitHub Container Registry 59 | uses: docker/login-action@v3 60 | with: 61 | registry: ghcr.io 62 | username: ${{ github.repository_owner }} 63 | password: ${{ secrets.GITHUB_TOKEN }} 64 | 65 | - name: Extract metadata for Docker 66 | id: meta 67 | uses: docker/metadata-action@v5 68 | with: 69 | images: ghcr.io/${{ github.repository }} 70 | tags: | 71 | type=semver,pattern={{version}},value=${{ needs.release-please.outputs.tag_name }} 72 | type=semver,pattern={{major}}.{{minor}},value=${{ needs.release-please.outputs.tag_name }} 73 | type=semver,pattern={{major}},value=${{ needs.release-please.outputs.tag_name }} 74 | type=raw,value=latest 75 | 76 | - name: Build and push Docker image 77 | uses: docker/build-push-action@v6 78 | with: 79 | context: . 80 | platforms: linux/amd64,linux/arm64 81 | push: true 82 | tags: ${{ steps.meta.outputs.tags }} 83 | labels: ${{ steps.meta.outputs.labels }} 84 | provenance: false 85 | sbom: false 86 | cache-from: type=gha 87 | cache-to: type=gha,mode=max 88 | ``` -------------------------------------------------------------------------------- /.claude/commands/speckit.plan.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | description: Execute the implementation planning workflow using the plan template to generate design artifacts. 3 | --- 4 | 5 | ## User Input 6 | 7 | ```text 8 | $ARGUMENTS 9 | ``` 10 | 11 | You **MUST** consider the user input before proceeding (if not empty). 12 | 13 | ## Outline 14 | 15 | 1. **Setup**: Run `.specify/scripts/bash/setup-plan.sh --json` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. 16 | 17 | 2. **Load context**: Read FEATURE_SPEC and `.specify.specify/memory/constitution.md`. Load IMPL_PLAN template (already copied). 18 | 19 | 3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to: 20 | - Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION") 21 | - Fill Constitution Check section from constitution 22 | - Evaluate gates (ERROR if violations unjustified) 23 | - Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION) 24 | - Phase 1: Generate data-model.md, contracts/, quickstart.md 25 | - Phase 1: Update agent context by running the agent script 26 | - Re-evaluate Constitution Check post-design 27 | 28 | 4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts. 29 | 30 | ## Phases 31 | 32 | ### Phase 0: Outline & Research 33 | 34 | 1. **Extract unknowns from Technical Context** above: 35 | - For each NEEDS CLARIFICATION → research task 36 | - For each dependency → best practices task 37 | - For each integration → patterns task 38 | 39 | 2. **Generate and dispatch research agents**: 40 | ``` 41 | For each unknown in Technical Context: 42 | Task: "Research {unknown} for {feature context}" 43 | For each technology choice: 44 | Task: "Find best practices for {tech} in {domain}" 45 | ``` 46 | 47 | 3. **Consolidate findings** in `research.md` using format: 48 | - Decision: [what was chosen] 49 | - Rationale: [why chosen] 50 | - Alternatives considered: [what else evaluated] 51 | 52 | **Output**: research.md with all NEEDS CLARIFICATION resolved 53 | 54 | ### Phase 1: Design & Contracts 55 | 56 | **Prerequisites:** `research.md` complete 57 | 58 | 1. **Extract entities from feature spec** → `data-model.md`: 59 | - Entity name, fields, relationships 60 | - Validation rules from requirements 61 | - State transitions if applicable 62 | 63 | 2. **Generate API contracts** from functional requirements: 64 | - For each user action → endpoint 65 | - Use standard REST/GraphQL patterns 66 | - Output OpenAPI/GraphQL schema to `/contracts/` 67 | 68 | 3. **Agent context update**: 69 | - Run `.specify/scripts/bash/update-agent-context.sh claude` 70 | - These scripts detect which AI agent is in use 71 | - Update the appropriate agent-specific context file 72 | - Add only new technology from current plan 73 | - Preserve manual additions between markers 74 | 75 | **Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file 76 | 77 | ## Key rules 78 | 79 | - Use absolute paths 80 | - ERROR on gate failures or unresolved clarifications 81 | ``` -------------------------------------------------------------------------------- /.specify/scripts/bash/create-new-feature.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | JSON_MODE=false 6 | ARGS=() 7 | for arg in "$@"; do 8 | case "$arg" in 9 | --json) JSON_MODE=true ;; 10 | --help|-h) echo "Usage: $0 [--json] <feature_description>"; exit 0 ;; 11 | *) ARGS+=("$arg") ;; 12 | esac 13 | done 14 | 15 | FEATURE_DESCRIPTION="${ARGS[*]}" 16 | if [ -z "$FEATURE_DESCRIPTION" ]; then 17 | echo "Usage: $0 [--json] <feature_description>" >&2 18 | exit 1 19 | fi 20 | 21 | # Function to find the repository root by searching for existing project markers 22 | find_repo_root() { 23 | local dir="$1" 24 | while [ "$dir" != "/" ]; do 25 | if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then 26 | echo "$dir" 27 | return 0 28 | fi 29 | dir="$(dirname "$dir")" 30 | done 31 | return 1 32 | } 33 | 34 | # Resolve repository root. Prefer git information when available, but fall back 35 | # to searching for repository markers so the workflow still functions in repositories that 36 | # were initialised with --no-git. 37 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 38 | 39 | if git rev-parse --show-toplevel >/dev/null 2>&1; then 40 | REPO_ROOT=$(git rev-parse --show-toplevel) 41 | HAS_GIT=true 42 | else 43 | REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")" 44 | if [ -z "$REPO_ROOT" ]; then 45 | echo "Error: Could not determine repository root. Please run this script from within the repository." >&2 46 | exit 1 47 | fi 48 | HAS_GIT=false 49 | fi 50 | 51 | cd "$REPO_ROOT" 52 | 53 | SPECS_DIR="$REPO_ROOT/specs" 54 | mkdir -p "$SPECS_DIR" 55 | 56 | HIGHEST=0 57 | if [ -d "$SPECS_DIR" ]; then 58 | for dir in "$SPECS_DIR"/*; do 59 | [ -d "$dir" ] || continue 60 | dirname=$(basename "$dir") 61 | number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0") 62 | number=$((10#$number)) 63 | if [ "$number" -gt "$HIGHEST" ]; then HIGHEST=$number; fi 64 | done 65 | fi 66 | 67 | NEXT=$((HIGHEST + 1)) 68 | FEATURE_NUM=$(printf "%03d" "$NEXT") 69 | 70 | BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//') 71 | WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//') 72 | BRANCH_NAME="${FEATURE_NUM}-${WORDS}" 73 | 74 | if [ "$HAS_GIT" = true ]; then 75 | git checkout -b "$BRANCH_NAME" 76 | else 77 | >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME" 78 | fi 79 | 80 | FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" 81 | mkdir -p "$FEATURE_DIR" 82 | 83 | TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md" 84 | SPEC_FILE="$FEATURE_DIR/spec.md" 85 | if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi 86 | 87 | # Set the SPECIFY_FEATURE environment variable for the current session 88 | export SPECIFY_FEATURE="$BRANCH_NAME" 89 | 90 | if $JSON_MODE; then 91 | printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" 92 | else 93 | echo "BRANCH_NAME: $BRANCH_NAME" 94 | echo "SPEC_FILE: $SPEC_FILE" 95 | echo "FEATURE_NUM: $FEATURE_NUM" 96 | echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME" 97 | fi 98 | ``` -------------------------------------------------------------------------------- /.specify/scripts/bash/common.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/usr/bin/env bash 2 | # Common functions and variables for all scripts 3 | 4 | # Get repository root, with fallback for non-git repositories 5 | get_repo_root() { 6 | if git rev-parse --show-toplevel >/dev/null 2>&1; then 7 | git rev-parse --show-toplevel 8 | else 9 | # Fall back to script location for non-git repos 10 | local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 11 | (cd "$script_dir/../../.." && pwd) 12 | fi 13 | } 14 | 15 | # Get current branch, with fallback for non-git repositories 16 | get_current_branch() { 17 | # First check if SPECIFY_FEATURE environment variable is set 18 | if [[ -n "${SPECIFY_FEATURE:-}" ]]; then 19 | echo "$SPECIFY_FEATURE" 20 | return 21 | fi 22 | 23 | # Then check git if available 24 | if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then 25 | git rev-parse --abbrev-ref HEAD 26 | return 27 | fi 28 | 29 | # For non-git repos, try to find the latest feature directory 30 | local repo_root=$(get_repo_root) 31 | local specs_dir="$repo_root/specs" 32 | 33 | if [[ -d "$specs_dir" ]]; then 34 | local latest_feature="" 35 | local highest=0 36 | 37 | for dir in "$specs_dir"/*; do 38 | if [[ -d "$dir" ]]; then 39 | local dirname=$(basename "$dir") 40 | if [[ "$dirname" =~ ^([0-9]{3})- ]]; then 41 | local number=${BASH_REMATCH[1]} 42 | number=$((10#$number)) 43 | if [[ "$number" -gt "$highest" ]]; then 44 | highest=$number 45 | latest_feature=$dirname 46 | fi 47 | fi 48 | fi 49 | done 50 | 51 | if [[ -n "$latest_feature" ]]; then 52 | echo "$latest_feature" 53 | return 54 | fi 55 | fi 56 | 57 | echo "main" # Final fallback 58 | } 59 | 60 | # Check if we have git available 61 | has_git() { 62 | git rev-parse --show-toplevel >/dev/null 2>&1 63 | } 64 | 65 | check_feature_branch() { 66 | local branch="$1" 67 | local has_git_repo="$2" 68 | 69 | # For non-git repos, we can't enforce branch naming but still provide output 70 | if [[ "$has_git_repo" != "true" ]]; then 71 | echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 72 | return 0 73 | fi 74 | 75 | if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then 76 | echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 77 | echo "Feature branches should be named like: 001-feature-name" >&2 78 | return 1 79 | fi 80 | 81 | return 0 82 | } 83 | 84 | get_feature_dir() { echo "$1/specs/$2"; } 85 | 86 | get_feature_paths() { 87 | local repo_root=$(get_repo_root) 88 | local current_branch=$(get_current_branch) 89 | local has_git_repo="false" 90 | 91 | if has_git; then 92 | has_git_repo="true" 93 | fi 94 | 95 | local feature_dir=$(get_feature_dir "$repo_root" "$current_branch") 96 | 97 | cat <<EOF 98 | REPO_ROOT='$repo_root' 99 | CURRENT_BRANCH='$current_branch' 100 | HAS_GIT='$has_git_repo' 101 | FEATURE_DIR='$feature_dir' 102 | FEATURE_SPEC='$feature_dir/spec.md' 103 | IMPL_PLAN='$feature_dir/plan.md' 104 | TASKS='$feature_dir/tasks.md' 105 | RESEARCH='$feature_dir/research.md' 106 | DATA_MODEL='$feature_dir/data-model.md' 107 | QUICKSTART='$feature_dir/quickstart.md' 108 | CONTRACTS_DIR='$feature_dir/contracts' 109 | EOF 110 | } 111 | 112 | check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } 113 | check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } 114 | ``` -------------------------------------------------------------------------------- /tools/jira_history.go: -------------------------------------------------------------------------------- ```go 1 | package tools 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/mark3labs/mcp-go/mcp" 9 | "github.com/mark3labs/mcp-go/server" 10 | "github.com/nguyenvanduocit/jira-mcp/services" 11 | ) 12 | 13 | // GetIssueHistoryInput defines the input parameters for getting issue history 14 | type GetIssueHistoryInput struct { 15 | IssueKey string `json:"issue_key" validate:"required"` 16 | } 17 | 18 | // HistoryItem represents a single change in the issue history 19 | type HistoryItem struct { 20 | Field string `json:"field"` 21 | FromString string `json:"from_string"` 22 | ToString string `json:"to_string"` 23 | } 24 | 25 | // HistoryEntry represents a single history entry with multiple changes 26 | type HistoryEntry struct { 27 | Date string `json:"date"` 28 | Author string `json:"author"` 29 | Changes []HistoryItem `json:"changes"` 30 | } 31 | 32 | // GetIssueHistoryOutput defines the output structure for issue history 33 | type GetIssueHistoryOutput struct { 34 | IssueKey string `json:"issue_key"` 35 | History []HistoryEntry `json:"history"` 36 | Count int `json:"count"` 37 | } 38 | 39 | func RegisterJiraHistoryTool(s *server.MCPServer) { 40 | jiraGetIssueHistoryTool := mcp.NewTool("jira_get_issue_history", 41 | mcp.WithDescription("Retrieve the complete change history of a Jira issue"), 42 | mcp.WithString("issue_key", mcp.Required(), mcp.Description("The unique identifier of the Jira issue (e.g., KP-2, PROJ-123)")), 43 | ) 44 | s.AddTool(jiraGetIssueHistoryTool, mcp.NewTypedToolHandler(jiraGetIssueHistoryHandler)) 45 | } 46 | 47 | func jiraGetIssueHistoryHandler(ctx context.Context, request mcp.CallToolRequest, input GetIssueHistoryInput) (*mcp.CallToolResult, error) { 48 | client := services.JiraClient() 49 | 50 | // Get issue with changelog expanded 51 | issue, response, err := client.Issue.Get(ctx, input.IssueKey, nil, []string{"changelog"}) 52 | if err != nil { 53 | if response != nil { 54 | return mcp.NewToolResultError(fmt.Sprintf("failed to get issue history: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)), nil 55 | } 56 | return mcp.NewToolResultError(fmt.Sprintf("failed to get issue history: %v", err)), nil 57 | } 58 | 59 | if len(issue.Changelog.Histories) == 0 { 60 | return mcp.NewToolResultText(fmt.Sprintf("No history found for issue %s", input.IssueKey)), nil 61 | } 62 | 63 | // Build structured output 64 | var historyEntries []HistoryEntry 65 | 66 | // Process each history entry 67 | for _, history := range issue.Changelog.Histories { 68 | var formattedDate string 69 | 70 | // Parse the created time 71 | createdTime, err := time.Parse("2006-01-02T15:04:05.999-0700", history.Created) 72 | if err != nil { 73 | // If parse fails, use the original string 74 | formattedDate = history.Created 75 | } else { 76 | // Format the time in a more readable format 77 | formattedDate = createdTime.Format("2006-01-02 15:04:05") 78 | } 79 | 80 | // Process change items 81 | var changes []HistoryItem 82 | for _, item := range history.Items { 83 | fromString := item.FromString 84 | if fromString == "" { 85 | fromString = "(empty)" 86 | } 87 | 88 | toString := item.ToString 89 | if toString == "" { 90 | toString = "(empty)" 91 | } 92 | 93 | changes = append(changes, HistoryItem{ 94 | Field: item.Field, 95 | FromString: fromString, 96 | ToString: toString, 97 | }) 98 | } 99 | 100 | historyEntries = append(historyEntries, HistoryEntry{ 101 | Date: formattedDate, 102 | Author: history.Author.DisplayName, 103 | Changes: changes, 104 | }) 105 | } 106 | 107 | output := GetIssueHistoryOutput{ 108 | IssueKey: input.IssueKey, 109 | History: historyEntries, 110 | Count: len(historyEntries), 111 | } 112 | 113 | return mcp.NewToolResultJSON(output) 114 | } ``` -------------------------------------------------------------------------------- /tools/jira_comment.go: -------------------------------------------------------------------------------- ```go 1 | package tools 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/ctreminiom/go-atlassian/pkg/infra/models" 8 | "github.com/mark3labs/mcp-go/mcp" 9 | "github.com/mark3labs/mcp-go/server" 10 | "github.com/nguyenvanduocit/jira-mcp/services" 11 | ) 12 | 13 | // Input types for typed tools 14 | type AddCommentInput struct { 15 | IssueKey string `json:"issue_key" validate:"required"` 16 | Comment string `json:"comment" validate:"required"` 17 | } 18 | 19 | type GetCommentsInput struct { 20 | IssueKey string `json:"issue_key" validate:"required"` 21 | } 22 | 23 | func RegisterJiraCommentTools(s *server.MCPServer) { 24 | jiraAddCommentTool := mcp.NewTool("jira_add_comment", 25 | mcp.WithDescription("Add a comment to a Jira issue"), 26 | mcp.WithString("issue_key", mcp.Required(), mcp.Description("The unique identifier of the Jira issue (e.g., KP-2, PROJ-123)")), 27 | mcp.WithString("comment", mcp.Required(), mcp.Description("The comment text to add to the issue")), 28 | ) 29 | s.AddTool(jiraAddCommentTool, mcp.NewTypedToolHandler(jiraAddCommentHandler)) 30 | 31 | jiraGetCommentsTool := mcp.NewTool("jira_get_comments", 32 | mcp.WithDescription("Retrieve all comments from a Jira issue"), 33 | mcp.WithString("issue_key", mcp.Required(), mcp.Description("The unique identifier of the Jira issue (e.g., KP-2, PROJ-123)")), 34 | ) 35 | s.AddTool(jiraGetCommentsTool, mcp.NewTypedToolHandler(jiraGetCommentsHandler)) 36 | } 37 | 38 | func jiraAddCommentHandler(ctx context.Context, request mcp.CallToolRequest, input AddCommentInput) (*mcp.CallToolResult, error) { 39 | client := services.JiraClient() 40 | 41 | // Create proper ADF structure: document → paragraph → text 42 | commentPayload := &models.CommentPayloadScheme{ 43 | Body: &models.CommentNodeScheme{ 44 | Version: 1, 45 | Type: "doc", 46 | Content: []*models.CommentNodeScheme{ 47 | { 48 | Type: "paragraph", 49 | Content: []*models.CommentNodeScheme{ 50 | { 51 | Type: "text", 52 | Text: input.Comment, 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | } 59 | 60 | comment, response, err := client.Issue.Comment.Add(ctx, input.IssueKey, commentPayload, nil) 61 | if err != nil { 62 | if response != nil { 63 | return nil, fmt.Errorf("failed to add comment: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint) 64 | } 65 | return nil, fmt.Errorf("failed to add comment: %v", err) 66 | } 67 | 68 | result := fmt.Sprintf("Comment added successfully!\nID: %s\nAuthor: %s\nCreated: %s", 69 | comment.ID, 70 | comment.Author.DisplayName, 71 | comment.Created) 72 | 73 | return mcp.NewToolResultText(result), nil 74 | } 75 | 76 | func jiraGetCommentsHandler(ctx context.Context, request mcp.CallToolRequest, input GetCommentsInput) (*mcp.CallToolResult, error) { 77 | client := services.JiraClient() 78 | 79 | // Retrieve up to 50 comments starting from the first one. 80 | // Passing 0 for maxResults results in Jira returning only the first comment. 81 | comments, response, err := client.Issue.Comment.Gets(ctx, input.IssueKey, "", nil, 0, 50) 82 | if err != nil { 83 | if response != nil { 84 | return nil, fmt.Errorf("failed to get comments: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint) 85 | } 86 | return nil, fmt.Errorf("failed to get comments: %v", err) 87 | } 88 | 89 | if len(comments.Comments) == 0 { 90 | return mcp.NewToolResultText("No comments found for this issue."), nil 91 | } 92 | 93 | var result string 94 | for _, comment := range comments.Comments { 95 | authorName := "Unknown" 96 | if comment.Author != nil { 97 | authorName = comment.Author.DisplayName 98 | } 99 | 100 | result += fmt.Sprintf("ID: %s\nAuthor: %s\nCreated: %s\nUpdated: %s\nBody: %s\n\n", 101 | comment.ID, 102 | authorName, 103 | comment.Created, 104 | comment.Updated, 105 | comment.Body) 106 | } 107 | 108 | return mcp.NewToolResultText(result), nil 109 | } 110 | ``` -------------------------------------------------------------------------------- /tools/jira_worklog.go: -------------------------------------------------------------------------------- ```go 1 | package tools 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/ctreminiom/go-atlassian/pkg/infra/models" 10 | "github.com/mark3labs/mcp-go/mcp" 11 | "github.com/mark3labs/mcp-go/server" 12 | "github.com/nguyenvanduocit/jira-mcp/services" 13 | ) 14 | 15 | // Input types for typed tools 16 | type AddWorklogInput struct { 17 | IssueKey string `json:"issue_key" validate:"required"` 18 | TimeSpent string `json:"time_spent" validate:"required"` 19 | Comment string `json:"comment,omitempty"` 20 | Started string `json:"started,omitempty"` 21 | } 22 | 23 | func RegisterJiraWorklogTool(s *server.MCPServer) { 24 | jiraAddWorklogTool := mcp.NewTool("jira_add_worklog", 25 | mcp.WithDescription("Add a worklog to a Jira issue to track time spent on the issue"), 26 | mcp.WithString("issue_key", mcp.Required(), mcp.Description("The unique identifier of the Jira issue (e.g., KP-2, PROJ-123)")), 27 | mcp.WithString("time_spent", mcp.Required(), mcp.Description("Time spent working on the issue (e.g., 3h, 30m, 1h 30m)")), 28 | mcp.WithString("comment", mcp.Description("Comment describing the work done")), 29 | mcp.WithString("started", mcp.Description("When the work began, in ISO 8601 format (e.g., 2023-05-01T10:00:00.000+0000). Defaults to current time.")), 30 | ) 31 | s.AddTool(jiraAddWorklogTool, mcp.NewTypedToolHandler(jiraAddWorklogHandler)) 32 | } 33 | 34 | func jiraAddWorklogHandler(ctx context.Context, request mcp.CallToolRequest, input AddWorklogInput) (*mcp.CallToolResult, error) { 35 | client := services.JiraClient() 36 | 37 | // Convert timeSpent to seconds (this is a simplification - in a real implementation 38 | // you would need to parse formats like "1h 30m" properly) 39 | timeSpentSeconds, err := parseTimeSpent(input.TimeSpent) 40 | if err != nil { 41 | return nil, fmt.Errorf("invalid time_spent format: %v", err) 42 | } 43 | 44 | // Get started time if provided, otherwise use current time 45 | var started string 46 | if input.Started != "" { 47 | started = input.Started 48 | } else { 49 | // Format current time in ISO 8601 format 50 | started = time.Now().Format("2006-01-02T15:04:05.000-0700") 51 | } 52 | 53 | options := &models.WorklogOptionsScheme{ 54 | Notify: true, 55 | AdjustEstimate: "auto", 56 | } 57 | 58 | payload := &models.WorklogADFPayloadScheme{ 59 | TimeSpentSeconds: timeSpentSeconds, 60 | Started: started, 61 | } 62 | 63 | // Add comment if provided 64 | if input.Comment != "" { 65 | payload.Comment = &models.CommentNodeScheme{ 66 | Type: "text", 67 | Text: input.Comment, 68 | } 69 | } 70 | 71 | // Call the Jira API to add the worklog 72 | worklog, response, err := client.Issue.Worklog.Add(ctx, input.IssueKey, payload, options) 73 | if err != nil { 74 | if response != nil { 75 | return nil, fmt.Errorf("failed to add worklog: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint) 76 | } 77 | return nil, fmt.Errorf("failed to add worklog: %v", err) 78 | } 79 | 80 | result := fmt.Sprintf(`Worklog added successfully! 81 | Issue: %s 82 | Worklog ID: %s 83 | Time Spent: %s (%d seconds) 84 | Date Started: %s 85 | Author: %s`, 86 | input.IssueKey, 87 | worklog.ID, 88 | input.TimeSpent, 89 | worklog.TimeSpentSeconds, 90 | worklog.Started, 91 | worklog.Author.DisplayName, 92 | ) 93 | 94 | return mcp.NewToolResultText(result), nil 95 | } 96 | 97 | // parseTimeSpent converts time formats like "3h", "30m", "1h 30m" to seconds 98 | func parseTimeSpent(timeSpent string) (int, error) { 99 | // This is a simplified version - a real implementation would be more robust 100 | // For this example, we'll just handle hours (h) and minutes (m) 101 | 102 | // Simple case: if it's just a number, treat it as seconds 103 | seconds, err := strconv.Atoi(timeSpent) 104 | if err == nil { 105 | return seconds, nil 106 | } 107 | 108 | // Otherwise, try to parse as a duration 109 | duration, err := time.ParseDuration(timeSpent) 110 | if err == nil { 111 | return int(duration.Seconds()), nil 112 | } 113 | 114 | // If all else fails, return an error 115 | return 0, fmt.Errorf("could not parse time: %s", timeSpent) 116 | } ``` -------------------------------------------------------------------------------- /prompts/jira_prompts.go: -------------------------------------------------------------------------------- ```go 1 | package prompts 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/mark3labs/mcp-go/mcp" 8 | "github.com/mark3labs/mcp-go/server" 9 | ) 10 | 11 | // RegisterJiraPrompts registers all Jira-related prompts with the MCP server 12 | func RegisterJiraPrompts(s *server.MCPServer) { 13 | // Prompt 1: List all development work for an issue and its subtasks 14 | s.AddPrompt(mcp.NewPrompt("issue_development_tree", 15 | mcp.WithPromptDescription("List all development work (branches, PRs, commits) for a Jira issue and all its child issues/subtasks"), 16 | mcp.WithArgument("issue_key", 17 | mcp.ArgumentDescription("The Jira issue key to analyze (e.g., PROJ-123)"), 18 | mcp.RequiredArgument(), 19 | ), 20 | ), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { 21 | issueKey := request.Params.Arguments["issue_key"] 22 | if issueKey == "" { 23 | return nil, fmt.Errorf("issue_key is required") 24 | } 25 | 26 | return mcp.NewGetPromptResult( 27 | "Development work tree for issue and subtasks", 28 | []mcp.PromptMessage{ 29 | mcp.NewPromptMessage( 30 | mcp.RoleUser, 31 | mcp.NewTextContent(fmt.Sprintf(`Please analyze all development work for issue %s and its child issues: 32 | 33 | 1. First, use jira_get_issue with issue_key=%s and expand=subtasks to retrieve the parent issue and all its subtasks 34 | 2. Then, use jira_get_development_information to get branches, pull requests, and commits for the parent issue %s 35 | 3. For each subtask found, call jira_get_development_information to get their development work 36 | 4. Format the results as a hierarchical tree showing: 37 | - Parent issue: %s 38 | - Development work (branches, PRs, commits) 39 | - Each subtask: 40 | - Development work (branches, PRs, commits) 41 | 42 | Please provide a clear summary of all development activity across the entire issue tree.`, issueKey, issueKey, issueKey, issueKey)), 43 | ), 44 | }, 45 | ), nil 46 | }) 47 | 48 | // Prompt 2: List all issues and their development work for a release/version 49 | s.AddPrompt(mcp.NewPrompt("release_development_overview", 50 | mcp.WithPromptDescription("List all issues and their development work (branches, PRs, commits) for a specific release/version"), 51 | mcp.WithArgument("version", 52 | mcp.ArgumentDescription("The version/release name (e.g., v1.0.0, Sprint 23)"), 53 | mcp.RequiredArgument(), 54 | ), 55 | mcp.WithArgument("project_key", 56 | mcp.ArgumentDescription("The Jira project key (e.g., PROJ, KP)"), 57 | mcp.RequiredArgument(), 58 | ), 59 | ), func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { 60 | version := request.Params.Arguments["version"] 61 | projectKey := request.Params.Arguments["project_key"] 62 | 63 | if version == "" { 64 | return nil, fmt.Errorf("version is required") 65 | } 66 | if projectKey == "" { 67 | return nil, fmt.Errorf("project_key is required") 68 | } 69 | 70 | return mcp.NewGetPromptResult( 71 | "Development overview for release", 72 | []mcp.PromptMessage{ 73 | mcp.NewPromptMessage( 74 | mcp.RoleUser, 75 | mcp.NewTextContent(fmt.Sprintf(`Please provide a comprehensive development overview for release "%s" in project %s: 76 | 77 | 1. First, use jira_search_issue with JQL: fixVersion = "%s" AND project = %s 78 | 2. For each issue found in the search results, call jira_get_development_information to retrieve: 79 | - Branches associated with the issue 80 | - Pull requests (status, reviewers, etc.) 81 | - Commits and code changes 82 | 3. Organize the results by issue and provide a summary that includes: 83 | - Total number of issues in the release 84 | - List each issue with its key, summary, and status 85 | - Development work for each issue (branches, PRs, commits) 86 | - Overall statistics (total PRs, merged PRs, open branches, etc.) 87 | 88 | Please format the output clearly so it's easy to review the entire release's development status.`, version, projectKey, version, projectKey)), 89 | ), 90 | }, 91 | ), nil 92 | }) 93 | } 94 | ``` -------------------------------------------------------------------------------- /tools/jira_version.go: -------------------------------------------------------------------------------- ```go 1 | package tools 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/mark3labs/mcp-go/mcp" 9 | "github.com/mark3labs/mcp-go/server" 10 | "github.com/nguyenvanduocit/jira-mcp/services" 11 | ) 12 | 13 | // Input types for version tools 14 | type GetVersionInput struct { 15 | VersionID string `json:"version_id" validate:"required"` 16 | } 17 | 18 | type ListProjectVersionsInput struct { 19 | ProjectKey string `json:"project_key" validate:"required"` 20 | } 21 | 22 | func RegisterJiraVersionTool(s *server.MCPServer) { 23 | jiraGetVersionTool := mcp.NewTool("jira_get_version", 24 | mcp.WithDescription("Retrieve detailed information about a specific Jira project version including its name, description, release date, and status"), 25 | mcp.WithString("version_id", mcp.Required(), mcp.Description("The unique identifier of the version to retrieve (e.g., 10000)")), 26 | ) 27 | s.AddTool(jiraGetVersionTool, mcp.NewTypedToolHandler(jiraGetVersionHandler)) 28 | 29 | jiraListProjectVersionsTool := mcp.NewTool("jira_list_project_versions", 30 | mcp.WithDescription("List all versions in a Jira project with their details including names, descriptions, release dates, and statuses"), 31 | mcp.WithString("project_key", mcp.Required(), mcp.Description("Project identifier to list versions for (e.g., KP, PROJ)")), 32 | ) 33 | s.AddTool(jiraListProjectVersionsTool, mcp.NewTypedToolHandler(jiraListProjectVersionsHandler)) 34 | } 35 | 36 | func jiraGetVersionHandler(ctx context.Context, request mcp.CallToolRequest, input GetVersionInput) (*mcp.CallToolResult, error) { 37 | client := services.JiraClient() 38 | 39 | version, response, err := client.Project.Version.Get(ctx, input.VersionID, nil) 40 | if err != nil { 41 | if response != nil { 42 | return nil, fmt.Errorf("failed to get version: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint) 43 | } 44 | return nil, fmt.Errorf("failed to get version: %v", err) 45 | } 46 | 47 | var result strings.Builder 48 | result.WriteString(fmt.Sprintf("Version Details:\n\n")) 49 | result.WriteString(fmt.Sprintf("ID: %s\n", version.ID)) 50 | result.WriteString(fmt.Sprintf("Name: %s\n", version.Name)) 51 | 52 | if version.Description != "" { 53 | result.WriteString(fmt.Sprintf("Description: %s\n", version.Description)) 54 | } 55 | 56 | if version.ProjectID != 0 { 57 | result.WriteString(fmt.Sprintf("Project ID: %d\n", version.ProjectID)) 58 | } 59 | 60 | result.WriteString(fmt.Sprintf("Released: %t\n", version.Released)) 61 | result.WriteString(fmt.Sprintf("Archived: %t\n", version.Archived)) 62 | 63 | if version.ReleaseDate != "" { 64 | result.WriteString(fmt.Sprintf("Release Date: %s\n", version.ReleaseDate)) 65 | } 66 | 67 | if version.Self != "" { 68 | result.WriteString(fmt.Sprintf("URL: %s\n", version.Self)) 69 | } 70 | 71 | return mcp.NewToolResultText(result.String()), nil 72 | } 73 | 74 | func jiraListProjectVersionsHandler(ctx context.Context, request mcp.CallToolRequest, input ListProjectVersionsInput) (*mcp.CallToolResult, error) { 75 | client := services.JiraClient() 76 | 77 | versions, response, err := client.Project.Version.Gets(ctx, input.ProjectKey) 78 | if err != nil { 79 | if response != nil { 80 | return nil, fmt.Errorf("failed to list project versions: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint) 81 | } 82 | return nil, fmt.Errorf("failed to list project versions: %v", err) 83 | } 84 | 85 | if len(versions) == 0 { 86 | return mcp.NewToolResultText(fmt.Sprintf("No versions found for project %s.", input.ProjectKey)), nil 87 | } 88 | 89 | var result strings.Builder 90 | result.WriteString(fmt.Sprintf("Project %s Versions:\n\n", input.ProjectKey)) 91 | 92 | for i, version := range versions { 93 | if i > 0 { 94 | result.WriteString("\n") 95 | } 96 | 97 | result.WriteString(fmt.Sprintf("ID: %s\n", version.ID)) 98 | result.WriteString(fmt.Sprintf("Name: %s\n", version.Name)) 99 | 100 | if version.Description != "" { 101 | result.WriteString(fmt.Sprintf("Description: %s\n", version.Description)) 102 | } 103 | 104 | status := "In Development" 105 | if version.Released { 106 | status = "Released" 107 | } 108 | if version.Archived { 109 | status = "Archived" 110 | } 111 | result.WriteString(fmt.Sprintf("Status: %s\n", status)) 112 | 113 | if version.ReleaseDate != "" { 114 | result.WriteString(fmt.Sprintf("Release Date: %s\n", version.ReleaseDate)) 115 | } 116 | 117 | if version.Released { 118 | result.WriteString(fmt.Sprintf("Start Date: %t\n", version.Released)) 119 | } 120 | } 121 | 122 | return mcp.NewToolResultText(result.String()), nil 123 | } 124 | ``` -------------------------------------------------------------------------------- /.specify/templates/spec-template.md: -------------------------------------------------------------------------------- ```markdown 1 | # Feature Specification: [FEATURE NAME] 2 | 3 | **Feature Branch**: `[###-feature-name]` 4 | **Created**: [DATE] 5 | **Status**: Draft 6 | **Input**: User description: "$ARGUMENTS" 7 | 8 | ## User Scenarios & Testing *(mandatory)* 9 | 10 | <!-- 11 | IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance. 12 | Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them, 13 | you should still have a viable MVP (Minimum Viable Product) that delivers value. 14 | 15 | Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical. 16 | Think of each story as a standalone slice of functionality that can be: 17 | - Developed independently 18 | - Tested independently 19 | - Deployed independently 20 | - Demonstrated to users independently 21 | --> 22 | 23 | ### User Story 1 - [Brief Title] (Priority: P1) 24 | 25 | [Describe this user journey in plain language] 26 | 27 | **Why this priority**: [Explain the value and why it has this priority level] 28 | 29 | **Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"] 30 | 31 | **Acceptance Scenarios**: 32 | 33 | 1. **Given** [initial state], **When** [action], **Then** [expected outcome] 34 | 2. **Given** [initial state], **When** [action], **Then** [expected outcome] 35 | 36 | --- 37 | 38 | ### User Story 2 - [Brief Title] (Priority: P2) 39 | 40 | [Describe this user journey in plain language] 41 | 42 | **Why this priority**: [Explain the value and why it has this priority level] 43 | 44 | **Independent Test**: [Describe how this can be tested independently] 45 | 46 | **Acceptance Scenarios**: 47 | 48 | 1. **Given** [initial state], **When** [action], **Then** [expected outcome] 49 | 50 | --- 51 | 52 | ### User Story 3 - [Brief Title] (Priority: P3) 53 | 54 | [Describe this user journey in plain language] 55 | 56 | **Why this priority**: [Explain the value and why it has this priority level] 57 | 58 | **Independent Test**: [Describe how this can be tested independently] 59 | 60 | **Acceptance Scenarios**: 61 | 62 | 1. **Given** [initial state], **When** [action], **Then** [expected outcome] 63 | 64 | --- 65 | 66 | [Add more user stories as needed, each with an assigned priority] 67 | 68 | ### Edge Cases 69 | 70 | <!-- 71 | ACTION REQUIRED: The content in this section represents placeholders. 72 | Fill them out with the right edge cases. 73 | --> 74 | 75 | - What happens when [boundary condition]? 76 | - How does system handle [error scenario]? 77 | 78 | ## Requirements *(mandatory)* 79 | 80 | <!-- 81 | ACTION REQUIRED: The content in this section represents placeholders. 82 | Fill them out with the right functional requirements. 83 | 84 | For Jira MCP context: Requirements should specify MCP tools, parameters, and expected output. 85 | Example: "System MUST expose jira_create_issue tool accepting project_key, summary, description" 86 | --> 87 | 88 | ### Functional Requirements 89 | 90 | - **FR-001**: System MUST [specific MCP tool capability, e.g., "expose jira_<operation> tool"] 91 | - **FR-002**: Tool MUST [input validation, e.g., "validate required parameters via typed input struct"] 92 | - **FR-003**: Tool MUST [output format, e.g., "return human-readable text formatted for LLM consumption"] 93 | - **FR-004**: Tool MUST [error handling, e.g., "include endpoint and response body in error messages"] 94 | - **FR-005**: System MUST [resource behavior, e.g., "reuse singleton Jira client connection"] 95 | 96 | *Example of marking unclear requirements:* 97 | 98 | - **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?] 99 | - **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified] 100 | 101 | ### Key Entities *(include if feature involves data)* 102 | 103 | - **[Entity 1]**: [What it represents, key attributes without implementation] 104 | - **[Entity 2]**: [What it represents, relationships to other entities] 105 | 106 | ## Success Criteria *(mandatory)* 107 | 108 | <!-- 109 | ACTION REQUIRED: Define measurable success criteria. 110 | These must be technology-agnostic and measurable. 111 | --> 112 | 113 | ### Measurable Outcomes 114 | 115 | - **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"] 116 | - **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"] 117 | - **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"] 118 | - **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"] 119 | ``` -------------------------------------------------------------------------------- /tools/jira_search.go: -------------------------------------------------------------------------------- ```go 1 | package tools 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/mark3labs/mcp-go/mcp" 13 | "github.com/mark3labs/mcp-go/server" 14 | "github.com/nguyenvanduocit/jira-mcp/services" 15 | "github.com/nguyenvanduocit/jira-mcp/util" 16 | jira "github.com/ctreminiom/go-atlassian/jira/v3" 17 | "github.com/ctreminiom/go-atlassian/pkg/infra/models" 18 | ) 19 | 20 | // Input types for typed tools 21 | type SearchIssueInput struct { 22 | JQL string `json:"jql" validate:"required"` 23 | Fields string `json:"fields,omitempty"` 24 | Expand string `json:"expand,omitempty"` 25 | } 26 | 27 | // searchIssuesJQL performs JQL search using the new /rest/api/3/search/jql endpoint 28 | func searchIssuesJQL(ctx context.Context, client *jira.Client, jql string, fields []string, expand []string, startAt, maxResults int) (*models.IssueSearchScheme, error) { 29 | // Prepare query parameters 30 | params := url.Values{} 31 | params.Set("jql", jql) 32 | 33 | if len(fields) > 0 { 34 | params.Set("fields", strings.Join(fields, ",")) 35 | } 36 | 37 | if len(expand) > 0 { 38 | params.Set("expand", strings.Join(expand, ",")) 39 | } 40 | 41 | if startAt > 0 { 42 | params.Set("startAt", strconv.Itoa(startAt)) 43 | } 44 | 45 | if maxResults > 0 { 46 | params.Set("maxResults", strconv.Itoa(maxResults)) 47 | } 48 | 49 | // Build the URL 50 | endpoint := fmt.Sprintf("%s/rest/api/3/search/jql?%s", client.Site.String(), params.Encode()) 51 | 52 | // Create HTTP request 53 | req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) 54 | if err != nil { 55 | return nil, fmt.Errorf("failed to create request: %w", err) 56 | } 57 | 58 | // Add authentication headers - the client already has basic auth configured 59 | if client.Auth != nil && client.Auth.HasBasicAuth() { 60 | username, password := client.Auth.GetBasicAuth() 61 | req.SetBasicAuth(username, password) 62 | } 63 | 64 | req.Header.Set("Accept", "application/json") 65 | 66 | // Perform the request 67 | resp, err := client.HTTP.Do(req) 68 | if err != nil { 69 | return nil, fmt.Errorf("failed to perform request: %w", err) 70 | } 71 | defer resp.Body.Close() 72 | 73 | if resp.StatusCode != http.StatusOK { 74 | return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, resp.Status) 75 | } 76 | 77 | // Parse the response 78 | var searchResult models.IssueSearchScheme 79 | if err := json.NewDecoder(resp.Body).Decode(&searchResult); err != nil { 80 | return nil, fmt.Errorf("failed to decode response: %w", err) 81 | } 82 | 83 | return &searchResult, nil 84 | } 85 | 86 | func RegisterJiraSearchTool(s *server.MCPServer) { 87 | jiraSearchTool := mcp.NewTool("jira_search_issue", 88 | mcp.WithDescription("Search for Jira issues using JQL (Jira Query Language). Returns key details like summary, status, assignee, and priority for matching issues"), 89 | mcp.WithString("jql", mcp.Required(), mcp.Description("JQL query string (e.g., 'project = SHTP AND status = \"In Progress\"')")), 90 | mcp.WithString("fields", mcp.Description("Comma-separated list of fields to retrieve (e.g., 'summary,status,assignee'). If not specified, all fields are returned.")), 91 | mcp.WithString("expand", mcp.Description("Comma-separated list of fields to expand for additional details (e.g., 'transitions,changelog,subtasks,description').")), 92 | ) 93 | s.AddTool(jiraSearchTool, mcp.NewTypedToolHandler(jiraSearchHandler)) 94 | } 95 | 96 | func jiraSearchHandler(ctx context.Context, request mcp.CallToolRequest, input SearchIssueInput) (*mcp.CallToolResult, error) { 97 | client := services.JiraClient() 98 | 99 | // Parse fields parameter 100 | var fields []string 101 | if input.Fields != "" { 102 | fields = strings.Split(strings.ReplaceAll(input.Fields, " ", ""), ",") 103 | } 104 | 105 | // Parse expand parameter 106 | var expand []string = []string{"transitions", "changelog", "subtasks", "description"} 107 | if input.Expand != "" { 108 | expand = strings.Split(strings.ReplaceAll(input.Expand, " ", ""), ",") 109 | } 110 | 111 | searchResult, err := searchIssuesJQL(ctx, client, input.JQL, fields, expand, 0, 30) 112 | if err != nil { 113 | return nil, fmt.Errorf("failed to search issues: %v", err) 114 | } 115 | 116 | if len(searchResult.Issues) == 0 { 117 | return mcp.NewToolResultText("No issues found matching the search criteria."), nil 118 | } 119 | 120 | var sb strings.Builder 121 | for index, issue := range searchResult.Issues { 122 | // Use the comprehensive formatter for each issue 123 | formattedIssue := util.FormatJiraIssue(issue) 124 | sb.WriteString(formattedIssue) 125 | if index < len(searchResult.Issues) - 1 { 126 | sb.WriteString("\n===\n") 127 | } 128 | } 129 | 130 | return mcp.NewToolResultText(sb.String()), nil 131 | } 132 | ``` -------------------------------------------------------------------------------- /.specify/templates/plan-template.md: -------------------------------------------------------------------------------- ```markdown 1 | # Implementation Plan: [FEATURE] 2 | 3 | **Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] 4 | **Input**: Feature specification from `/specs/[###-feature-name]/spec.md` 5 | 6 | **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. 7 | 8 | ## Summary 9 | 10 | [Extract from feature spec: primary requirement + technical approach from research] 11 | 12 | ## Technical Context 13 | 14 | <!-- 15 | ACTION REQUIRED: Replace the content in this section with the technical details 16 | for the project. The structure here is presented in advisory capacity to guide 17 | the iteration process. 18 | --> 19 | 20 | **Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION] 21 | **Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION] 22 | **Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A] 23 | **Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION] 24 | **Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION] 25 | **Project Type**: [single/web/mobile - determines source structure] 26 | **Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION] 27 | **Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION] 28 | **Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION] 29 | 30 | ## Constitution Check 31 | 32 | *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* 33 | 34 | **MCP Protocol Compliance:** 35 | - [ ] All features exposed via MCP tools (no direct API bypass) 36 | - [ ] Tool names follow `jira_<operation>` convention 37 | - [ ] STDIO mode primary, HTTP mode development-only 38 | 39 | **AI-First Output:** 40 | - [ ] Responses formatted for LLM readability 41 | - [ ] Error messages include diagnostic context (endpoint, response body) 42 | - [ ] Output uses `util.Format*` functions or similar formatting 43 | 44 | **Simplicity:** 45 | - [ ] No unnecessary abstraction layers (managers, facades, orchestrators) 46 | - [ ] Direct client calls preferred over wrappers 47 | - [ ] Complexity violations documented in "Complexity Tracking" below 48 | 49 | **Type Safety:** 50 | - [ ] Input structs defined with JSON tags and validation 51 | - [ ] Typed handlers used (`mcp.NewTypedToolHandler`) 52 | 53 | **Resource Efficiency:** 54 | - [ ] Singleton pattern for client connections (`services.JiraClient()`) 55 | - [ ] No per-request client creation 56 | 57 | **Testing Gates:** 58 | - [ ] Integration tests for new tool categories 59 | - [ ] Contract tests for tool registration and parameters 60 | 61 | ## Project Structure 62 | 63 | ### Documentation (this feature) 64 | 65 | ``` 66 | specs/[###-feature]/ 67 | ├── plan.md # This file (/speckit.plan command output) 68 | ├── research.md # Phase 0 output (/speckit.plan command) 69 | ├── data-model.md # Phase 1 output (/speckit.plan command) 70 | ├── quickstart.md # Phase 1 output (/speckit.plan command) 71 | ├── contracts/ # Phase 1 output (/speckit.plan command) 72 | └── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) 73 | ``` 74 | 75 | ### Source Code (repository root) 76 | <!-- 77 | ACTION REQUIRED: Replace the placeholder tree below with the concrete layout 78 | for this feature. Delete unused options and expand the chosen structure with 79 | real paths (e.g., apps/admin, packages/something). The delivered plan must 80 | not include Option labels. 81 | --> 82 | 83 | ``` 84 | # [REMOVE IF UNUSED] Option 1: Single project (DEFAULT) 85 | src/ 86 | ├── models/ 87 | ├── services/ 88 | ├── cli/ 89 | └── lib/ 90 | 91 | tests/ 92 | ├── contract/ 93 | ├── integration/ 94 | └── unit/ 95 | 96 | # [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected) 97 | backend/ 98 | ├── src/ 99 | │ ├── models/ 100 | │ ├── services/ 101 | │ └── api/ 102 | └── tests/ 103 | 104 | frontend/ 105 | ├── src/ 106 | │ ├── components/ 107 | │ ├── pages/ 108 | │ └── services/ 109 | └── tests/ 110 | 111 | # [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected) 112 | api/ 113 | └── [same as backend above] 114 | 115 | ios/ or android/ 116 | └── [platform-specific structure: feature modules, UI flows, platform tests] 117 | ``` 118 | 119 | **Structure Decision**: [Document the selected structure and reference the real 120 | directories captured above] 121 | 122 | ## Complexity Tracking 123 | 124 | *Fill ONLY if Constitution Check has violations that must be justified* 125 | 126 | | Violation | Why Needed | Simpler Alternative Rejected Because | 127 | |-----------|------------|-------------------------------------| 128 | | [e.g., 4th project] | [current need] | [why 3 projects insufficient] | 129 | | [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] | 130 | ``` -------------------------------------------------------------------------------- /.claude/commands/speckit.implement.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | description: Execute the implementation plan by processing and executing all tasks defined in tasks.md 3 | --- 4 | 5 | ## User Input 6 | 7 | ```text 8 | $ARGUMENTS 9 | ``` 10 | 11 | You **MUST** consider the user input before proceeding (if not empty). 12 | 13 | ## Outline 14 | 15 | 1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. 16 | 17 | 2. **Check checklists status** (if FEATURE_DIR/checklists/ exists): 18 | - Scan all checklist files in the checklists/ directory 19 | - For each checklist, count: 20 | * Total items: All lines matching `- [ ]` or `- [X]` or `- [x]` 21 | * Completed items: Lines matching `- [X]` or `- [x]` 22 | * Incomplete items: Lines matching `- [ ]` 23 | - Create a status table: 24 | ``` 25 | | Checklist | Total | Completed | Incomplete | Status | 26 | |-----------|-------|-----------|------------|--------| 27 | | ux.md | 12 | 12 | 0 | ✓ PASS | 28 | | test.md | 8 | 5 | 3 | ✗ FAIL | 29 | | security.md | 6 | 6 | 0 | ✓ PASS | 30 | ``` 31 | - Calculate overall status: 32 | * **PASS**: All checklists have 0 incomplete items 33 | * **FAIL**: One or more checklists have incomplete items 34 | 35 | - **If any checklist is incomplete**: 36 | * Display the table with incomplete item counts 37 | * **STOP** and ask: "Some checklists are incomplete. Do you want to proceed with implementation anyway? (yes/no)" 38 | * Wait for user response before continuing 39 | * If user says "no" or "wait" or "stop", halt execution 40 | * If user says "yes" or "proceed" or "continue", proceed to step 3 41 | 42 | - **If all checklists are complete**: 43 | * Display the table showing all checklists passed 44 | * Automatically proceed to step 3 45 | 46 | 3. Load and analyze the implementation context: 47 | - **REQUIRED**: Read tasks.md for the complete task list and execution plan 48 | - **REQUIRED**: Read plan.md for tech stack, architecture, and file structure 49 | - **IF EXISTS**: Read data-model.md for entities and relationships 50 | - **IF EXISTS**: Read contracts/ for API specifications and test requirements 51 | - **IF EXISTS**: Read research.md for technical decisions and constraints 52 | - **IF EXISTS**: Read quickstart.md for integration scenarios 53 | 54 | 4. Parse tasks.md structure and extract: 55 | - **Task phases**: Setup, Tests, Core, Integration, Polish 56 | - **Task dependencies**: Sequential vs parallel execution rules 57 | - **Task details**: ID, description, file paths, parallel markers [P] 58 | - **Execution flow**: Order and dependency requirements 59 | 60 | 5. Execute implementation following the task plan: 61 | - **Phase-by-phase execution**: Complete each phase before moving to the next 62 | - **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together 63 | - **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks 64 | - **File-based coordination**: Tasks affecting the same files must run sequentially 65 | - **Validation checkpoints**: Verify each phase completion before proceeding 66 | 67 | 6. Implementation execution rules: 68 | - **Setup first**: Initialize project structure, dependencies, configuration 69 | - **Tests before code**: If you need to write tests for contracts, entities, and integration scenarios 70 | - **Core development**: Implement models, services, CLI commands, endpoints 71 | - **Integration work**: Database connections, middleware, logging, external services 72 | - **Polish and validation**: Unit tests, performance optimization, documentation 73 | 74 | 7. Progress tracking and error handling: 75 | - Report progress after each completed task 76 | - Halt execution if any non-parallel task fails 77 | - For parallel tasks [P], continue with successful tasks, report failed ones 78 | - Provide clear error messages with context for debugging 79 | - Suggest next steps if implementation cannot proceed 80 | - **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file. 81 | 82 | 8. Completion validation: 83 | - Verify all required tasks are completed 84 | - Check that implemented features match the original specification 85 | - Validate that tests pass and coverage meets requirements 86 | - Confirm the implementation follows the technical plan 87 | - Report final status with summary of completed work 88 | 89 | Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/tasks` first to regenerate the task list. 90 | ``` -------------------------------------------------------------------------------- /.specify/scripts/bash/check-prerequisites.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/usr/bin/env bash 2 | 3 | # Consolidated prerequisite checking script 4 | # 5 | # This script provides unified prerequisite checking for Spec-Driven Development workflow. 6 | # It replaces the functionality previously spread across multiple scripts. 7 | # 8 | # Usage: ./check-prerequisites.sh [OPTIONS] 9 | # 10 | # OPTIONS: 11 | # --json Output in JSON format 12 | # --require-tasks Require tasks.md to exist (for implementation phase) 13 | # --include-tasks Include tasks.md in AVAILABLE_DOCS list 14 | # --paths-only Only output path variables (no validation) 15 | # --help, -h Show help message 16 | # 17 | # OUTPUTS: 18 | # JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]} 19 | # Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md 20 | # Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc. 21 | 22 | set -e 23 | 24 | # Parse command line arguments 25 | JSON_MODE=false 26 | REQUIRE_TASKS=false 27 | INCLUDE_TASKS=false 28 | PATHS_ONLY=false 29 | 30 | for arg in "$@"; do 31 | case "$arg" in 32 | --json) 33 | JSON_MODE=true 34 | ;; 35 | --require-tasks) 36 | REQUIRE_TASKS=true 37 | ;; 38 | --include-tasks) 39 | INCLUDE_TASKS=true 40 | ;; 41 | --paths-only) 42 | PATHS_ONLY=true 43 | ;; 44 | --help|-h) 45 | cat << 'EOF' 46 | Usage: check-prerequisites.sh [OPTIONS] 47 | 48 | Consolidated prerequisite checking for Spec-Driven Development workflow. 49 | 50 | OPTIONS: 51 | --json Output in JSON format 52 | --require-tasks Require tasks.md to exist (for implementation phase) 53 | --include-tasks Include tasks.md in AVAILABLE_DOCS list 54 | --paths-only Only output path variables (no prerequisite validation) 55 | --help, -h Show this help message 56 | 57 | EXAMPLES: 58 | # Check task prerequisites (plan.md required) 59 | ./check-prerequisites.sh --json 60 | 61 | # Check implementation prerequisites (plan.md + tasks.md required) 62 | ./check-prerequisites.sh --json --require-tasks --include-tasks 63 | 64 | # Get feature paths only (no validation) 65 | ./check-prerequisites.sh --paths-only 66 | 67 | EOF 68 | exit 0 69 | ;; 70 | *) 71 | echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2 72 | exit 1 73 | ;; 74 | esac 75 | done 76 | 77 | # Source common functions 78 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 79 | source "$SCRIPT_DIR/common.sh" 80 | 81 | # Get feature paths and validate branch 82 | eval $(get_feature_paths) 83 | check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 84 | 85 | # If paths-only mode, output paths and exit (support JSON + paths-only combined) 86 | if $PATHS_ONLY; then 87 | if $JSON_MODE; then 88 | # Minimal JSON paths payload (no validation performed) 89 | printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ 90 | "$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS" 91 | else 92 | echo "REPO_ROOT: $REPO_ROOT" 93 | echo "BRANCH: $CURRENT_BRANCH" 94 | echo "FEATURE_DIR: $FEATURE_DIR" 95 | echo "FEATURE_SPEC: $FEATURE_SPEC" 96 | echo "IMPL_PLAN: $IMPL_PLAN" 97 | echo "TASKS: $TASKS" 98 | fi 99 | exit 0 100 | fi 101 | 102 | # Validate required directories and files 103 | if [[ ! -d "$FEATURE_DIR" ]]; then 104 | echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2 105 | echo "Run /speckit.specify first to create the feature structure." >&2 106 | exit 1 107 | fi 108 | 109 | if [[ ! -f "$IMPL_PLAN" ]]; then 110 | echo "ERROR: plan.md not found in $FEATURE_DIR" >&2 111 | echo "Run /speckit.plan first to create the implementation plan." >&2 112 | exit 1 113 | fi 114 | 115 | # Check for tasks.md if required 116 | if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then 117 | echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2 118 | echo "Run /speckit.tasks first to create the task list." >&2 119 | exit 1 120 | fi 121 | 122 | # Build list of available documents 123 | docs=() 124 | 125 | # Always check these optional docs 126 | [[ -f "$RESEARCH" ]] && docs+=("research.md") 127 | [[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") 128 | 129 | # Check contracts directory (only if it exists and has files) 130 | if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then 131 | docs+=("contracts/") 132 | fi 133 | 134 | [[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") 135 | 136 | # Include tasks.md if requested and it exists 137 | if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then 138 | docs+=("tasks.md") 139 | fi 140 | 141 | # Output results 142 | if $JSON_MODE; then 143 | # Build JSON array of documents 144 | if [[ ${#docs[@]} -eq 0 ]]; then 145 | json_docs="[]" 146 | else 147 | json_docs=$(printf '"%s",' "${docs[@]}") 148 | json_docs="[${json_docs%,}]" 149 | fi 150 | 151 | printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" 152 | else 153 | # Text output 154 | echo "FEATURE_DIR:$FEATURE_DIR" 155 | echo "AVAILABLE_DOCS:" 156 | 157 | # Show status of each potential document 158 | check_file "$RESEARCH" "research.md" 159 | check_file "$DATA_MODEL" "data-model.md" 160 | check_dir "$CONTRACTS_DIR" "contracts/" 161 | check_file "$QUICKSTART" "quickstart.md" 162 | 163 | if $INCLUDE_TASKS; then 164 | check_file "$TASKS" "tasks.md" 165 | fi 166 | fi ``` -------------------------------------------------------------------------------- /specs/001-i-want-to/test-dev-api.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Load environment variables from .env file 4 | set -a 5 | source "$(dirname "$0")/../../.env" 6 | set +a 7 | 8 | JIRA_HOST="${ATLASSIAN_HOST}" 9 | JIRA_EMAIL="${ATLASSIAN_EMAIL}" 10 | JIRA_TOKEN="${ATLASSIAN_TOKEN}" 11 | ISSUE_KEY="SHTP-6050" 12 | 13 | echo "=========================================" 14 | echo "Step 1: Getting numeric issue ID for ${ISSUE_KEY}" 15 | echo "=========================================" 16 | 17 | ISSUE_RESPONSE=$(curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 18 | -H "Accept: application/json" \ 19 | "${JIRA_HOST}/rest/api/3/issue/${ISSUE_KEY}?fields=id") 20 | 21 | echo "$ISSUE_RESPONSE" | jq '.' 22 | 23 | ISSUE_ID=$(echo "$ISSUE_RESPONSE" | jq -r '.id') 24 | 25 | if [ -z "$ISSUE_ID" ] || [ "$ISSUE_ID" = "null" ]; then 26 | echo "ERROR: Could not get issue ID" 27 | exit 1 28 | fi 29 | 30 | echo "" 31 | echo "Issue ID: ${ISSUE_ID}" 32 | echo "" 33 | 34 | echo "=========================================" 35 | echo "Step 2: Getting development summary" 36 | echo "=========================================" 37 | 38 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 39 | -H "Accept: application/json" \ 40 | "${JIRA_HOST}/rest/dev-status/latest/issue/summary?issueId=${ISSUE_ID}" | jq '.' 41 | 42 | echo "" 43 | echo "=========================================" 44 | echo "Step 3: Getting development details (repository) - GitLab" 45 | echo "=========================================" 46 | 47 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 48 | -H "Accept: application/json" \ 49 | "${JIRA_HOST}/rest/dev-status/latest/issue/detail?issueId=${ISSUE_ID}&applicationType=GitLab&dataType=repository" | jq '.' 50 | 51 | echo "" 52 | echo "=========================================" 53 | echo "Step 4: Getting development details (branch) - GitLab" 54 | echo "=========================================" 55 | 56 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 57 | -H "Accept: application/json" \ 58 | "${JIRA_HOST}/rest/dev-status/latest/issue/detail?issueId=${ISSUE_ID}&applicationType=GitLab&dataType=branch" | jq '.' 59 | 60 | echo "" 61 | echo "=========================================" 62 | echo "Step 5: Getting development details (pullrequest) - GitLab" 63 | echo "=========================================" 64 | 65 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 66 | -H "Accept: application/json" \ 67 | "${JIRA_HOST}/rest/dev-status/latest/issue/detail?issueId=${ISSUE_ID}&applicationType=GitLab&dataType=pullrequest" | jq '.' 68 | 69 | echo "" 70 | echo "=========================================" 71 | echo "Step 6: Getting development details (commit) - GitLab" 72 | echo "=========================================" 73 | 74 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 75 | -H "Accept: application/json" \ 76 | "${JIRA_HOST}/rest/dev-status/latest/issue/detail?issueId=${ISSUE_ID}&applicationType=GitLab&dataType=commit" | jq '.' 77 | 78 | echo "" 79 | echo "=========================================" 80 | echo "Step 7: Getting build information - All types" 81 | echo "=========================================" 82 | 83 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 84 | -H "Accept: application/json" \ 85 | "${JIRA_HOST}/rest/dev-status/latest/issue/detail?issueId=${ISSUE_ID}&dataType=build" | jq '.' 86 | 87 | echo "" 88 | echo "=========================================" 89 | echo "Step 8: Getting build information - cloud-providers" 90 | echo "=========================================" 91 | 92 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 93 | -H "Accept: application/json" \ 94 | "${JIRA_HOST}/rest/dev-status/latest/issue/detail?issueId=${ISSUE_ID}&applicationType=cloud-providers&dataType=build" | jq '.' 95 | 96 | echo "" 97 | echo "=========================================" 98 | echo "Step 9: Getting deployment information - All types" 99 | echo "=========================================" 100 | 101 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 102 | -H "Accept: application/json" \ 103 | "${JIRA_HOST}/rest/dev-status/latest/issue/detail?issueId=${ISSUE_ID}&dataType=deployment" | jq '.' 104 | 105 | echo "" 106 | echo "=========================================" 107 | echo "Step 10: Getting deployment-environment information" 108 | echo "=========================================" 109 | 110 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 111 | -H "Accept: application/json" \ 112 | "${JIRA_HOST}/rest/dev-status/latest/issue/detail?issueId=${ISSUE_ID}&dataType=deployment-environment" | jq '.' 113 | 114 | echo "" 115 | echo "=========================================" 116 | echo "Step 11: Getting review information" 117 | echo "=========================================" 118 | 119 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 120 | -H "Accept: application/json" \ 121 | "${JIRA_HOST}/rest/dev-status/latest/issue/detail?issueId=${ISSUE_ID}&dataType=review" | jq '.' 122 | 123 | echo "" 124 | echo "=========================================" 125 | echo "Step 12: Getting ALL development details - GitLab" 126 | echo "=========================================" 127 | 128 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 129 | -H "Accept: application/json" \ 130 | "${JIRA_HOST}/rest/dev-status/latest/issue/detail?issueId=${ISSUE_ID}&applicationType=GitLab" | jq '.' 131 | 132 | echo "" 133 | echo "=========================================" 134 | echo "Step 13: Getting ALL development details - No filters" 135 | echo "=========================================" 136 | 137 | curl -s -u "${JIRA_EMAIL}:${JIRA_TOKEN}" \ 138 | -H "Accept: application/json" \ 139 | "${JIRA_HOST}/rest/dev-status/latest/issue/detail?issueId=${ISSUE_ID}" | jq '.' 140 | ``` -------------------------------------------------------------------------------- /.claude/commands/speckit.constitution.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | description: Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync. 3 | --- 4 | 5 | ## User Input 6 | 7 | ```text 8 | $ARGUMENTS 9 | ``` 10 | 11 | You **MUST** consider the user input before proceeding (if not empty). 12 | 13 | ## Outline 14 | 15 | You are updating the project constitution at `.specify/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts. 16 | 17 | Follow this execution flow: 18 | 19 | 1. Load the existing constitution template at `.specify/memory/constitution.md`. 20 | - Identify every placeholder token of the form `[ALL_CAPS_IDENTIFIER]`. 21 | **IMPORTANT**: The user might require less or more principles than the ones used in the template. If a number is specified, respect that - follow the general template. You will update the doc accordingly. 22 | 23 | 2. Collect/derive values for placeholders: 24 | - If user input (conversation) supplies a value, use it. 25 | - Otherwise infer from existing repo context (README, docs, prior constitution versions if embedded). 26 | - For governance dates: `RATIFICATION_DATE` is the original adoption date (if unknown ask or mark TODO), `LAST_AMENDED_DATE` is today if changes are made, otherwise keep previous. 27 | - `CONSTITUTION_VERSION` must increment according to semantic versioning rules: 28 | * MAJOR: Backward incompatible governance/principle removals or redefinitions. 29 | * MINOR: New principle/section added or materially expanded guidance. 30 | * PATCH: Clarifications, wording, typo fixes, non-semantic refinements. 31 | - If version bump type ambiguous, propose reasoning before finalizing. 32 | 33 | 3. Draft the updated constitution content: 34 | - Replace every placeholder with concrete text (no bracketed tokens left except intentionally retained template slots that the project has chosen not to define yet—explicitly justify any left). 35 | - Preserve heading hierarchy and comments can be removed once replaced unless they still add clarifying guidance. 36 | - Ensure each Principle section: succinct name line, paragraph (or bullet list) capturing non‑negotiable rules, explicit rationale if not obvious. 37 | - Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations. 38 | 39 | 4. Consistency propagation checklist (convert prior checklist into active validations): 40 | - Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles. 41 | - Read `.specify/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints. 42 | - Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline). 43 | - Read each command file in `.specify/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required. 44 | - Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed. 45 | 46 | 5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update): 47 | - Version change: old → new 48 | - List of modified principles (old title → new title if renamed) 49 | - Added sections 50 | - Removed sections 51 | - Templates requiring updates (✅ updated / ⚠ pending) with file paths 52 | - Follow-up TODOs if any placeholders intentionally deferred. 53 | 54 | 6. Validation before final output: 55 | - No remaining unexplained bracket tokens. 56 | - Version line matches report. 57 | - Dates ISO format YYYY-MM-DD. 58 | - Principles are declarative, testable, and free of vague language ("should" → replace with MUST/SHOULD rationale where appropriate). 59 | 60 | 7. Write the completed constitution back to `.specify/memory/constitution.md` (overwrite). 61 | 62 | 8. Output a final summary to the user with: 63 | - New version and bump rationale. 64 | - Any files flagged for manual follow-up. 65 | - Suggested commit message (e.g., `docs: amend constitution to vX.Y.Z (principle additions + governance update)`). 66 | 67 | Formatting & Style Requirements: 68 | - Use Markdown headings exactly as in the template (do not demote/promote levels). 69 | - Wrap long rationale lines to keep readability (<100 chars ideally) but do not hard enforce with awkward breaks. 70 | - Keep a single blank line between sections. 71 | - Avoid trailing whitespace. 72 | 73 | If the user supplies partial updates (e.g., only one principle revision), still perform validation and version decision steps. 74 | 75 | If critical info missing (e.g., ratification date truly unknown), insert `TODO(<FIELD_NAME>): explanation` and include in the Sync Impact Report under deferred items. 76 | 77 | Do not create a new template; always operate on the existing `.specify/memory/constitution.md` file. 78 | ``` -------------------------------------------------------------------------------- /tools/jira_relationship.go: -------------------------------------------------------------------------------- ```go 1 | package tools 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/ctreminiom/go-atlassian/pkg/infra/models" 9 | "github.com/mark3labs/mcp-go/mcp" 10 | "github.com/mark3labs/mcp-go/server" 11 | "github.com/nguyenvanduocit/jira-mcp/services" 12 | ) 13 | 14 | // Input types for typed tools 15 | type GetRelatedIssuesInput struct { 16 | IssueKey string `json:"issue_key" validate:"required"` 17 | } 18 | 19 | type LinkIssuesInput struct { 20 | InwardIssue string `json:"inward_issue" validate:"required"` 21 | OutwardIssue string `json:"outward_issue" validate:"required"` 22 | LinkType string `json:"link_type" validate:"required"` 23 | Comment string `json:"comment,omitempty"` 24 | } 25 | 26 | func RegisterJiraRelationshipTool(s *server.MCPServer) { 27 | jiraRelationshipTool := mcp.NewTool("jira_get_related_issues", 28 | mcp.WithDescription("Retrieve issues that have a relationship with a given issue, such as blocks, is blocked by, relates to, etc."), 29 | mcp.WithString("issue_key", mcp.Required(), mcp.Description("The unique identifier of the Jira issue (e.g., KP-2, PROJ-123)")), 30 | ) 31 | s.AddTool(jiraRelationshipTool, mcp.NewTypedToolHandler(jiraRelationshipHandler)) 32 | 33 | jiraLinkTool := mcp.NewTool("jira_link_issues", 34 | mcp.WithDescription("Create a link between two Jira issues, defining their relationship (e.g., blocks, duplicates, relates to)"), 35 | mcp.WithString("inward_issue", mcp.Required(), mcp.Description("The key of the inward issue (e.g., KP-1, PROJ-123)")), 36 | mcp.WithString("outward_issue", mcp.Required(), mcp.Description("The key of the outward issue (e.g., KP-2, PROJ-123)")), 37 | mcp.WithString("link_type", mcp.Required(), mcp.Description("The type of link between issues (e.g., Duplicate, Blocks, Relates)")), 38 | mcp.WithString("comment", mcp.Description("Optional comment to add when creating the link")), 39 | ) 40 | s.AddTool(jiraLinkTool, mcp.NewTypedToolHandler(jiraLinkHandler)) 41 | } 42 | 43 | func jiraRelationshipHandler(ctx context.Context, request mcp.CallToolRequest, input GetRelatedIssuesInput) (*mcp.CallToolResult, error) { 44 | client := services.JiraClient() 45 | 46 | // Get the issue with the 'issuelinks' field 47 | issue, response, err := client.Issue.Get(ctx, input.IssueKey, nil, []string{"issuelinks"}) 48 | if err != nil { 49 | if response != nil { 50 | return nil, fmt.Errorf("failed to get issue: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint) 51 | } 52 | return nil, fmt.Errorf("failed to get issue: %v", err) 53 | } 54 | 55 | if issue.Fields.IssueLinks == nil || len(issue.Fields.IssueLinks) == 0 { 56 | return mcp.NewToolResultText(fmt.Sprintf("Issue %s has no linked issues.", input.IssueKey)), nil 57 | } 58 | 59 | var sb strings.Builder 60 | sb.WriteString(fmt.Sprintf("Related issues for %s:\n\n", input.IssueKey)) 61 | 62 | for _, link := range issue.Fields.IssueLinks { 63 | // Determine the relationship type and related issue 64 | var relatedIssue string 65 | var relationshipType string 66 | var direction string 67 | 68 | if link.InwardIssue != nil { 69 | relatedIssue = link.InwardIssue.Key 70 | relationshipType = link.Type.Inward 71 | direction = "inward" 72 | } else if link.OutwardIssue != nil { 73 | relatedIssue = link.OutwardIssue.Key 74 | relationshipType = link.Type.Outward 75 | direction = "outward" 76 | } else { 77 | continue // Skip if no related issue 78 | } 79 | 80 | var summary string 81 | if direction == "inward" && link.InwardIssue.Fields.Summary != "" { 82 | summary = link.InwardIssue.Fields.Summary 83 | } else if direction == "outward" && link.OutwardIssue.Fields.Summary != "" { 84 | summary = link.OutwardIssue.Fields.Summary 85 | } 86 | 87 | var status string 88 | if direction == "inward" && link.InwardIssue.Fields.Status != nil { 89 | status = link.InwardIssue.Fields.Status.Name 90 | } else if direction == "outward" && link.OutwardIssue.Fields.Status != nil { 91 | status = link.OutwardIssue.Fields.Status.Name 92 | } else { 93 | status = "Unknown" 94 | } 95 | 96 | sb.WriteString(fmt.Sprintf("Relationship: %s\n", relationshipType)) 97 | sb.WriteString(fmt.Sprintf("Issue: %s\n", relatedIssue)) 98 | sb.WriteString(fmt.Sprintf("Summary: %s\n", summary)) 99 | sb.WriteString(fmt.Sprintf("Status: %s\n", status)) 100 | sb.WriteString("\n") 101 | } 102 | 103 | return mcp.NewToolResultText(sb.String()), nil 104 | } 105 | 106 | 107 | func jiraLinkHandler(ctx context.Context, request mcp.CallToolRequest, input LinkIssuesInput) (*mcp.CallToolResult, error) { 108 | client := services.JiraClient() 109 | 110 | // Create the link payload 111 | payload := &models.LinkPayloadSchemeV3{ 112 | InwardIssue: &models.LinkedIssueScheme{ 113 | Key: input.InwardIssue, 114 | }, 115 | OutwardIssue: &models.LinkedIssueScheme{ 116 | Key: input.OutwardIssue, 117 | }, 118 | Type: &models.LinkTypeScheme{ 119 | Name: input.LinkType, 120 | }, 121 | } 122 | 123 | // Add comment if provided 124 | if input.Comment != "" { 125 | payload.Comment = &models.CommentPayloadScheme{ 126 | Body: &models.CommentNodeScheme{ 127 | Type: "text", 128 | Text: input.Comment, 129 | }, 130 | } 131 | } 132 | 133 | // Create the link 134 | response, err := client.Issue.Link.Create(ctx, payload) 135 | if err != nil { 136 | if response != nil { 137 | return nil, fmt.Errorf("failed to link issues: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint) 138 | } 139 | return nil, fmt.Errorf("failed to link issues: %v", err) 140 | } 141 | 142 | return mcp.NewToolResultText(fmt.Sprintf("Successfully linked issues %s and %s with link type \"%s\"", input.InwardIssue, input.OutwardIssue, input.LinkType)), nil 143 | } ``` -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- ```go 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "strings" 11 | 12 | "github.com/joho/godotenv" 13 | "github.com/mark3labs/mcp-go/server" 14 | "github.com/nguyenvanduocit/jira-mcp/prompts" 15 | "github.com/nguyenvanduocit/jira-mcp/tools" 16 | ) 17 | 18 | func main() { 19 | envFile := flag.String("env", "", "Path to environment file (optional when environment variables are set directly)") 20 | httpPort := flag.String("http_port", "", "Port for HTTP server. If not provided, will use stdio") 21 | flag.Parse() 22 | 23 | // Load environment file if specified 24 | if *envFile != "" { 25 | if err := godotenv.Load(*envFile); err != nil { 26 | fmt.Printf("⚠️ Warning: Error loading env file %s: %v\n", *envFile, err) 27 | } else { 28 | fmt.Printf("✅ Loaded environment variables from %s\n", *envFile) 29 | } 30 | } 31 | 32 | // Check required environment variables 33 | requiredEnvs := []string{"ATLASSIAN_HOST", "ATLASSIAN_EMAIL", "ATLASSIAN_TOKEN"} 34 | missingEnvs := []string{} 35 | for _, env := range requiredEnvs { 36 | if os.Getenv(env) == "" { 37 | missingEnvs = append(missingEnvs, env) 38 | } 39 | } 40 | 41 | if len(missingEnvs) > 0 { 42 | fmt.Println("❌ Configuration Error: Missing required environment variables") 43 | fmt.Println() 44 | fmt.Println("Missing variables:") 45 | for _, env := range missingEnvs { 46 | fmt.Printf(" - %s\n", env) 47 | } 48 | fmt.Println() 49 | fmt.Println("📋 Setup Instructions:") 50 | fmt.Println("1. Get your Atlassian API token from: https://id.atlassian.com/manage-profile/security/api-tokens") 51 | fmt.Println("2. Set the environment variables:") 52 | fmt.Println() 53 | fmt.Println(" Option A - Using .env file:") 54 | fmt.Println(" Create a .env file with:") 55 | fmt.Println(" ATLASSIAN_HOST=https://your-domain.atlassian.net") 56 | fmt.Println(" [email protected]") 57 | fmt.Println(" ATLASSIAN_TOKEN=your-api-token") 58 | fmt.Println() 59 | fmt.Println(" Option B - Using environment variables:") 60 | fmt.Println(" export ATLASSIAN_HOST=https://your-domain.atlassian.net") 61 | fmt.Println(" export [email protected]") 62 | fmt.Println(" export ATLASSIAN_TOKEN=your-api-token") 63 | fmt.Println() 64 | fmt.Println(" Option C - Using Docker:") 65 | fmt.Printf(" docker run -e ATLASSIAN_HOST=https://your-domain.atlassian.net \\\n") 66 | fmt.Printf(" -e [email protected] \\\n") 67 | fmt.Printf(" -e ATLASSIAN_TOKEN=your-api-token \\\n") 68 | fmt.Printf(" ghcr.io/nguyenvanduocit/jira-mcp:latest\n") 69 | fmt.Println() 70 | os.Exit(1) 71 | } 72 | 73 | fmt.Println("✅ All required environment variables are set") 74 | fmt.Printf("🔗 Connected to: %s\n", os.Getenv("ATLASSIAN_HOST")) 75 | 76 | mcpServer := server.NewMCPServer( 77 | "Jira MCP", 78 | "1.0.1", 79 | server.WithLogging(), 80 | server.WithPromptCapabilities(true), 81 | server.WithResourceCapabilities(true, true), 82 | server.WithRecovery(), 83 | ) 84 | 85 | // Register all Jira tools 86 | tools.RegisterJiraIssueTool(mcpServer) 87 | tools.RegisterJiraSearchTool(mcpServer) 88 | tools.RegisterJiraSprintTool(mcpServer) 89 | tools.RegisterJiraStatusTool(mcpServer) 90 | tools.RegisterJiraTransitionTool(mcpServer) 91 | tools.RegisterJiraWorklogTool(mcpServer) 92 | tools.RegisterJiraCommentTools(mcpServer) 93 | tools.RegisterJiraHistoryTool(mcpServer) 94 | tools.RegisterJiraRelationshipTool(mcpServer) 95 | tools.RegisterJiraVersionTool(mcpServer) 96 | tools.RegisterJiraDevelopmentTool(mcpServer) 97 | 98 | // Register all Jira prompts 99 | prompts.RegisterJiraPrompts(mcpServer) 100 | 101 | if *httpPort != "" { 102 | fmt.Println() 103 | fmt.Println("🚀 Starting Jira MCP Server in HTTP mode...") 104 | fmt.Printf("📡 Server will be available at: http://localhost:%s/mcp\n", *httpPort) 105 | fmt.Println() 106 | fmt.Println("📋 Cursor Configuration:") 107 | fmt.Println("Add the following to your Cursor MCP settings (.cursor/mcp.json):") 108 | fmt.Println() 109 | fmt.Println("```json") 110 | fmt.Println("{") 111 | fmt.Println(" \"mcpServers\": {") 112 | fmt.Println(" \"jira\": {") 113 | fmt.Printf(" \"url\": \"http://localhost:%s/mcp\"\n", *httpPort) 114 | fmt.Println(" }") 115 | fmt.Println(" }") 116 | fmt.Println("}") 117 | fmt.Println("```") 118 | fmt.Println() 119 | fmt.Println("💡 Tips:") 120 | fmt.Println("- Restart Cursor after adding the configuration") 121 | fmt.Println("- Test the connection by asking Claude: 'List my Jira projects'") 122 | fmt.Println("- Use '@jira' in Cursor to reference Jira-related context") 123 | fmt.Println() 124 | fmt.Println("🔄 Server starting...") 125 | 126 | httpServer := server.NewStreamableHTTPServer(mcpServer, server.WithEndpointPath("/mcp")) 127 | if err := httpServer.Start(fmt.Sprintf(":%s", *httpPort)); err != nil && !isContextCanceled(err) { 128 | log.Fatalf("❌ Server error: %v", err) 129 | } 130 | } else { 131 | if err := server.ServeStdio(mcpServer); err != nil && !isContextCanceled(err) { 132 | log.Fatalf("❌ Server error: %v", err) 133 | } 134 | } 135 | } 136 | 137 | // IsContextCanceled checks if the error is related to context cancellation 138 | func isContextCanceled(err error) bool { 139 | if err == nil { 140 | return false 141 | } 142 | 143 | // Check if it's directly context.Canceled 144 | if errors.Is(err, context.Canceled) { 145 | return true 146 | } 147 | 148 | // Check if the error message contains context canceled 149 | errMsg := strings.ToLower(err.Error()) 150 | return strings.Contains(errMsg, "context canceled") || 151 | strings.Contains(errMsg, "operation was canceled") || 152 | strings.Contains(errMsg, "context deadline exceeded") 153 | } 154 | ``` -------------------------------------------------------------------------------- /.claude/commands/speckit.tasks.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts. 3 | --- 4 | 5 | ## User Input 6 | 7 | ```text 8 | $ARGUMENTS 9 | ``` 10 | 11 | You **MUST** consider the user input before proceeding (if not empty). 12 | 13 | ## Outline 14 | 15 | 1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. 16 | 17 | 2. **Load design documents**: Read from FEATURE_DIR: 18 | - **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities) 19 | - **Optional**: data-model.md (entities), contracts/ (API endpoints), research.md (decisions), quickstart.md (test scenarios) 20 | - Note: Not all projects have all documents. Generate tasks based on what's available. 21 | 22 | 3. **Execute task generation workflow** (follow the template structure): 23 | - Load plan.md and extract tech stack, libraries, project structure 24 | - **Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.)** 25 | - If data-model.md exists: Extract entities → map to user stories 26 | - If contracts/ exists: Each file → map endpoints to user stories 27 | - If research.md exists: Extract decisions → generate setup tasks 28 | - **Generate tasks ORGANIZED BY USER STORY**: 29 | - Setup tasks (shared infrastructure needed by all stories) 30 | - **Foundational tasks (prerequisites that must complete before ANY user story can start)** 31 | - For each user story (in priority order P1, P2, P3...): 32 | - Group all tasks needed to complete JUST that story 33 | - Include models, services, endpoints, UI components specific to that story 34 | - Mark which tasks are [P] parallelizable 35 | - If tests requested: Include tests specific to that story 36 | - Polish/Integration tasks (cross-cutting concerns) 37 | - **Tests are OPTIONAL**: Only generate test tasks if explicitly requested in the feature spec or user asks for TDD approach 38 | - Apply task rules: 39 | - Different files = mark [P] for parallel 40 | - Same file = sequential (no [P]) 41 | - If tests requested: Tests before implementation (TDD order) 42 | - Number tasks sequentially (T001, T002...) 43 | - Generate dependency graph showing user story completion order 44 | - Create parallel execution examples per user story 45 | - Validate task completeness (each user story has all needed tasks, independently testable) 46 | 47 | 4. **Generate tasks.md**: Use `.specify.specify/templates/tasks-template.md` as structure, fill with: 48 | - Correct feature name from plan.md 49 | - Phase 1: Setup tasks (project initialization) 50 | - Phase 2: Foundational tasks (blocking prerequisites for all user stories) 51 | - Phase 3+: One phase per user story (in priority order from spec.md) 52 | - Each phase includes: story goal, independent test criteria, tests (if requested), implementation tasks 53 | - Clear [Story] labels (US1, US2, US3...) for each task 54 | - [P] markers for parallelizable tasks within each story 55 | - Checkpoint markers after each story phase 56 | - Final Phase: Polish & cross-cutting concerns 57 | - Numbered tasks (T001, T002...) in execution order 58 | - Clear file paths for each task 59 | - Dependencies section showing story completion order 60 | - Parallel execution examples per story 61 | - Implementation strategy section (MVP first, incremental delivery) 62 | 63 | 5. **Report**: Output path to generated tasks.md and summary: 64 | - Total task count 65 | - Task count per user story 66 | - Parallel opportunities identified 67 | - Independent test criteria for each story 68 | - Suggested MVP scope (typically just User Story 1) 69 | 70 | Context for task generation: $ARGUMENTS 71 | 72 | The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context. 73 | 74 | ## Task Generation Rules 75 | 76 | **IMPORTANT**: Tests are optional. Only generate test tasks if the user explicitly requested testing or TDD approach in the feature specification. 77 | 78 | **CRITICAL**: Tasks MUST be organized by user story to enable independent implementation and testing. 79 | 80 | 1. **From User Stories (spec.md)** - PRIMARY ORGANIZATION: 81 | - Each user story (P1, P2, P3...) gets its own phase 82 | - Map all related components to their story: 83 | - Models needed for that story 84 | - Services needed for that story 85 | - Endpoints/UI needed for that story 86 | - If tests requested: Tests specific to that story 87 | - Mark story dependencies (most stories should be independent) 88 | 89 | 2. **From Contracts**: 90 | - Map each contract/endpoint → to the user story it serves 91 | - If tests requested: Each contract → contract test task [P] before implementation in that story's phase 92 | 93 | 3. **From Data Model**: 94 | - Map each entity → to the user story(ies) that need it 95 | - If entity serves multiple stories: Put in earliest story or Setup phase 96 | - Relationships → service layer tasks in appropriate story phase 97 | 98 | 4. **From Setup/Infrastructure**: 99 | - Shared infrastructure → Setup phase (Phase 1) 100 | - Foundational/blocking tasks → Foundational phase (Phase 2) 101 | - Examples: Database schema setup, authentication framework, core libraries, base configurations 102 | - These MUST complete before any user story can be implemented 103 | - Story-specific setup → within that story's phase 104 | 105 | 5. **Ordering**: 106 | - Phase 1: Setup (project initialization) 107 | - Phase 2: Foundational (blocking prerequisites - must complete before user stories) 108 | - Phase 3+: User Stories in priority order (P1, P2, P3...) 109 | - Within each story: Tests (if requested) → Models → Services → Endpoints → Integration 110 | - Final Phase: Polish & Cross-Cutting Concerns 111 | - Each user story phase should be a complete, independently testable increment 112 | ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # Changelog 2 | 3 | ## [1.17.1](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.17.0...v1.17.1) (2025-10-09) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * disable provenance and SBOM to prevent unknown/unknown platform entry ([2acf42f](https://github.com/nguyenvanduocit/jira-mcp/commit/2acf42f1c1fbb43f078c2c4828bf0fd23493c026)) 9 | 10 | ## [1.17.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.16.0...v1.17.0) (2025-10-09) 11 | 12 | 13 | ### Features 14 | 15 | * add ARM64 support for Docker images ([d535e52](https://github.com/nguyenvanduocit/jira-mcp/commit/d535e52a3c966ca2af3f67d386282ed640e7b387)) 16 | 17 | ## [1.16.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.15.0...v1.16.0) (2025-10-07) 18 | 19 | 20 | ### Features 21 | 22 | * add build information support and prompts functionality ([d3df256](https://github.com/nguyenvanduocit/jira-mcp/commit/d3df256f80a181e80eca0510583e9771c47b9a58)) 23 | 24 | ## [1.15.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.14.0...v1.15.0) (2025-10-07) 25 | 26 | 27 | ### Features 28 | 29 | * add development information tool for Jira issues ([b0c2ec8](https://github.com/nguyenvanduocit/jira-mcp/commit/b0c2ec84de8d71be7ddb7cdaf976c31ea803ef8b)) 30 | 31 | ## [1.14.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.13.0...v1.14.0) (2025-09-23) 32 | 33 | 34 | ### Features 35 | 36 | * add jira_ prefix to all tool names for better LLM discoverability ([f44cd03](https://github.com/nguyenvanduocit/jira-mcp/commit/f44cd03a7c467255192f65404cfc15cc53ae0b76)) 37 | 38 | ## [1.13.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.12.1...v1.13.0) (2025-09-23) 39 | 40 | 41 | ### Features 42 | 43 | * **docs:** rewrite README with clear USP and 2‑minute quick start ([69b7b20](https://github.com/nguyenvanduocit/jira-mcp/commit/69b7b20166c424efe485fa2403193105012f2973)) 44 | 45 | ## [1.12.1](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.12.0...v1.12.1) (2025-09-23) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * migrate JQL search to new API endpoint ([bf6b6a2](https://github.com/nguyenvanduocit/jira-mcp/commit/bf6b6a2a105c5e34a0cbc6c3495f711d70cd47aa)) 51 | 52 | ## [1.12.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.11.0...v1.12.0) (2025-09-08) 53 | 54 | 55 | ### Features 56 | 57 | * upgrade to Jira API v3 ([1b65607](https://github.com/nguyenvanduocit/jira-mcp/commit/1b6560750ac0f4d37b5cc2fdf004d15932b4346b)) 58 | 59 | ## [1.11.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.10.0...v1.11.0) (2025-06-11) 60 | 61 | 62 | ### Features 63 | 64 | * support streamable http ([a67657a](https://github.com/nguyenvanduocit/jira-mcp/commit/a67657a9649b53ec02a5b2a4eb0789810bc7b372)) 65 | 66 | ## [1.10.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.9.0...v1.10.0) (2025-06-11) 67 | 68 | 69 | ### Features 70 | 71 | * update foỏmat of sprint ([d378f75](https://github.com/nguyenvanduocit/jira-mcp/commit/d378f7510fb21b8f4fd3ee677d3a1cf54c2ad025)) 72 | 73 | ## [1.9.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.8.0...v1.9.0) (2025-06-06) 74 | 75 | 76 | ### Features 77 | 78 | * add get_active_sprint tool ([3095319](https://github.com/nguyenvanduocit/jira-mcp/commit/30953192ce5e88d4940fea09f7a2331ae2516b54)) 79 | * **sprint:** add get_active_sprint tool ([89724b2](https://github.com/nguyenvanduocit/jira-mcp/commit/89724b26b7e9c19dfc16d1c8d81455801145fb27)) 80 | * **sprint:** enhance list_sprints and get_active_sprint tools with project_key support ([f31cac4](https://github.com/nguyenvanduocit/jira-mcp/commit/f31cac4701ef0e88e2c2b73cc950770c6b5cda02)) 81 | 82 | ## [1.8.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.7.0...v1.8.0) (2025-06-05) 83 | 84 | 85 | ### Features 86 | 87 | * **tools:** enhance issue tools with flexible field selection ([40fef1d](https://github.com/nguyenvanduocit/jira-mcp/commit/40fef1dcaa4ef0ed7833cd6db24e3e31c0a35f73)) 88 | 89 | ## [1.7.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.6.0...v1.7.0) (2025-06-05) 90 | 91 | 92 | ### Features 93 | 94 | * **jira:** enhance issue retrieval with changelog and story point estimate ([6e9902c](https://github.com/nguyenvanduocit/jira-mcp/commit/6e9902c464430ffc759124792fe0907697d80fab)) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * retrieve all issue comments ([a7cfae5](https://github.com/nguyenvanduocit/jira-mcp/commit/a7cfae5e459fd50b3a62f80223bddfc659a5453b)) 100 | 101 | ## [1.6.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.5.0...v1.6.0) (2025-05-06) 102 | 103 | 104 | ### Features 105 | 106 | * bump version ([cd2a85c](https://github.com/nguyenvanduocit/jira-mcp/commit/cd2a85c42c8594240e8718524ac0082acf1b7db7)) 107 | 108 | ## [1.5.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.4.0...v1.5.0) (2025-04-18) 109 | 110 | 111 | ### Features 112 | 113 | * **docker:** add GitHub Container Registry support ([f939b62](https://github.com/nguyenvanduocit/jira-mcp/commit/f939b629e764d4fe470f6954cc0d281eccde913f)) 114 | * **sprint:** add get_sprint tool to retrieve details of a specific sprint ([6756a1c](https://github.com/nguyenvanduocit/jira-mcp/commit/6756a1c79ed0692aeac9d12287fd92ef6bc5f1c2)) 115 | 116 | ## [1.4.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.3.0...v1.4.0) (2025-04-18) 117 | 118 | 119 | ### Features 120 | 121 | * **tools:** simplify tool naming and add issue relationship features ([02279ea](https://github.com/nguyenvanduocit/jira-mcp/commit/02279ead729b7a9bd6e78d6ed7903931d39c1580)) 122 | 123 | ## [1.3.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.2.0...v1.3.0) (2025-04-17) 124 | 125 | 126 | ### Features 127 | 128 | * **jira:** add issue history and relationship tools ([fc44bd4](https://github.com/nguyenvanduocit/jira-mcp/commit/fc44bd4384775260bf8ea7a0373c89d7053b6450)) 129 | 130 | ## [1.2.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.1.0...v1.2.0) (2025-04-15) 131 | 132 | 133 | ### Features 134 | 135 | * **docker:** add Docker support with build and run instructions ([979cd45](https://github.com/nguyenvanduocit/jira-mcp/commit/979cd459663c0004c566cda658efbf9fca50bf52)) 136 | * **docker:** add Docker support with build and run instructions ([078062e](https://github.com/nguyenvanduocit/jira-mcp/commit/078062ed7ba2686483a9df4c6000462d5b4fed3a)) 137 | 138 | ## [1.1.0](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.0.1...v1.1.0) (2025-04-04) 139 | 140 | 141 | ### Features 142 | 143 | * **jira:** add worklog functionality ([82ea857](https://github.com/nguyenvanduocit/jira-mcp/commit/82ea85767653ea5de4f20beb6585d9f694696a9a)) 144 | 145 | ## [1.0.1](https://github.com/nguyenvanduocit/jira-mcp/compare/v1.0.0...v1.0.1) (2025-04-04) 146 | 147 | 148 | ### Bug Fixes 149 | 150 | * **api:** update Jira comment API implementation ([12798ee](https://github.com/nguyenvanduocit/jira-mcp/commit/12798ee285f0b8d5c70689db87fa60b74e72376d)) 151 | 152 | ## 1.0.0 (2025-03-25) 153 | 154 | 155 | ### Features 156 | 157 | * add gitleaks workflow for secret scanning ([9bb46a0](https://github.com/nguyenvanduocit/jira-mcp/commit/9bb46a0f63793934470a1701a42d2413f29898f8)) 158 | * init ([6e960c0](https://github.com/nguyenvanduocit/jira-mcp/commit/6e960c048f69fe61baee42c3061aef0a44602be3)) 159 | ``` -------------------------------------------------------------------------------- /.claude/commands/speckit.analyze.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | description: Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation. 3 | --- 4 | 5 | ## User Input 6 | 7 | ```text 8 | $ARGUMENTS 9 | ``` 10 | 11 | You **MUST** consider the user input before proceeding (if not empty). 12 | 13 | ## Goal 14 | 15 | Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/tasks` has successfully produced a complete `tasks.md`. 16 | 17 | ## Operating Constraints 18 | 19 | **STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually). 20 | 21 | **Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/analyze`. 22 | 23 | ## Execution Steps 24 | 25 | ### 1. Initialize Analysis Context 26 | 27 | Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths: 28 | 29 | - SPEC = FEATURE_DIR/spec.md 30 | - PLAN = FEATURE_DIR/plan.md 31 | - TASKS = FEATURE_DIR/tasks.md 32 | 33 | Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command). 34 | 35 | ### 2. Load Artifacts (Progressive Disclosure) 36 | 37 | Load only the minimal necessary context from each artifact: 38 | 39 | **From spec.md:** 40 | 41 | - Overview/Context 42 | - Functional Requirements 43 | - Non-Functional Requirements 44 | - User Stories 45 | - Edge Cases (if present) 46 | 47 | **From plan.md:** 48 | 49 | - Architecture/stack choices 50 | - Data Model references 51 | - Phases 52 | - Technical constraints 53 | 54 | **From tasks.md:** 55 | 56 | - Task IDs 57 | - Descriptions 58 | - Phase grouping 59 | - Parallel markers [P] 60 | - Referenced file paths 61 | 62 | **From constitution:** 63 | 64 | - Load `.specify/memory/constitution.md` for principle validation 65 | 66 | ### 3. Build Semantic Models 67 | 68 | Create internal representations (do not include raw artifacts in output): 69 | 70 | - **Requirements inventory**: Each functional + non-functional requirement with a stable key (derive slug based on imperative phrase; e.g., "User can upload file" → `user-can-upload-file`) 71 | - **User story/action inventory**: Discrete user actions with acceptance criteria 72 | - **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases) 73 | - **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements 74 | 75 | ### 4. Detection Passes (Token-Efficient Analysis) 76 | 77 | Focus on high-signal findings. Limit to 50 findings total; aggregate remainder in overflow summary. 78 | 79 | #### A. Duplication Detection 80 | 81 | - Identify near-duplicate requirements 82 | - Mark lower-quality phrasing for consolidation 83 | 84 | #### B. Ambiguity Detection 85 | 86 | - Flag vague adjectives (fast, scalable, secure, intuitive, robust) lacking measurable criteria 87 | - Flag unresolved placeholders (TODO, TKTK, ???, `<placeholder>`, etc.) 88 | 89 | #### C. Underspecification 90 | 91 | - Requirements with verbs but missing object or measurable outcome 92 | - User stories missing acceptance criteria alignment 93 | - Tasks referencing files or components not defined in spec/plan 94 | 95 | #### D. Constitution Alignment 96 | 97 | - Any requirement or plan element conflicting with a MUST principle 98 | - Missing mandated sections or quality gates from constitution 99 | 100 | #### E. Coverage Gaps 101 | 102 | - Requirements with zero associated tasks 103 | - Tasks with no mapped requirement/story 104 | - Non-functional requirements not reflected in tasks (e.g., performance, security) 105 | 106 | #### F. Inconsistency 107 | 108 | - Terminology drift (same concept named differently across files) 109 | - Data entities referenced in plan but absent in spec (or vice versa) 110 | - Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note) 111 | - Conflicting requirements (e.g., one requires Next.js while other specifies Vue) 112 | 113 | ### 5. Severity Assignment 114 | 115 | Use this heuristic to prioritize findings: 116 | 117 | - **CRITICAL**: Violates constitution MUST, missing core spec artifact, or requirement with zero coverage that blocks baseline functionality 118 | - **HIGH**: Duplicate or conflicting requirement, ambiguous security/performance attribute, untestable acceptance criterion 119 | - **MEDIUM**: Terminology drift, missing non-functional task coverage, underspecified edge case 120 | - **LOW**: Style/wording improvements, minor redundancy not affecting execution order 121 | 122 | ### 6. Produce Compact Analysis Report 123 | 124 | Output a Markdown report (no file writes) with the following structure: 125 | 126 | ## Specification Analysis Report 127 | 128 | | ID | Category | Severity | Location(s) | Summary | Recommendation | 129 | |----|----------|----------|-------------|---------|----------------| 130 | | A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version | 131 | 132 | (Add one row per finding; generate stable IDs prefixed by category initial.) 133 | 134 | **Coverage Summary Table:** 135 | 136 | | Requirement Key | Has Task? | Task IDs | Notes | 137 | |-----------------|-----------|----------|-------| 138 | 139 | **Constitution Alignment Issues:** (if any) 140 | 141 | **Unmapped Tasks:** (if any) 142 | 143 | **Metrics:** 144 | 145 | - Total Requirements 146 | - Total Tasks 147 | - Coverage % (requirements with >=1 task) 148 | - Ambiguity Count 149 | - Duplication Count 150 | - Critical Issues Count 151 | 152 | ### 7. Provide Next Actions 153 | 154 | At end of report, output a concise Next Actions block: 155 | 156 | - If CRITICAL issues exist: Recommend resolving before `/implement` 157 | - If only LOW/MEDIUM: User may proceed, but provide improvement suggestions 158 | - Provide explicit command suggestions: e.g., "Run /specify with refinement", "Run /plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'" 159 | 160 | ### 8. Offer Remediation 161 | 162 | Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.) 163 | 164 | ## Operating Principles 165 | 166 | ### Context Efficiency 167 | 168 | - **Minimal high-signal tokens**: Focus on actionable findings, not exhaustive documentation 169 | - **Progressive disclosure**: Load artifacts incrementally; don't dump all content into analysis 170 | - **Token-efficient output**: Limit findings table to 50 rows; summarize overflow 171 | - **Deterministic results**: Rerunning without changes should produce consistent IDs and counts 172 | 173 | ### Analysis Guidelines 174 | 175 | - **NEVER modify files** (this is read-only analysis) 176 | - **NEVER hallucinate missing sections** (if absent, report them accurately) 177 | - **Prioritize constitution violations** (these are always CRITICAL) 178 | - **Use examples over exhaustive rules** (cite specific instances, not generic patterns) 179 | - **Report zero issues gracefully** (emit success report with coverage statistics) 180 | 181 | ## Context 182 | 183 | $ARGUMENTS 184 | ``` -------------------------------------------------------------------------------- /specs/001-i-want-to/spec.md: -------------------------------------------------------------------------------- ```markdown 1 | # Feature Specification: Retrieve Development Information from Jira Issue 2 | 3 | **Feature Branch**: `001-i-want-to` 4 | **Created**: 2025-10-07 5 | **Status**: Draft 6 | **Input**: User description: "i want to have tool to get all branch, merge request, from a jira issue" 7 | 8 | ## User Scenarios & Testing *(mandatory)* 9 | 10 | ### User Story 1 - View Linked Development Work (Priority: P1) 11 | 12 | As an AI assistant user or developer, I want to retrieve all branches and merge requests associated with a specific Jira issue so that I can understand what code changes are related to that issue and their current status. 13 | 14 | **Why this priority**: This is the core functionality that delivers immediate value by providing visibility into development activity linked to a Jira issue. It enables users to track code changes, review progress, and understand the implementation status of an issue. 15 | 16 | **Independent Test**: Can be fully tested by requesting development information for a Jira issue that has linked branches and merge requests, and verifying that the returned data includes branch names, merge request titles, states, and URLs. 17 | 18 | **Acceptance Scenarios**: 19 | 20 | 1. **Given** a Jira issue with linked branches and merge requests, **When** the user requests development information for that issue, **Then** the system returns a list of all branches with their names and repositories 21 | 2. **Given** a Jira issue with linked branches and merge requests, **When** the user requests development information for that issue, **Then** the system returns a list of all merge requests with their titles, states (open/merged/closed), and URLs 22 | 3. **Given** a Jira issue with no linked development work, **When** the user requests development information, **Then** the system returns an empty result or clear message indicating no development work is linked 23 | 4. **Given** an invalid issue key, **When** the user requests development information, **Then** the system returns a clear error message explaining the issue was not found 24 | 25 | --- 26 | 27 | ### User Story 2 - Filter Development Information by Type (Priority: P2) 28 | 29 | As a user, I want to optionally filter the development information to retrieve only branches or only merge requests so that I can focus on the specific information I need. 30 | 31 | **Why this priority**: This enhances usability by allowing users to retrieve targeted information, reducing noise when they only need specific types of development data. 32 | 33 | **Independent Test**: Can be tested by requesting only branches for an issue that has both branches and merge requests, and verifying only branch information is returned. 34 | 35 | **Acceptance Scenarios**: 36 | 37 | 1. **Given** a Jira issue with both branches and merge requests, **When** the user requests only branches, **Then** the system returns only branch information and excludes merge request data 38 | 2. **Given** a Jira issue with both branches and merge requests, **When** the user requests only merge requests, **Then** the system returns only merge request information and excludes branch data 39 | 3. **Given** a Jira issue with development work, **When** the user requests all development information (no filter), **Then** the system returns both branches and merge requests 40 | 41 | --- 42 | 43 | ### User Story 3 - View Commit Information (Priority: P3) 44 | 45 | As a user, I want to see commits associated with a Jira issue so that I can understand the specific code changes that have been made. 46 | 47 | **Why this priority**: This provides additional detail about the work done, but branches and merge requests are more commonly used for tracking development progress at a high level. 48 | 49 | **Independent Test**: Can be tested by requesting development information for an issue with linked commits, and verifying commit messages, authors, and timestamps are returned. 50 | 51 | **Acceptance Scenarios**: 52 | 53 | 1. **Given** a Jira issue with linked commits, **When** the user requests development information including commits, **Then** the system returns commit messages, authors, dates, and commit IDs 54 | 2. **Given** commits from multiple repositories, **When** the user requests development information, **Then** commits are grouped or labeled by repository 55 | 56 | --- 57 | 58 | ### Edge Cases 59 | 60 | - What happens when a Jira issue has development information from multiple repositories or Git hosting services (GitHub, GitLab, Bitbucket)? 61 | - How does the system handle issues where development information exists but the user lacks permissions to view the linked repositories? 62 | - What happens when branch or merge request data is incomplete or the external Git service is unavailable? 63 | - How does the system handle very large numbers of branches or merge requests (e.g., 50+ branches)? 64 | - What happens when development information includes both Cloud and Data Center Jira instances? 65 | 66 | ## Requirements *(mandatory)* 67 | 68 | ### Functional Requirements 69 | 70 | - **FR-001**: System MUST expose a `jira_get_development_information` tool that accepts an issue key parameter 71 | - **FR-002**: Tool MUST retrieve branches linked to the specified Jira issue, including branch name, repository name, and repository URL 72 | - **FR-003**: Tool MUST retrieve merge requests (pull requests) linked to the specified Jira issue, including title, state, URL, author, and creation date 73 | - **FR-004**: Tool MUST retrieve commit information linked to the specified Jira issue, including commit message, author, timestamp, and commit ID 74 | - **FR-005**: Tool MUST validate that the issue_key parameter is provided and follows valid Jira issue key format (e.g., PROJ-123) 75 | - **FR-006**: Tool MUST return human-readable formatted text suitable for LLM consumption, organizing information by type (branches, merge requests, commits) 76 | - **FR-007**: Tool MUST handle errors gracefully, including invalid issue keys, non-existent issues, permission errors, and API failures 77 | - **FR-008**: Tool MUST support optional filtering parameters to retrieve only specific types of development information (branches, merge requests, or commits) 78 | - **FR-009**: System MUST reuse the singleton Jira client connection established by the services package 79 | - **FR-010**: Tool MUST handle cases where no development information is linked to an issue by returning a clear message 80 | - **FR-011**: System MUST use Jira's standard `/rest/dev-status/1.0/issue/detail` endpoint via the go-atlassian client to retrieve development information 81 | 82 | ### Key Entities 83 | 84 | - **Development Information**: Aggregated data about code development activity related to a Jira issue, including branches, merge requests, and commits 85 | - **Branch**: A Git branch linked to a Jira issue, containing name, repository identifier, and URL 86 | - **Merge Request**: A pull request or merge request linked to a Jira issue, containing title, state (open/merged/closed/declined), URL, author, and timestamps 87 | - **Commit**: A Git commit linked to a Jira issue, containing message, author, timestamp, commit ID, and repository information 88 | - **Repository**: The source code repository where branches, commits, and merge requests exist, identified by name, URL, and hosting service type 89 | 90 | ## Success Criteria *(mandatory)* 91 | 92 | ### Measurable Outcomes 93 | 94 | - **SC-001**: Users can retrieve complete development information for a Jira issue in a single tool call 95 | - **SC-002**: The tool returns results within 3 seconds for issues with up to 50 linked development items 96 | - **SC-003**: The formatted output clearly distinguishes between branches, merge requests, and commits with appropriate grouping and labels 97 | - **SC-004**: 100% of valid Jira issue keys return either development information or a clear "no development work linked" message 98 | - **SC-005**: Error messages clearly identify the issue (invalid key format, issue not found, permission denied, API error) in 100% of failure cases 99 | - **SC-006**: The tool successfully handles development information from all major Git hosting services (GitHub, GitLab, Bitbucket) supported by Jira 100 | 101 | ## Assumptions 102 | 103 | - The Atlassian instance has development tool integrations configured (e.g., GitHub, GitLab, Bitbucket integrations enabled) 104 | - Users have appropriate Jira permissions to view development information for the issues they query 105 | - The go-atlassian library provides access to Jira's development information APIs 106 | - Development information is retrieved from Jira's stored data, not by directly querying Git hosting services 107 | - The default behavior (when no filter is specified) is to return all types of development information 108 | - Branch and merge request URLs point to the external Git hosting service and are provided by Jira's development information API 109 | ``` -------------------------------------------------------------------------------- /util/jira_formatter.go: -------------------------------------------------------------------------------- ```go 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/ctreminiom/go-atlassian/pkg/infra/models" 8 | ) 9 | 10 | // FormatJiraIssue converts a Jira issue struct to a formatted string representation 11 | // It handles all available fields from IssueFieldsSchemeV2 and related schemas 12 | func FormatJiraIssue(issue *models.IssueScheme) string { 13 | var sb strings.Builder 14 | 15 | // Basic issue information 16 | sb.WriteString(fmt.Sprintf("Key: %s\n", issue.Key)) 17 | 18 | if issue.ID != "" { 19 | sb.WriteString(fmt.Sprintf("ID: %s\n", issue.ID)) 20 | } 21 | 22 | if issue.Self != "" { 23 | sb.WriteString(fmt.Sprintf("URL: %s\n", issue.Self)) 24 | } 25 | 26 | // Fields information 27 | if issue.Fields != nil { 28 | fields := issue.Fields 29 | 30 | // Summary and Description 31 | if fields.Summary != "" { 32 | sb.WriteString(fmt.Sprintf("Summary: %s\n", fields.Summary)) 33 | } 34 | 35 | if fields.Description != nil { 36 | sb.WriteString(fmt.Sprintf("Description: %s\n", fields.Description)) 37 | } 38 | 39 | // Issue Type 40 | if fields.IssueType != nil { 41 | sb.WriteString(fmt.Sprintf("Type: %s\n", fields.IssueType.Name)) 42 | if fields.IssueType.Description != "" { 43 | sb.WriteString(fmt.Sprintf("Type Description: %s\n", fields.IssueType.Description)) 44 | } 45 | } 46 | 47 | // Status 48 | if fields.Status != nil { 49 | sb.WriteString(fmt.Sprintf("Status: %s\n", fields.Status.Name)) 50 | if fields.Status.Description != "" { 51 | sb.WriteString(fmt.Sprintf("Status Description: %s\n", fields.Status.Description)) 52 | } 53 | } 54 | 55 | // Priority 56 | if fields.Priority != nil { 57 | sb.WriteString(fmt.Sprintf("Priority: %s\n", fields.Priority.Name)) 58 | } else { 59 | sb.WriteString("Priority: None\n") 60 | } 61 | 62 | // Resolution 63 | if fields.Resolution != nil { 64 | sb.WriteString(fmt.Sprintf("Resolution: %s\n", fields.Resolution.Name)) 65 | if fields.Resolution.Description != "" { 66 | sb.WriteString(fmt.Sprintf("Resolution Description: %s\n", fields.Resolution.Description)) 67 | } 68 | } 69 | 70 | // Resolution Date 71 | if fields.Resolutiondate != "" { 72 | sb.WriteString(fmt.Sprintf("Resolution Date: %s\n", fields.Resolutiondate)) 73 | } 74 | 75 | // People 76 | if fields.Reporter != nil { 77 | sb.WriteString(fmt.Sprintf("Reporter: %s", fields.Reporter.DisplayName)) 78 | if fields.Reporter.EmailAddress != "" { 79 | sb.WriteString(fmt.Sprintf(" (%s)", fields.Reporter.EmailAddress)) 80 | } 81 | sb.WriteString("\n") 82 | } else { 83 | sb.WriteString("Reporter: Unassigned\n") 84 | } 85 | 86 | if fields.Assignee != nil { 87 | sb.WriteString(fmt.Sprintf("Assignee: %s", fields.Assignee.DisplayName)) 88 | if fields.Assignee.EmailAddress != "" { 89 | sb.WriteString(fmt.Sprintf(" (%s)", fields.Assignee.EmailAddress)) 90 | } 91 | sb.WriteString("\n") 92 | } else { 93 | sb.WriteString("Assignee: Unassigned\n") 94 | } 95 | 96 | if fields.Creator != nil { 97 | sb.WriteString(fmt.Sprintf("Creator: %s", fields.Creator.DisplayName)) 98 | if fields.Creator.EmailAddress != "" { 99 | sb.WriteString(fmt.Sprintf(" (%s)", fields.Creator.EmailAddress)) 100 | } 101 | sb.WriteString("\n") 102 | } 103 | 104 | // Dates 105 | if fields.Created != "" { 106 | sb.WriteString(fmt.Sprintf("Created: %s\n", fields.Created)) 107 | } 108 | 109 | if fields.Updated != "" { 110 | sb.WriteString(fmt.Sprintf("Updated: %s\n", fields.Updated)) 111 | } 112 | 113 | if fields.LastViewed != "" { 114 | sb.WriteString(fmt.Sprintf("Last Viewed: %s\n", fields.LastViewed)) 115 | } 116 | 117 | if fields.StatusCategoryChangeDate != "" { 118 | sb.WriteString(fmt.Sprintf("Status Category Change Date: %s\n", fields.StatusCategoryChangeDate)) 119 | } 120 | 121 | // Project information 122 | if fields.Project != nil { 123 | sb.WriteString(fmt.Sprintf("Project: %s", fields.Project.Name)) 124 | if fields.Project.Key != "" { 125 | sb.WriteString(fmt.Sprintf(" (%s)", fields.Project.Key)) 126 | } 127 | sb.WriteString("\n") 128 | } 129 | 130 | // Parent issue 131 | if fields.Parent != nil { 132 | sb.WriteString(fmt.Sprintf("Parent: %s", fields.Parent.Key)) 133 | if fields.Parent.Fields != nil && fields.Parent.Fields.Summary != "" { 134 | sb.WriteString(fmt.Sprintf(" - %s", fields.Parent.Fields.Summary)) 135 | } 136 | sb.WriteString("\n") 137 | } 138 | 139 | // Work information 140 | if fields.Workratio > 0 { 141 | sb.WriteString(fmt.Sprintf("Work Ratio: %d\n", fields.Workratio)) 142 | } 143 | 144 | // Labels 145 | if len(fields.Labels) > 0 { 146 | sb.WriteString(fmt.Sprintf("Labels: %s\n", strings.Join(fields.Labels, ", "))) 147 | } 148 | 149 | // Components 150 | if len(fields.Components) > 0 { 151 | sb.WriteString("Components:\n") 152 | for _, component := range fields.Components { 153 | sb.WriteString(fmt.Sprintf("- %s", component.Name)) 154 | if component.Description != "" { 155 | sb.WriteString(fmt.Sprintf(" (%s)", component.Description)) 156 | } 157 | sb.WriteString("\n") 158 | } 159 | } 160 | 161 | // Fix Versions 162 | if len(fields.FixVersions) > 0 { 163 | sb.WriteString("Fix Versions:\n") 164 | for _, version := range fields.FixVersions { 165 | sb.WriteString(fmt.Sprintf("- %s", version.Name)) 166 | if version.Description != "" { 167 | sb.WriteString(fmt.Sprintf(" (%s)", version.Description)) 168 | } 169 | sb.WriteString("\n") 170 | } 171 | } 172 | 173 | // Affected Versions 174 | if len(fields.Versions) > 0 { 175 | sb.WriteString("Affected Versions:\n") 176 | for _, version := range fields.Versions { 177 | sb.WriteString(fmt.Sprintf("- %s", version.Name)) 178 | if version.Description != "" { 179 | sb.WriteString(fmt.Sprintf(" (%s)", version.Description)) 180 | } 181 | sb.WriteString("\n") 182 | } 183 | } 184 | 185 | // Security Level 186 | if fields.Security != nil { 187 | sb.WriteString(fmt.Sprintf("Security Level: %s\n", fields.Security.Name)) 188 | } 189 | 190 | // Subtasks 191 | if len(fields.Subtasks) > 0 { 192 | sb.WriteString("Subtasks:\n") 193 | for _, subtask := range fields.Subtasks { 194 | sb.WriteString(fmt.Sprintf("- %s", subtask.Key)) 195 | if subtask.Fields != nil && subtask.Fields.Summary != "" { 196 | sb.WriteString(fmt.Sprintf(": %s", subtask.Fields.Summary)) 197 | } 198 | if subtask.Fields != nil && subtask.Fields.Status != nil { 199 | sb.WriteString(fmt.Sprintf(" [%s]", subtask.Fields.Status.Name)) 200 | } 201 | sb.WriteString("\n") 202 | } 203 | } 204 | 205 | // Issue Links 206 | if len(fields.IssueLinks) > 0 { 207 | sb.WriteString("Issue Links:\n") 208 | for _, link := range fields.IssueLinks { 209 | if link.OutwardIssue != nil { 210 | sb.WriteString(fmt.Sprintf("- %s %s", link.Type.Outward, link.OutwardIssue.Key)) 211 | if link.OutwardIssue.Fields != nil && link.OutwardIssue.Fields.Summary != "" { 212 | sb.WriteString(fmt.Sprintf(": %s", link.OutwardIssue.Fields.Summary)) 213 | } 214 | sb.WriteString("\n") 215 | } 216 | if link.InwardIssue != nil { 217 | sb.WriteString(fmt.Sprintf("- %s %s", link.Type.Inward, link.InwardIssue.Key)) 218 | if link.InwardIssue.Fields != nil && link.InwardIssue.Fields.Summary != "" { 219 | sb.WriteString(fmt.Sprintf(": %s", link.InwardIssue.Fields.Summary)) 220 | } 221 | sb.WriteString("\n") 222 | } 223 | } 224 | } 225 | 226 | // Watchers 227 | if fields.Watcher != nil { 228 | sb.WriteString(fmt.Sprintf("Watchers: %d\n", fields.Watcher.WatchCount)) 229 | } 230 | 231 | // Votes 232 | if fields.Votes != nil { 233 | sb.WriteString(fmt.Sprintf("Votes: %d\n", fields.Votes.Votes)) 234 | } 235 | 236 | // Comments (summary only to avoid too much text) 237 | if fields.Comment != nil && fields.Comment.Total > 0 { 238 | sb.WriteString(fmt.Sprintf("Comments: %d total\n", fields.Comment.Total)) 239 | } 240 | 241 | // Worklogs (summary only) 242 | if fields.Worklog != nil && fields.Worklog.Total > 0 { 243 | sb.WriteString(fmt.Sprintf("Worklogs: %d entries\n", fields.Worklog.Total)) 244 | } 245 | } 246 | 247 | // Available Transitions 248 | if len(issue.Transitions) > 0 { 249 | sb.WriteString("\nAvailable Transitions:\n") 250 | for _, transition := range issue.Transitions { 251 | sb.WriteString(fmt.Sprintf("- %s (ID: %s)\n", transition.Name, transition.ID)) 252 | } 253 | } 254 | 255 | // Story point estimate from changelog (if available) 256 | if issue.Changelog != nil && issue.Changelog.Histories != nil { 257 | storyPoint := "" 258 | for _, history := range issue.Changelog.Histories { 259 | for _, item := range history.Items { 260 | if item.Field == "Story point estimate" && item.ToString != "" { 261 | storyPoint = item.ToString 262 | } 263 | } 264 | } 265 | if storyPoint != "" { 266 | sb.WriteString(fmt.Sprintf("Story Point Estimate: %s\n", storyPoint)) 267 | } 268 | } 269 | 270 | return sb.String() 271 | } 272 | 273 | // FormatJiraIssueCompact returns a compact single-line representation of a Jira issue 274 | // Useful for search results or lists 275 | func FormatJiraIssueCompact(issue *models.IssueScheme) string { 276 | if issue == nil { 277 | return "" 278 | } 279 | 280 | var parts []string 281 | 282 | parts = append(parts, fmt.Sprintf("Key: %s", issue.Key)) 283 | 284 | if issue.Fields != nil { 285 | fields := issue.Fields 286 | 287 | if fields.Summary != "" { 288 | parts = append(parts, fmt.Sprintf("Summary: %s", fields.Summary)) 289 | } 290 | 291 | if fields.Status != nil { 292 | parts = append(parts, fmt.Sprintf("Status: %s", fields.Status.Name)) 293 | } 294 | 295 | if fields.Assignee != nil { 296 | parts = append(parts, fmt.Sprintf("Assignee: %s", fields.Assignee.DisplayName)) 297 | } else { 298 | parts = append(parts, "Assignee: Unassigned") 299 | } 300 | 301 | if fields.Priority != nil { 302 | parts = append(parts, fmt.Sprintf("Priority: %s", fields.Priority.Name)) 303 | } 304 | } 305 | 306 | return strings.Join(parts, " | ") 307 | } ```