#
tokens: 42981/50000 8/150 files (page 4/5)
lines: off (toggle) GitHub
raw markdown copy
This is page 4 of 5. Use http://codebase.md/geropl/linear-mcp-go?page={x} to view the full context.

# Directory Structure

```
├── .clinerules
│   └── memory-bank.md
├── .devcontainer
│   ├── devcontainer.json
│   └── Dockerfile
├── .github
│   └── workflows
│       └── release.yml
├── .gitignore
├── .gitpod.yml
├── cmd
│   ├── root.go
│   ├── serve.go
│   ├── setup_test.go
│   ├── setup.go
│   └── version.go
├── docs
│   ├── design
│   │   ├── 001-mcp-go-upgrade.md
│   │   └── 002-project-milestone-initiative.md
│   └── prd
│       ├── 000-tool-standardization-overview.md
│       ├── 001-api-refresher.md
│       ├── 002-tool-standardization.md
│       ├── 003-tool-standardization-implementation.md
│       ├── 004-tool-standardization-tracking.md
│       ├── 005-sample-implementation.md
│       ├── 006-issue-comments-pagination.md
│       └── README.md
├── go.mod
├── go.sum
├── main.go
├── memory-bank
│   ├── activeContext.md
│   ├── developmentWorkflows.md
│   ├── productContext.md
│   ├── progress.md
│   ├── projectbrief.md
│   ├── systemPatterns.md
│   └── techContext.md
├── pkg
│   ├── linear
│   │   ├── client.go
│   │   ├── models.go
│   │   ├── rate_limiter.go
│   │   └── test_helpers.go
│   ├── server
│   │   ├── resources_test.go
│   │   ├── resources.go
│   │   ├── server.go
│   │   ├── test_helpers.go
│   │   └── tools_test.go
│   └── tools
│       ├── add_comment.go
│       ├── common.go
│       ├── create_issue.go
│       ├── get_issue_comments.go
│       ├── get_issue.go
│       ├── get_teams.go
│       ├── get_user_issues.go
│       ├── initiative_tools.go
│       ├── milestone_tools.go
│       ├── priority_test.go
│       ├── priority.go
│       ├── project_tools.go
│       ├── rendering.go
│       ├── reply_to_comment.go
│       ├── search_issues.go
│       ├── update_issue_comment.go
│       └── update_issue.go
├── README.md
├── scripts
│   └── register-cline.sh
└── testdata
    ├── fixtures
    │   ├── add_comment_handler_Missing body.yaml
    │   ├── add_comment_handler_Missing issue.yaml
    │   ├── add_comment_handler_Missing issueId.yaml
    │   ├── add_comment_handler_Reply with shorthand.yaml
    │   ├── add_comment_handler_Reply with URL.yaml
    │   ├── add_comment_handler_Reply_to_comment.yaml
    │   ├── add_comment_handler_Valid comment.yaml
    │   ├── create_initiative_handler_Missing name.yaml
    │   ├── create_initiative_handler_Valid initiative.yaml
    │   ├── create_initiative_handler_With description.yaml
    │   ├── create_issue_handler_Create issue with invalid project.yaml
    │   ├── create_issue_handler_Create issue with labels.yaml
    │   ├── create_issue_handler_Create issue with project ID.yaml
    │   ├── create_issue_handler_Create issue with project name.yaml
    │   ├── create_issue_handler_Create issue with project slug.yaml
    │   ├── create_issue_handler_Create sub issue from identifier.yaml
    │   ├── create_issue_handler_Create sub issue with labels.yaml
    │   ├── create_issue_handler_Create sub issue.yaml
    │   ├── create_issue_handler_Invalid team.yaml
    │   ├── create_issue_handler_Missing team.yaml
    │   ├── create_issue_handler_Missing teamId.yaml
    │   ├── create_issue_handler_Missing title.yaml
    │   ├── create_issue_handler_Valid issue with team key.yaml
    │   ├── create_issue_handler_Valid issue with team name.yaml
    │   ├── create_issue_handler_Valid issue with team UUID.yaml
    │   ├── create_issue_handler_Valid issue with team.yaml
    │   ├── create_issue_handler_Valid issue with teamId.yaml
    │   ├── create_issue_handler_Valid issue.yaml
    │   ├── create_milestone_handler_Invalid project ID.yaml
    │   ├── create_milestone_handler_Missing name.yaml
    │   ├── create_milestone_handler_Valid milestone.yaml
    │   ├── create_milestone_handler_With all optional fields.yaml
    │   ├── create_project_handler_Invalid team ID.yaml
    │   ├── create_project_handler_Missing name.yaml
    │   ├── create_project_handler_Valid project.yaml
    │   ├── create_project_handler_With all optional fields.yaml
    │   ├── get_initiative_handler_By name.yaml
    │   ├── get_initiative_handler_Non-existent name.yaml
    │   ├── get_initiative_handler_Valid initiative.yaml
    │   ├── get_issue_comments_handler_Invalid issue.yaml
    │   ├── get_issue_comments_handler_Missing issue.yaml
    │   ├── get_issue_comments_handler_Thread_with_pagination.yaml
    │   ├── get_issue_comments_handler_Valid issue.yaml
    │   ├── get_issue_comments_handler_With limit.yaml
    │   ├── get_issue_comments_handler_With_thread_parameter.yaml
    │   ├── get_issue_handler_Get comment issue.yaml
    │   ├── get_issue_handler_Missing issue.yaml
    │   ├── get_issue_handler_Missing issueId.yaml
    │   ├── get_issue_handler_Valid issue.yaml
    │   ├── get_milestone_handler_By name.yaml
    │   ├── get_milestone_handler_Non-existent milestone.yaml
    │   ├── get_milestone_handler_Valid milestone.yaml
    │   ├── get_project_handler_By ID.yaml
    │   ├── get_project_handler_By name.yaml
    │   ├── get_project_handler_By slug.yaml
    │   ├── get_project_handler_Invalid project.yaml
    │   ├── get_project_handler_Missing project param.yaml
    │   ├── get_project_handler_Non-existent slug.yaml
    │   ├── get_teams_handler_Get Teams.yaml
    │   ├── get_user_issues_handler_Current user issues.yaml
    │   ├── get_user_issues_handler_Specific user issues.yaml
    │   ├── reply_to_comment_handler_Missing body.yaml
    │   ├── reply_to_comment_handler_Missing thread.yaml
    │   ├── reply_to_comment_handler_Reply with URL.yaml
    │   ├── reply_to_comment_handler_Valid reply.yaml
    │   ├── resource_TeamResourceHandler_Fetch By ID.yaml
    │   ├── resource_TeamResourceHandler_Fetch By Key.yaml
    │   ├── resource_TeamResourceHandler_Fetch By Name.yaml
    │   ├── resource_TeamResourceHandler_Invalid ID.yaml
    │   ├── resource_TeamResourceHandler_Missing ID.yaml
    │   ├── resource_TeamsResourceHandler_List All.yaml
    │   ├── search_issues_handler_Search by query.yaml
    │   ├── search_issues_handler_Search by team.yaml
    │   ├── search_projects_handler_Empty query.yaml
    │   ├── search_projects_handler_Multiple results.yaml
    │   ├── search_projects_handler_No results.yaml
    │   ├── search_projects_handler_Search by query.yaml
    │   ├── update_comment_handler_Invalid comment identifier.yaml
    │   ├── update_comment_handler_Missing body.yaml
    │   ├── update_comment_handler_Missing comment.yaml
    │   ├── update_comment_handler_Valid comment update with hash only.yaml
    │   ├── update_comment_handler_Valid comment update with shorthand.yaml
    │   ├── update_comment_handler_Valid comment update.yaml
    │   ├── update_initiative_handler_Non-existent initiative.yaml
    │   ├── update_initiative_handler_Valid update.yaml
    │   ├── update_issue_handler_Missing id.yaml
    │   ├── update_issue_handler_Valid update.yaml
    │   ├── update_milestone_handler_Non-existent milestone.yaml
    │   ├── update_milestone_handler_Valid update.yaml
    │   ├── update_project_handler_Non-existent project.yaml
    │   ├── update_project_handler_Update name and description.yaml
    │   ├── update_project_handler_Update only description.yaml
    │   └── update_project_handler_Valid update.yaml
    └── golden
        ├── add_comment_handler_Missing body.golden
        ├── add_comment_handler_Missing issue.golden
        ├── add_comment_handler_Missing issueId.golden
        ├── add_comment_handler_Reply with shorthand.golden
        ├── add_comment_handler_Reply with URL.golden
        ├── add_comment_handler_Reply_to_comment.golden
        ├── add_comment_handler_Valid comment.golden
        ├── create_initiative_handler_Missing name.golden
        ├── create_initiative_handler_Valid initiative.golden
        ├── create_initiative_handler_With description.golden
        ├── create_issue_handler_Create issue with invalid project.golden
        ├── create_issue_handler_Create issue with labels.golden
        ├── create_issue_handler_Create issue with project ID.golden
        ├── create_issue_handler_Create issue with project name.golden
        ├── create_issue_handler_Create issue with project slug.golden
        ├── create_issue_handler_Create sub issue from identifier.golden
        ├── create_issue_handler_Create sub issue with labels.golden
        ├── create_issue_handler_Create sub issue.golden
        ├── create_issue_handler_Invalid team.golden
        ├── create_issue_handler_Missing team.golden
        ├── create_issue_handler_Missing teamId.golden
        ├── create_issue_handler_Missing title.golden
        ├── create_issue_handler_Valid issue with team key.golden
        ├── create_issue_handler_Valid issue with team name.golden
        ├── create_issue_handler_Valid issue with team UUID.golden
        ├── create_issue_handler_Valid issue with team.golden
        ├── create_issue_handler_Valid issue with teamId.golden
        ├── create_issue_handler_Valid issue.golden
        ├── create_milestone_handler_Invalid project ID.golden
        ├── create_milestone_handler_Missing name.golden
        ├── create_milestone_handler_Valid milestone.golden
        ├── create_milestone_handler_With all optional fields.golden
        ├── create_project_handler_Invalid team ID.golden
        ├── create_project_handler_Missing name.golden
        ├── create_project_handler_Valid project.golden
        ├── create_project_handler_With all optional fields.golden
        ├── get_initiative_handler_By name.golden
        ├── get_initiative_handler_Non-existent name.golden
        ├── get_initiative_handler_Valid initiative.golden
        ├── get_issue_comments_handler_Invalid issue.golden
        ├── get_issue_comments_handler_Missing issue.golden
        ├── get_issue_comments_handler_Thread_with_pagination.golden
        ├── get_issue_comments_handler_Valid issue.golden
        ├── get_issue_comments_handler_With limit.golden
        ├── get_issue_comments_handler_With_thread_parameter.golden
        ├── get_issue_handler_Get comment issue.golden
        ├── get_issue_handler_Missing issue.golden
        ├── get_issue_handler_Missing issueId.golden
        ├── get_issue_handler_Valid issue.golden
        ├── get_milestone_handler_By name.golden
        ├── get_milestone_handler_Non-existent milestone.golden
        ├── get_milestone_handler_Valid milestone.golden
        ├── get_project_handler_By ID.golden
        ├── get_project_handler_By name.golden
        ├── get_project_handler_By slug.golden
        ├── get_project_handler_Invalid project.golden
        ├── get_project_handler_Missing project param.golden
        ├── get_project_handler_Non-existent slug.golden
        ├── get_teams_handler_Get Teams.golden
        ├── get_user_issues_handler_Current user issues.golden
        ├── get_user_issues_handler_Specific user issues.golden
        ├── reply_to_comment_handler_Missing body.golden
        ├── reply_to_comment_handler_Missing thread.golden
        ├── reply_to_comment_handler_Reply with URL.golden
        ├── reply_to_comment_handler_Valid reply.golden
        ├── resource_TeamResourceHandler_Fetch By ID.golden
        ├── resource_TeamResourceHandler_Fetch By Key.golden
        ├── resource_TeamResourceHandler_Fetch By Name.golden
        ├── resource_TeamResourceHandler_Invalid ID.golden
        ├── resource_TeamResourceHandler_Missing ID.golden
        ├── resource_TeamsResourceHandler_List All.golden
        ├── search_issues_handler_Search by query.golden
        ├── search_issues_handler_Search by team.golden
        ├── search_projects_handler_Empty query.golden
        ├── search_projects_handler_Multiple results.golden
        ├── search_projects_handler_No results.golden
        ├── search_projects_handler_Search by query.golden
        ├── update_comment_handler_Invalid comment identifier.golden
        ├── update_comment_handler_Missing body.golden
        ├── update_comment_handler_Missing comment.golden
        ├── update_comment_handler_Valid comment update with hash only.golden
        ├── update_comment_handler_Valid comment update with shorthand.golden
        ├── update_comment_handler_Valid comment update.golden
        ├── update_initiative_handler_Non-existent initiative.golden
        ├── update_initiative_handler_Valid update.golden
        ├── update_issue_handler_Missing id.golden
        ├── update_issue_handler_Valid update.golden
        ├── update_milestone_handler_Non-existent milestone.golden
        ├── update_milestone_handler_Valid update.golden
        ├── update_project_handler_Non-existent project.golden
        ├── update_project_handler_Update name and description.golden
        ├── update_project_handler_Update only description.golden
        └── update_project_handler_Valid update.golden
```

# Files

--------------------------------------------------------------------------------
/pkg/linear/models.go:
--------------------------------------------------------------------------------

```go
package linear

import "time"

// Issue represents a Linear issue
type Issue struct {
	ID               string                   `json:"id"`
	Identifier       string                   `json:"identifier"`
	Title            string                   `json:"title"`
	Description      string                   `json:"description"`
	Priority         int                      `json:"priority"`
	Status           string                   `json:"status"`
	Assignee         *User                    `json:"assignee,omitempty"`
	Team             *Team                    `json:"team,omitempty"`
	Project          *Project                 `json:"project,omitempty"`
	ProjectMilestone *ProjectMilestone        `json:"projectMilestone,omitempty"`
	URL              string                   `json:"url"`
	CreatedAt        time.Time                `json:"createdAt"`
	UpdatedAt        time.Time                `json:"updatedAt"`
	Labels           *LabelConnection         `json:"labels,omitempty"`
	State            *State                   `json:"state,omitempty"`
	Estimate         *float64                 `json:"estimate,omitempty"`
	Comments         *CommentConnection       `json:"comments,omitempty"`
	Relations        *IssueRelationConnection `json:"relations,omitempty"`
	InverseRelations *IssueRelationConnection `json:"inverseRelations,omitempty"`
	Attachments      *AttachmentConnection    `json:"attachments,omitempty"`
}

// User represents a Linear user
type User struct {
	ID    string `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
	Admin bool   `json:"admin"`
}

// Team represents a Linear team
type Team struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	Key  string `json:"key"`
}

// Project represents a Linear project
type Project struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	SlugID      string `json:"slugId"`
	State       string `json:"state"`
	Creator     *User  `json:"creator,omitempty"`
	Lead        *User  `json:"lead,omitempty"`
	// Members     *UserConnection `json:"members,omitempty"`
	// Teams       *TeamConnection `json:"teams,omitempty"`
	Initiatives *InitiativeConnection `json:"initiatives,omitempty"`
	StartDate   *string               `json:"startDate,omitempty"`
	TargetDate  *string               `json:"targetDate,omitempty"`
	Color       string                `json:"color"`
	Icon        string                `json:"icon,omitempty"`
	URL         string                `json:"url"`
}

// ProjectConnection represents a connection of projects
type ProjectConnection struct {
	Nodes []Project `json:"nodes"`
}

// ProjectMilestoneConnection represents a connection of project milestones.
type ProjectMilestoneConnection struct {
	Nodes []ProjectMilestone `json:"nodes"`
}

// ProjectMilestone represents a Linear project milestone
type ProjectMilestone struct {
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	Description string   `json:"description,omitempty"`
	TargetDate  *string  `json:"targetDate,omitempty"`
	Project     *Project `json:"project,omitempty"`
	SortOrder   float64  `json:"sortOrder"`
}

// InitiativeConnection represents a connection of initiatives.
type InitiativeConnection struct {
	Nodes []Initiative `json:"nodes"`
}

// Initiative represents a Linear initiative
type Initiative struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Owner       *User  `json:"owner,omitempty"`
	Color       string `json:"color,omitempty"`
	Icon        string `json:"icon,omitempty"`
	SlugID      string `json:"slugId"`
	URL         string `json:"url"`
}

// State represents a workflow state in Linear
type State struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

// LabelConnection represents a connection of labels
type LabelConnection struct {
	Nodes []Label `json:"nodes"`
}

// Label represents a Linear issue label
type Label struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

// CommentConnection represents a connection of comments
type CommentConnection struct {
	Nodes []Comment `json:"nodes"`
}

// PageInfo represents pagination information
type PageInfo struct {
	HasNextPage bool   `json:"hasNextPage"`
	EndCursor   string `json:"endCursor"`
}

// PaginatedCommentConnection represents a paginated connection of comments
type PaginatedCommentConnection struct {
	Nodes    []Comment `json:"nodes"`
	PageInfo PageInfo  `json:"pageInfo"`
}

// Comment represents a comment on a Linear issue
type Comment struct {
	ID        string             `json:"id"`
	Body      string             `json:"body"`
	User      *User              `json:"user,omitempty"`
	CreatedAt time.Time          `json:"createdAt"`
	URL       string             `json:"url,omitempty"`
	Parent    *Comment           `json:"parent,omitempty"`
	Children  *CommentConnection `json:"children,omitempty"`
	Issue     *Issue             `json:"issue,omitempty"`
}

// IssueRelationConnection represents a connection of issue relations
type IssueRelationConnection struct {
	Nodes []IssueRelation `json:"nodes"`
}

// IssueRelation represents a relation between two issues
type IssueRelation struct {
	ID           string `json:"id"`
	Type         string `json:"type"`
	RelatedIssue *Issue `json:"relatedIssue,omitempty"`
	Issue        *Issue `json:"issue,omitempty"`
}

// AttachmentConnection represents a connection of attachments
type AttachmentConnection struct {
	Nodes []Attachment `json:"nodes"`
}

// Attachment represents an external resource linked to an issue
type Attachment struct {
	ID         string                 `json:"id"`
	Title      string                 `json:"title"`
	Subtitle   string                 `json:"subtitle,omitempty"`
	URL        string                 `json:"url"`
	SourceType string                 `json:"sourceType,omitempty"`
	Metadata   map[string]interface{} `json:"metadata,omitempty"`
	CreatedAt  time.Time              `json:"createdAt"`
}

// Organization represents a Linear organization
type Organization struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	URLKey string `json:"urlKey"`
	Teams  []Team `json:"teams,omitempty"`
	Users  []User `json:"users,omitempty"`
}

// LinearIssueResponse represents a simplified issue response
type LinearIssueResponse struct {
	ID               string            `json:"id"`
	Identifier       string            `json:"identifier"`
	Title            string            `json:"title"`
	Priority         int               `json:"priority"`
	Status           string            `json:"status,omitempty"`
	StateName        string            `json:"stateName,omitempty"`
	URL              string            `json:"url"`
	Project          *Project          `json:"project,omitempty"`
	ProjectMilestone *ProjectMilestone `json:"projectMilestone,omitempty"`
}

// APIMetrics represents metrics about API usage
type APIMetrics struct {
	RequestsInLastHour int    `json:"requestsInLastHour"`
	RemainingRequests  int    `json:"remainingRequests"`
	AverageRequestTime string `json:"averageRequestTime"`
	QueueLength        int    `json:"queueLength"`
	LastRequestTime    string `json:"lastRequestTime"`
}

// CreateIssueInput represents input for creating an issue
type CreateIssueInput struct {
	Title       string   `json:"title"`
	TeamID      string   `json:"teamId"`
	Description string   `json:"description,omitempty"`
	Priority    *int     `json:"priority,omitempty"`
	Status      string   `json:"status,omitempty"`
	ParentID    *string  `json:"parentId,omitempty"`
	LabelIDs    []string `json:"labelIds,omitempty"`
	ProjectID   string   `json:"projectId,omitempty"`
}

