Skip to content

Commit 5c41266

Browse files
Copilotalexec
andcommitted
Merge main into copilot/add-issue-creation-functionality
Co-authored-by: alexec <1142830+alexec@users.noreply.github.com>
1 parent 1ef6d2b commit 5c41266

4 files changed

Lines changed: 237 additions & 0 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ Usage:
7979
jira get-comments <issue-key> - Get comments of the specified JIRA issue
8080
jira add-comment <issue-key> <comment> - Add a comment to the specified JIRA issue
8181
jira attach-file <issue-key> <file-path> - Attach a file to the specified JIRA issue
82+
jira assign-issue <issue-key> <assignee> - Assign an issue to a user
83+
jira add-issue-to-sprint <issue-key> - Add an issue to the current sprint
8284
jira mcp-server - Start MCP server (Model Context Protocol)
8385
```
8486

@@ -138,6 +140,18 @@ jira attach-file PROJ-123 /path/to/document.pdf
138140
jira attach-file PROJ-456 ~/screenshots/bug-screenshot.png
139141
```
140142

143+
**Assign an issue:**
144+
```bash
145+
jira assign-issue PROJ-123 john.doe
146+
# Assigns the issue PROJ-123 to user john.doe
147+
```
148+
149+
**Add an issue to the current sprint:**
150+
```bash
151+
jira add-issue-to-sprint PROJ-123
152+
# Adds the issue to the currently active sprint for its project
153+
```
154+
141155
### MCP Server Mode
142156

143157
The MCP (Model Context Protocol) server allows AI assistants and other tools to interact with JIRA through a standardized JSON-RPC protocol over stdio. This enables seamless integration with AI coding assistants and other automation tools.
@@ -175,6 +189,8 @@ The server exposes the following tools:
175189
- `create_issue` - Create a new JIRA issue with specified project, issue type (Story/Bug/Task), title, description, and optional assignee
176190
- `list_issues` - List issues assigned to the current user that are unresolved and updated in the last 14 days
177191
- `attach_file` - Attach a file to a JIRA issue
192+
- `assign_issue` - Assign a JIRA issue to a user
193+
- `add_issue_to_sprint` - Add a JIRA issue to the current active sprint
178194

179195
**Example usage from an AI assistant:**
180196
> "Get the details of issue PROJ-123 and add a comment saying the work is in progress."

