From 85ad108996ac7b861f087e7f1bca35f66f018fbb Mon Sep 17 00:00:00 2001 From: Alex Collins Date: Sat, 22 Nov 2025 17:10:21 -0800 Subject: [PATCH 1/7] feat: add field flags and filters to issue commands - Add -f flag support for update-issue-status to set custom fields during transitions - Add filters to list-issues: -a (assignee), -t (type), -p (project) - Move FieldFlag type to internal/flag package - Improve issue listing output with type and better formatting - Use DoTransitionWithPayloadWithContext for all transitions --- internal/flag/field_flag.go | 23 ++++++ main.go | 137 ++++++++++++++++++++++++++++++------ 2 files changed, 140 insertions(+), 20 deletions(-) create mode 100644 internal/flag/field_flag.go diff --git a/internal/flag/field_flag.go b/internal/flag/field_flag.go new file mode 100644 index 0000000..95f3077 --- /dev/null +++ b/internal/flag/field_flag.go @@ -0,0 +1,23 @@ +package flag + +import ( + "fmt" + "strings" +) + +// FieldFlag is a custom flag type that accumulates multiple field=value pairs +type FieldFlag map[string]string + +func (f FieldFlag) String() string { + return "" +} + +func (f FieldFlag) Set(value string) error { + parts := strings.SplitN(value, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid field format: %s (expected field=value)", value) + } + f[parts[0]] = parts[1] + return nil +} + diff --git a/main.go b/main.go index 0fa757f..c429ec4 100644 --- a/main.go +++ b/main.go @@ -8,11 +8,13 @@ import ( "os/signal" "path/filepath" "reflect" + "strconv" "strings" "syscall" "github.com/andygrunwald/go-jira" "github.com/kitproj/jira-cli/internal/config" + flagpkg "github.com/kitproj/jira-cli/internal/flag" "golang.org/x/term" ) @@ -34,8 +36,8 @@ func main() { fmt.Fprintln(w, " jira configure - Configure JIRA host and token (reads token from stdin)") fmt.Fprintln(w, " jira create-issue <description> [assignee] - Create a new JIRA issue") fmt.Fprintln(w, " jira get-issue <issue-key> - Get details of the specified JIRA issue") - fmt.Fprintln(w, " jira list-issues - List issues assigned to the current user") - fmt.Fprintln(w, " jira update-issue-status <issue-key> <status> - Update the status of the specified JIRA issue") + fmt.Fprintln(w, " jira list-issues [-a=user] [-t=type] [-p=key] - List issues with optional filters") + fmt.Fprintln(w, " jira update-issue-status <issue-key> <status> [-f field=value]... - Update the status of the specified JIRA issue") fmt.Fprintln(w, " jira get-comments <issue-key> - Get comments of the specified JIRA issue") fmt.Fprintln(w, " jira add-comment <issue-key> <comment> - Add a comment to the specified JIRA issue") fmt.Fprintln(w, " jira attach-file <issue-key> <file-path> - Attach a file to the specified JIRA issue") @@ -92,12 +94,23 @@ func run(ctx context.Context, args []string) error { return executeCommand(ctx, getIssue) case "update-issue-status": if len(args) < 3 { - return fmt.Errorf("usage: jira update-issue-status <issue-key> <status>") + return fmt.Errorf("usage: jira update-issue-status <issue-key> <status> [-f field=value]") } issueKey = args[1] statusName := args[2] + + // Create a new flag set for this command + fs := flag.NewFlagSet("update-issue-status", flag.ContinueOnError) + fields := make(flagpkg.FieldFlag) + fs.Var(fields, "f", "field=value pair to include in transition (can be specified multiple times)") + + // Parse flags from remaining args + if err := fs.Parse(args[3:]); err != nil { + return fmt.Errorf("failed to parse flags: %w", err) + } + return executeCommand(ctx, func(ctx context.Context) error { - return updateIssueStatus(ctx, statusName) + return updateIssueStatus(ctx, statusName, map[string]string(fields)) }) case "add-comment": if len(args) < 3 { @@ -115,7 +128,20 @@ func run(ctx context.Context, args []string) error { issueKey = args[1] return executeCommand(ctx, getComments) case "list-issues": - return executeCommand(ctx, listIssues) + // Create a new flag set for this command + fs := flag.NewFlagSet("list-issues", flag.ContinueOnError) + assignee := fs.String("a", "me", "filter by assignee (default: current user, use 'me' for current user)") + issueType := fs.String("t", "", "filter by issue type (e.g., Story, Bug, Task)") + project := fs.String("p", "", "filter by project key") + + // Parse flags from args + if err := fs.Parse(args[1:]); err != nil { + return fmt.Errorf("failed to parse flags: %w", err) + } + + return executeCommand(ctx, func(ctx context.Context) error { + return listIssues(ctx, *assignee, *issueType, *project) + }) case "attach-file": if len(args) < 3 { return fmt.Errorf("usage: jira attach-file <issue-key> <file-path>") @@ -195,6 +221,7 @@ func getIssue(ctx context.Context) error { } printField("Key", issue.Key) + printField("Type", issue.Fields.Type.Name) printField("Status", issue.Fields.Status.Name) printField("Summary", issue.Fields.Summary) printField("Reporter", fmt.Sprintf("%s (%s)", issue.Fields.Reporter.DisplayName, issue.Fields.Reporter.Name)) @@ -234,19 +261,19 @@ func isPrimitive(v any) bool { func printField(key string, value any) { valueStr := fmt.Sprint(value) multiLine := strings.Contains(valueStr, "\n") - fmt.Printf("%-20s", key+":") + fmt.Printf("%-32s", key+":") if !multiLine { fmt.Printf(" %s\n", valueStr) } else { fmt.Println() for line := range strings.SplitSeq(valueStr, "\n") { - fmt.Printf("%-20s %s\n", "", line) + fmt.Printf("%-32s %s\n", "", line) } } } // updateIssueStatus updates the status of a Jira issue using transitions -func updateIssueStatus(ctx context.Context, statusName string) error { +func updateIssueStatus(ctx context.Context, statusName string, extra map[string]string) error { // First, get the issue to check current status issue, _, err := client.Issue.GetWithContext(ctx, issueKey, nil) if err != nil { @@ -283,10 +310,47 @@ func updateIssueStatus(ctx context.Context, statusName string) error { return fmt.Errorf("no transition found to status '%s'. Available statuses: %v", statusName, strings.Join(availableStatuses, ", ")) } + // Get edit metadata to map field names to field IDs + editMetaInfo, _, err := client.Issue.GetEditMetaWithContext(ctx, issue) + if err != nil { + return fmt.Errorf("failed to get field metadata: %w", err) + } + + fieldNameByID := make(map[string]string) + for fieldID, value := range editMetaInfo.Fields { + name := value.(map[string]any)["name"].(string) + fieldNameByID[fieldID] = name + } + + fields := make(map[string]any) + + fmt.Println("Fields to update:") + for fieldID := range targetTransition.Fields { + fieldName := fieldNameByID[fieldID] + fieldValue := extra[fieldName] + printField(fmt.Sprintf("%s (%s)", fieldName, fieldID), fieldValue) + + i, err := strconv.Atoi(fieldValue) + if err != nil { + fields[fieldID] = fieldValue + } else { + fields[fieldID] = i + } + + } + + // Build transition payload + payload := map[string]any{ + "transition": map[string]any{ + "id": targetTransition.ID, + }, + "fields": fields, + } + // Perform the transition - _, err = client.Issue.DoTransition(issueKey, targetTransition.ID) + _, err = client.Issue.DoTransitionWithPayloadWithContext(ctx, issueKey, payload) if err != nil { - return fmt.Errorf("failed to update issue status: %w", err) + return fmt.Errorf("failed to update issue status: %v", err) } fmt.Printf("Successfully updated issue %s to status: %s (https://%s/browse/%s)\n", issueKey, statusName, host, issueKey) @@ -354,7 +418,7 @@ func createIssue(ctx context.Context, projectKey, issueType, title, description, } // Create the issue - createdIssue, _, err := client.Issue.Create(issue) + createdIssue, _, err := client.Issue.CreateWithContext(ctx, issue) if err != nil { return fmt.Errorf("failed to create issue: %w", err) } @@ -399,33 +463,65 @@ func configure(host string) error { return nil } -// listIssues lists issues assigned to the current user -func listIssues(ctx context.Context) error { - // JQL to find issues assigned to the current user, excluding closed issues, updated in last 14 days - jql := "assignee = currentUser() AND resolution = Unresolved AND updated >= -14d ORDER BY updated DESC" +// listIssues lists issues with optional filters +func listIssues(ctx context.Context, assigneeFilter, issueTypeFilter, projectFilter string) error { + // Build JQL query dynamically based on filters + var conditions []string + + // Assignee filter (default to currentUser() if not specified) + if assigneeFilter == "me" { + conditions = append(conditions, "assignee = currentUser()") + } else if assigneeFilter != "" { + conditions = append(conditions, fmt.Sprintf("assignee = %q", assigneeFilter)) + } + + // Project filter + if projectFilter != "" { + conditions = append(conditions, fmt.Sprintf("project = %q", projectFilter)) + } + + // Issue type filter + if issueTypeFilter != "" { + conditions = append(conditions, fmt.Sprintf("issuetype = %q", issueTypeFilter)) + } + + // Default: exclude resolved issues + conditions = append(conditions, "resolution = Unresolved") + + // Default: updated in last 14 days if no specific filters + if projectFilter == "" && issueTypeFilter == "" { + conditions = append(conditions, "updated >= -14d") + } + + // Build JQL query + jql := strings.Join(conditions, " AND ") + " ORDER BY updated DESC" // Search for issues using JQL issues, _, err := client.Issue.SearchWithContext(ctx, jql, &jira.SearchOptions{ MaxResults: 50, - Fields: []string{"key", "summary", "status"}, + Fields: []string{"key", "issuetype", "summary", "status", "assignee", "project"}, }) if err != nil { return fmt.Errorf("failed to search issues: %w", err) } if len(issues) == 0 { - fmt.Println("No issues assigned to you in the last 14 days") + fmt.Println("No issues found matching the specified criteria") return nil } - fmt.Printf("Found %d issue(s) in the last 14 days", len(issues)) + fmt.Printf("Found %d issue(s)", len(issues)) if len(issues) >= 50 { fmt.Printf(" (showing first 50 only)") } fmt.Printf(":\n\n") for _, issue := range issues { - fmt.Printf("%-15s %-20s %s\n", issue.Key, issue.Fields.Status.Name, issue.Fields.Summary) + assigneeName := "-" + if issue.Fields.Assignee != nil { + assigneeName = issue.Fields.Assignee.DisplayName + } + fmt.Printf("%-10s %-10s %-16s %-20s %s\n", issue.Key, issue.Fields.Type.Name, issue.Fields.Status.Name, assigneeName, issue.Fields.Summary) } return nil @@ -487,6 +583,7 @@ func addIssueToSprint(ctx context.Context) error { // Get all boards to find the one that contains this issue's project boards, _, err := client.Board.GetAllBoardsWithContext(ctx, &jira.BoardListOptions{ ProjectKeyOrID: issue.Fields.Project.Key, + BoardType: "scrum", }) if err != nil { return fmt.Errorf("failed to get boards: %w", err) @@ -520,6 +617,6 @@ func addIssueToSprint(ctx context.Context) error { return fmt.Errorf("failed to add issue to sprint: %w", err) } - fmt.Printf("Successfully added issue %s to sprint %s (ID: %d)\n", issueKey, sprints.Values[0].Name, sprintID) + fmt.Printf("Successfully added issue %s to sprint %q\n", issueKey, sprints.Values[0].Name) return nil } From d1cd18933541bf7cabbe7c7a1bab1363b6454467 Mon Sep 17 00:00:00 2001 From: Alex Collins <alex_collins@intuit.com> Date: Sat, 22 Nov 2025 17:11:15 -0800 Subject: [PATCH 2/7] fix: remove unnecessary blank line in field_flag.go --- internal/flag/field_flag.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/flag/field_flag.go b/internal/flag/field_flag.go index 95f3077..72d888d 100644 --- a/internal/flag/field_flag.go +++ b/internal/flag/field_flag.go @@ -20,4 +20,3 @@ func (f FieldFlag) Set(value string) error { f[parts[0]] = parts[1] return nil } - From 1676a681d492df7af22bf594867b1f6dacf4e8d6 Mon Sep 17 00:00:00 2001 From: Alex Collins <alex_collins@intuit.com> Date: Sat, 22 Nov 2025 17:12:40 -0800 Subject: [PATCH 3/7] sync: sync mcp.go handlers with main.go - Update getIssueHandler to include Type field - Use DoTransitionWithPayloadWithContext in updateIssueStatusHandler - Use CreateWithContext in createIssueHandler - Add filter support to listIssuesHandler (assignee, issue_type, project) - Update listIssuesHandler output format to match main.go - Add BoardType filter to addIssueToSprintHandler - Update tool definitions to include filter parameters --- mcp.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/mcp.go b/mcp.go index 880f4f0..aaf50d3 100644 --- a/mcp.go +++ b/mcp.go @@ -132,7 +132,16 @@ func runMCPServer(ctx context.Context) error { // Add list-issues tool listIssuesTool := mcp.NewTool("list_issues", - mcp.WithDescription("List issues assigned to the current user that are unresolved and updated in the last 14 days"), + mcp.WithDescription("List issues with optional filters (assignee, issue type, project)"), + mcp.WithString("assignee", + mcp.Description("Filter by assignee (default: current user, use 'me' for current user)"), + ), + mcp.WithString("issue_type", + mcp.Description("Filter by issue type (e.g., Story, Bug, Task)"), + ), + mcp.WithString("project", + mcp.Description("Filter by project key"), + ), ) s.AddTool(listIssuesTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return listIssuesHandler(ctx, api, request) @@ -197,8 +206,9 @@ func getIssueHandler(ctx context.Context, client *jira.Client, request mcp.CallT return mcp.NewToolResultError(fmt.Sprintf("Failed to get issue: %v", err)), nil } - result := fmt.Sprintf("Key: %s\nStatus: %s\nSummary: %s\nReporter: %s (%s)\nDescription: %s", + result := fmt.Sprintf("Key: %s\nType: %s\nStatus: %s\nSummary: %s\nReporter: %s (%s)\nDescription: %s", issue.Key, + issue.Fields.Type.Name, issue.Fields.Status.Name, issue.Fields.Summary, issue.Fields.Reporter.DisplayName, @@ -266,7 +276,12 @@ func updateIssueStatusHandler(ctx context.Context, client *jira.Client, host str } // Perform the transition - _, err = client.Issue.DoTransition(issueKey, targetTransition.ID) + payload := jira.CreateTransitionPayload{ + Transition: jira.TransitionPayload{ + ID: targetTransition.ID, + }, + } + _, err = client.Issue.DoTransitionWithPayloadWithContext(ctx, issueKey, payload) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to update issue status: %v", err)), nil } @@ -372,7 +387,7 @@ func createIssueHandler(ctx context.Context, client *jira.Client, host string, r } // Create the issue - createdIssue, _, err := client.Issue.Create(issue) + createdIssue, _, err := client.Issue.CreateWithContext(ctx, issue) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to create issue: %v", err)), nil } @@ -381,30 +396,67 @@ func createIssueHandler(ctx context.Context, client *jira.Client, host string, r } func listIssuesHandler(ctx context.Context, client *jira.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - // JQL to find issues assigned to the current user, excluding closed issues, updated in last 14 days - jql := "assignee = currentUser() AND resolution = Unresolved AND updated >= -14d ORDER BY updated DESC" + // Get optional filters from request + assigneeFilter := request.GetString("assignee", "me") + issueTypeFilter := request.GetString("issue_type", "") + projectFilter := request.GetString("project", "") + + // Build JQL query dynamically based on filters + var conditions []string + + // Assignee filter (default to currentUser() if not specified) + if assigneeFilter == "me" || assigneeFilter == "" { + conditions = append(conditions, "assignee = currentUser()") + } else if assigneeFilter != "" { + conditions = append(conditions, fmt.Sprintf("assignee = %q", assigneeFilter)) + } + + // Project filter + if projectFilter != "" { + conditions = append(conditions, fmt.Sprintf("project = %q", projectFilter)) + } + + // Issue type filter + if issueTypeFilter != "" { + conditions = append(conditions, fmt.Sprintf("issuetype = %q", issueTypeFilter)) + } + + // Default: exclude resolved issues + conditions = append(conditions, "resolution = Unresolved") + + // Default: updated in last 14 days if no specific filters + if projectFilter == "" && issueTypeFilter == "" { + conditions = append(conditions, "updated >= -14d") + } + + // Build JQL query + jql := strings.Join(conditions, " AND ") + " ORDER BY updated DESC" // Search for issues using JQL issues, _, err := client.Issue.SearchWithContext(ctx, jql, &jira.SearchOptions{ MaxResults: 50, - Fields: []string{"key", "summary", "status"}, + Fields: []string{"key", "issuetype", "summary", "status", "assignee", "project"}, }) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to search issues: %v", err)), nil } if len(issues) == 0 { - return mcp.NewToolResultText("No issues assigned to you in the last 14 days"), nil + return mcp.NewToolResultText("No issues found matching the specified criteria"), nil } - result := fmt.Sprintf("Found %d issue(s) in the last 14 days", len(issues)) + result := fmt.Sprintf("Found %d issue(s)", len(issues)) if len(issues) >= 50 { result += " (showing first 50 only)" } result += ":\n\n" for _, issue := range issues { - result += fmt.Sprintf("%-15s %-20s %s\n", issue.Key, issue.Fields.Status.Name, issue.Fields.Summary) + assigneeName := "-" + if issue.Fields.Assignee != nil { + assigneeName = issue.Fields.Assignee.DisplayName + } + result += fmt.Sprintf("%-10s %-10s %-16s %-20s %s\n", issue.Key, issue.Fields.Type.Name, issue.Fields.Status.Name, assigneeName, issue.Fields.Summary) } return mcp.NewToolResultText(result), nil @@ -485,6 +537,7 @@ func addIssueToSprintHandler(ctx context.Context, client *jira.Client, request m // Get all boards to find the one that contains this issue's project boards, _, err := client.Board.GetAllBoardsWithContext(ctx, &jira.BoardListOptions{ ProjectKeyOrID: issue.Fields.Project.Key, + BoardType: "scrum", }) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to get boards: %v", err)), nil @@ -518,5 +571,5 @@ func addIssueToSprintHandler(ctx context.Context, client *jira.Client, request m return mcp.NewToolResultError(fmt.Sprintf("Failed to add issue to sprint: %v", err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Successfully added issue %s to sprint %s (ID: %d)", issueKey, sprints.Values[0].Name, sprintID)), nil + return mcp.NewToolResultText(fmt.Sprintf("Successfully added issue %s to sprint %q", issueKey, sprints.Values[0].Name)), nil } From a1ea399f1859daa12a1c9ac1b4cbfd99d7e6b4be Mon Sep 17 00:00:00 2001 From: Alex Collins <alexec@users.noreply.github.com> Date: Sat, 22 Nov 2025 17:13:09 -0800 Subject: [PATCH 4/7] Update main.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- main.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index c429ec4..f346852 100644 --- a/main.go +++ b/main.go @@ -318,7 +318,14 @@ func updateIssueStatus(ctx context.Context, statusName string, extra map[string] fieldNameByID := make(map[string]string) for fieldID, value := range editMetaInfo.Fields { - name := value.(map[string]any)["name"].(string) + valueMap, ok := value.(map[string]any) + if !ok { + continue + } + name, ok := valueMap["name"].(string) + if !ok { + continue + } fieldNameByID[fieldID] = name } From bfc92360116eb94845dfc153d16e89b4906e8201 Mon Sep 17 00:00:00 2001 From: Alex Collins <alexec@users.noreply.github.com> Date: Sat, 22 Nov 2025 17:13:33 -0800 Subject: [PATCH 5/7] Update main.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index f346852..6d1d6be 100644 --- a/main.go +++ b/main.go @@ -357,7 +357,7 @@ func updateIssueStatus(ctx context.Context, statusName string, extra map[string] // Perform the transition _, err = client.Issue.DoTransitionWithPayloadWithContext(ctx, issueKey, payload) if err != nil { - return fmt.Errorf("failed to update issue status: %v", err) + return fmt.Errorf("failed to update issue status: %w", err) } fmt.Printf("Successfully updated issue %s to status: %s (https://%s/browse/%s)\n", issueKey, statusName, host, issueKey) From 012e83eb27b310a1a7ed4a9186e4417cf144bf0f Mon Sep 17 00:00:00 2001 From: Alex Collins <alex_collins@intuit.com> Date: Sat, 22 Nov 2025 17:14:16 -0800 Subject: [PATCH 6/7] test: add tests for FieldFlag type --- internal/flag/field_flag_test.go | 112 +++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 internal/flag/field_flag_test.go diff --git a/internal/flag/field_flag_test.go b/internal/flag/field_flag_test.go new file mode 100644 index 0000000..5d8da78 --- /dev/null +++ b/internal/flag/field_flag_test.go @@ -0,0 +1,112 @@ +package flag + +import ( + "testing" +) + +func TestFieldFlag_String(t *testing.T) { + f := make(FieldFlag) + if f.String() != "" { + t.Errorf("String() should return empty string, got %q", f.String()) + } +} + +func TestFieldFlag_Set(t *testing.T) { + tests := []struct { + name string + value string + wantKey string + wantValue string + wantErr bool + }{ + { + name: "valid field=value", + value: "Effort Estimate=0", + wantKey: "Effort Estimate", + wantValue: "0", + wantErr: false, + }, + { + name: "valid with spaces", + value: "Story Points=5", + wantKey: "Story Points", + wantValue: "5", + wantErr: false, + }, + { + name: "valid with equals in value", + value: "field=value=with=equals", + wantKey: "field", + wantValue: "value=with=equals", + wantErr: false, + }, + { + name: "missing equals", + value: "noequals", + wantErr: true, + }, + { + name: "empty value", + value: "field=", + wantKey: "field", + wantValue: "", + wantErr: false, + }, + { + name: "empty key", + value: "=value", + wantKey: "", + wantValue: "value", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := make(FieldFlag) + err := f.Set(tt.value) + if (err != nil) != tt.wantErr { + t.Errorf("Set() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if gotValue, ok := f[tt.wantKey]; !ok { + t.Errorf("Set() key %q not found in map", tt.wantKey) + } else if gotValue != tt.wantValue { + t.Errorf("Set() value = %q, want %q", gotValue, tt.wantValue) + } + } + }) + } +} + +func TestFieldFlag_Set_Multiple(t *testing.T) { + f := make(FieldFlag) + + values := []string{ + "Effort Estimate=0", + "Story Points=5", + "Priority=High", + } + + for _, v := range values { + if err := f.Set(v); err != nil { + t.Errorf("Set(%q) error = %v", v, err) + } + } + + if len(f) != len(values) { + t.Errorf("Expected %d entries, got %d", len(values), len(f)) + } + + if f["Effort Estimate"] != "0" { + t.Errorf("Effort Estimate = %q, want %q", f["Effort Estimate"], "0") + } + if f["Story Points"] != "5" { + t.Errorf("Story Points = %q, want %q", f["Story Points"], "5") + } + if f["Priority"] != "High" { + t.Errorf("Priority = %q, want %q", f["Priority"], "High") + } +} + From 84afa4a274a706e6143ccfa69e4d91cff27f5c1c Mon Sep 17 00:00:00 2001 From: Alex Collins <alex_collins@intuit.com> Date: Sat, 22 Nov 2025 17:15:25 -0800 Subject: [PATCH 7/7] docs: update README with new filter and field flag features --- README.md | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3ae8157..ac802a4 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,8 @@ Usage: jira configure <host> - Configure JIRA host and token (reads token from stdin) jira create-issue <project> <issue-type> <title> <description> [assignee] - Create a new JIRA issue jira get-issue <issue-key> - Get details of the specified JIRA issue - jira list-issues - List issues assigned to the current user - jira update-issue-status <issue-key> <status> - Update the status of the specified JIRA issue + jira list-issues [-a=user] [-t=type] [-p=key] - List issues with optional filters + jira update-issue-status <issue-key> <status> [-f field=value]... - Update the status of the specified JIRA issue jira get-comments <issue-key> - Get comments of the specified JIRA issue jira add-comment <issue-key> <comment> - Add a comment to the specified JIRA issue jira attach-file <issue-key> <file-path> - Attach a file to the specified JIRA issue @@ -100,13 +100,28 @@ jira get-issue PROJ-123 **List your current issues:** ```bash +# List issues assigned to you (default) jira list-issues + +# Filter by project +jira list-issues -p=PROJ + +# Filter by issue type +jira list-issues -t=Story + +# Filter by assignee (use 'me' for current user) +jira list-issues -a=me +jira list-issues -a=john.doe + +# Combine multiple filters +jira list-issues -p=PROJ -t=Bug -a=me + # Output: -# Found 3 issue(s) in the last 14 days: +# Found 3 issue(s): # -# PROJ-123 In Progress Implement new feature -# PROJ-124 To Do Fix critical bug -# PROJ-125 In Review Update documentation +# PROJ-123 Story In Progress John Doe Implement new feature +# PROJ-124 Bug To Do Jane Smith Fix critical bug +# PROJ-125 Task In Review John Doe Update documentation ``` **Create a new issue:** @@ -126,8 +141,16 @@ jira create-issue PROJ Task "Update documentation" "Add API documentation for ne **Update issue status:** ```bash +# Basic status update jira update-issue-status PROJ-123 "In Progress" # Note: Status names must match your Jira workflow (e.g., "To Do", "In Progress", "Done") + +# Update status with custom fields (e.g., effort estimate) +jira update-issue-status PROJ-123 "In Progress" -f "Effort Estimate"=0 + +# Multiple custom fields +jira update-issue-status PROJ-123 "In Progress" -f "Effort Estimate"=0 -f "Story Points"=5 +# Note: Field names must match the exact field names in your Jira instance ``` **Add a comment:** @@ -190,11 +213,11 @@ Learn more about MCP: https://modelcontextprotocol.io The server exposes the following tools: - `get_issue` - Get details of a JIRA issue (e.g., status, summary, reporter, description) -- `update_issue_status` - Update the status of a JIRA issue using transitions +- `update_issue_status` - Update the status of a JIRA issue using transitions. Supports custom fields via the `-f` flag - `add_comment` - Add a comment to a JIRA issue - `get_comments` - Get all comments on a JIRA issue - `create_issue` - Create a new JIRA issue with specified project, issue type (Story/Bug/Task), title, description, and optional assignee -- `list_issues` - List issues assigned to the current user that are unresolved and updated in the last 14 days +- `list_issues` - List issues with optional filters (assignee, issue_type, project). Defaults to current user's unresolved issues updated in the last 14 days - `attach_file` - Attach a file to a JIRA issue - `assign_issue` - Assign a JIRA issue to a user - `add_issue_to_sprint` - Add a JIRA issue to the current active sprint