// UpdateIssueInput represents input for updating an issue
type UpdateIssueInput struct {
	ID          string `json:"id"`
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
	Priority    *int   `json:"priority,omitempty"`
	Status      string `json:"status,omitempty"`
	TeamID      string `json:"teamId,omitempty"`
	ProjectID   string `json:"projectId,omitempty"`
	MilestoneID string `json:"milestoneId,omitempty"`
}

// SearchIssuesInput represents input for searching issues
type SearchIssuesInput struct {
	Query           string   `json:"query,omitempty"`
	TeamID          string   `json:"teamId,omitempty"`
	Status          string   `json:"status,omitempty"`
	AssigneeID      string   `json:"assigneeId,omitempty"`
	Labels          []string `json:"labels,omitempty"`
	Priority        *int     `json:"priority,omitempty"`
	Estimate        *float64 `json:"estimate,omitempty"`
	IncludeArchived bool     `json:"includeArchived,omitempty"`
	Limit           int      `json:"limit,omitempty"`
}

// GetUserIssuesInput represents input for getting user issues
type GetUserIssuesInput struct {
	UserID          string `json:"userId,omitempty"`
	IncludeArchived bool   `json:"includeArchived,omitempty"`
	Limit           int    `json:"limit,omitempty"`
}

// GetIssueCommentsInput represents input for getting issue comments
type GetIssueCommentsInput struct {
	IssueID     string `json:"issueId"`
	ParentID    string `json:"parentId,omitempty"`
	Limit       int    `json:"limit,omitempty"`
	AfterCursor string `json:"afterCursor,omitempty"`
}

// AddCommentInput represents input for adding a comment
type AddCommentInput struct {
	IssueID      string `json:"issueId"`
	Body         string `json:"body"`
	CreateAsUser string `json:"createAsUser,omitempty"`
	ParentID     string `json:"parentId,omitempty"`
}

// UpdateCommentInput represents the input for updating a comment
type UpdateCommentInput struct {
	CommentID string `json:"commentId"`
	Body      string `json:"body"`
}

// ProjectCreateInput represents the input for creating a project.
type ProjectCreateInput struct {
	Name        string   `json:"name"`
	TeamIDs     []string `json:"teamIds"`
	Description string   `json:"description,omitempty"`
	LeadID      string   `json:"leadId,omitempty"`
	StartDate   string   `json:"startDate,omitempty"`
	TargetDate  string   `json:"targetDate,omitempty"`
}

// ProjectUpdateInput represents the input for updating a project.
type ProjectUpdateInput struct {
	Name        string   `json:"name,omitempty"`
	Description string   `json:"description,omitempty"`
	LeadID      string   `json:"leadId,omitempty"`
	StartDate   string   `json:"startDate,omitempty"`
	TargetDate  string   `json:"targetDate,omitempty"`
	TeamIDs     []string `json:"teamIds,omitempty"`
}

// ProjectMilestoneCreateInput represents the input for creating a project milestone.
type ProjectMilestoneCreateInput struct {
	Name        string `json:"name"`
	ProjectID   string `json:"projectId"`
	Description string `json:"description,omitempty"`
	TargetDate  string `json:"targetDate,omitempty"`
}

// ProjectMilestoneUpdateInput represents the input for updating a project milestone.
type ProjectMilestoneUpdateInput struct {
	Name        string `json:"name,omitempty"`
	Description string `json:"description,omitempty"`
	TargetDate  string `json:"targetDate,omitempty"`
}

// InitiativeCreateInput represents the input for creating an initiative.
type InitiativeCreateInput struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
}

// InitiativeUpdateInput represents the input for updating an initiative.
type InitiativeUpdateInput struct {
	Name        string `json:"name,omitempty"`
	Description string `json:"description,omitempty"`
}

// GraphQLRequest represents a GraphQL request
type GraphQLRequest struct {
	Query     string                 `json:"query"`
	Variables map[string]interface{} `json:"variables,omitempty"`
}

// GraphQLResponse represents a GraphQL response
type GraphQLResponse struct {
	Data   map[string]interface{} `json:"data,omitempty"`
	Errors []GraphQLError         `json:"errors,omitempty"`
}

// GraphQLError represents a GraphQL error
type GraphQLError struct {
	Message string `json:"message"`
}

```

--------------------------------------------------------------------------------
/docs/prd/005-sample-implementation.md:
--------------------------------------------------------------------------------

```markdown
# Sample Implementation

This document provides sample implementations for the key components of the tool standardization effort. These samples can be used as references when implementing the actual changes.

## 1. Shared Utility Functions

### rendering.go

```go
package tools

import (
	"fmt"
	"strings"

	"github.com/geropl/linear-mcp-go/pkg/linear"
)

// Full Entity Rendering Functions

// formatIssue returns a consistently formatted full representation of an issue
func formatIssue(issue *linear.Issue) string {
	if issue == nil {
		return "Issue: Unknown"
	}
	
	var result strings.Builder
	result.WriteString(fmt.Sprintf("Issue: %s (UUID: %s)\n", issue.Identifier, issue.ID))
	result.WriteString(fmt.Sprintf("Title: %s\n", issue.Title))
	result.WriteString(fmt.Sprintf("URL: %s\n", issue.URL))
	
	if issue.Description != "" {
		result.WriteString(fmt.Sprintf("Description: %s\n", issue.Description))
	}
	
	priorityStr := "None"
	if issue.Priority > 0 {
		priorityStr = fmt.Sprintf("%d", issue.Priority)
	}
	result.WriteString(fmt.Sprintf("Priority: %s\n", priorityStr))
	
	statusStr := "None"
	if issue.Status != "" {
		statusStr = issue.Status
	} else if issue.State != nil {
		statusStr = issue.State.Name
	}
	result.WriteString(fmt.Sprintf("Status: %s\n", statusStr))
	
	if issue.Assignee != nil {
		result.WriteString(fmt.Sprintf("Assignee: %s\n", formatUserIdentifier(issue.Assignee)))
	}
	
	if issue.Team != nil {
		result.WriteString(fmt.Sprintf("Team: %s\n", formatTeamIdentifier(issue.Team)))
	}
	
	return result.String()
}

// formatTeam returns a consistently formatted full representation of a team
func formatTeam(team *linear.Team) string {
	if team == nil {
		return "Team: Unknown"
	}
	
	var result strings.Builder
	result.WriteString(fmt.Sprintf("Team: %s (UUID: %s)\n", team.Name, team.ID))
	result.WriteString(fmt.Sprintf("Key: %s\n", team.Key))
	
	return result.String()
}

// formatUser returns a consistently formatted full representation of a user
func formatUser(user *linear.User) string {
	if user == nil {
		return "User: Unknown"
	}
	
	var result strings.Builder
	result.WriteString(fmt.Sprintf("User: %s (UUID: %s)\n", user.Name, user.ID))
	
	if user.Email != "" {
		result.WriteString(fmt.Sprintf("Email: %s\n", user.Email))
	}
	
	return result.String()
}

// formatComment returns a consistently formatted full representation of a comment
func formatComment(comment *linear.Comment) string {
	if comment == nil {
		return "Comment: Unknown"
	}
	
	userName := "Unknown"
	if comment.User != nil {
		userName = comment.User.Name
	}
	
	var result strings.Builder
	result.WriteString(fmt.Sprintf("Comment by %s (UUID: %s)\n", userName, comment.ID))
	result.WriteString(fmt.Sprintf("Body: %s\n", comment.Body))
	
	if comment.CreatedAt != nil {
		result.WriteString(fmt.Sprintf("Created: %s\n", comment.CreatedAt.Format("2006-01-02 15:04:05")))
	}
	
	return result.String()
}

// Entity Identifier Rendering Functions

// formatIssueIdentifier returns a consistently formatted identifier for an issue
func formatIssueIdentifier(issue *linear.Issue) string {
	if issue == nil {
		return "Issue: Unknown"
	}
	return fmt.Sprintf("Issue: %s (UUID: %s)", issue.Identifier, issue.ID)
}

// formatTeamIdentifier returns a consistently formatted identifier for a team
func formatTeamIdentifier(team *linear.Team) string {
	if team == nil {
		return "Team: Unknown"
	}
	return fmt.Sprintf("Team: %s (UUID: %s)", team.Name, team.ID)
}

// formatUserIdentifier returns a consistently formatted identifier for a user
func formatUserIdentifier(user *linear.User) string {
	if user == nil {
		return "User: Unknown"
	}
	return fmt.Sprintf("User: %s (UUID: %s)", user.Name, user.ID)
}

// formatCommentIdentifier returns a consistently formatted identifier for a comment
func formatCommentIdentifier(comment *linear.Comment) string {
	if comment == nil {
		return "Comment: Unknown"
	}
	
	userName := "Unknown"
	if comment.User != nil {
		userName = comment.User.Name
	}
	
	return fmt.Sprintf("Comment by %s (UUID: %s)", userName, comment.ID)
}
```

### Updated common.go

```go
// resolveIssueIdentifier resolves an issue identifier (UUID or "TEAM-123") to a UUID
func resolveIssueIdentifier(linearClient *linear.LinearClient, identifier string) (string, error) {
	// If it's a valid UUID, use it directly
	if isValidUUID(identifier) {
		return identifier, nil
	}

	// Otherwise, try to find an issue by identifier
	issue, err := linearClient.GetIssueByIdentifier(identifier)
	if err != nil {
		return "", fmt.Errorf("failed to resolve issue identifier '%s': %v", identifier, err)
	}

	return issue.ID, nil
}

// resolveUserIdentifier resolves a user identifier (UUID, name, or email) to a UUID
func resolveUserIdentifier(linearClient *linear.LinearClient, identifier string) (string, error) {
	// If it's a valid UUID, use it directly
	if isValidUUID(identifier) {
		return identifier, nil
	}

	// Otherwise, try to find a user by name or email
	users, err := linearClient.GetUsers()
	if err != nil {
		return "", fmt.Errorf("failed to get users: %v", err)
	}

	// First try exact match on name or email
	for _, user := range users {
		if user.Name == identifier || user.Email == identifier {
			return user.ID, nil
		}
	}

	// If no exact match, try case-insensitive match
	identifierLower := strings.ToLower(identifier)
	for _, user := range users {
		if strings.ToLower(user.Name) == identifierLower || strings.ToLower(user.Email) == identifierLower {
			return user.ID, nil
		}
	}

	return "", fmt.Errorf("no user found with identifier '%s'", identifier)
}
```

## 2. Updated Tool Examples

### linear_create_issue

```go
// Before
var CreateIssueTool = mcp.NewTool("linear_create_issue",
	mcp.WithDescription("Creates a new Linear issue with specified details. Use this to create tickets for tasks, bugs, or feature requests. Returns the created issue's identifier and URL. Supports creating sub-issues and assigning labels."),
	// ... parameters ...
)

// After
var CreateIssueTool = mcp.NewTool("linear_create_issue",
	mcp.WithDescription("Creates a new Linear issue."),
	// ... parameters ...
)

// Before (result formatting)
resultText := fmt.Sprintf("Created issue: %s\nTitle: %s\nURL: %s", issue.Identifier, issue.Title, issue.URL)

// After (result formatting)
resultText := fmt.Sprintf("Created %s\nTitle: %s\nURL: %s", formatIssue(issue), issue.Title, issue.URL)
```

### linear_update_issue

```go
// Before
var UpdateIssueTool = mcp.NewTool("linear_update_issue",
	mcp.WithDescription("Updates an existing Linear issue's properties. Use this to modify issue details like title, description, priority, or status. Requires the issue ID and accepts any combination of updatable fields. Returns the updated issue's identifier and URL."),
	// ... parameters ...
)

// After
var UpdateIssueTool = mcp.NewTool("linear_update_issue",
	mcp.WithDescription("Updates an existing Linear issue."),
	// ... parameters ...
)

// Before (parameter handling)
id, ok := args["id"].(string)
if !ok || id == "" {
	return mcp.NewToolResultError("id must be a non-empty string"), nil
}

// After (parameter handling)
issueID, ok := args["id"].(string)
if !ok || issueID == "" {
	return mcp.NewToolResultError("id must be a non-empty string"), nil
}

// Resolve the issue identifier
id, err := resolveIssueIdentifier(linearClient, issueID)
if err != nil {
	return mcp.NewToolResultError(fmt.Sprintf("Failed to resolve issue: %v", err)), nil
}

// Before (result formatting)
resultText := fmt.Sprintf("Updated issue %s\nURL: %s", issue.Identifier, issue.URL)

// After (result formatting)
resultText := fmt.Sprintf("Updated %s\nURL: %s", formatIssue(issue), issue.URL)
```

### linear_get_issue

```go
// Before
var GetIssueTool = mcp.NewTool("linear_get_issue",
	mcp.WithDescription("Retrieves a single Linear issue by its ID. Returns detailed information about the issue including title, description, priority, status, assignee, team, full comment history (including nested comments), related issues, and all attachments (pull requests, design files, documents, etc.)."),
	// ... parameters ...
)

// After
var GetIssueTool = mcp.NewTool("linear_get_issue",
	mcp.WithDescription("Retrieves a single Linear issue."),
	// ... parameters ...
)

// Before (parameter handling)
issueID, ok := args["issueId"].(string)
if !ok || issueID == "" {
	return mcp.NewToolResultError("issueId must be a non-empty string"), nil
}

// After (parameter handling)
issueIdentifier, ok := args["issueId"].(string)
if !ok || issueIdentifier == "" {
	return mcp.NewToolResultError("issueId must be a non-empty string"), nil
}

// Resolve the issue identifier
issueID, err := resolveIssueIdentifier(linearClient, issueIdentifier)
if err != nil {
	return mcp.NewToolResultError(fmt.Sprintf("Failed to resolve issue: %v", err)), nil
}

// Before (result formatting)
resultText := fmt.Sprintf("Issue %s: %s\n", issue.Identifier, issue.Title)
// ... more formatting ...

// After (result formatting)
// Use the full formatIssue function for the main entity
resultText := formatIssue(issue)

// When referencing related entities, use the identifier formatting functions
if issue.Assignee != nil {
    resultText += fmt.Sprintf("Assignee: %s\n", formatUserIdentifier(issue.Assignee))
}

if issue.Team != nil {
    resultText += fmt.Sprintf("Team: %s\n", formatTeamIdentifier(issue.Team))
}

// For related issues, use the identifier formatting
if issue.Relations != nil && len(issue.Relations.Nodes) > 0 {
    resultText += "\nRelated Issues:\n"
    for _, relation := range issue.Relations.Nodes {
        if relation.RelatedIssue != nil {
            resultText += fmt.Sprintf("- %s\n  RelationType: %s\n", 
                formatIssueIdentifier(relation.RelatedIssue),
                relation.Type)
        }
    }
}
```

## 3. Testing Examples

### Test for resolveIssueIdentifier

```go
func TestResolveIssueIdentifier(t *testing.T) {
	// Create test client
	client, cleanup := linear.NewTestClient(t, "resolve_issue_identifier", true)
	defer cleanup()

	// Test cases
	tests := []struct {
		name       string
		identifier string
		wantErr    bool
	}{
		{
			name:       "Valid UUID",
			identifier: "1c2de93f-4321-4015-bfde-ee893ef7976f",
			wantErr:    false,
		},
		{
			name:       "Valid identifier",
			identifier: "TEST-10",
			wantErr:    false,
		},
		{
			name:       "Invalid identifier",
			identifier: "NONEXISTENT-123",
			wantErr:    true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := resolveIssueIdentifier(client, tt.identifier)
			if (err != nil) != tt.wantErr {
				t.Errorf("resolveIssueIdentifier() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !tt.wantErr && got == "" {
				t.Errorf("resolveIssueIdentifier() returned empty UUID")
			}
		})
	}
}
```

### Test for Formatting Functions

```go
func TestFormatIssue(t *testing.T) {
	tests := []struct {
		name  string
		issue *linear.Issue
		want  string
	}{
		{
			name: "Valid issue",
			issue: &linear.Issue{
				ID:         "1c2de93f-4321-4015-bfde-ee893ef7976f",
				Identifier: "TEST-10",
			},
			want: "Issue: TEST-10 (UUID: 1c2de93f-4321-4015-bfde-ee893ef7976f)",
		},
		{
			name:  "Nil issue",
			issue: nil,
			want:  "Issue: Unknown",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := formatIssue(tt.issue); got != tt.want {
				t.Errorf("formatIssue() = %v, want %v", got, tt.want)
			}
		})
	}
}
```

## 4. Implementation Strategy

1. Start with creating the shared utility functions in `rendering.go` and updating `common.go`
2. Implement the changes for one tool (e.g., `linear_create_issue`) as a reference
3. Review the reference implementation to ensure it meets all requirements
4. Apply the same patterns to all remaining tools
5. Update tests to verify the changes

## 5. Potential Challenges and Solutions

### Challenge: Backward Compatibility
Changing the output format of tools could break existing integrations that parse the output.

**Solution:** Consider versioning the API or providing a compatibility mode.

### Challenge: Test Fixtures
Updating the output format will require updating all test fixtures.

**Solution:** Use the `--golden` flag to update all golden files at once after implementing the changes.

### Challenge: Consistent Implementation
Ensuring consistency across all tools can be challenging.

**Solution:** Create a code review checklist to verify that each tool follows the same patterns.

```

--------------------------------------------------------------------------------
/docs/prd/003-tool-standardization-implementation.md:
--------------------------------------------------------------------------------

```markdown
# Tool Standardization Implementation Guide

This document provides a detailed implementation plan for standardizing the Linear MCP Server tools according to the requirements outlined in [002-tool-standardization.md](./002-tool-standardization.md).

## Implementation Approach

We'll implement the standardization in phases, focusing on one rule at a time across all tools to ensure consistency:

1. First, create the necessary shared utility functions
2. Then, update each tool one by one, applying all three rules
3. Finally, update tests to verify the changes

## Shared Utility Functions

### 1. Identifier Resolution Functions

Create or update the following functions in `pkg/tools/common.go`:

```go
// resolveIssueIdentifier resolves an issue identifier (UUID or "TEAM-123") to a UUID
// This is an extension of the existing resolveParentIssueIdentifier function
func resolveIssueIdentifier(linearClient *linear.LinearClient, identifier string) (string, error) {
    // Implementation similar to resolveParentIssueIdentifier
}

// resolveUserIdentifier resolves a user identifier (UUID, name, or email) to a UUID
func resolveUserIdentifier(linearClient *linear.LinearClient, identifier string) (string, error) {
    // Implementation
}
```

### 2. Entity Rendering Functions

Create a new file `pkg/tools/rendering.go` with two types of formatting functions:

#### Full Entity Rendering Functions

```go
// formatIssue returns a consistently formatted full representation of an issue
func formatIssue(issue *linear.Issue) string {
    if issue == nil {
        return "Issue: Unknown"
    }
    
    var result strings.Builder
    result.WriteString(fmt.Sprintf("Issue: %s (UUID: %s)\n", issue.Identifier, issue.ID))
    result.WriteString(fmt.Sprintf("Title: %s\n", issue.Title))
    result.WriteString(fmt.Sprintf("URL: %s\n", issue.URL))
    
    // Add other required fields
    
    return result.String()
}

// formatTeam returns a consistently formatted full representation of a team
func formatTeam(team *linear.Team) string {
    if team == nil {
        return "Team: Unknown"
    }
    
    var result strings.Builder
    result.WriteString(fmt.Sprintf("Team: %s (UUID: %s)\n", team.Name, team.ID))
    result.WriteString(fmt.Sprintf("Key: %s\n", team.Key))
    
    // Add other required fields
    
    return result.String()
}

// formatUser returns a consistently formatted full representation of a user
func formatUser(user *linear.User) string {
    if user == nil {
        return "User: Unknown"
    }
    
    var result strings.Builder
    result.WriteString(fmt.Sprintf("User: %s (UUID: %s)\n", user.Name, user.ID))
    
    // Add other required fields
    
    return result.String()
}

// formatComment returns a consistently formatted full representation of a comment
func formatComment(comment *linear.Comment) string {
    if comment == nil {
        return "Comment: Unknown"
    }
    
    userName := "Unknown"
    if comment.User != nil {
        userName = comment.User.Name
    }
    
    var result strings.Builder
    result.WriteString(fmt.Sprintf("Comment by %s (UUID: %s)\n", userName, comment.ID))
    result.WriteString(fmt.Sprintf("Body: %s\n", comment.Body))
    
    // Add other required fields
    
    return result.String()
}
```