main.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ func main() {
3939
fmt.Fprintln(w, " jira get-comments <issue-key> - Get comments of the specified JIRA issue")
4040
fmt.Fprintln(w, " jira add-comment <issue-key> <comment> - Add a comment to the specified JIRA issue")
4141
fmt.Fprintln(w, " jira attach-file <issue-key> <file-path> - Attach a file to the specified JIRA issue")
42+
fmt.Fprintln(w, " jira assign-issue <issue-key> <assignee> - Assign an issue to a user")
43+
fmt.Fprintln(w, " jira add-issue-to-sprint <issue-key> - Add an issue to the current sprint")
4244
fmt.Fprintln(w, " jira mcp-server - Start MCP server (stdio transport)")
4345
fmt.Fprintln(w)
4446
fmt.Fprintln(w, "Options:")
@@ -123,6 +125,21 @@ func run(ctx context.Context, args []string) error {
123125
return executeCommand(ctx, func(ctx context.Context) error {
124126
return attachFile(ctx, filePath)
125127
})
128+
case "assign-issue":
129+
if len(args) < 3 {
130+
return fmt.Errorf("usage: jira assign-issue <issue-key> <assignee>")
131+
}
132+
issueKey = args[1]
133+
assignee := args[2]
134+
return executeCommand(ctx, func(ctx context.Context) error {
135+
return assignIssue(ctx, assignee)
136+
})
137+
case "add-issue-to-sprint":
138+
if len(args) < 2 {
139+
return fmt.Errorf("usage: jira add-issue-to-sprint <issue-key>")
140+
}
141+
issueKey = args[1]
142+
return executeCommand(ctx, addIssueToSprint)
126143
case "mcp-server":
127144
return runMCPServer(ctx)
128145
default:
@@ -441,3 +458,68 @@ func attachFile(ctx context.Context, filePath string) error {
441458

442459
return nil
443460
}
461+
462+
// assignIssue assigns an issue to a user
463+
func assignIssue(ctx context.Context, assignee string) error {
464+
// Create a User object with the assignee name
465+
user := &jira.User{
466+
Name: assignee,
467+
}
468+
469+
// Update the assignee
470+
_, err := client.Issue.UpdateAssigneeWithContext(ctx, issueKey, user)
471+
if err != nil {
472+
return fmt.Errorf("failed to assign issue: %w", err)
473+
}
474+
475+
fmt.Printf("Successfully assigned issue %s to %s\n", issueKey, assignee)
476+
return nil
477+
}
478+
479+
// addIssueToSprint adds an issue to the current sprint
480+
func addIssueToSprint(ctx context.Context) error {
481+
// First, get the issue to find its project/board
482+
issue, _, err := client.Issue.GetWithContext(ctx, issueKey, nil)
483+
if err != nil {
484+
return fmt.Errorf("failed to get issue: %w", err)
485+
}
486+
487+
// Get all boards to find the one that contains this issue's project
488+
boards, _, err := client.Board.GetAllBoardsWithContext(ctx, &jira.BoardListOptions{
489+
ProjectKeyOrID: issue.Fields.Project.Key,
490+
})
491+
if err != nil {
492+
return fmt.Errorf("failed to get boards: %w", err)
493+
}
494+
495+
if len(boards.Values) == 0 {
496+
return fmt.Errorf("no boards found for project %s", issue.Fields.Project.Key)
497+
}
498+
499+
// Use the first board (typically the main board for the project)
500+
boardID := boards.Values[0].ID
501+
502+
// Get all sprints for this board
503+
sprints, _, err := client.Board.GetAllSprintsWithOptionsWithContext(ctx, boardID, &jira.GetAllSprintsOptions{
504+
State: "active",
505+
})
506+
if err != nil {
507+
return fmt.Errorf("failed to get sprints: %w", err)
508+
}
509+
510+
if len(sprints.Values) == 0 {
511+
return fmt.Errorf("no active sprint found for board %d", boardID)
512+
}
513+
514+
// Use the first active sprint
515+
sprintID := sprints.Values[0].ID
516+
517+
// Move the issue to the sprint
518+
_, err = client.Sprint.MoveIssuesToSprintWithContext(ctx, sprintID, []string{issueKey})
519+
if err != nil {
520+
return fmt.Errorf("failed to add issue to sprint: %w", err)
521+
}
522+
523+
fmt.Printf("Successfully added issue %s to sprint %s (ID: %d)\n", issueKey, sprints.Values[0].Name, sprintID)
524+
return nil
525+
}

mcp.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,34 @@ func runMCPServer(ctx context.Context) error {
154154
return attachFileHandler(ctx, api, request)
155155
})
156156

157+
// Add assign-issue tool
158+
assignIssueTool := mcp.NewTool("assign_issue",
159+
mcp.WithDescription("Assign a JIRA issue to a user"),
160+
mcp.WithString("issue_key",
161+
mcp.Required(),
162+
mcp.Description("JIRA issue key (e.g., 'PROJ-123')"),
163+
),
164+
mcp.WithString("assignee",
165+
mcp.Required(),
166+
mcp.Description("Username of the assignee"),
167+
),
168+
)
169+
s.AddTool(assignIssueTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
170+
return assignIssueHandler(ctx, api, request)
171+
})
172+
173+
// Add add-issue-to-sprint tool
174+
addIssueToSprintTool := mcp.NewTool("add_issue_to_sprint",
175+
mcp.WithDescription("Add a JIRA issue to the current active sprint"),
176+
mcp.WithString("issue_key",
177+
mcp.Required(),
178+
mcp.Description("JIRA issue key (e.g., 'PROJ-123')"),
179+
),
180+
)
181+
s.AddTool(addIssueToSprintTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
182+
return addIssueToSprintHandler(ctx, api, request)
183+
})
184+
157185
// Start the stdio server
158186
return server.ServeStdio(s)
159187
}
@@ -416,3 +444,79 @@ func attachFileHandler(ctx context.Context, client *jira.Client, request mcp.Cal
416444

417445
return mcp.NewToolResultText(fmt.Sprintf("Successfully attached file to issue %s", issueKey)), nil
418446
}
447+
448+
func assignIssueHandler(ctx context.Context, client *jira.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
449+
issueKey, err := request.RequireString("issue_key")
450+
if err != nil {
451+
return mcp.NewToolResultError(fmt.Sprintf("Missing or invalid 'issue_key' argument: %v", err)), nil
452+
}
453+
454+
assignee, err := request.RequireString("assignee")
455+
if err != nil {
456+
return mcp.NewToolResultError(fmt.Sprintf("Missing or invalid 'assignee' argument: %v", err)), nil
457+
}
458+
459+
// Create a User object with the assignee name
460+
user := &jira.User{
461+
Name: assignee,
462+
}
463+
464+
// Update the assignee
465+
_, err = client.Issue.UpdateAssigneeWithContext(ctx, issueKey, user)
466+
if err != nil {
467+
return mcp.NewToolResultError(fmt.Sprintf("Failed to assign issue: %v", err)), nil
468+
}
469+
470+
return mcp.NewToolResultText(fmt.Sprintf("Successfully assigned issue %s to %s", issueKey, assignee)), nil
471+
}
472+
473+
func addIssueToSprintHandler(ctx context.Context, client *jira.Client, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
474+
issueKey, err := request.RequireString("issue_key")
475+
if err != nil {
476+
return mcp.NewToolResultError(fmt.Sprintf("Missing or invalid 'issue_key' argument: %v", err)), nil
477+
}
478+
479+
// First, get the issue to find its project/board
480+
issue, _, err := client.Issue.GetWithContext(ctx, issueKey, nil)
481+
if err != nil {
482+
return mcp.NewToolResultError(fmt.Sprintf("Failed to get issue: %v", err)), nil
483+
}
484+
485+
// Get all boards to find the one that contains this issue's project
486+
boards, _, err := client.Board.GetAllBoardsWithContext(ctx, &jira.BoardListOptions{
487+
ProjectKeyOrID: issue.Fields.Project.Key,
488+
})
489+
if err != nil {
490+
return mcp.NewToolResultError(fmt.Sprintf("Failed to get boards: %v", err)), nil
491+
}
492+
493+
if len(boards.Values) == 0 {
494+
return mcp.NewToolResultError(fmt.Sprintf("No boards found for project %s", issue.Fields.Project.Key)), nil
495+
}
496+
497+
// Use the first board (typically the main board for the project)
498+
boardID := boards.Values[0].ID
499+
500+
// Get all sprints for this board
501+
sprints, _, err := client.Board.GetAllSprintsWithOptionsWithContext(ctx, boardID, &jira.GetAllSprintsOptions{
502+
State: "active",
503+
})
504+
if err != nil {
505+
return mcp.NewToolResultError(fmt.Sprintf("Failed to get sprints: %v", err)), nil
506+
}
507+
508+
if len(sprints.Values) == 0 {
509+
return mcp.NewToolResultError(fmt.Sprintf("No active sprint found for board %d", boardID)), nil
510+
}
511+
512+
// Use the first active sprint
513+
sprintID := sprints.Values[0].ID
514+
515+
// Move the issue to the sprint
516+
_, err = client.Sprint.MoveIssuesToSprintWithContext(ctx, sprintID, []string{issueKey})
517+
if err != nil {
518+
return mcp.NewToolResultError(fmt.Sprintf("Failed to add issue to sprint: %v", err)), nil
519+
}
520+
521+
return mcp.NewToolResultText(fmt.Sprintf("Successfully added issue %s to sprint %s (ID: %d)", issueKey, sprints.Values[0].Name, sprintID)), nil
522+
}

