Textwell MCP Server
by worldnine
- tools
package tools
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/ctreminiom/go-atlassian/pkg/infra/models"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/nguyenvanduocit/all-in-one-model-context-protocol/services"
"github.com/nguyenvanduocit/all-in-one-model-context-protocol/util"
)
// RegisterJiraTool registers the Jira tools to the server
func RegisterJiraTool(s *server.MCPServer) {
// Get issue details tool
jiraGetIssueTool := mcp.NewTool("jira_get_issue",
mcp.WithDescription("Retrieve detailed information about a specific Jira issue including its status, assignee, description, subtasks, and available transitions"),
mcp.WithString("issue_key", mcp.Required(), mcp.Description("The unique identifier of the Jira issue (e.g., KP-2, PROJ-123)")),
)
s.AddTool(jiraGetIssueTool, util.ErrorGuard(jiraIssueHandler))
// Search issues tool
jiraSearchTool := mcp.NewTool("jira_search_issue",
mcp.WithDescription("Search for Jira issues using JQL (Jira Query Language). Returns key details like summary, status, assignee, and priority for matching issues"),
mcp.WithString("jql", mcp.Required(), mcp.Description("JQL query string (e.g., 'project = KP AND status = \"In Progress\"')")),
)
// List sprints tool
jiraListSprintTool := mcp.NewTool("jira_list_sprints",
mcp.WithDescription("List all active and future sprints for a specific Jira board, including sprint IDs, names, states, and dates"),
mcp.WithString("board_id", mcp.Required(), mcp.Description("Numeric ID of the Jira board (can be found in board URL)")),
)
// Create issue tool
jiraCreateIssueTool := mcp.NewTool("jira_create_issue",
mcp.WithDescription("Create a new Jira issue with specified details. Returns the created issue's key, ID, and URL"),
mcp.WithString("project_key", mcp.Required(), mcp.Description("Project identifier where the issue will be created (e.g., KP, PROJ)")),
mcp.WithString("summary", mcp.Required(), mcp.Description("Brief title or headline of the issue")),
mcp.WithString("description", mcp.Required(), mcp.Description("Detailed explanation of the issue")),
mcp.WithString("issue_type", mcp.Required(), mcp.Description("Type of issue to create (common types: Bug, Task, Story, Epic)")),
)
// Update issue tool
jiraUpdateIssueTool := mcp.NewTool("jira_update_issue",
mcp.WithDescription("Modify an existing Jira issue's details. Supports partial updates - only specified fields will be changed"),
mcp.WithString("issue_key", mcp.Required(), mcp.Description("The unique identifier of the issue to update (e.g., KP-2)")),
mcp.WithString("summary", mcp.Description("New title for the issue (optional)")),
mcp.WithString("description", mcp.Description("New description for the issue (optional)")),
)
// Add status list tool
jiraStatusListTool := mcp.NewTool("jira_list_statuses",
mcp.WithDescription("Retrieve all available issue status IDs and their names for a specific Jira project"),
mcp.WithString("project_key", mcp.Required(), mcp.Description("Project identifier (e.g., KP, PROJ)")),
)
// Add new tool definition in RegisterJiraTool function
jiraTransitionTool := mcp.NewTool("jira_transition_issue",
mcp.WithDescription("Transition an issue through its workflow using a valid transition ID. Get available transitions from jira_get_issue"),
mcp.WithString("issue_key", mcp.Required(), mcp.Description("The issue to transition (e.g., KP-123)")),
mcp.WithString("transition_id", mcp.Required(), mcp.Description("Transition ID from available transitions list")),
mcp.WithString("comment", mcp.Description("Optional comment to add with transition")),
)
s.AddTool(jiraSearchTool, util.ErrorGuard(jiraSearchHandler))
s.AddTool(jiraListSprintTool, util.ErrorGuard(jiraListSprintHandler))
s.AddTool(jiraCreateIssueTool, util.ErrorGuard(jiraCreateIssueHandler))
s.AddTool(jiraUpdateIssueTool, util.ErrorGuard(jiraUpdateIssueHandler))
s.AddTool(jiraStatusListTool, util.ErrorGuard(jiraGetStatusesHandler))
s.AddTool(jiraTransitionTool, util.ErrorGuard(jiraTransitionIssueHandler))
}
func jiraUpdateIssueHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
client := services.JiraClient()
issueKey, ok := arguments["issue_key"].(string)
if !ok {
return nil, fmt.Errorf("issue_key argument is required")
}
// Create update payload
payload := &models.IssueSchemeV2{
Fields: &models.IssueFieldsSchemeV2{},
}
// Check and add optional fields if provided
if summary, ok := arguments["summary"].(string); ok && summary != "" {
payload.Fields.Summary = summary
}
if description, ok := arguments["description"].(string); ok && description != "" {
payload.Fields.Description = description
}
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
response, err := client.Issue.Update(ctx, issueKey, true, payload, nil, nil)
if err != nil {
if response != nil {
return nil, fmt.Errorf("failed to update issue: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
}
return nil, fmt.Errorf("failed to update issue: %v", err)
}
return mcp.NewToolResultText("Issue updated successfully!"), nil
}
func jiraCreateIssueHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
client := services.JiraClient()
projectKey, ok := arguments["project_key"].(string)
if !ok {
return nil, fmt.Errorf("project_key argument is required")
}
summary, ok := arguments["summary"].(string)
if !ok {
return nil, fmt.Errorf("summary argument is required")
}
description, ok := arguments["description"].(string)
if !ok {
return nil, fmt.Errorf("description argument is required")
}
issueType, ok := arguments["issue_type"].(string)
if !ok {
return nil, fmt.Errorf("issue_type argument is required")
}
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
var payload = models.IssueSchemeV2{
Fields: &models.IssueFieldsSchemeV2{
Summary: summary,
Project: &models.ProjectScheme{Key: projectKey},
Description: description,
IssueType: &models.IssueTypeScheme{Name: issueType},
},
}
issue, response, err := client.Issue.Create(ctx, &payload, nil)
if err != nil {
if response != nil {
return nil, fmt.Errorf("failed to create issue: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
}
return nil, fmt.Errorf("failed to create issue: %v", err)
}
result := fmt.Sprintf("Issue created successfully!\nKey: %s\nID: %s\nURL: %s", issue.Key, issue.ID, issue.Self)
return mcp.NewToolResultText(result), nil
}
func jiraListSprintHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
boardIDStr, ok := arguments["board_id"].(string)
if !ok {
return nil, fmt.Errorf("board_id argument is required")
}
boardID, err := strconv.Atoi(boardIDStr)
if err != nil {
return nil, fmt.Errorf("invalid board_id: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
sprints, response, err := services.AgileClient().Board.Sprints(ctx, boardID, 0, 50, []string{"active", "future"})
if err != nil {
if response != nil {
return nil, fmt.Errorf("failed to get sprints: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
}
return nil, fmt.Errorf("failed to get sprints: %v", err)
}
if len(sprints.Values) == 0 {
return mcp.NewToolResultText("No sprints found for this board."), nil
}
var result string
for _, sprint := range sprints.Values {
result += fmt.Sprintf("ID: %d\nName: %s\nState: %s\nStartDate: %s\nEndDate: %s\n\n", sprint.ID, sprint.Name, sprint.State, sprint.StartDate, sprint.EndDate)
}
return mcp.NewToolResultText(result), nil
}
func jiraSearchHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
client := services.JiraClient()
// Get search text from arguments
jql, ok := arguments["jql"].(string)
if !ok {
return nil, fmt.Errorf("jql argument is required")
}
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
searchResult, response, err := client.Issue.Search.Get(ctx, jql, nil, nil, 0, 30, "")
if err != nil {
if response != nil {
return nil, fmt.Errorf("failed to search issues: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
}
return nil, fmt.Errorf("failed to search issues: %v", err)
}
if len(searchResult.Issues) == 0 {
return mcp.NewToolResultText("No issues found matching the search criteria."), nil
}
var sb strings.Builder
for _, issue := range searchResult.Issues {
sb.WriteString(fmt.Sprintf("Key: %s\n", issue.Key))
if issue.Fields.Summary != "" {
sb.WriteString(fmt.Sprintf("Summary: %s\n", issue.Fields.Summary))
}
if issue.Fields.Status != nil && issue.Fields.Status.Name != "" {
sb.WriteString(fmt.Sprintf("Status: %s\n", issue.Fields.Status.Name))
}
if issue.Fields.Created != "" {
sb.WriteString(fmt.Sprintf("Created: %s\n", issue.Fields.Created))
}
if issue.Fields.Updated != "" {
sb.WriteString(fmt.Sprintf("Updated: %s\n", issue.Fields.Updated))
}
if issue.Fields.Assignee != nil {
sb.WriteString(fmt.Sprintf("Assignee: %s\n", issue.Fields.Assignee.DisplayName))
} else {
sb.WriteString("Assignee: Unassigned\n")
}
if issue.Fields.Priority != nil {
sb.WriteString(fmt.Sprintf("Priority: %s\n", issue.Fields.Priority.Name))
} else {
sb.WriteString("Priority: Unset\n")
}
if issue.Fields.Resolutiondate != "" {
sb.WriteString(fmt.Sprintf("Resolution date: %s\n", issue.Fields.Resolutiondate))
}
sb.WriteString("\n")
}
return mcp.NewToolResultText(sb.String()), nil
}
func jiraIssueHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
client := services.JiraClient()
// Get issue key from arguments
issueKey, ok := arguments["issue_key"].(string)
if !ok {
return nil, fmt.Errorf("issue_key argument is required")
}
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
issue, response, err := client.Issue.Get(ctx, issueKey, nil, []string{"transitions"})
if err != nil {
if response != nil {
return nil, fmt.Errorf("failed to get issue: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
}
return nil, fmt.Errorf("failed to get issue: %v", err)
}
// Build subtasks string if they exist
var subtasks string
if issue.Fields.Subtasks != nil {
subtasks = "\nSubtasks:\n"
for _, subTask := range issue.Fields.Subtasks {
subtasks += fmt.Sprintf("- %s: %s\n", subTask.Key, subTask.Fields.Summary)
}
}
// Build transitions string
var transitions string
for _, transition := range issue.Transitions {
transitions += fmt.Sprintf("- %s (ID: %s)\n", transition.Name, transition.ID)
}
// Get reporter name, handling nil case
reporterName := "Unassigned"
if issue.Fields.Reporter != nil {
reporterName = issue.Fields.Reporter.DisplayName
}
// Get assignee name, handling nil case
assigneeName := "Unassigned"
if issue.Fields.Assignee != nil {
assigneeName = issue.Fields.Assignee.DisplayName
}
// Get priority name, handling nil case
priorityName := "None"
if issue.Fields.Priority != nil {
priorityName = issue.Fields.Priority.Name
}
result := fmt.Sprintf(`
Key: %s
Summary: %s
Status: %s
Reporter: %s
Assignee: %s
Created: %s
Updated: %s
Priority: %s
Description:
%s
%s
Available Transitions:
%s`,
issue.Key,
issue.Fields.Summary,
issue.Fields.Status.Name,
reporterName,
assigneeName,
issue.Fields.Created,
issue.Fields.Updated,
priorityName,
issue.Fields.Description,
subtasks,
transitions,
)
return mcp.NewToolResultText(result), nil
}
func jiraGetStatusesHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
client := services.JiraClient()
projectKey, ok := arguments["project_key"].(string)
if !ok {
return nil, fmt.Errorf("project_key argument is required")
}
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
issueTypes, response, err := client.Project.Statuses(ctx, projectKey)
if err != nil {
if response != nil {
return nil, fmt.Errorf("failed to get statuses: %s (endpoint: %s)", response.Bytes.String(), response.Endpoint)
}
return nil, fmt.Errorf("failed to get statuses: %v", err)
}
if len(issueTypes) == 0 {
return mcp.NewToolResultText("No issue types found for this project."), nil
}
var result strings.Builder
result.WriteString("Available Statuses:\n")
for _, issueType := range issueTypes {
result.WriteString(fmt.Sprintf("\nIssue Type: %s\n", issueType.Name))
for _, status := range issueType.Statuses {
result.WriteString(fmt.Sprintf(" - %s: %s\n", status.Name, status.ID))
}
}
return mcp.NewToolResultText(result.String()), nil
}
func jiraTransitionIssueHandler(arguments map[string]interface{}) (*mcp.CallToolResult, error) {
client := services.JiraClient()
issueKey, ok := arguments["issue_key"].(string)
if !ok || issueKey == "" {
return nil, fmt.Errorf("valid issue_key is required")
}
transitionID, ok := arguments["transition_id"].(string)
if !ok || transitionID == "" {
return nil, fmt.Errorf("valid transition_id is required")
}
var options *models.IssueMoveOptionsV2
if comment, ok := arguments["comment"].(string); ok && comment != "" {
options = &models.IssueMoveOptionsV2{
Fields: &models.IssueSchemeV2{
},
}
}
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
response, err := client.Issue.Move(ctx, issueKey, transitionID, options)
if err != nil {
if response != nil {
return nil, fmt.Errorf("transition failed: %s (endpoint: %s)",
response.Bytes.String(),
response.Endpoint)
}
return nil, fmt.Errorf("transition failed: %v", err)
}
return mcp.NewToolResultText("Issue transition completed successfully"), nil
}