#### Entity Identifier Rendering Functions

```go
// formatIssueIdentifier returns a consistently formatted identifier for an issue
func formatIssueIdentifier(issue *linear.Issue) string {
    if issue == nil {
        return "Issue: Unknown"
    }
    return fmt.Sprintf("Issue: %s (UUID: %s)", issue.Identifier, issue.ID)
}

// formatTeamIdentifier returns a consistently formatted identifier for a team
func formatTeamIdentifier(team *linear.Team) string {
    if team == nil {
        return "Team: Unknown"
    }
    return fmt.Sprintf("Team: %s (UUID: %s)", team.Name, team.ID)
}

// formatUserIdentifier returns a consistently formatted identifier for a user
func formatUserIdentifier(user *linear.User) string {
    if user == nil {
        return "User: Unknown"
    }
    return fmt.Sprintf("User: %s (UUID: %s)", user.Name, user.ID)
}

// formatCommentIdentifier returns a consistently formatted identifier for a comment
func formatCommentIdentifier(comment *linear.Comment) string {
    if comment == nil {
        return "Comment: Unknown"
    }
    
    userName := "Unknown"
    if comment.User != nil {
        userName = comment.User.Name
    }
    
    return fmt.Sprintf("Comment by %s (UUID: %s)", userName, comment.ID)
}
```

## Detailed Implementation Tasks

### Phase 1: Create Shared Utility Functions

1. Update `pkg/tools/common.go`:
   - Refactor `resolveParentIssueIdentifier` to `resolveIssueIdentifier`
   - Add `resolveUserIdentifier`
   - Ensure all resolution functions follow the same pattern

2. Create `pkg/tools/rendering.go`:
   - Add formatting functions for each entity type
   - Ensure consistent formatting across all entity types

### Phase 2: Update Tools

For each tool, perform the following tasks:

1. Update the tool description to be concise
2. Update parameter handling to use the appropriate resolution functions
3. Update result formatting to use the rendering functions
4. Ensure retrieval methods include all fields that can be set in create/update methods

#### Implementing Rule 4: Field Superset for Retrieval Methods

For each entity type, follow these steps:

1. **Identify Modifiable Fields**:
   - Review all create and update methods to identify fields that can be set or modified
   - Create a comprehensive list of these fields for each entity type

2. **Categorize Retrieval Methods**:
   - **Detail Retrieval Methods** (e.g., `linear_get_issue`): Must include all fields
   - **Overview Retrieval Methods** (e.g., `linear_search_issues`, `linear_get_user_issues`): Only need metadata fields

3. **Update Detail Retrieval Methods**:
   - Ensure they include all fields that can be set in create/update methods
   - Modify the formatting functions to include all required fields

4. **Update Overview Retrieval Methods**:
   - Ensure they include key metadata fields (ID, title, status, priority, etc.)
   - No need to include full content like descriptions or comments

5. **Entity-Specific Considerations**:
   - **Issues**: 
     - `linear_get_issue` must include all fields from `linear_create_issue` and `linear_update_issue`
     - `linear_search_issues` and `linear_get_user_issues` only need metadata fields
   - **Comments**: 
     - Comments returned in `linear_get_issue` must include all necessary fields from `linear_add_comment`
     - Overview methods don't need to display comments
   - **Teams**: 
     - `linear_get_teams` should include all team fields that can be referenced

#### Tool-Specific Tasks

| Tool | Description Update | Identifier Resolution Update | Rendering Update |
|------|-------------------|----------------------------|-----------------|
| linear_create_issue | Remove parameter listing and result format explanation | Already uses resolution functions | Use formatIssue for result |
| linear_update_issue | Remove parameter listing and result format explanation | Update to use resolveIssueIdentifier | Use formatIssue for result |
| linear_search_issues | Remove parameter listing and result format explanation | Update to use resolveTeamIdentifier for teamId | Use formatIssue for each issue in results |
| linear_get_user_issues | Remove parameter listing and result format explanation | Add resolveUserIdentifier for userId | Use formatIssue for each issue in results |
| linear_get_issue | Remove parameter listing and result format explanation | Update to use resolveIssueIdentifier | Use formatIssue, formatTeam, formatUser, and formatComment |
| linear_add_comment | Remove parameter listing and result format explanation | Update to use resolveIssueIdentifier | Use formatIssue and formatComment |
| linear_get_teams | Remove parameter listing and result format explanation | No changes needed | Use formatTeam for each team in results |

### Phase 3: Update Tests

1. Update test fixtures to reflect the new formatting
2. Add tests for the new resolution functions
3. Verify that all tests pass with the new implementation

## Detailed Tracking Table

| Tool | Task | Status | Notes |
|------|------|--------|-------|
| Shared | Create resolveIssueIdentifier | Not Started | Extend from resolveParentIssueIdentifier |
| Shared | Create resolveUserIdentifier | Not Started | New function |
| Shared | Create rendering.go with formatting functions | Not Started | New file |
| linear_create_issue | Update description | Not Started | |
| linear_create_issue | Update result formatting | Not Started | |
| linear_update_issue | Update description | Not Started | |
| linear_update_issue | Add issue identifier resolution | Not Started | |
| linear_update_issue | Update result formatting | Not Started | |
| linear_search_issues | Update description | Not Started | |
| linear_search_issues | Add team identifier resolution | Not Started | |
| linear_search_issues | Update result formatting | Not Started | |
| linear_get_user_issues | Update description | Not Started | |
| linear_get_user_issues | Add user identifier resolution | Not Started | |
| linear_get_user_issues | Update result formatting | Not Started | |
| linear_get_issue | Update description | Not Started | |
| linear_get_issue | Add issue identifier resolution | Not Started | |
| linear_get_issue | Update result formatting | Not Started | |
| linear_get_issue | Ensure all fields from create/update are included | Not Started | Rule 4 implementation |
| linear_get_issue | Ensure all comment fields are included | Not Started | Rule 4 implementation |
| linear_get_user_issues | Ensure all relevant issue fields are included | Not Started | Rule 4 implementation |
| linear_search_issues | Ensure all relevant issue fields are included | Not Started | Rule 4 implementation |
| linear_get_teams | Ensure all team fields are included | Not Started | Rule 4 implementation |
| linear_add_comment | Update description | Not Started | |
| linear_add_comment | Add issue identifier resolution | Not Started | |
| linear_add_comment | Update result formatting | Not Started | |
| linear_get_teams | Update description | Not Started | |
| linear_get_teams | Update result formatting | Not Started | |
| Tests | Update test fixtures | Not Started | |
| Tests | Add tests for new resolution functions | Not Started | |
| Tests | Add tests for field superset compliance | Not Started | Rule 4 testing |

## Example Implementation: linear_create_issue

Here's an example of how the `linear_create_issue` tool would be updated:

### Before

```go
var CreateIssueTool = mcp.NewTool("linear_create_issue",
    mcp.WithDescription("Creates a new Linear issue with specified details. Use this to create tickets for tasks, bugs, or feature requests. Returns the created issue's identifier and URL. Supports creating sub-issues and assigning labels."),
    // ... parameters ...
)

// CreateIssueHandler handles the linear_create_issue tool
func CreateIssueHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // ... implementation ...
    
    // Return the result
    resultText := fmt.Sprintf("Created issue: %s\nTitle: %s\nURL: %s", issue.Identifier, issue.Title, issue.URL)
    return mcp.NewToolResultText(resultText), nil
}
```

### After

```go
var CreateIssueTool = mcp.NewTool("linear_create_issue",
    mcp.WithDescription("Creates a new Linear issue."),
    // ... parameters ...
)

// CreateIssueHandler handles the linear_create_issue tool
func CreateIssueHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    // ... implementation ...
    
    // Return the result
    resultText := fmt.Sprintf("%s\nTitle: %s\nURL: %s", formatIssue(issue), issue.Title, issue.URL)
    return mcp.NewToolResultText(resultText), nil
}
```

## Timeline

| Phase | Estimated Duration | Dependencies |
|-------|-------------------|--------------|
| Phase 1: Create Shared Utility Functions | 1 day | None |
| Phase 2: Update Tools | 3 days | Phase 1 |
| Phase 3: Update Tests | 1 day | Phase 2 |

## Risks and Mitigations

| Risk | Impact | Mitigation |
|------|--------|------------|
| Breaking changes to API | High | Ensure backward compatibility or version the API |
| Test failures | Medium | Update test fixtures and add new tests |
| Inconsistent implementation | Medium | Review each tool implementation for consistency |

## Success Criteria

1. All tool descriptions are concise
2. All tools that reference Linear objects accept multiple identifier types
3. All tools render entities in a consistent format
4. Retrieval methods include all fields that can be set in create/update methods
5. All tests pass with the new implementation
6. Code review confirms consistency across all tools

```

--------------------------------------------------------------------------------
/docs/design/002-project-milestone-initiative.md:
--------------------------------------------------------------------------------

```markdown
# Design Doc: Project, Milestone, and Initiative Support

## 1. Overview

This document outlines the plan to extend the Linear MCP Server with tools to read and manipulate `Project`, `ProjectMilestone`, and `Initiative` entities. This will enhance the server's capabilities, allowing AI assistants to manage a broader range of Linear workflows.

## 2. Guiding Principles

*   **Consistency**: The new tools and code will follow the existing architecture and design patterns of the server.
*   **Modularity**: Each entity will be implemented in a modular way, with clear separation between models, client methods, and tool handlers.
*   **User-Friendliness**: Tools will accept user-friendly identifiers (names, slugs) in addition to UUIDs, similar to the existing `issue` and `team` parameters.
*   **Testability**: All new functionality will be covered by unit tests using the existing `go-vcr` framework.

## 3. Architecture Changes

The existing architecture is well-suited for extension. The primary changes will be:

*   **`pkg/linear/models.go`**: Add new structs for `Project`, `ProjectMilestone`, `Initiative`, and their related types (e.g., `ProjectConnection`, `ProjectCreateInput`).
*   **`pkg/linear/client.go`**: Add new methods to the `LinearClient` for interacting with the new entities.
*   **`pkg/tools/`**: Create new files for each entity's tools (e.g., `project_tools.go`, `milestone_tools.go`, `initiative_tools.go`).
*   **`pkg/server/server.go`**: Update `RegisterTools` to include the new tools.

No fundamental changes to the core server logic or command structure are anticipated.

## 4. Implementation Plan

This section details the sub-tasks for implementing each handler.

### 4.1. Project Entity

#### 4.1.1. `linear_get_project` Handler

-   [x] **Model**: Define `Project` and `ProjectConnection` structs in `pkg/linear/models.go`.
-   [x] **Client**: Implement `GetProject(identifier string)` in `pkg/linear/client.go`.
    -   [x] Add a resolver function to handle UUIDs, names, and slug IDs.
    -   [x] Implement the GraphQL query to fetch a single project.
-   [x] **Tool**: Create `GetProjectTool` in `pkg/tools/project_tools.go`.
    -   [x] Define the tool with the name `linear_get_project`.
    -   [x] Add a description: "Get a single project by its identifier (ID, name, or slug)."
    -   [x] Define a required `project` string parameter.
-   [x] **Handler**: Implement `GetProjectHandler` in `pkg/tools/project_tools.go`.
    -   [x] Extract the `project` identifier from the request.
    -   [x] Call `linearClient.GetProject()` with the identifier.
    -   [x] Format the returned `Project` object into a user-friendly string.
    -   [x] Handle errors for not found projects.
-   [x] **Server**: Register the tool in `pkg/server/server.go`.
-   [x] **Test**: Add test cases to `pkg/server/tools_test.go`.

#### 4.1.2. `linear_search_projects` Handler

-   [x] **Client**: Implement `SearchProjects(query string)` in `pkg/linear/client.go`.
    -   [x] Implement the GraphQL query for searching projects.
-   [x] **Tool**: Create `SearchProjectsTool` in `pkg/tools/project_tools.go`.
    -   [x] Define the tool with the name `linear_search_projects`.
    -   [x] Add a description: "Search for projects."
    -   [x] Define an optional `query` string parameter.
-   [x] **Handler**: Implement `SearchProjectsHandler` in `pkg/tools/project_tools.go`.
    -   [x] Extract the `query` from the request.
    -   [x] Call `linearClient.SearchProjects()`.
    -   [x] Format the list of `Project` objects.
-   [x] **Server**: Register the tool in `pkg/server/server.go`.
-   [x] **Test**: Add test cases to `pkg/server/tools_test.go`.

#### 4.1.3. `linear_create_project` Handler

-   [x] **Model**: Define `ProjectCreateInput` struct in `pkg/linear/models.go`.
-   [x] **Client**: Implement `CreateProject(input ProjectCreateInput)` in `pkg/linear/client.go`.
    -   [x] Implement the GraphQL mutation to create a project.
-   [x] **Tool**: Create `CreateProjectTool` in `pkg/tools/project_tools.go`.
    -   [x] Define the tool with the name `linear_create_project`.
    -   [x] Add a description: "Create a new project."
    -   [x] Define required parameters: `name`, `teamIds`.
    -   [x] Define optional parameters: `description`, `leadId`, `startDate`, `targetDate`, etc.
-   [x] **Handler**: Implement `CreateProjectHandler` in `pkg/tools/project_tools.go`.
    -   [x] Build the `ProjectCreateInput` from the request parameters.
    -   [x] Call `linearClient.CreateProject()`.
    -   [x] Format the newly created `Project` object.
-   [x] **Server**: Register the tool in `pkg/server/server.go` (respecting `writeAccess`).
-   [x] **Test**: Add test cases to `pkg/server/tools_test.go`.

#### 4.1.4. `linear_update_project` Handler

-   [x] **Model**: Define `ProjectUpdateInput` struct in `pkg/linear/models.go`.
-   [x] **Client**: Implement `UpdateProject(id string, input ProjectUpdateInput)` in `pkg/linear/client.go`.
    -   [x] Implement the GraphQL mutation to update a project.
-   [x] **Tool**: Create `UpdateProjectTool` in `pkg/tools/project_tools.go`.
    -   [x] Define the tool with the name `linear_update_project`.
    -   [x] Add a description: "Update an existing project."
    -   [x] Define a required `project` parameter.
    -   [x] Define optional parameters for updatable fields (`name`, `description`, etc.).
-   [x] **Handler**: Implement `UpdateProjectHandler` in `pkg/tools/project_tools.go`.
    -   [x] Resolve the `project` identifier to a UUID.
    -   [x] Build the `ProjectUpdateInput`.
    -   [x] Call `linearClient.UpdateProject()`.
    -   [x] Format the updated `Project` object.
-   [x] **Server**: Register the tool in `pkg/server/server.go` (respecting `writeAccess`).
-   [x] **Test**: Add test cases to `pkg/server/tools_test.go`.

### 4.2. ProjectMilestone Entity

#### 4.2.1. `linear_get_milestone` Handler

-   [x] **Model**: Define `ProjectMilestone` and `ProjectMilestoneConnection` structs in `pkg/linear/models.go`.
-   [x] **Client**: Implement `GetMilestone(id string)` in `pkg/linear/client.go`.
    -   [x] Implement the GraphQL query to fetch a single milestone.
-   [x] **Tool**: Create `GetMilestoneTool` in `pkg/tools/milestone_tools.go`.
    -   [x] Define the tool with the name `linear_get_milestone`.
    -   [x] Add a description: "Get a single project milestone by its ID."
    -   [x] Define a required `milestoneId` string parameter.
-   [x] **Handler**: Implement `GetMilestoneHandler` in `pkg/tools/milestone_tools.go`.
    -   [x] Call `linearClient.GetMilestone()`.
    -   [x] Format the returned `ProjectMilestone` object.
-   [x] **Server**: Register the tool in `pkg/server/server.go`.
-   [x] **Test**: Add test cases to `pkg/server/tools_test.go`.

#### 4.2.2. `linear_create_milestone` Handler

-   [x] **Model**: Define `ProjectMilestoneCreateInput` in `pkg/linear/models.go`.
-   [x] **Client**: Implement `CreateMilestone(input ProjectMilestoneCreateInput)` in `pkg/linear/client.go`.
    -   [x] Implement the GraphQL mutation.
-   [x] **Tool**: Create `CreateMilestoneTool` in `pkg/tools/milestone_tools.go`.
    -   [x] Define the tool with the name `linear_create_milestone`.
    -   [x] Add a description: "Create a new project milestone."
    -   [x] Define required parameters: `name`, `projectId`.
    -   [x] Define optional parameters: `description`, `targetDate`.
-   [x] **Handler**: Implement `CreateMilestoneHandler` in `pkg/tools/milestone_tools.go`.
    -   [x] Resolve `projectId`.
    -   [x] Build and call `linearClient.CreateMilestone()`.
    -   [x] Format the new `ProjectMilestone`.
-   [x] **Server**: Register the tool in `pkg/server/server.go` (respecting `writeAccess`).
-   [x] **Test**: Add test cases to `pkg/server/tools_test.go`.

### 4.3. Initiative Entity

#### 4.3.1. `linear_get_initiative` Handler

-   [x] **Model**: Define `Initiative` and `InitiativeConnection` structs in `pkg/linear/models.go`.
-   [x] **Client**: Implement `GetInitiative(identifier string)` in `pkg/linear/client.go`.
    -   [x] Add a resolver for UUID and name.
    -   [x] Implement the GraphQL query.
-   [x] **Tool**: Create `GetInitiativeTool` in `pkg/tools/initiative_tools.go`.
    -   [x] Define the tool with the name `linear_get_initiative`.
    -   [x] Add a description: "Get a single initiative by its identifier (ID or name)."
    -   [x] Define a required `initiative` string parameter.
-   [x] **Handler**: Implement `GetInitiativeHandler` in `pkg/tools/initiative_tools.go`.
    -   [x] Call `linearClient.GetInitiative()`.
    -   [x] Format the returned `Initiative` object.
-   [x] **Server**: Register the tool in `pkg/server/server.go`.
-   [x] **Test**: Add test cases to `pkg/server/tools_test.go`.

#### 4.3.2. `linear_create_initiative` Handler

-   [x] **Model**: Define `InitiativeCreateInput` in `pkg/linear/models.go`.
-   [x] **Client**: Implement `CreateInitiative(input InitiativeCreateInput)` in `pkg/linear/client.go`.
    -   [x] Implement the GraphQL mutation.
-   [x] **Tool**: Create `CreateInitiativeTool` in `pkg/tools/initiative_tools.go`.
    -   [x] Define the tool with the name `linear_create_initiative`.
    -   [x] Add a description: "Create a new initiative."
    -   [x] Define a required `name` parameter.
    -   [x] Define optional parameters like `description`.
-   [x] **Handler**: Implement `CreateInitiativeHandler` in `pkg/tools/initiative_tools.go`.
    -   [x] Build and call `linearClient.CreateInitiative()`.
    -   [x] Format the new `Initiative`.
-   [x] **Server**: Register the tool in `pkg/server/server.go` (respecting `writeAccess`).
-   [x] **Test**: Add test cases to `pkg/server/tools_test.go`.

### 4.4.1 Re-record tests

-   [x] Coordinate with user to prepare test data and re-record requests
-   [x] Iterate over each new test cases and validate that it works

### 4.4.2 Issues detected during testing

-   [x] get_project: filters by slugId ('e1153169a428'), but takes slug ('created-test-project-e1153169a428') as input. Should split string by '-' and used the last element
-   [x] search_projects: Cannot find multiple projects (prefix search only?)
-   [x] issues:
    -   [x] display milestone and project association
    -   [x] allow to update milestone and project association
-   [x] projects:
    -   [x] display initiative association
    -   [x] allow to update initiative association
-   [x] create_milestone: date parsing issue

## 5. Testing Strategy

Recording new test requests and data is the final step of this effort. We rely on the relevant test cases being added beforehand during each step.

*   **Unit Tests**: Tests are implemented in `pkg/server/tools_test.go`.
*   **Fixtures**: Use `go-vcr` to record real API interactions for each new client method. Fixtures will be stored in `testdata/fixtures/`.
*   **Golden Files**: Expected outputs for each test case will be stored in `testdata/golden/`.
*   **Coverage**: Ensure that both success and error paths are tested for each handler. This includes testing with invalid identifiers, missing parameters, and API errors.