mcp_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,41 @@ func TestRun_AttachFileNonExistentFile(t *testing.T) {
109109
}
110110
}
111111

112+
func TestRun_AssignIssueMissingArgs(t *testing.T) {
113+
ctx := context.Background()
114+
115+
// Test with no arguments
116+
err := run(ctx, []string{"assign-issue"})
117+
if err == nil {
118+
t.Error("Expected error for missing arguments, got nil")
119+
}
120+
if !strings.Contains(err.Error(), "usage: jira assign-issue") {
121+
t.Errorf("Expected usage error, got: %v", err)
122+
}
123+
124+
// Test with only issue key
125+
err = run(ctx, []string{"assign-issue", "TEST-123"})
126+
if err == nil {
127+
t.Error("Expected error for missing assignee, got nil")
128+
}
129+
if !strings.Contains(err.Error(), "usage: jira assign-issue") {
130+
t.Errorf("Expected usage error, got: %v", err)
131+
}
132+
}
133+
134+
func TestRun_AddIssueToSprintMissingArgs(t *testing.T) {
135+
ctx := context.Background()
136+
137+
// Test with no arguments
138+
err := run(ctx, []string{"add-issue-to-sprint"})
139+
if err == nil {
140+
t.Error("Expected error for missing arguments, got nil")
141+
}
142+
if !strings.Contains(err.Error(), "usage: jira add-issue-to-sprint") {
143+
t.Errorf("Expected usage error, got: %v", err)
144+
}
145+
}
146+
112147
func TestRun_CreateIssueMissingArgs(t *testing.T) {
113148
ctx := context.Background()
114149

0 commit comments

Comments
 (0)