## 6. Future Considerations

*   **Full CRUD**: This plan covers the most common operations. Full CRUD (including delete) can be added later if needed.
*   **Relationships**: Add tools for managing relationships between these entities (e.g., adding a project to an initiative).
*   **Resources**: Expose these new entities as MCP resources for easy reference in other tools.

## 7. Critique and Improvements

Based on a review of the initial implementation, several areas for improvement have been identified to enhance tool symmetry and consistency.

### 7.1. Enhance `linear_update_project`

-   [x] **Symmetry**: The `linear_update_project` tool should support the same set of optional parameters as `linear_create_project`.
-   [x] **Task**: Add `leadId`, `startDate`, `targetDate`, and `teamIds` as optional parameters to the `UpdateProjectTool` definition in `pkg/tools/project_tools.go`.
-   [x] **Task**: Update the `UpdateProjectHandler` to handle these new parameters.
-   [x] **Task**: Update the `linearClient.UpdateProject` method and `ProjectUpdateInput` struct to include these fields.
-   [ ] **Test**: Add test cases to verify that each field can be updated individually and in combination.

### 7.2. Add `linear_update_milestone`

-   [x] **Symmetry**: Add a `linear_update_milestone` tool to provide full CRUD operations for milestones.
-   [x] **Model**: Define `ProjectMilestoneUpdateInput` struct in `pkg/linear/models.go`.
-   [x] **Client**: Implement `UpdateMilestone(id string, input ProjectMilestoneUpdateInput)` in `pkg/linear/client.go`.
-   [x] **Tool**: Create `UpdateMilestoneTool` in `pkg/tools/milestone_tools.go` with a required `milestone` parameter and optional `name`, `description`, and `targetDate` parameters.
-   [x] **Handler**: Implement `UpdateMilestoneHandler` in `pkg/tools/milestone_tools.go`.
-   [x] **Server**: Register the new tool in `pkg/server/server.go`.
-   [ ] **Test**: Add test cases for the new tool.

### 7.3. Add `linear_update_initiative`

-   [x] **Symmetry**: Add a `linear_update_initiative` tool to provide full CRUD operations for initiatives.
-   [x] **Model**: Define `InitiativeUpdateInput` struct in `pkg/linear/models.go`.
-   [x] **Client**: Implement `UpdateInitiative(id string, input InitiativeUpdateInput)` in `pkg/linear/client.go`.
-   [x] **Tool**: Create `UpdateInitiativeTool` in `pkg/tools/initiative_tools.go` with a required `initiative` parameter and optional `name` and `description` parameters.
-   [x] **Handler**: Implement `UpdateInitiativeHandler` in `pkg/tools/initiative_tools.go`.
-   [x] **Server**: Register the new tool in `pkg/server/server.go`.
-   [ ] **Test**: Add test cases for the new tool.

### 7.4. Standardize Identifier Parameters

-   [x] **Consistency**: Ensure all `get` and `update` tools use a consistent and user-friendly identifier parameter.
-   [x] **Task**: In `pkg/tools/milestone_tools.go`, rename the `milestoneId` parameter of `GetMilestoneTool` to `milestone`.
-   [x] **Task**: Update the `GetMilestoneHandler` to use the `milestone` parameter.
-   [x] **Task**: Enhance `linearClient.GetMilestone` to resolve the milestone by name in addition to ID, similar to how `GetProject` works.
-   [ ] **Test**: Update existing tests for `linear_get_milestone` and add new tests for name-based resolution.

```

--------------------------------------------------------------------------------
/memory-bank/activeContext.md:
--------------------------------------------------------------------------------

```markdown
# Active Context: Linear MCP Server

## Current Work Focus
The current focus is on enhancing the functionality and user experience of the Linear MCP Server. This includes:
1. Improving the user experience by adding a setup command that simplifies the installation and configuration process
2. Enhancing the Linear API integration with support for more advanced features
3. Supporting multiple AI assistants (starting with Cline)
4. Ensuring cross-platform compatibility
5. Expanding the capabilities of existing MCP tools

## Recent Changes
1. Completed Tool Standardization Testing:
   - Updated test fixtures to reflect the new standardized format
   - Updated test cases to use the new parameter names (e.g., `issue` instead of `issueId`)
   - Verified that all tests pass with the new implementation
   - Updated tracking document to mark Phase 3 (Update Tests) as completed
   - Updated progress.md to reflect the completion of Tool Standardization testing

2. Implemented Tool Standardization:
   - Created shared utility functions for entity rendering and identifier resolution
   - Updated all tools to follow standardization rules:
     - Concise descriptions that focus only on functionality
     - Flexible identifier resolution for all entity references
     - Consistent entity rendering with both full and identifier formats
     - Consistent parameter naming that reflects the entity type (e.g., `issue` instead of `issueId`)
   - Created comprehensive documentation in a series of PRD files (000, 002, 003, 004, 005)
   - Updated tracking document to reflect implementation progress

2. Implemented CLI framework with subcommands:
   - Added the Cobra library for command-line handling
   - Restructured the main.go file to support subcommands
   - Created a root command that serves as the base for all subcommands
   - Moved the existing server functionality to a server subcommand

2. Created a setup command:
   - Implemented a setup command that automates the installation and configuration process
   - Added support for the Cline AI assistant
   - Implemented binary discovery and download functionality
   - Added configuration file management for AI assistants

3. Updated documentation:
   - Updated README.md with information about the new setup command
   - Added examples of how to use the setup command
   - Clarified the usage of the server command

4. Enhanced `linear_create_issue` tool:
   - Added support for creating sub-issues by specifying a parent issue ID or identifier (e.g., "TEAM-123")
   - Added support for assigning labels during issue creation using label IDs or names
   - Implemented resolution functions for parent issue identifiers and label names
   - Updated the Linear client to handle these new parameters
   - Added test cases and fixtures for the new functionality
   - Updated documentation to reflect the new capabilities

5. Fixed JSON unmarshaling issue with Labels field:
   - Updated the `Issue` struct in `models.go` to change the `Labels` field from `[]Label` to `*LabelConnection`
   - Added a new `LabelConnection` struct to match the structure returned by the Linear API
   - Updated test fixtures and golden files to reflect the changes
   - Added a new test case for creating issues with team key

6. Fixed label resolution issue:
   - Updated the GraphQL query in `GetLabelsByName` function to change the `$teamId` parameter type from `ID!` to `String!`
   - Re-recorded test fixtures for label-related tests
   - Updated golden files to reflect the new error messages
   - All tests now pass successfully

7. Fixed parent issue identifier resolution:
   - Updated the `GetIssueByIdentifier` function to split the identifier (e.g., "TEAM-123") into team key and number parts
   - Modified the GraphQL query to use the team key and number in the filter instead of the full identifier
   - Added proper error handling for invalid identifier formats
   - Added a new test case for creating sub-issues using human-readable identifiers
   - All tests now pass successfully

8. Enhanced Claude Code Support in Setup Command:
   - **Feature 1**: Register to all existing projects when no --project-path is specified
     - Modified `setupClaudeCode` function to read existing projects from `.claude.json`
     - Added `getAllExistingProjects` helper function to extract project paths
     - Implemented logic to register Linear MCP server to all existing projects automatically
     - Added proper error handling when no existing projects are found
   - **Feature 2**: Support multiple project paths separated by commas
     - Added comma-separated project path parsing with whitespace trimming
     - Modified registration logic to handle multiple target projects
     - Added validation for empty project path lists
   - **Implementation Details**:
     - Created `registerLinearToProject` helper function for reusable project registration logic
     - Preserved all existing configuration settings and project structures
     - Added comprehensive logging to show which projects are being registered
     - Updated flag help text to document the new behavior
   - **Testing**:
     - Added 5 new comprehensive test cases covering all scenarios:
       - Register to all existing projects (empty project path)
       - Multiple comma-separated project paths with whitespace handling
       - Mixed existing and new projects
       - Error handling for empty project lists
       - Error handling when no existing projects found
     - All tests pass successfully
     - Manual testing confirmed functionality works as expected

9. **UPDATED**: Implemented User-Scoped MCP Server Registration for Claude Code:
   - **Problem Identified**: The previous implementation tried to register to all existing projects when no project path was specified, which was complex and didn't match Claude Code's intended behavior
   - **Solution Implemented**: Use user-scoped `mcpServers` registration instead of project-scoped when no project path is specified
   - **Key Changes**:
     - **Updated `setupClaudeCode` function**: Now registers to user-scoped `mcpServers` (root level) when `projectPath` is empty
     - **Added `registerLinearToUserScope` function**: Handles registration to the root-level `mcpServers` object that applies to all projects
     - **Removed obsolete `getAllExistingProjects` function**: No longer needed since we use user-scoped registration
     - **Updated flag help text**: Changed from "register to all existing projects" to "register to user scope for all projects"
   - **Benefits of User-Scoped Approach**:
     - **Simpler logic**: No need to iterate through existing projects
     - **Better user experience**: Works even when no projects exist yet
     - **Future-proof**: Automatically applies to new projects created later
     - **Matches Claude Code design**: Uses the intended global registration mechanism
   - **JSON Structure Changes**:
     - **When `projectPath` is empty**: Registers to root-level `mcpServers` (user-scoped)
     - **When `projectPath` is specified**: Continues using project-scoped registration in `projects[path].mcpServers`
   - **Updated Test Cases**:
     - **Modified existing test**: "Claude Code Register to All Existing Projects" → "Claude Code Register to User Scope with Existing Projects"
     - **Updated error test**: "Claude Code No Existing Projects and No Project Path" now expects success with user-scoped registration
     - **Added new test cases**:
       - User-scoped registration with existing user-scoped servers
       - User-scoped update of existing Linear server
       - Comprehensive coverage of both user-scoped and project-scoped scenarios
   - **Implementation Status**: Complete and ready for testing

## Next Steps
1. **Testing the Setup Command**:
   - Test the setup command on different platforms (Linux, macOS, Windows)
   - Verify that the configuration files are correctly created
   - Ensure that the binary download works correctly

2. **Adding Support for More AI Assistants**:
   - Research other AI assistants that could benefit from Linear integration
   - Implement support for these assistants in the setup command
   - Update documentation with information about the new assistants

3. **Future Enhancements**:
   - Add more Linear API features as MCP tools
   - Improve error handling and reporting
   - Add configuration file support for the server

## Active Decisions and Considerations

### Tool Standardization Approach
- **Decision**: Implement standardization in phases, focusing on shared utility functions first
  - **Rationale**: Creating shared functions first ensures consistency across all tools
  - **Alternatives Considered**: Updating each tool individually, creating a new set of tools
  - **Implications**: More maintainable codebase with consistent patterns

### Tool Description Style
- **Decision**: Make tool descriptions concise and focused on functionality
  - **Rationale**: Concise descriptions are easier to read and understand
  - **Alternatives Considered**: Keeping verbose descriptions, creating separate documentation
  - **Implications**: Improved user experience with clearer tool descriptions

### Parameter Naming Convention
- **Decision**: Use entity names for parameters that accept identifiers (e.g., `issue` instead of `issueId`)
  - **Rationale**: Parameter names should reflect what they represent rather than implementation details
  - **Alternatives Considered**: Keeping technical names like `issueId`, using different naming patterns
  - **Implications**: More intuitive API that aligns with the flexible identifier resolution approach

### Identifier Resolution Strategy
- **Decision**: Extend existing resolution functions and create new ones as needed
  - **Rationale**: Builds on existing functionality while ensuring consistency
  - **Alternatives Considered**: Creating entirely new resolution system, handling resolution in each tool
  - **Implications**: More flexible parameter handling with consistent behavior

### Entity Rendering Approach
- **Decision**: Create two types of formatting functions for each entity type
  - **Rationale**: Distinguishes between full entity rendering and entity identifier rendering
  - **Alternatives Considered**: Single formatting function, custom formatting in each tool, using templates
  - **Implications**: Consistent user experience with standardized output format that is appropriate for the context

### Full Entity vs. Identifier Rendering
- **Decision**: Use full entity rendering for primary entities and identifier rendering for referenced entities
  - **Rationale**: Provides comprehensive information for primary entities while keeping references concise
  - **Alternatives Considered**: Using only full rendering or only identifier rendering for all cases
  - **Implications**: Better readability and more intuitive output format

### CLI Framework Selection
- **Decision**: Use the Cobra library for command-line handling
  - **Rationale**: Cobra is a widely used library for Go CLI applications with good documentation and community support
  - **Alternatives Considered**: urfave/cli, flag package
  - **Implications**: Provides a consistent way to handle subcommands and flags

### Setup Command Design
- **Decision**: Implement a setup command that automates the installation and configuration process
  - **Rationale**: Simplifies the user experience by automating manual steps
  - **Alternatives Considered**: Keeping the bash script, creating a separate tool
  - **Implications**: Users can easily set up the server for use with AI assistants

### AI Assistant Support
- **Decision**: Start with Cline support and design for extensibility
  - **Rationale**: Cline is the primary target, but the design should allow for adding more assistants
  - **Alternatives Considered**: Supporting only Cline, supporting multiple assistants from the start
  - **Implications**: The code is structured to easily add support for more assistants in the future

### Binary Management
- **Decision**: Check for existing binary before downloading
  - **Rationale**: Avoids unnecessary downloads if the binary is already installed
  - **Alternatives Considered**: Always downloading the latest version
  - **Implications**: Faster setup process for users who already have the binary

### Configuration File Management
- **Decision**: Merge new settings with existing settings
  - **Rationale**: Preserves user's existing configuration while adding the Linear MCP server
  - **Alternatives Considered**: Overwriting the entire file
  - **Implications**: Users can have multiple MCP servers configured

### Linear Issue Creation Enhancement
- **Decision**: Enhance the `linear_create_issue` tool with support for user-friendly identifiers
  - **Rationale**: Provides more flexibility and better user experience when creating issues
  - **Alternatives Considered**: Requiring UUIDs only, creating separate tools for different identifier types
  - **Implications**: Users can create issues with more intuitive identifiers without needing to look up UUIDs

### Identifier Resolution Implementation
- **Decision**: Implement separate resolution functions for parent issues and labels
  - **Rationale**: Keeps the code modular and easier to maintain
  - **Alternatives Considered**: Implementing a generic resolution function, handling resolution in the handler directly
  - **Implications**: Code is more maintainable and easier to extend for future enhancements

### JSON Structure Handling
- **Decision**: Update the `Issue` struct to match the nested structure returned by the Linear API
  - **Rationale**: Ensures proper JSON unmarshaling of API responses
  - **Alternatives Considered**: Custom unmarshaling logic, flattening the structure in the client
  - **Implications**: More robust handling of API responses and fewer unmarshaling errors

### GraphQL Parameter Type Correction
- **Decision**: Update the GraphQL query parameter types to match the Linear API expectations
  - **Rationale**: Ensures proper validation of GraphQL queries by the Linear API
  - **Alternatives Considered**: Custom error handling for API validation errors
  - **Implications**: More reliable API requests and fewer validation errors

### Parent Issue Identifier Resolution
- **Decision**: Split the identifier into team key and number parts for the GraphQL query
  - **Rationale**: The Linear API doesn't support searching for issues by the full identifier directly
  - **Alternatives Considered**: Using a different API endpoint, implementing a custom search function
  - **Implications**: More reliable resolution of human-readable identifiers to UUIDs

## Open Questions
1. Should we add support for more AI assistants in the setup command?
2. Do we need to add any additional validation steps for the API key?
3. Should we implement automatic updates for the binary?
4. How can we improve the error handling for network and file system operations?

```

--------------------------------------------------------------------------------
/cmd/setup.go:
--------------------------------------------------------------------------------

```go
package cmd

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/geropl/linear-mcp-go/pkg/server"
	"github.com/spf13/cobra"
)

// setupCmd represents the setup command
var setupCmd = &cobra.Command{
	Use:   "setup",
	Short: "Set up the Linear MCP server for use with an AI assistant",
	Long: `Set up the Linear MCP server for use with an AI assistant.
This command installs the Linear MCP server and configures it for use with the specified AI assistant tool(s).
Currently supported tools: cline, roo-code, claude-code, ona`,
	Run: func(cmd *cobra.Command, args []string) {
		toolParam, _ := cmd.Flags().GetString("tool")
		writeAccess, _ := cmd.Flags().GetBool("write-access")
		writeAccessChanged := cmd.Flags().Changed("write-access")
		autoApprove, _ := cmd.Flags().GetString("auto-approve")
		projectPath, _ := cmd.Flags().GetString("project-path")

		// Check if the Linear API key is provided in the environment (for tools that need it)
		apiKey := os.Getenv("LINEAR_API_KEY")
		tools := strings.Split(toolParam, ",")
		for _, t := range tools {
			doesNotNeedApiKey := strings.TrimSpace(t) == "ona"
			if doesNotNeedApiKey {
				continue
			}

			if apiKey == "" {
				fmt.Println("Error: LINEAR_API_KEY environment variable is required")
				fmt.Println("Please set it before running the setup command:")
				fmt.Println("export LINEAR_API_KEY=your_linear_api_key")
				os.Exit(1)
			}
		}

		// Create the MCP servers directory if it doesn't exist
		homeDir, err := os.UserHomeDir()
		if err != nil {
			fmt.Printf("Error getting user home directory: %v\n", err)
			os.Exit(1)
		}

		mcpServersDir := filepath.Join(homeDir, "mcp-servers")
		if err := os.MkdirAll(mcpServersDir, 0755); err != nil {
			fmt.Printf("Error creating MCP servers directory: %v\n", err)
			os.Exit(1)
		}

		// Check if the Linear MCP binary is already on the path
		binaryPath, found := checkBinary()
		if !found {
			fmt.Printf("Linear MCP binary not found on path, copying current binary to '%s'...\n", binaryPath)
			err := copySelfToBinaryPath(binaryPath)
			if err != nil {
				fmt.Printf("Error copying Linear MCP binary: %v\n", err)
				os.Exit(1)
			}
		}

		// Process each tool
		hasErrors := false
		for _, t := range tools {
			t = strings.TrimSpace(t)
			if t == "" {
				continue
			}

			fmt.Printf("Setting up tool: %s\n", t)

			// Set up the tool-specific configuration
			var err error
			switch strings.ToLower(t) {
			case "cline":
				err = setupCline(binaryPath, apiKey, writeAccess, autoApprove)
			case "roo-code":
				err = setupRooCode(binaryPath, apiKey, writeAccess, autoApprove)
			case "claude-code":
				err = setupClaudeCode(binaryPath, apiKey, writeAccess, autoApprove, projectPath)
			case "ona":
				err = setupOna(binaryPath, apiKey, writeAccess, writeAccessChanged, autoApprove, projectPath)
			default:
				fmt.Printf("Unsupported tool: %s\n", t)
				fmt.Println("Currently supported tools: cline, roo-code, claude-code, ona")
				hasErrors = true
				continue
			}

			if err != nil {
				fmt.Printf("Error setting up %s: %v\n", t, err)
				hasErrors = true
			} else {
				fmt.Printf("Linear MCP server successfully set up for %s\n", t)
			}
		}

		if hasErrors {
			os.Exit(1)
		}
	},
}

func init() {
	rootCmd.AddCommand(setupCmd)

	// Add flags to the setup command
	setupCmd.Flags().String("tool", "cline", "The AI assistant tool(s) to set up for (comma-separated, e.g., cline,roo-code,claude-code,ona)")
	setupCmd.Flags().Bool("write-access", false, "Enable write operations (default: false)")
	setupCmd.Flags().String("auto-approve", "", "Comma-separated list of tool names to auto-approve, or 'allow-read-only' to auto-approve all read-only tools")
	setupCmd.Flags().String("project-path", "", "The project path(s) for claude-code project-scoped configuration (comma-separated for multiple projects, or empty to register to user scope for all projects)")
}

// checkBinary checks if the Linear MCP binary is already on the path
func checkBinary() (string, bool) {
	// Try to find the binary on the path
	path, err := exec.LookPath("linear-mcp-go")
	if err == nil {
		fmt.Printf("Found Linear MCP binary at %s\n", path)
		return path, true
	}

	// Check if the binary exists in the home directory
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return "", false
	}

	binaryPath := filepath.Join(homeDir, "mcp-servers", "linear-mcp-go")
	if runtime.GOOS == "windows" {
		binaryPath += ".exe"
	}

	if _, err := os.Stat(binaryPath); err == nil {
		fmt.Printf("Found Linear MCP binary at %s\n", binaryPath)
		return binaryPath, true
	}

	return binaryPath, false
}

// copySelfToBinaryPath copies the current executable to the specified path
func copySelfToBinaryPath(binaryPath string) error {
	// Get the path to the current executable
	execPath, err := os.Executable()
	if err != nil {
		return fmt.Errorf("failed to get executable path: %w", err)
	}

	// Check if the destination is the same as the source
	absExecPath, _ := filepath.Abs(execPath)
	absDestPath, _ := filepath.Abs(binaryPath)
	if absExecPath == absDestPath {
		return nil // Already in the right place
	}

	// Copy the file
	sourceFile, err := os.Open(execPath)
	if err != nil {
		return fmt.Errorf("failed to open source file: %w", err)
	}
	defer sourceFile.Close()

	err = os.MkdirAll(filepath.Dir(binaryPath), 0755)
	if err != nil {
		return fmt.Errorf("failed to create destination directory: %w", err)
	}

	destFile, err := os.Create(binaryPath)
	if err != nil {
		return fmt.Errorf("failed to create destination file: %w", err)
	}
	defer destFile.Close()

	if _, err := io.Copy(destFile, sourceFile); err != nil {
		return fmt.Errorf("failed to copy file: %w", err)
	}

	// Make the binary executable
	if runtime.GOOS != "windows" {
		if err := os.Chmod(binaryPath, 0755); err != nil {
			return fmt.Errorf("failed to make binary executable: %w", err)
		}
	}

	fmt.Printf("Linear MCP server installed successfully at %s\n", binaryPath)
	return nil
}

// getOnaConfigPath determines the configuration file path for Ona
func getOnaConfigPath(projectPath string) (string, error) {
	cwd, err := os.Getwd()
	if err != nil {
		return "", fmt.Errorf("failed to get current working directory: %w", err)
	}

	// Default to current working directory
	baseDir := cwd

	// If project path is specified, use the first one
	if projectPath != "" {
		paths := strings.Split(projectPath, ",")
		trimmedPath := strings.TrimSpace(paths[0])
		if trimmedPath != "" {
			baseDir = trimmedPath
			// If absolute path doesn't exist, treat as relative to current directory
			if filepath.IsAbs(trimmedPath) {
				if _, err := os.Stat(trimmedPath); os.IsNotExist(err) {
					relativePath := strings.TrimPrefix(trimmedPath, "/")
					baseDir = filepath.Join(cwd, relativePath)
				}
			}
		}
	}

	return filepath.Join(baseDir, ".ona", "mcp-config.json"), nil
}

// extractTrailingWhitespace extracts trailing whitespace (newlines, spaces, tabs) from a string
func extractTrailingWhitespace(content string) string {
	// Find the last non-whitespace character
	i := len(content) - 1
	for i >= 0 && (content[i] == ' ' || content[i] == '\t' || content[i] == '\n' || content[i] == '\r') {
		i--
	}
	// Return everything after the last non-whitespace character
	if i < len(content)-1 {
		return content[i+1:]
	}
	return ""
}

// setupOna sets up the Linear MCP server for Ona
func setupOna(binaryPath, apiKey string, writeAccess bool, writeAccessChanged bool, autoApprove, projectPath string) error {
	configPath, err := getOnaConfigPath(projectPath)
	if err != nil {
		return err
	}

	// Create the .ona directory if it doesn't exist
	if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
		return fmt.Errorf("failed to create .ona directory: %w", err)
	}

	// Prepare server arguments
	serverArgs := []string{"serve"}
	// Only add write-access argument if it was explicitly set
	if writeAccessChanged {
		serverArgs = append(serverArgs, fmt.Sprintf("--write-access=%t", writeAccess))
	}

	// Create the linear server configuration
	linearServerConfig := map[string]interface{}{
		"command":  binaryPath,
		"args":     serverArgs,
		"disabled": false,
	}

	// Ona will automatically provide LINEAR_API_KEY from environment
	// No need to explicitly set it in the configuration

	// Read existing configuration or create new one
	var config map[string]interface{}
	var originalTrailingWhitespace string
	data, err := os.ReadFile(configPath)
	if err != nil {
		if !os.IsNotExist(err) {
			return fmt.Errorf("failed to read existing ona config: %w", err)
		}
		// Initialize with empty structure if file doesn't exist
		config = map[string]interface{}{
			"mcpServers": map[string]interface{}{},
		}
	} else {
		// Preserve trailing whitespace from original file
		originalContent := string(data)
		originalTrailingWhitespace = extractTrailingWhitespace(originalContent)
		
		if err := json.Unmarshal(data, &config); err != nil {
			return fmt.Errorf("failed to parse existing ona config: %w", err)
		}
		// Ensure mcpServers field exists
		if config["mcpServers"] == nil {
			config["mcpServers"] = map[string]interface{}{}
		}
	}

	// Get or create mcpServers map
	mcpServers, ok := config["mcpServers"].(map[string]interface{})
	if !ok {
		mcpServers = map[string]interface{}{}
		config["mcpServers"] = mcpServers
	}

	// Add/update the linear server configuration
	mcpServers["linear"] = linearServerConfig

	// Write the updated configuration
	updatedData, err := json.MarshalIndent(config, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal ona config: %w", err)
	}

	// Append the original trailing whitespace to preserve formatting
	finalData := append(updatedData, []byte(originalTrailingWhitespace)...)

	if err := os.WriteFile(configPath, finalData, 0644); err != nil {
		return fmt.Errorf("failed to write ona config: %w", err)
	}

	fmt.Printf("Ona MCP configuration updated at %s\n", configPath)
	return nil
}

// setupTool sets up the Linear MCP server for a specific tool
func setupTool(toolName string, binaryPath, apiKey string, writeAccess bool, autoApprove string, configDir string) error {
	// Create the config directory if it doesn't exist
	if err := os.MkdirAll(configDir, 0755); err != nil {
		return fmt.Errorf("failed to create config directory: %w", err)
	}

	serverArgs := []string{"serve"}
	if writeAccess {
		serverArgs = append(serverArgs, "--write-access=true")
	}

	// Process auto-approve flag
	autoApproveTools := []string{}
	if autoApprove != "" {
		if autoApprove == "allow-read-only" {
			// Get the list of read-only tools
			for k := range server.GetReadOnlyToolNames() {
				autoApproveTools = append(autoApproveTools, k)
			}
		} else {
			// Split comma-separated list
			for _, tool := range strings.Split(autoApprove, ",") {
				trimmedTool := strings.TrimSpace(tool)
				if trimmedTool != "" {
					autoApproveTools = append(autoApproveTools, trimmedTool)
				}
			}
		}
	}

	// Create the MCP settings file
	settingsPath := filepath.Join(configDir, "cline_mcp_settings.json")
	newSettings := map[string]interface{}{
		"mcpServers": map[string]interface{}{
			"linear": map[string]interface{}{
				"command":     binaryPath,
				"args":        serverArgs,
				"env":         map[string]string{"LINEAR_API_KEY": apiKey},
				"disabled":    false,
				"autoApprove": autoApproveTools,
			},
		},
	}

	// Check if the settings file already exists
	var settings map[string]interface{}
	if _, err := os.Stat(settingsPath); err == nil {
		// Read the existing settings
		data, err := os.ReadFile(settingsPath)
		if err != nil {
			return fmt.Errorf("failed to read existing settings: %w", err)
		}

		// Parse the existing settings
		if err := json.Unmarshal(data, &settings); err != nil {
			return fmt.Errorf("failed to parse existing settings: %w", err)
		}

		// Merge the new settings with the existing settings
		if mcpServers, ok := settings["mcpServers"].(map[string]interface{}); ok {
			mcpServers["linear"] = newSettings["mcpServers"].(map[string]interface{})["linear"]
		} else {
			settings["mcpServers"] = newSettings["mcpServers"]
		}
	} else {
		// Use the new settings
		settings = newSettings
	}

	// Write the settings to the file
	data, err := json.MarshalIndent(settings, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal settings: %w", err)
	}

	if err := os.WriteFile(settingsPath, data, 0644); err != nil {
		return fmt.Errorf("failed to write settings: %w", err)
	}

	fmt.Printf("%s MCP settings updated at %s\n", toolName, settingsPath)
	return nil
}

// setupCline sets up the Linear MCP server for Cline
func setupCline(binaryPath, apiKey string, writeAccess bool, autoApprove string) error {
	// Determine the Cline config directory
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return fmt.Errorf("failed to get user home directory: %w", err)
	}

	var configDir string
	switch runtime.GOOS {
	case "darwin":
		configDir = filepath.Join(homeDir, "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings")
	case "linux":
		configDir = filepath.Join(homeDir, ".vscode-server", "data", "User", "globalStorage", "saoudrizwan.claude-dev", "settings")
	case "windows":
		configDir = filepath.Join(homeDir, "AppData", "Roaming", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings")
	default:
		return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
	}

	return setupTool("Cline", binaryPath, apiKey, writeAccess, autoApprove, configDir)
}

// setupRooCode sets up the Linear MCP server for Roo Code
func setupRooCode(binaryPath, apiKey string, writeAccess bool, autoApprove string) error {
	// Determine the Roo Code config directory
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return fmt.Errorf("failed to get user home directory: %w", err)
	}

	var configDir string
	switch runtime.GOOS {
	case "darwin":
		configDir = filepath.Join(homeDir, "Library", "Application Support", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings")
	case "linux":
		configDir = filepath.Join(homeDir, ".vscode-server", "data", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings")
	case "windows":
		configDir = filepath.Join(homeDir, "AppData", "Roaming", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings")
	default:
		return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
	}

	return setupTool("Roo Code", binaryPath, apiKey, writeAccess, autoApprove, configDir)
}

// setupClaudeCode sets up the Linear MCP server for Claude Code
func setupClaudeCode(binaryPath, apiKey string, writeAccess bool, autoApprove, projectPath string) error {
	if runtime.GOOS != "linux" {
		return fmt.Errorf("claude-code is only supported on Linux")
	}

	homeDir, err := os.UserHomeDir()
	if err != nil {
		return fmt.Errorf("failed to get user home directory: %w", err)
	}

	configPath := filepath.Join(homeDir, ".claude.json")
	if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
		return fmt.Errorf("failed to create directory '%s': %w", filepath.Dir(configPath), err)
	}

	// Use flexible map structure to preserve all existing settings
	var settings map[string]interface{}
	data, err := os.ReadFile(configPath)
	if err != nil {
		if !os.IsNotExist(err) {
			return fmt.Errorf("failed to read claude code settings: %w", err)
		}
		// Initialize with empty structure if file doesn't exist
		settings = map[string]interface{}{
			"projects": map[string]interface{}{},
		}
	} else {
		if err := json.Unmarshal(data, &settings); err != nil {
			return fmt.Errorf("failed to parse claude code settings: %w", err)
		}
		// Ensure projects field exists
		if settings["projects"] == nil {
			settings["projects"] = map[string]interface{}{}
		}
	}

	serverArgs := []string{"serve"}
	if writeAccess {
		serverArgs = append(serverArgs, "--write-access=true")
	}

	autoApproveTools := []string{}
	if autoApprove != "" {
		if autoApprove == "allow-read-only" {
			for k := range server.GetReadOnlyToolNames() {
				autoApproveTools = append(autoApproveTools, k)
			}
		} else {
			for _, tool := range strings.Split(autoApprove, ",") {
				trimmedTool := strings.TrimSpace(tool)
				if trimmedTool != "" {
					autoApproveTools = append(autoApproveTools, trimmedTool)
				}
			}
		}
	}

	linearServerConfig := map[string]interface{}{
		"type":        "stdio",
		"command":     binaryPath,
		"args":        serverArgs,
		"env":         map[string]string{"LINEAR_API_KEY": apiKey},
		"disabled":    false,
		"autoApprove": autoApproveTools,
	}

	if projectPath == "" {
		// Register to user-scoped mcpServers (applies to all projects)
		if err := registerLinearToUserScope(settings, linearServerConfig); err != nil {
			return fmt.Errorf("failed to register Linear MCP server to user scope: %w", err)
		}
		fmt.Printf("Registered Linear MCP server to user scope (applies to all projects)\n")
	} else {
		// Parse comma-separated project paths and register to specific projects
		var targetProjects []string
		for _, path := range strings.Split(projectPath, ",") {
			trimmedPath := strings.TrimSpace(path)
			if trimmedPath != "" {
				targetProjects = append(targetProjects, trimmedPath)
			}
		}
		if len(targetProjects) == 0 {
			return fmt.Errorf("no valid project paths provided")
		}

		fmt.Printf("Registering Linear MCP server to %d specified projects\n", len(targetProjects))
		for _, projPath := range targetProjects {
			if err := registerLinearToProject(settings, projPath, linearServerConfig); err != nil {
				return fmt.Errorf("failed to register Linear MCP server to project '%s': %w", projPath, err)
			}
			fmt.Printf("  - Registered to project: %s\n", projPath)
		}
	}

	updatedData, err := json.MarshalIndent(settings, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal claude code settings: %w", err)
	}

	if err := os.WriteFile(configPath, updatedData, 0644); err != nil {
		return fmt.Errorf("failed to write claude code settings: %w", err)
	}

	fmt.Printf("Claude Code MCP settings updated at %s\n", configPath)
	return nil
}

// registerLinearToUserScope registers the Linear MCP server to user-scoped mcpServers (applies to all projects)
func registerLinearToUserScope(settings map[string]interface{}, linearServerConfig map[string]interface{}) error {
	// Get or create user-scoped mcpServers
	var mcpServers map[string]interface{}
	if existingMcpServers, exists := settings["mcpServers"]; exists {
		if mcpServersMap, ok := existingMcpServers.(map[string]interface{}); ok {
			mcpServers = mcpServersMap
		} else {
			// If existing mcpServers is not a map, create a new one
			mcpServers = map[string]interface{}{}
		}
	} else {
		mcpServers = map[string]interface{}{}
	}

	// Add/update the linear server configuration
	mcpServers["linear"] = linearServerConfig
	settings["mcpServers"] = mcpServers

	return nil
}

// registerLinearToProject registers the Linear MCP server to a specific project
func registerLinearToProject(settings map[string]interface{}, projectPath string, linearServerConfig map[string]interface{}) error {
	// Get projects map
	projects, ok := settings["projects"].(map[string]interface{})
	if !ok {
		projects = map[string]interface{}{}
		settings["projects"] = projects
	}

	// Get or create the specific project
	var project map[string]interface{}
	if existingProject, exists := projects[projectPath]; exists {
		if projectMap, ok := existingProject.(map[string]interface{}); ok {
			project = projectMap
		} else {
			// If existing project is not a map, create a new one
			project = map[string]interface{}{}
		}
	} else {
		project = map[string]interface{}{}
	}

	// Get or create mcpServers for this project
	var mcpServers map[string]interface{}
	if existingMcpServers, exists := project["mcpServers"]; exists {
		if mcpServersMap, ok := existingMcpServers.(map[string]interface{}); ok {
			mcpServers = mcpServersMap
		} else {
			// If existing mcpServers is not a map, create a new one
			mcpServers = map[string]interface{}{}
		}
	} else {
		mcpServers = map[string]interface{}{}
	}

	// Add/update the linear server configuration
	mcpServers["linear"] = linearServerConfig
	project["mcpServers"] = mcpServers
	projects[projectPath] = project

	return nil
}

```

--------------------------------------------------------------------------------
/pkg/server/tools_test.go:
--------------------------------------------------------------------------------

```go
package server

import (
	"context"
	"path/filepath"
	"testing"

	"github.com/geropl/linear-mcp-go/pkg/linear"
	"github.com/geropl/linear-mcp-go/pkg/tools"
	"github.com/google/go-cmp/cmp"
	"github.com/mark3labs/mcp-go/mcp"
)

// Shared constants and expectation struct are defined in test_helpers.go

func TestHandlers(t *testing.T) {
	// Define test cases
	tests := []struct {
		handler string
		name    string
		args    map[string]interface{}
		write   bool
	}{
		// GetTeamsHandler test cases
		{
			handler: "get_teams",
			name:    "Get Teams",
			args: map[string]interface{}{
				"name": TEAM_NAME,
			},
		},
		// CreateIssueHandler test cases
		{
			handler: "create_issue",
			name:    "Valid issue with team",
			args: map[string]interface{}{
				"title": "Test Issue",
				"team":  TEAM_ID,
			},
			write: true,
		},
		{
			handler: "create_issue",
			name:    "Valid issue with team UUID",
			args: map[string]interface{}{
				"title": "Test Issue with team UUID",
				"team":  TEAM_ID,
			},
			write: true,
		},
		{
			handler: "create_issue",
			name:    "Valid issue with team name",
			args: map[string]interface{}{
				"title": "Test Issue with team name",
				"team":  TEAM_NAME,
			},
			write: true,
		},
		{
			handler: "create_issue",
			name:    "Valid issue with team key",
			args: map[string]interface{}{
				"title": "Test Issue with team key",
				"team":  TEAM_KEY,
			},
			write: true,
		},
		{
			handler: "create_issue",
			name:    "Create sub issue",
			args: map[string]interface{}{
				"title":          "Sub Issue",
				"team":           TEAM_ID,
				"makeSubissueOf": "1c2de93f-4321-4015-bfde-ee893ef7976f", // UUID for TEST-10
			},
			write: true,
		},
		{
			handler: "create_issue",
			name:    "Create sub issue from identifier",
			args: map[string]interface{}{
				"title":          "Sub Issue",
				"team":           TEAM_ID,
				"makeSubissueOf": "TEST-10",
			},
			write: true,
		},
		{
			handler: "create_issue",
			name:    "Create issue with labels",
			args: map[string]interface{}{
				"title":  "Issue with Labels",
				"team":   TEAM_ID,
				"labels": "team label 1",
			},
			write: true,
		},
		{
			handler: "create_issue",
			name:    "Create sub issue with labels",
			args: map[string]interface{}{
				"title":          "Sub Issue with Labels",
				"team":           TEAM_ID,
				"makeSubissueOf": "1c2de93f-4321-4015-bfde-ee893ef7976f", // UUID for TEST-10
				"labels":         "ws-label 2,Feature",
			},
			write: true,
		},
		{
			handler: "create_issue",
			name:    "Create issue with project ID",
			args: map[string]interface{}{
				"title":   "Issue with Project ID",
				"team":    TEAM_ID,
				"project": PROJECT_ID,
			},
			write: true,
		},
		{
			handler: "create_issue",
			name:    "Create issue with project name",
			args: map[string]interface{}{
				"title":   "Issue with Project Name",
				"team":    TEAM_ID,
				"project": "MCP tool investigation",
			},
			write: true,
		},
		{
			handler: "create_issue",
			name:    "Create issue with project slug",
			args: map[string]interface{}{
				"title":   "Issue with Project Slug",
				"team":    TEAM_ID,
				"project": "mcp-tool-investigation-ae44897e42a7",
			},
			write: true,
		},
		{
			handler: "create_issue",
			name:    "Create issue with invalid project",
			args: map[string]interface{}{
				"title":   "Issue with Invalid Project",
				"team":    TEAM_ID,
				"project": "non-existent-project",
			},
			write: true,
		},
		{
			handler: "create_issue",
			name:    "Missing title",
			args: map[string]interface{}{
				"team": TEAM_ID,
			},
		},
		{
			handler: "create_issue",
			name:    "Missing team",
			args: map[string]interface{}{
				"title": "Test Issue",
			},
		},
		{
			handler: "create_issue",
			name:    "Invalid team",
			args: map[string]interface{}{
				"title": "Test Issue",
				"team":  "NonExistentTeam",
			},
		},

		// UpdateIssueHandler test cases
		{
			handler: "update_issue",
			name:    "Valid update",
			args: map[string]interface{}{
				"issue": ISSUE_ID,
				"title": "Updated Test Issue",
			},
			write: true,
		},
		{
			handler: "update_issue",
			name:    "Missing id",
			args: map[string]interface{}{
				"title": "Updated Test Issue",
			},
		},

		// SearchIssuesHandler test cases
		{
			handler: "search_issues",
			name:    "Search by team",
			args: map[string]interface{}{
				"team":  TEAM_ID,
				"limit": float64(5),
			},
		},
		{
			handler: "search_issues",
			name:    "Search by query",
			args: map[string]interface{}{
				"query": "test",
				"limit": float64(5),
			},
		},

		// GetUserIssuesHandler test cases
		{
			handler: "get_user_issues",
			name:    "Current user issues",
			args: map[string]interface{}{
				"limit": float64(5),
			},
		},
		{
			handler: "get_user_issues",
			name:    "Specific user issues",
			args: map[string]interface{}{
				"user":  USER_ID,
				"limit": float64(5),
			},
		},

		// GetIssueHandler test cases
		{
			handler: "get_issue",
			name:    "Valid issue",
			args: map[string]interface{}{
				"issue": ISSUE_ID,
			},
		},
		{
			handler: "get_issue",
			name:    "Get comment issue",
			args: map[string]interface{}{
				"issue": COMMENT_ISSUE_ID,
			},
		},
		{
			handler: "get_issue",
			name:    "Missing issue",
			args:    map[string]interface{}{},
		},
		{
			handler: "get_issue",
			name:    "Missing issueId",
			args: map[string]interface{}{
				"issue": "NONEXISTENT-123",
			},
		},

		// GetIssueCommentsHandler test cases
		{
			handler: "get_issue_comments",
			name:    "Valid issue",
			args: map[string]interface{}{
				"issue": ISSUE_ID,
			},
		},
		{
			handler: "get_issue_comments",
			name:    "Missing issue",
			args:    map[string]interface{}{},
		},
		{
			handler: "get_issue_comments",
			name:    "Invalid issue",
			args: map[string]interface{}{
				"issue": "NONEXISTENT-123",
			},
		},
		{
			handler: "get_issue_comments",
			name:    "With limit",
			args: map[string]interface{}{
				"issue": ISSUE_ID,
				"limit": float64(3),
			},
		},
		{
			handler: "get_issue_comments",
			name:    "With_thread_parameter",
			args: map[string]interface{}{
				"issue":  ISSUE_ID,
				"thread": "ae3d62d6-3f40-4990-867b-5c97dd265a40", // ID of a comment to get replies for
			},
		},
		{
			handler: "get_issue_comments",
			name:    "Thread_with_pagination",
			args: map[string]interface{}{
				"issue":  ISSUE_ID,
				"thread": "ae3d62d6-3f40-4990-867b-5c97dd265a40", // ID of a comment to get replies for
				"limit":  float64(2),
			},
		},

		// AddCommentHandler test cases
		{
			handler: "add_comment",
			name:    "Valid comment",
			write:   true,
			args: map[string]interface{}{
				"issue": ISSUE_ID,
				"body":  "Test comment",
			},
		},
		{
			handler: "add_comment",
			name:    "Reply_to_comment",
			write:   true,
			args: map[string]interface{}{
				"issue":  ISSUE_ID,
				"body":   "This is a reply to the comment",
				"thread": "ae3d62d6-3f40-4990-867b-5c97dd265a40", // ID of the comment to reply to
			},
		},
		{
			handler: "add_comment",
			name:    "Missing issue",
			args: map[string]interface{}{
				"body": "Test comment",
			},
		},
		{
			handler: "add_comment",
			name:    "Missing body",
			args: map[string]interface{}{
				"issue": ISSUE_ID,
			},
		},
		{
			handler: "add_comment",
			name:    "Reply with URL",
			write:   true,
			args: map[string]interface{}{
				"issue":  ISSUE_ID,
				"body":   "Reply using comment URL",
				"thread": "https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-ae3d62d6",
			},
		},
		{
			handler: "add_comment",
			name:    "Reply with shorthand",
			write:   true,
			args: map[string]interface{}{
				"issue":  ISSUE_ID,
				"body":   "Reply using shorthand",
				"thread": "comment-ae3d62d6",
			},
		},
		// ReplyToCommentHandler test cases
		{
			handler: "reply_to_comment",
			name:    "Valid reply",
			write:   true,
			args: map[string]interface{}{
				"thread": "ae3d62d6-3f40-4990-867b-5c97dd265a40",
				"body":   "This is a reply using the dedicated tool",
			},
		},
		{
			handler: "reply_to_comment",
			name:    "Reply with URL",
			write:   true,
			args: map[string]interface{}{
				"thread": "https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-ae3d62d6",
				"body":   "Reply using URL in dedicated tool",
			},
		},
		{
			handler: "reply_to_comment",
			name:    "Missing thread",
			args: map[string]interface{}{
				"body": "Reply without thread",
			},
		},
		{
			handler: "reply_to_comment",
			name:    "Missing body",
			args: map[string]interface{}{
				"thread": "ae3d62d6-3f40-4990-867b-5c97dd265a40",
			},
		},
		// UpdateCommentHandler test cases
		{
			handler: "update_comment",
			name:    "Valid comment update",
			write:   true,
			args: map[string]interface{}{
				"comment": "ae3d62d6-3f40-4990-867b-5c97dd265a40",
				"body":    "Updated comment text",
			},
		},
		{
			handler: "update_comment",
			name:    "Valid comment update with shorthand",
			write:   true,
			args: map[string]interface{}{
				"comment": "comment-ae3d62d6",
				"body":    "Updated comment text via shorthand",
			},
		},
		{
			handler: "update_comment",
			name:    "Valid comment update with hash only",
			write:   true,
			args: map[string]interface{}{
				"comment": "ae3d62d6",
				"body":    "Updated comment text via hash",
			},
		},
		{
			handler: "update_comment",
			name:    "Missing comment",
			args: map[string]interface{}{
				"body": "Updated comment text",
			},
		},
		{
			handler: "update_comment",
			name:    "Missing body",
			args: map[string]interface{}{
				"comment": "ae3d62d6-3f40-4990-867b-5c97dd265a40",
			},
		},
		{
			handler: "update_comment",
			name:    "Invalid comment identifier",
			write:   true,
			args: map[string]interface{}{
				"comment": "invalid-comment-id",
				"body":    "Updated comment text",
			},
		},
		// GetProjectHandler test cases
		{
			handler: "get_project",
			name:    "By ID",
			args: map[string]interface{}{
				"project": "01bff2dd-ab7f-4464-b425-97073862013f",
			},
		},
		{
			handler: "get_project",
			name:    "Missing project param",
			args:    map[string]interface{}{},
		},
		{
			handler: "get_project",
			name:    "Invalid project",
			args: map[string]interface{}{
				"project": "NONEXISTENT-PROJECT",
			},
		},
		{
			handler: "get_project",
			name:    "By slug",
			args: map[string]interface{}{
				"project": "mcp-tool-investigation-ae44897e42a7",
			},
		},
		{
			handler: "get_project",
			name:    "By name",
			args: map[string]interface{}{
				"project": "MCP tool investigation",
			},
		},
		{
			handler: "get_project",
			name:    "Non-existent slug",
			args: map[string]interface{}{
				"project": "non-existent-slug",
			},
		},
		// SearchProjectsHandler test cases
		{
			handler: "search_projects",
			name:    "Empty query",
			args: map[string]interface{}{
				"query": "",
			},
		},
		{
			handler: "search_projects",
			name:    "No results",
			args: map[string]interface{}{
				"query": "non-existent-project-query",
			},
		},
		{
			handler: "search_projects",
			name:    "Multiple results",
			args: map[string]interface{}{
				"query": "MCP",
			},
		},
		// CreateProjectHandler test cases
		{
			handler: "create_project",
			name:    "Valid project",
			args: map[string]interface{}{
				"name":    "Created Test Project",
				"teamIds": TEAM_ID,
			},
			write: true,
		},
		{
			handler: "create_project",
			name:    "With all optional fields",
			args: map[string]interface{}{
				"name":        "Test Project 2",
				"teamIds":     TEAM_ID,
				"description": "Test Description",
				"leadId":      USER_ID,
				"startDate":   "2024-01-01",
				"targetDate":  "2024-12-31",
			},
			write: true,
		},
		{
			handler: "create_project",
			name:    "Missing name",
			args: map[string]interface{}{
				"teamIds": TEAM_ID,
			},
			write: true,
		},
		{
			handler: "create_project",
			name:    "Invalid team ID",
			args: map[string]interface{}{
				"name":    "Test Project 3",
				"teamIds": "invalid-team-id",
			},
			write: true,
		},
		// UpdateProjectHandler test cases
		{
			handler: "update_project",
			name:    "Valid update",
			args: map[string]interface{}{
				"project": UPDATE_PROJECT_ID,
				"name":    "Updated Project Name",
			},
			write: true,
		},
		{
			handler: "update_project",
			name:    "Update name and description",
			args: map[string]interface{}{
				"project":     UPDATE_PROJECT_ID,
				"name":        "Updated Project Name 2",
				"description": "Updated Description",
			},
			write: true,
		},
		{
			handler: "update_project",
			name:    "Non-existent project",
			args: map[string]interface{}{
				"project": "non-existent-project",
				"name":    "Updated Project Name",
			},
			write: true,
		},
		{
			handler: "update_project",
			name:    "Update only description",
			args: map[string]interface{}{
				"project":     UPDATE_PROJECT_ID,
				"description": "Updated Description Only",
			},
			write: true,
		},
		// GetMilestoneHandler test cases
		{
			handler: "get_milestone",
			name:    "Valid milestone",
			args: map[string]interface{}{
				"milestone": MILESTONE_ID,
			},
		},
		{
			handler: "get_milestone",
			name:    "By name",
			args: map[string]interface{}{
				"milestone": "Test Milestone 2",
			},
		},
		{
			handler: "get_milestone",
			name:    "Non-existent milestone",
			args: map[string]interface{}{
				"milestone": "non-existent-milestone",
			},
		},
		// CreateMilestoneHandler test cases
		{
			handler: "create_milestone",
			name:    "Valid milestone",
			args: map[string]interface{}{
				"name":      "Test Milestone 2.2",
				"projectId": UPDATE_PROJECT_ID,
			},
			write: true,
		},
		{
			handler: "create_milestone",
			name:    "With all optional fields",
			args: map[string]interface{}{
				"name":        "Test Milestone 3.2",
				"projectId":   UPDATE_PROJECT_ID,
				"description": "Test Description",
				"targetDate":  "2024-12-31",
			},
			write: true,
		},
		{
			handler: "create_milestone",
			name:    "Missing name",
			args: map[string]interface{}{
				"projectId": UPDATE_PROJECT_ID,
			},
			write: true,
		},
		{
			handler: "create_milestone",
			name:    "Invalid project ID",
			args: map[string]interface{}{
				"name":      "Test Milestone 3.1",
				"projectId": "invalid-project-id",
			},
			write: true,
		},
		// UpdateMilestoneHandler test cases
		{
			handler: "update_milestone",
			name:    "Valid update",
			args: map[string]interface{}{
				"milestone":   UPDATE_MILESTONE_ID,
				"name":        "Updated Milestone Name 22",
				"description": "Updated Description",
				"targetDate":  "2025-01-01",
			},
			write: true,
		},
		{
			handler: "update_milestone",
			name:    "Non-existent milestone",
			args: map[string]interface{}{
				"milestone": "non-existent-milestone",
				"name":      "Updated Milestone Name",
			},
			write: true,
		},
		// GetInitiativeHandler test cases
		{
			handler: "get_initiative",
			name:    "Valid initiative",
			args: map[string]interface{}{
				"initiative": INITIATIVE_ID,
			},
		},
		{
			handler: "get_initiative",
			name:    "By name",
			args: map[string]interface{}{
				"initiative": "Push for MCP",
			},
		},
		{
			handler: "get_initiative",
			name:    "Non-existent name",
			args: map[string]interface{}{
				"initiative": "non-existent-name",
			},
		},
		// CreateInitiativeHandler test cases
		{
			handler: "create_initiative",
			name:    "Valid initiative",
			args: map[string]interface{}{
				"name": "Created Test Initiative",
			},
			write: true,
		},
		{
			handler: "create_initiative",
			name:    "With description",
			args: map[string]interface{}{
				"name":        "Created Test Initiative 2",
				"description": "Test Description",
			},
			write: true,
		},
		{
			handler: "create_initiative",
			name:    "Missing name",
			args:    map[string]interface{}{},
			write:   true,
		},
		// UpdateInitiativeHandler test cases
		{
			handler: "update_initiative",
			name:    "Valid update",
			args: map[string]interface{}{
				"initiative":  UPDATE_INITIATIVE_ID,
				"name":        "Updated Initiative Name",
				"description": "Updated Description",
			},
			write: true,
		},
		{
			handler: "update_initiative",
			name:    "Non-existent initiative",
			args: map[string]interface{}{
				"initiative": "non-existent-initiative",
				"name":       "Updated Initiative Name",
			},
			write: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.handler+"_"+tt.name, func(t *testing.T) {
			if tt.write && *record && !*recordWrites {
				t.Skip("Skipping write test when recordWrites=false")
				return
			}

			// Generate golden file path
			goldenPath := filepath.Join("../../testdata/golden", tt.handler+"_handler_"+tt.name+".golden")

			// Create test client
			client, cleanup := linear.NewTestClient(t, tt.handler+"_handler_"+tt.name, *record || *recordWrites)
			defer cleanup()

			// Create the appropriate handler based on tt.handler
			var handler func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error)
			switch tt.handler {
			case "get_teams":
				handler = tools.GetTeamsHandler(client)
			case "create_issue":
				handler = tools.CreateIssueHandler(client)
			case "update_issue":
				handler = tools.UpdateIssueHandler(client)
			case "search_issues":
				handler = tools.SearchIssuesHandler(client)
			case "get_user_issues":
				handler = tools.GetUserIssuesHandler(client)
			case "get_issue":
				handler = tools.GetIssueHandler(client)
			case "get_issue_comments":
				handler = tools.GetIssueCommentsHandler(client)
			case "add_comment":
				handler = tools.AddCommentHandler(client)
			case "reply_to_comment":
				handler = tools.ReplyToCommentHandler(client)
			case "update_comment":
				handler = tools.UpdateCommentHandler(client)
			case "get_project":
				handler = tools.GetProjectHandler(client)
			case "search_projects":
				handler = tools.SearchProjectsHandler(client)
			case "create_project":
				handler = tools.CreateProjectHandler(client)
			case "update_project":
				handler = tools.UpdateProjectHandler(client)
			case "get_milestone":
				handler = tools.GetMilestoneHandler(client)
			case "create_milestone":
				handler = tools.CreateMilestoneHandler(client)
			case "update_milestone":
				handler = tools.UpdateMilestoneHandler(client)
			case "get_initiative":
				handler = tools.GetInitiativeHandler(client)
			case "create_initiative":
				handler = tools.CreateInitiativeHandler(client)
			case "update_initiative":
				handler = tools.UpdateInitiativeHandler(client)
			default:
				t.Fatalf("Unknown handler type: %s", tt.handler)
			}

			// Create the request
			request := mcp.CallToolRequest{}
			request.Params.Name = "linear_" + tt.handler
			request.Params.Arguments = tt.args

			// Call the handler
			result, err := handler(context.Background(), request)

			// Check for errors
			if err != nil {
				t.Fatalf("Handler returned error: %v", err)
			}

			// Extract the actual output and error
			var actualOutput, actualErr string
			if result.IsError {
				// For error results, the error message is in the text content
				for _, content := range result.Content {
					if textContent, ok := content.(mcp.TextContent); ok {
						actualErr = textContent.Text
						break
					}
				}
			} else {
				// For success results, the output is in the text content
				for _, content := range result.Content {
					if textContent, ok := content.(mcp.TextContent); ok {
						actualOutput = textContent.Text
						break
					}
				}
			}

			// If golden flag is set, update the golden file
			if *golden {
				writeGoldenFile(t, goldenPath, expectation{
					Err:    actualErr,
					Output: actualOutput,
				})
				return
			}

			// Otherwise, read the golden file and compare
			expected := readGoldenFile(t, goldenPath)

			// Compare error
			if diff := cmp.Diff(expected.Err, actualErr); diff != "" {
				t.Errorf("Error mismatch (-want +got):\n%s", diff)
			}

			// Compare output (only if no error is expected)
			if expected.Err == "" {
				if diff := cmp.Diff(expected.Output, actualOutput); diff != "" {
					t.Errorf("Output mismatch (-want +got):\n%s", diff)
				}
			}
		})
	}
}

// readGoldenFile and writeGoldenFile are defined in test_helpers.go

```

--------------------------------------------------------------------------------
/cmd/setup_test.go:
--------------------------------------------------------------------------------

```go
package cmd

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
)

// Define expectation types
type fileExpectation struct {
	path      string
	content   string
	mustExist bool
}

type preExistingFile struct {
	path    string
	content string
}

type expectations struct {
	files    map[string]fileExpectation
	errors   []string
	exitCode int
}

// TestSetupCommand tests the setup command with various combinations of parameters
func TestSetupCommand(t *testing.T) {
	// Build the binary
	binaryPath, err := buildBinary()
	if err != nil {
		t.Fatalf("Failed to build binary: %v", err)
	}
	defer os.RemoveAll(filepath.Dir(binaryPath))

	// Define test cases
	testCases := []struct {
		name             string
		toolParam        string
		writeAccess      bool
		autoApprove      string
		projectPath      string
		preExistingFiles map[string]preExistingFile
		expect           expectations
	}{
		{
			name:        "Cline Only",
			toolParam:   "cline",
			writeAccess: true,
			autoApprove: "allow-read-only",
			expect: expectations{
				files: map[string]fileExpectation{
					"cline": {
						path:      "home/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
						mustExist: true,
						content: `{
							"mcpServers": {
								"linear": {
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve", "--write-access=true"],
									"env": {
										"LINEAR_API_KEY": "test-api-key"
									},
									"autoApprove": ["linear_get_initiative", "linear_get_issue", "linear_get_issue_comments", "linear_get_milestone", "linear_get_project", "linear_get_teams", "linear_get_user_issues", "linear_search_issues", "linear_search_projects"],
									"disabled": false
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Roo Code Only",
			toolParam:   "roo-code",
			writeAccess: true,
			autoApprove: "allow-read-only",
			expect: expectations{
				files: map[string]fileExpectation{
					"roo-code": {
						path:      "home/.vscode-server/data/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json",
						mustExist: true,
						content: `{
							"mcpServers": {
								"linear": {
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve", "--write-access=true"],
									"env": {
										"LINEAR_API_KEY": "test-api-key"
									},
									"autoApprove": ["linear_get_initiative", "linear_get_issue", "linear_get_issue_comments", "linear_get_milestone", "linear_get_project", "linear_get_teams", "linear_get_user_issues", "linear_search_issues", "linear_search_projects"],
									"disabled": false
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Multiple Tools",
			toolParam:   "cline,roo-code",
			writeAccess: true,
			autoApprove: "allow-read-only",
			expect: expectations{
				files: map[string]fileExpectation{
					"cline": {
						path:      "home/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
						mustExist: true,
						content: `{
							"mcpServers": {
								"linear": {
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve", "--write-access=true"],
									"env": {
										"LINEAR_API_KEY": "test-api-key"
									},
									"autoApprove": ["linear_get_initiative", "linear_get_issue", "linear_get_issue_comments", "linear_get_milestone", "linear_get_project", "linear_get_teams", "linear_get_user_issues", "linear_search_issues", "linear_search_projects"],
									"disabled": false
								}
							}
						}`,
					},
					"roo-code": {
						path:      "home/.vscode-server/data/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json",
						mustExist: true,
						content: `{
							"mcpServers": {
								"linear": {
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve", "--write-access=true"],
									"env": {
										"LINEAR_API_KEY": "test-api-key"
									},
									"autoApprove": ["linear_get_initiative", "linear_get_issue", "linear_get_issue_comments", "linear_get_milestone", "linear_get_project", "linear_get_teams", "linear_get_user_issues", "linear_search_issues", "linear_search_projects"],
									"disabled": false
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Invalid Tool",
			toolParam:   "invalid-tool,cline",
			writeAccess: true,
			autoApprove: "allow-read-only",
			expect: expectations{
				files: map[string]fileExpectation{
					"cline": {
						path:      "home/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
						mustExist: true,
						content: `{
							"mcpServers": {
								"linear": {
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve", "--write-access=true"],
									"env": {
										"LINEAR_API_KEY": "test-api-key"
									},
									"autoApprove": ["linear_get_initiative", "linear_get_issue", "linear_get_issue_comments", "linear_get_milestone", "linear_get_project", "linear_get_teams", "linear_get_user_issues", "linear_search_issues", "linear_search_projects"],
									"disabled": false
								}
							}
						}`,
					},
				},
				errors:   []string{"Unsupported tool: invalid-tool"},
				exitCode: 1,
			},
		},
		{
			name:        "Preserve Existing Arrays in Config",
			toolParam:   "cline",
			writeAccess: true,
			autoApprove: "allow-read-only",
			preExistingFiles: map[string]preExistingFile{
				"cline": {
					path: "home/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
					content: `{
						"mcpServers": {
							"existing-server": {
								"command": "/path/to/existing/server",
								"args": ["serve", "--option1", "--option2"],
								"autoApprove": ["tool1", "tool2", "tool3"],
								"env": {
									"API_KEY": "existing-key"
								},
								"disabled": false,
								"customArray": ["item1", "item2"],
								"nestedObject": {
									"arrayField": ["nested1", "nested2"]
								}
							}
						},
						"otherTopLevelArray": ["value1", "value2"],
						"otherConfig": {
							"someArray": [1, 2, 3]
						}
					}`,
				},
			},
			expect: expectations{
				files: map[string]fileExpectation{
					"cline": {
						path:      "home/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
						mustExist: true,
						content: `{
							"mcpServers": {
								"existing-server": {
									"command": "/path/to/existing/server",
									"args": ["serve", "--option1", "--option2"],
									"autoApprove": ["tool1", "tool2", "tool3"],
									"env": {
										"API_KEY": "existing-key"
									},
									"disabled": false,
									"customArray": ["item1", "item2"],
									"nestedObject": {
										"arrayField": ["nested1", "nested2"]
									}
								},
								"linear": {
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve", "--write-access=true"],
									"env": {
										"LINEAR_API_KEY": "test-api-key"
									},
									"autoApprove": ["linear_get_initiative", "linear_get_issue", "linear_get_issue_comments", "linear_get_milestone", "linear_get_project", "linear_get_teams", "linear_get_user_issues", "linear_search_issues", "linear_search_projects"],
									"disabled": false
								}
							},
							"otherTopLevelArray": ["value1", "value2"],
							"otherConfig": {
								"someArray": [1, 2, 3]
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Complex Array Preservation Test",
			toolParam:   "cline",
			writeAccess: false,
			autoApprove: "linear_get_issue,linear_search_issues",
			preExistingFiles: map[string]preExistingFile{
				"cline": {
					path: "home/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
					content: `{
						"mcpServers": {
							"github": {
								"command": "npx",
								"args": ["-y", "@modelcontextprotocol/server-github"],
								"env": {
									"GITHUB_PERSONAL_ACCESS_TOKEN": "github_token"
								},
								"autoApprove": ["search_repositories", "get_file_contents"],
								"disabled": false
							},
							"filesystem": {
								"command": "npx",
								"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
								"autoApprove": [],
								"disabled": false
							}
						},
						"globalSettings": {
							"enabledFeatures": ["autocomplete", "syntax-highlighting"],
							"debugModes": ["verbose", "trace"]
						}
					}`,
				},
			},
			expect: expectations{
				files: map[string]fileExpectation{
					"cline": {
						path:      "home/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
						mustExist: true,
						content: `{
							"mcpServers": {
								"github": {
									"command": "npx",
									"args": ["-y", "@modelcontextprotocol/server-github"],
									"env": {
										"GITHUB_PERSONAL_ACCESS_TOKEN": "github_token"
									},
									"autoApprove": ["search_repositories", "get_file_contents"],
									"disabled": false
								},
								"filesystem": {
									"command": "npx",
									"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
									"autoApprove": [],
									"disabled": false
								},
								"linear": {
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve"],
									"env": {
										"LINEAR_API_KEY": "test-api-key"
									},
									"autoApprove": ["linear_get_issue", "linear_search_issues"],
									"disabled": false
								}
							},
							"globalSettings": {
								"enabledFeatures": ["autocomplete", "syntax-highlighting"],
								"debugModes": ["verbose", "trace"]
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Claude Code Only",
			toolParam:   "claude-code",
			projectPath: "/workspace/test-project",
			expect: expectations{
				files: map[string]fileExpectation{
					"claude-code": {
						path:      "home/.claude.json",
						mustExist: true,
						content: `{
							"projects": {
								"/workspace/test-project": {
									"mcpServers": {
										"linear": {
											"type": "stdio",
											"command": "home/mcp-servers/linear-mcp-go",
											"args": ["serve"],
											"env": {
												"LINEAR_API_KEY": "test-api-key"
											},
											"autoApprove": [],
											"disabled": false
										}
									}
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Claude Code with Existing File",
			toolParam:   "claude-code",
			projectPath: "/workspace/test-project",
			preExistingFiles: map[string]preExistingFile{
				"claude-code": {
					path: "home/.claude.json",
					content: `{
						"projects": {
							"/workspace/another-project": {
								"mcpServers": {
									"another-server": {
										"command": "/path/to/another/server"
									}
								}
							}
						}
					}`,
				},
			},
			expect: expectations{
				files: map[string]fileExpectation{
					"claude-code": {
						path:      "home/.claude.json",
						mustExist: true,
						content: `{
							"projects": {
								"/workspace/another-project": {
									"mcpServers": {
										"another-server": {
											"command": "/path/to/another/server"
										}
									}
								},
								"/workspace/test-project": {
									"mcpServers": {
										"linear": {
											"type": "stdio",
											"command": "home/mcp-servers/linear-mcp-go",
											"args": ["serve"],
											"env": {
												"LINEAR_API_KEY": "test-api-key"
											},
											"autoApprove": [],
											"disabled": false
										}
									}
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:      "Claude Code No Existing Projects and No Project Path - User Scope Registration",
			toolParam: "claude-code",
			expect: expectations{
				files: map[string]fileExpectation{
					"claude-code": {
						path:      "home/.claude.json",
						mustExist: true,
						content: `{
							"projects": {},
							"mcpServers": {
								"linear": {
									"type": "stdio",
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve"],
									"env": {
										"LINEAR_API_KEY": "test-api-key"
									},
									"autoApprove": [],
									"disabled": false
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:      "Claude Code Register to User Scope with Existing Projects",
			toolParam: "claude-code",
			preExistingFiles: map[string]preExistingFile{
				"claude-code": {
					path: "home/.claude.json",
					content: `{
						"projects": {
							"/workspace/project1": {
								"mcpServers": {
									"existing-server": {
										"command": "/path/to/existing/server"
									}
								}
							},
							"/workspace/project2": {
								"someOtherConfig": "value"
							}
						}
					}`,
				},
			},
			expect: expectations{
				files: map[string]fileExpectation{
					"claude-code": {
						path:      "home/.claude.json",
						mustExist: true,
						content: `{
							"projects": {
								"/workspace/project1": {
									"mcpServers": {
										"existing-server": {
											"command": "/path/to/existing/server"
										}
									}
								},
								"/workspace/project2": {
									"someOtherConfig": "value"
								}
							},
							"mcpServers": {
								"linear": {
									"type": "stdio",
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve"],
									"env": {
										"LINEAR_API_KEY": "test-api-key"
									},
									"autoApprove": [],
									"disabled": false
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Claude Code Multiple Project Paths",
			toolParam:   "claude-code",
			projectPath: "/workspace/proj1,/workspace/proj2, /workspace/proj3 ",
			writeAccess: true,
			autoApprove: "linear_get_issue,linear_search_issues",
			expect: expectations{
				files: map[string]fileExpectation{
					"claude-code": {
						path:      "home/.claude.json",
						mustExist: true,
						content: `{
							"projects": {
								"/workspace/proj1": {
									"mcpServers": {
										"linear": {
											"type": "stdio",
											"command": "home/mcp-servers/linear-mcp-go",
											"args": ["serve", "--write-access=true"],
											"env": {
												"LINEAR_API_KEY": "test-api-key"
											},
											"autoApprove": ["linear_get_issue", "linear_search_issues"],
											"disabled": false
										}
									}
								},
								"/workspace/proj2": {
									"mcpServers": {
										"linear": {
											"type": "stdio",
											"command": "home/mcp-servers/linear-mcp-go",
											"args": ["serve", "--write-access=true"],
											"env": {
												"LINEAR_API_KEY": "test-api-key"
											},
											"autoApprove": ["linear_get_issue", "linear_search_issues"],
											"disabled": false
										}
									}
								},
								"/workspace/proj3": {
									"mcpServers": {
										"linear": {
											"type": "stdio",
											"command": "home/mcp-servers/linear-mcp-go",
											"args": ["serve", "--write-access=true"],
											"env": {
												"LINEAR_API_KEY": "test-api-key"
											},
											"autoApprove": ["linear_get_issue", "linear_search_issues"],
											"disabled": false
										}
									}
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Claude Code Mixed Existing and New Projects",
			toolParam:   "claude-code",
			projectPath: "/workspace/existing-project,/workspace/new-project",
			preExistingFiles: map[string]preExistingFile{
				"claude-code": {
					path: "home/.claude.json",
					content: `{
						"projects": {
							"/workspace/existing-project": {
								"mcpServers": {
									"other-server": {
										"command": "/path/to/other/server"
									}
								},
								"customConfig": "value"
							}
						}
					}`,
				},
			},
			expect: expectations{
				files: map[string]fileExpectation{
					"claude-code": {
						path:      "home/.claude.json",
						mustExist: true,
						content: `{
							"projects": {
								"/workspace/existing-project": {
									"mcpServers": {
										"other-server": {
											"command": "/path/to/other/server"
										},
										"linear": {
											"type": "stdio",
											"command": "home/mcp-servers/linear-mcp-go",
											"args": ["serve"],
											"env": {
												"LINEAR_API_KEY": "test-api-key"
											},
											"autoApprove": [],
											"disabled": false
										}
									},
									"customConfig": "value"
								},
								"/workspace/new-project": {
									"mcpServers": {
										"linear": {
											"type": "stdio",
											"command": "home/mcp-servers/linear-mcp-go",
											"args": ["serve"],
											"env": {
												"LINEAR_API_KEY": "test-api-key"
											},
											"autoApprove": [],
											"disabled": false
										}
									}
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Claude Code Empty Project Path List",
			toolParam:   "claude-code",
			projectPath: " , , ",
			expect: expectations{
				errors:   []string{"no valid project paths provided"},
				exitCode: 1,
			},
		},
		{
			name:        "Claude Code Complex Settings Preservation",
			toolParam:   "claude-code",
			projectPath: "/workspace/new-project",
			writeAccess: true,
			autoApprove: "linear_get_issue,linear_search_issues",
			preExistingFiles: map[string]preExistingFile{
				"claude-code": {
					path: "home/.claude.json",
					content: `{
						"firstStartTime": "2025-06-11T14:49:28.932Z",
						"userID": "31553dcf54399f00daf126faf48dbb0e626926f50e9bf49c16cb05c06f65cfd8",
						"globalSettings": {
							"theme": "dark",
							"autoSave": true,
							"debugMode": false,
							"experimentalFeatures": ["feature1", "feature2", "feature3"],
							"limits": {
								"maxTokens": 4096,
								"timeout": 30000,
								"retries": 3
							},
							"customMappings": {
								"shortcuts": {
									"ctrl+s": "save",
									"ctrl+z": "undo"
								},
								"aliases": ["alias1", "alias2"]
							}
						},
						"recentProjects": ["/workspace/project1", "/workspace/project2", "/workspace/project3"],
						"projects": {
							"/workspace/existing-project": {
								"allowedTools": ["tool1", "tool2", "tool3"],
								"history": [
									{
										"timestamp": "2025-06-11T15:00:00.000Z",
										"action": "create_file",
										"details": {"filename": "test.js", "size": 1024}
									}
								],
								"dontCrawlDirectory": false,
								"mcpContextUris": ["file:///workspace/docs", "https://api.example.com/docs"],
								"mcpServers": {
									"github": {
										"type": "stdio",
										"command": "npx",
										"args": ["-y", "@modelcontextprotocol/server-github"],
										"env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "github_token_123"},
										"autoApprove": ["search_repositories", "get_file_contents"],
										"disabled": false,
										"customConfig": {"rateLimit": 5000, "features": ["search", "read"]}
									},
									"filesystem": {
										"type": "stdio",
										"command": "npx", 
										"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
										"autoApprove": ["read_file", "list_directory"],
										"disabled": false,
										"permissions": {"read": true, "write": false, "execute": false}
									}
								},
								"enabledMcpjsonServers": ["server1", "server2"],
								"disabledMcpjsonServers": ["server3", "server4"],
								"hasTrustDialogAccepted": true,
								"projectOnboardingSeenCount": 3,
								"customProjectSettings": {
									"linting": {"enabled": true, "rules": ["rule1", "rule2"]},
									"formatting": {"tabSize": 2, "insertSpaces": true}
								}
							}
						},
						"analytics": {
							"enabled": true,
							"sessionId": "session_12345",
							"metrics": {"commandsExecuted": 42, "filesModified": 15}
						},
						"version": "1.2.3"
					}`,
				},
			},
			expect: expectations{
				files: map[string]fileExpectation{
					"claude-code": {
						path:      "home/.claude.json",
						mustExist: true,
						content: `{
							"firstStartTime": "2025-06-11T14:49:28.932Z",
							"userID": "31553dcf54399f00daf126faf48dbb0e626926f50e9bf49c16cb05c06f65cfd8",
							"globalSettings": {
								"theme": "dark",
								"autoSave": true,
								"debugMode": false,
								"experimentalFeatures": ["feature1", "feature2", "feature3"],
								"limits": {
									"maxTokens": 4096,
									"timeout": 30000,
									"retries": 3
								},
								"customMappings": {
									"shortcuts": {
										"ctrl+s": "save",
										"ctrl+z": "undo"
									},
									"aliases": ["alias1", "alias2"]
								}
							},
							"recentProjects": ["/workspace/project1", "/workspace/project2", "/workspace/project3"],
							"projects": {
								"/workspace/existing-project": {
									"allowedTools": ["tool1", "tool2", "tool3"],
									"history": [
										{
											"timestamp": "2025-06-11T15:00:00.000Z",
											"action": "create_file",
											"details": {"filename": "test.js", "size": 1024}
										}
									],
									"dontCrawlDirectory": false,
									"mcpContextUris": ["file:///workspace/docs", "https://api.example.com/docs"],
									"mcpServers": {
										"github": {
											"type": "stdio",
											"command": "npx",
											"args": ["-y", "@modelcontextprotocol/server-github"],
											"env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "github_token_123"},
											"autoApprove": ["search_repositories", "get_file_contents"],
											"disabled": false,
											"customConfig": {"rateLimit": 5000, "features": ["search", "read"]}
										},
										"filesystem": {
											"type": "stdio",
											"command": "npx", 
											"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
											"autoApprove": ["read_file", "list_directory"],
											"disabled": false,
											"permissions": {"read": true, "write": false, "execute": false}
										}
									},
									"enabledMcpjsonServers": ["server1", "server2"],
									"disabledMcpjsonServers": ["server3", "server4"],
									"hasTrustDialogAccepted": true,
									"projectOnboardingSeenCount": 3,
									"customProjectSettings": {
										"linting": {"enabled": true, "rules": ["rule1", "rule2"]},
										"formatting": {"tabSize": 2, "insertSpaces": true}
									}
								},
								"/workspace/new-project": {
									"mcpServers": {
										"linear": {
											"type": "stdio",
											"command": "home/mcp-servers/linear-mcp-go",
											"args": ["serve", "--write-access=true"],
											"env": {"LINEAR_API_KEY": "test-api-key"},
											"autoApprove": ["linear_get_issue", "linear_search_issues"],
											"disabled": false
										}
									}
								}
							},
							"analytics": {
								"enabled": true,
								"sessionId": "session_12345",
								"metrics": {"commandsExecuted": 42, "filesModified": 15}
							},
							"version": "1.2.3"
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Claude Code Update Existing Linear Server",
			toolParam:   "claude-code",
			projectPath: "/workspace/existing-project",
			writeAccess: false,
			autoApprove: "allow-read-only",
			preExistingFiles: map[string]preExistingFile{
				"claude-code": {
					path: "home/.claude.json",
					content: `{
						"projects": {
							"/workspace/existing-project": {
								"mcpServers": {
									"linear": {
										"type": "stdio",
										"command": "/old/path/to/linear",
										"args": ["serve", "--old-flag"],
										"env": {"LINEAR_API_KEY": "old-key"},
										"autoApprove": ["old_tool"],
										"disabled": true
									},
									"other-server": {
										"command": "/path/to/other/server"
									}
								}
							}
						}
					}`,
				},
			},
			expect: expectations{
				files: map[string]fileExpectation{
					"claude-code": {
						path:      "home/.claude.json",
						mustExist: true,
						content: `{
							"projects": {
								"/workspace/existing-project": {
									"mcpServers": {
										"linear": {
											"type": "stdio",
											"command": "home/mcp-servers/linear-mcp-go",
											"args": ["serve"],
											"env": {"LINEAR_API_KEY": "test-api-key"},
											"autoApprove": ["linear_get_initiative", "linear_get_issue", "linear_get_issue_comments", "linear_get_milestone", "linear_get_project", "linear_get_teams", "linear_get_user_issues", "linear_search_issues", "linear_search_projects"],
											"disabled": false
										},
										"other-server": {
											"command": "/path/to/other/server"
										}
									}
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Claude Code User Scope with Existing User-Scoped Servers",
			toolParam:   "claude-code",
			writeAccess: true,
			autoApprove: "linear_get_issue,linear_search_issues",
			preExistingFiles: map[string]preExistingFile{
				"claude-code": {
					path: "home/.claude.json",
					content: `{
						"projects": {
							"/workspace/project1": {
								"mcpServers": {
									"project-specific-server": {
										"command": "/path/to/project/server"
									}
								}
							}
						},
						"mcpServers": {
							"existing-user-server": {
								"type": "stdio",
								"command": "/path/to/existing/user/server",
								"args": ["serve"],
								"env": {"API_KEY": "existing-key"},
								"autoApprove": ["existing_tool"],
								"disabled": false
							}
						}
					}`,
				},
			},
			expect: expectations{
				files: map[string]fileExpectation{
					"claude-code": {
						path:      "home/.claude.json",
						mustExist: true,
						content: `{
							"projects": {
								"/workspace/project1": {
									"mcpServers": {
										"project-specific-server": {
											"command": "/path/to/project/server"
										}
									}
								}
							},
							"mcpServers": {
								"existing-user-server": {
									"type": "stdio",
									"command": "/path/to/existing/user/server",
									"args": ["serve"],
									"env": {"API_KEY": "existing-key"},
									"autoApprove": ["existing_tool"],
									"disabled": false
								},
								"linear": {
									"type": "stdio",
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve", "--write-access=true"],
									"env": {"LINEAR_API_KEY": "test-api-key"},
									"autoApprove": ["linear_get_issue", "linear_search_issues"],
									"disabled": false
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Claude Code User Scope Update Existing User-Scoped Linear Server",
			toolParam:   "claude-code",
			writeAccess: false,
			autoApprove: "allow-read-only",
			preExistingFiles: map[string]preExistingFile{
				"claude-code": {
					path: "home/.claude.json",
					content: `{
						"projects": {},
						"mcpServers": {
							"linear": {
								"type": "stdio",
								"command": "/old/path/to/linear",
								"args": ["serve", "--old-flag"],
								"env": {"LINEAR_API_KEY": "old-key"},
								"autoApprove": ["old_tool"],
								"disabled": true
							},
							"other-user-server": {
								"command": "/path/to/other/user/server"
							}
						}
					}`,
				},
			},
			expect: expectations{
				files: map[string]fileExpectation{
					"claude-code": {
						path:      "home/.claude.json",
						mustExist: true,
						content: `{
							"projects": {},
							"mcpServers": {
								"linear": {
									"type": "stdio",
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve"],
									"env": {"LINEAR_API_KEY": "test-api-key"},
											"autoApprove": ["linear_get_initiative", "linear_get_issue", "linear_get_issue_comments", "linear_get_milestone", "linear_get_project", "linear_get_teams", "linear_get_user_issues", "linear_search_issues", "linear_search_projects"],
											"disabled": false
								},
								"other-user-server": {
									"command": "/path/to/other/user/server"
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Ona Only",
			toolParam:   "ona",
			writeAccess: true,
			expect: expectations{
				files: map[string]fileExpectation{
					"ona": {
						path:      ".ona/mcp-config.json",
						mustExist: true,
						content: `{
							"mcpServers": {
								"linear": {
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve", "--write-access=true"],
									"disabled": false
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Ona with Project Path",
			toolParam:   "ona",
			projectPath: "/workspace/test-project",
			writeAccess: false,
			expect: expectations{
				files: map[string]fileExpectation{
					"ona": {
						path:      "/workspace/test-project/.ona/mcp-config.json",
						mustExist: true,
						content: `{
							"mcpServers": {
								"linear": {
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve"],
									"disabled": false
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Ona with Existing Config",
			toolParam:   "ona",
			writeAccess: true,
			preExistingFiles: map[string]preExistingFile{
				"ona": {
					path: ".ona/mcp-config.json",
					content: `{
						"mcpServers": {
							"playwright": {
								"name": "playwright",
								"command": "npx",
								"args": ["-y", "@executeautomation/playwright-mcp-server"]
							}
						}
					}`,
				},
			},
			expect: expectations{
				files: map[string]fileExpectation{
					"ona": {
						path:      ".ona/mcp-config.json",
						mustExist: true,
						content: `{
							"mcpServers": {
								"playwright": {
									"name": "playwright",
									"command": "npx",
									"args": ["-y", "@executeautomation/playwright-mcp-server"]
								},
								"linear": {
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve", "--write-access=true"],
									"disabled": false
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},
		{
			name:        "Ona with Complex Nested Config",
			toolParam:   "ona",
			writeAccess: false,
			preExistingFiles: map[string]preExistingFile{
				"ona": {
					path: ".ona/mcp-config.json",
					content: `{
						"version": "1.0.0",
						"metadata": {
							"created": "2024-01-01T00:00:00Z",
							"author": "test-user",
							"tags": ["development", "testing", "automation"],
							"config": {
								"nested": {
									"deeply": {
										"properties": ["value1", "value2", "value3"],
										"settings": {
											"enabled": true,
											"timeout": 30000,
											"retries": 3,
											"features": {
												"advanced": {
													"caching": true,
													"compression": false,
													"encryption": {
														"algorithm": "AES-256",
														"keySize": 256,
														"modes": ["CBC", "GCM", "CTR"]
													}
												}
											}
										}
									}
								}
							}
						},
						"mcpServers": {
							"custom-server": {
								"name": "custom-server",
								"command": "/usr/local/bin/custom-mcp-server",
								"args": ["--mode", "production", "--verbose"],
								"env": {
									"CUSTOM_API_KEY": "secret-key-123",
									"CUSTOM_ENDPOINT": "https://api.example.com/v1"
								},
								"timeout": 60000,
								"retries": 5,
								"features": {
									"streaming": true,
									"batching": false,
									"compression": {
										"enabled": true,
										"algorithm": "gzip",
										"level": 6
									}
								},
								"customArrays": {
									"supportedFormats": ["json", "xml", "yaml"],
									"allowedOrigins": ["localhost", "*.example.com", "api.test.com"],
									"permissions": ["read", "write", "execute"]
								},
								"nestedConfig": {
									"database": {
										"connection": {
											"host": "localhost",
											"port": 5432,
											"ssl": {
												"enabled": true,
												"cert": "/path/to/cert.pem",
												"key": "/path/to/key.pem",
												"ca": "/path/to/ca.pem"
											}
										},
										"pool": {
											"min": 5,
											"max": 20,
											"idle": 300
										}
									}
								}
							}
						},
						"globalSettings": {
							"logLevel": "info",
							"enableMetrics": true,
							"metricsConfig": {
								"endpoint": "http://metrics.example.com:9090",
								"interval": 30,
								"labels": {
									"environment": "test",
									"service": "mcp-server",
									"version": "1.0.0"
								}
							}
						}
					}`,
				},
			},
			expect: expectations{
				files: map[string]fileExpectation{
					"ona": {
						path:      ".ona/mcp-config.json",
						mustExist: true,
						content: `{
							"version": "1.0.0",
							"metadata": {
								"created": "2024-01-01T00:00:00Z",
								"author": "test-user",
								"tags": ["development", "testing", "automation"],
								"config": {
									"nested": {
										"deeply": {
											"properties": ["value1", "value2", "value3"],
											"settings": {
												"enabled": true,
												"timeout": 30000,
												"retries": 3,
												"features": {
													"advanced": {
														"caching": true,
														"compression": false,
														"encryption": {
															"algorithm": "AES-256",
															"keySize": 256,
															"modes": ["CBC", "GCM", "CTR"]
														}
													}
												}
											}
										}
									}
								}
							},
							"mcpServers": {
								"custom-server": {
									"name": "custom-server",
									"command": "/usr/local/bin/custom-mcp-server",
									"args": ["--mode", "production", "--verbose"],
									"env": {
										"CUSTOM_API_KEY": "secret-key-123",
										"CUSTOM_ENDPOINT": "https://api.example.com/v1"
									},
									"timeout": 60000,
									"retries": 5,
									"features": {
										"streaming": true,
										"batching": false,
										"compression": {
											"enabled": true,
											"algorithm": "gzip",
											"level": 6
										}
									},
									"customArrays": {
										"supportedFormats": ["json", "xml", "yaml"],
										"allowedOrigins": ["localhost", "*.example.com", "api.test.com"],
										"permissions": ["read", "write", "execute"]
									},
									"nestedConfig": {
										"database": {
											"connection": {
												"host": "localhost",
												"port": 5432,
												"ssl": {
													"enabled": true,
													"cert": "/path/to/cert.pem",
													"key": "/path/to/key.pem",
													"ca": "/path/to/ca.pem"
												}
											},
											"pool": {
												"min": 5,
												"max": 20,
												"idle": 300
											}
										}
									}
								},
								"linear": {
									"command": "home/mcp-servers/linear-mcp-go",
									"args": ["serve"],
									"disabled": false
								}
							},
							"globalSettings": {
								"logLevel": "info",
								"enableMetrics": true,
								"metricsConfig": {
									"endpoint": "http://metrics.example.com:9090",
									"interval": 30,
									"labels": {
										"environment": "test",
										"service": "mcp-server",
										"version": "1.0.0"
									}
								}
							}
						}`,
					},
				},
				exitCode: 0,
			},
		},

	}

	// Run each test case
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Create a temporary directory
			rootDir, err := os.MkdirTemp("", "linear-mcp-go-test-*")
			if err != nil {
				t.Fatalf("Failed to create temp dir: %v", err)
			}
			defer os.RemoveAll(rootDir)

			// Set up the directory structure
			homeDir := filepath.Join(rootDir, "home")

			// Copy the binary to the temp directory
			tempBinaryPath := filepath.Join(rootDir, "linear-mcp-go")
			if err := copyFile(binaryPath, tempBinaryPath); err != nil {
				t.Fatalf("Failed to copy binary: %v", err)
			}
			if err := os.Chmod(tempBinaryPath, 0755); err != nil {
				t.Fatalf("Failed to make binary executable: %v", err)
			}

			// Set the HOME environment variable
			oldHome := os.Getenv("HOME")
			os.Setenv("HOME", homeDir)
			defer os.Setenv("HOME", oldHome)

			// Set the LINEAR_API_KEY environment variable
			oldApiKey := os.Getenv("LINEAR_API_KEY")
			os.Setenv("LINEAR_API_KEY", "test-api-key")
			defer os.Setenv("LINEAR_API_KEY", oldApiKey)

			// Create pre-existing files if specified
			for _, preFile := range tc.preExistingFiles {
				fullPath := filepath.Join(rootDir, preFile.path)
				if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
					t.Fatalf("Failed to create directory for pre-existing file %s: %v", fullPath, err)
				}
				if err := os.WriteFile(fullPath, []byte(preFile.content), 0644); err != nil {
					t.Fatalf("Failed to create pre-existing file %s: %v", fullPath, err)
				}
			}

			// Build the command
			args := []string{"setup", "--tool=" + tc.toolParam}
			if tc.writeAccess {
				args = append(args, "--write-access=true")
			}
			if tc.autoApprove != "" {
				args = append(args, "--auto-approve="+tc.autoApprove)
			}
			if tc.projectPath != "" {
				args = append(args, "--project-path="+tc.projectPath)
			}

			// Execute the command
			cmd := exec.Command(tempBinaryPath, args...)
			cmd.Dir = rootDir // Set working directory to the test root
			var stdout, stderr bytes.Buffer
			cmd.Stdout = &stdout
			cmd.Stderr = &stderr
			err = cmd.Run()

			// Check exit code
			exitCode := 0
			if err != nil {
				if exitError, ok := err.(*exec.ExitError); ok {
					exitCode = exitError.ExitCode()
				} else {
					t.Fatalf("Failed to run command: %v", err)
				}
			}

			// Verify exit code
			if exitCode != tc.expect.exitCode {
				t.Errorf("Expected exit code %d, got %d", tc.expect.exitCode, exitCode)
			}

			// Verify expected files
			verifyFileExpectations(t, rootDir, tc.expect.files)

			// Verify expected errors in output
			output := stdout.String() + stderr.String()
			for _, expectedError := range tc.expect.errors {
				if !strings.Contains(output, expectedError) {
					t.Errorf("Expected output to contain '%s', got: %s", expectedError, output)
				}
			}
		})
	}
}

// Helper function to verify file expectations
func verifyFileExpectations(t *testing.T, rootDir string, fileExpects map[string]fileExpectation) {
	for tool, expect := range fileExpects {
		filePath := filepath.Join(rootDir, expect.path)

		// Check if file exists
		_, err := os.Stat(filePath)
		if os.IsNotExist(err) {
			if expect.mustExist {
				t.Errorf("Expected file %s was not created for %s", filePath, tool)
			}
			continue
		}

		// File exists, verify content if expected
		if expect.content != "" {
			actualContent, err := os.ReadFile(filePath)
			if err != nil {
				t.Fatalf("Failed to read configuration file %s: %v", filePath, err)
			}

			// Parse both expected and actual content as JSON for comparison
			var expectedJSON, actualJSON map[string]interface{}

			if err := json.Unmarshal([]byte(expect.content), &expectedJSON); err != nil {
				t.Fatalf("Failed to parse expected JSON for %s: %v", tool, err)
			}

			if err := json.Unmarshal(actualContent, &actualJSON); err != nil {
				t.Fatalf("Failed to parse actual JSON in file %s: %v", filePath, err)
			}

			// Process the JSON objects to make them comparable
			normalizeJSON(expectedJSON)
			normalizeJSON(actualJSON)

			// Compare the JSON objects
			if diff := cmp.Diff(expectedJSON, actualJSON); diff != "" {
				t.Errorf("File content mismatch for %s (-want +got):\n%s", tool, diff)
			}
		}
	}
}

// normalizeJSON processes a JSON object to make it comparable
// by removing fields that may vary and sorting arrays
func normalizeJSON(jsonObj map[string]interface{}) {
	normalizeCommandPaths(jsonObj)
	normalizeJSONRecursive(jsonObj)
}

// normalizeCommandPaths normalizes command paths in server configurations
func normalizeCommandPaths(obj interface{}) {
	switch v := obj.(type) {
	case map[string]interface{}:
		// Look for server configuration containers
		if isServerContainer(v) {
			for _, serverConfig := range v {
				if serverMap, ok := serverConfig.(map[string]interface{}); ok {
					// Normalize the command field by stripping temporary directory prefix
					if command, ok := serverMap["command"].(string); ok {
						// Strip the temporary test directory prefix, keeping only the meaningful part
						// Pattern: /tmp/linear-mcp-go-test-*/home/... -> home/...
						if strings.Contains(command, "/home/") {
							parts := strings.Split(command, "/home/")
							if len(parts) > 1 {
								serverMap["command"] = "home/" + parts[1]
							}
						}
					}
				}
			}
		}

		// Process nested objects recursively
		for _, value := range v {
			normalizeCommandPaths(value)
		}

	case []interface{}:
		// Process array elements recursively
		for _, item := range v {
			normalizeCommandPaths(item)
		}
	}
}

// isServerContainer checks if a map contains server configurations
func isServerContainer(m map[string]interface{}) bool {
	// Check if this looks like a server container by examining its values
	for _, value := range m {
		if serverMap, ok := value.(map[string]interface{}); ok {
			// If it has command field, it's likely a server config container
			if _, hasCommand := serverMap["command"]; hasCommand {
				return true
			}
		}
	}
	return false
}

// normalizeJSONRecursive recursively processes JSON objects to normalize them for comparison
func normalizeJSONRecursive(obj interface{}) {
	switch v := obj.(type) {
	case map[string]interface{}:
		// Process all map entries recursively
		for _, value := range v {
			normalizeJSONRecursive(value)
		}

	case []interface{}:
		// Sort arrays if they contain strings
		if len(v) > 0 {
			// Check if all elements are strings
			allStrings := true
			for _, item := range v {
				if _, ok := item.(string); !ok {
					allStrings = false
					break
				}
			}

			if allStrings {
				// Convert to string slice, sort, and convert back
				strSlice := make([]string, len(v))
				for i, item := range v {
					strSlice[i] = item.(string)
				}
				sort.Strings(strSlice)

				// Update the original slice in place
				for i, str := range strSlice {
					v[i] = str
				}
			}
		}

		// Process array elements recursively
		for _, item := range v {
			normalizeJSONRecursive(item)
		}
	}
}

// Helper function to build the binary
func buildBinary() (string, error) {
	// Create a temporary directory for the binary
	tempDir, err := os.MkdirTemp("", "linear-mcp-go-build-*")
	if err != nil {
		return "", fmt.Errorf("failed to create temp dir: %w", err)
	}

	// Get the project root directory (parent of cmd directory)
	currentDir, err := os.Getwd()
	if err != nil {
		os.RemoveAll(tempDir)
		return "", fmt.Errorf("failed to get current directory: %w", err)
	}

	// Ensure we're building from the project root
	projectRoot := filepath.Dir(currentDir)
	if filepath.Base(currentDir) != "cmd" {
		// If we're already in the project root, use the current directory
		projectRoot = currentDir
	}

	fmt.Printf("Building binary from project root: %s\n", projectRoot)

	// Build the binary
	binaryPath := filepath.Join(tempDir, "linear-mcp-go")
	cmd := exec.Command("go", "build", "-o", binaryPath)
	cmd.Dir = projectRoot // Set the working directory to the project root

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	if err := cmd.Run(); err != nil {
		os.RemoveAll(tempDir)
		return "", fmt.Errorf("failed to build binary: %w\nstdout: %s\nstderr: %s",
			err, stdout.String(), stderr.String())
	}

	// Verify the binary exists and is executable
	info, err := os.Stat(binaryPath)
	if err != nil {
		os.RemoveAll(tempDir)
		return "", fmt.Errorf("failed to stat binary: %w", err)
	}

	if info.Size() == 0 {
		os.RemoveAll(tempDir)
		return "", fmt.Errorf("binary file is empty")
	}

	// Make sure the binary is executable
	if err := os.Chmod(binaryPath, 0755); err != nil {
		os.RemoveAll(tempDir)
		return "", fmt.Errorf("failed to make binary executable: %w", err)
	}

	fmt.Printf("Successfully built binary at %s (size: %d bytes)\n", binaryPath, info.Size())
	return binaryPath, nil
}

// Helper function to copy a file
func copyFile(src, dst string) error {
	sourceFile, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("failed to open source file: %w", err)
	}
	defer sourceFile.Close()

	destFile, err := os.Create(dst)
	if err != nil {
		return fmt.Errorf("failed to create destination file: %w", err)
	}
	defer destFile.Close()

	if _, err := io.Copy(destFile, sourceFile); err != nil {
		return fmt.Errorf("failed to copy file: %w", err)
	}

	return nil
}

// TestOnaNewlinePreservation specifically tests that the Ona setup preserves newlines and empty lines
func TestOnaNewlinePreservation(t *testing.T) {
	// Build the binary
	binaryPath, err := buildBinary()
	if err != nil {
		t.Fatalf("Failed to build binary: %v", err)
	}
	defer os.RemoveAll(filepath.Dir(binaryPath))

	// Create a temporary directory
	rootDir, err := os.MkdirTemp("", "linear-mcp-go-newline-test-*")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(rootDir)

	// Set up the directory structure
	homeDir := filepath.Join(rootDir, "home")
	configDir := filepath.Join(rootDir, ".ona")
	configPath := filepath.Join(configDir, "mcp-config.json")

	// Create the config directory
	if err := os.MkdirAll(configDir, 0755); err != nil {
		t.Fatalf("Failed to create config directory: %v", err)
	}

	// Create pre-existing config with trailing newlines and empty lines
	originalContent := `{
  "mcpServers": {
    "playwright": {
      "name": "playwright",
      "command": "npx",
      "args": ["-y", "@executeautomation/playwright-mcp-server"]
    }
  }
}


`
	if err := os.WriteFile(configPath, []byte(originalContent), 0644); err != nil {
		t.Fatalf("Failed to create pre-existing config: %v", err)
	}

	// Copy the binary to the temp directory
	tempBinaryPath := filepath.Join(rootDir, "linear-mcp-go")
	if err := copyFile(binaryPath, tempBinaryPath); err != nil {
		t.Fatalf("Failed to copy binary: %v", err)
	}
	if err := os.Chmod(tempBinaryPath, 0755); err != nil {
		t.Fatalf("Failed to make binary executable: %v", err)
	}

	// Set environment variables
	oldHome := os.Getenv("HOME")
	os.Setenv("HOME", homeDir)
	defer os.Setenv("HOME", oldHome)

	oldApiKey := os.Getenv("LINEAR_API_KEY")
	os.Setenv("LINEAR_API_KEY", "test-api-key")
	defer os.Setenv("LINEAR_API_KEY", oldApiKey)

	// Execute the setup command
	cmd := exec.Command(tempBinaryPath, "setup", "--tool=ona", "--write-access=true")
	cmd.Dir = rootDir
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	err = cmd.Run()

	if err != nil {
		t.Fatalf("Setup command failed: %v\nStdout: %s\nStderr: %s", err, stdout.String(), stderr.String())
	}

	// Read the updated config file
	updatedContent, err := os.ReadFile(configPath)
	if err != nil {
		t.Fatalf("Failed to read updated config: %v", err)
	}

	// Check if the trailing newlines are preserved
	updatedStr := string(updatedContent)
	if !strings.HasSuffix(updatedStr, "\n\n\n") {
		t.Errorf("Expected config to end with three newlines, but got:\n%q", updatedStr[len(updatedStr)-10:])
		t.Errorf("Full updated content:\n%q", updatedStr)
	}

	// Verify the JSON is still valid
	var config map[string]interface{}
	if err := json.Unmarshal(updatedContent, &config); err != nil {
		t.Fatalf("Updated config is not valid JSON: %v", err)
	}

	// Verify the linear server was added
	mcpServers, ok := config["mcpServers"].(map[string]interface{})
	if !ok {
		t.Fatalf("mcpServers not found in config")
	}

	linear, ok := mcpServers["linear"].(map[string]interface{})
	if !ok {
		t.Fatalf("linear server not found in mcpServers")
	}

	// Verify linear server configuration
	if linear["disabled"] != false {
		t.Errorf("Expected linear server to be enabled")
	}

	args, ok := linear["args"].([]interface{})
	if !ok || len(args) != 2 || args[0] != "serve" || args[1] != "--write-access=true" {
		t.Errorf("Expected linear server args to be [\"serve\", \"--write-access=true\"], got %v", args)
	}
}

```
Page 4/5FirstPrevNextLast