#
tokens: 48654/50000 22/150 files (page 3/5)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 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

--------------------------------------------------------------------------------
/testdata/fixtures/get_project_handler_By slug.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 732
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetProject($id: String!) {\n\t\t\tproject(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\tslugId\n\t\t\t\tstate\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tlead {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tmembers {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tteams {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinitiatives(first: 10) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstartDate\n\t\t\t\ttargetDate\n\t\t\t}\n\t\t}\n\t","variables":{"id":"mcp-tool-investigation-ae44897e42a7"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"errors":[{"message":"Entity not found: Project","path":["project"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced Project."}}],"data":null}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"113-pUQ9mkDn3KWYiQz0UBE51+d7gJ4"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 1
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 932
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetProjectByNameOrSlug($filter: ProjectFilter) {\n\t\t\tprojects(filter: $filter, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\tslugId\n\t\t\t\t\tstate\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tlead {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t\tmembers {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\temail\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tteams {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tinitiatives(first: 1) {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstartDate\n\t\t\t\t\ttargetDate\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"or":[{"name":{"eq":"mcp-tool-investigation-ae44897e42a7"}},{"slugId":{"eq":"ae44897e42a7"}}]}}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"projects":{"nodes":[{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation","description":"Summary text goes here","slugId":"ae44897e42a7","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/mcp-tool-investigation-ae44897e42a7","createdAt":"2025-06-28T18:06:47.606Z","updatedAt":"2025-06-28T18:07:51.899Z","lead":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"},"members":{"nodes":[{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"}]},"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}]},"initiatives":{"nodes":[{"id":"15e7c1bd-c0c5-4801-ac9a-8e98bf88ea7a","name":"Push for MCP"}]},"startDate":"2025-06-02","targetDate":"2025-06-30"}]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"355-Jji1j11utIAgJU/7ATKvhRyba4g"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/pkg/linear/rate_limiter.go:
--------------------------------------------------------------------------------

```go
package linear

import (
	"fmt"
	"sync"
	"time"
)

// RateLimiter manages API request rate limiting
type RateLimiter struct {
	requestsPerHour int
	minDelayMs      int64
	queue           chan func() error
	processing      bool
	lastRequestTime int64
	requestTimes    []int64
	requestDurations []int64
	mu              sync.Mutex
}

// RateLimiterMetrics contains metrics about the rate limiter
type RateLimiterMetrics struct {
	TotalRequests      int    `json:"totalRequests"`
	RequestsInLastHour int    `json:"requestsInLastHour"`
	AverageRequestTime int64  `json:"averageRequestTime"`
	QueueLength        int    `json:"queueLength"`
	LastRequestTime    int64  `json:"lastRequestTime"`
}

// NewRateLimiter creates a new rate limiter with the specified requests per hour limit
func NewRateLimiter(requestsPerHour int) *RateLimiter {
	minDelayMs := int64(3600000 / requestsPerHour)
	rl := &RateLimiter{
		requestsPerHour:  requestsPerHour,
		minDelayMs:       minDelayMs,
		queue:            make(chan func() error, 100), // Buffer size of 100
		processing:       false,
		lastRequestTime:  0,
		requestTimes:     []int64{},
		requestDurations: []int64{},
	}

	// Start the queue processor
	go rl.processQueue()

	return rl
}

// Enqueue adds a function to the rate limiter queue
func (rl *RateLimiter) Enqueue(fn func() error, operation string) error {
	startTime := time.Now().UnixMilli()
	queuePosition := len(rl.queue)

	fmt.Printf("[Linear API] Enqueueing request%s (Queue position: %d)\n", 
		formatOperation(operation), queuePosition)

	// Create a channel to receive the result
	resultCh := make(chan error, 1)

	// Wrap the function to capture its result
	wrappedFn := func() error {
		fmt.Printf("[Linear API] Starting request%s\n", formatOperation(operation))
		result := fn()
		endTime := time.Now().UnixMilli()
		duration := endTime - startTime

		fmt.Printf("[Linear API] Completed request%s (Duration: %dms)\n", 
			formatOperation(operation), duration)
		
		rl.trackRequest(startTime, endTime, operation)
		resultCh <- result
		return result
	}

	// Add to queue
	rl.queue <- wrappedFn

	// Wait for the result
	err := <-resultCh
	return err
}

// Batch processes a batch of items with the rate limiter
func (rl *RateLimiter) Batch(items []interface{}, batchSize int, fn func(item interface{}) error, operation string) []error {
	results := make([]error, len(items))
	var wg sync.WaitGroup

	for i := 0; i < len(items); i += batchSize {
		end := i + batchSize
		if end > len(items) {
			end = len(items)
		}

		batch := items[i:end]
		wg.Add(len(batch))

		for j, item := range batch {
			index := i + j
			go func(idx int, itm interface{}) {
				defer wg.Done()
				err := rl.Enqueue(func() error {
					return fn(itm)
				}, operation)
				results[idx] = err
			}(index, item)
		}

		wg.Wait()
	}

	return results
}

// processQueue processes the queue of functions
func (rl *RateLimiter) processQueue() {
	for {
		// If there are no items in the queue, wait for one
		fn := <-rl.queue

		// Process the item
		rl.mu.Lock()
		now := time.Now().UnixMilli()
		timeSinceLastRequest := now - rl.lastRequestTime

		// Check if we need to wait to respect rate limits
		requestsInLastHour := 0
		oneHourAgo := now - 3600000
		for _, t := range rl.requestTimes {
			if t > oneHourAgo {
				requestsInLastHour++
			}
		}

		if requestsInLastHour >= int(float64(rl.requestsPerHour)*0.9) && timeSinceLastRequest < rl.minDelayMs {
			waitTime := rl.minDelayMs - timeSinceLastRequest
			rl.mu.Unlock()
			time.Sleep(time.Duration(waitTime) * time.Millisecond)
		} else {
			rl.mu.Unlock()
		}

		// Execute the function
		rl.mu.Lock()
		rl.lastRequestTime = time.Now().UnixMilli()
		rl.mu.Unlock()

		_ = fn() // Execute the function
	}
}

// trackRequest tracks a request for metrics
func (rl *RateLimiter) trackRequest(startTime, endTime int64, operation string) {
	duration := endTime - startTime

	rl.mu.Lock()
	defer rl.mu.Unlock()

	rl.requestTimes = append(rl.requestTimes, startTime)
	rl.requestDurations = append(rl.requestDurations, duration)

	// Keep only the last hour of requests
	oneHourAgo := time.Now().UnixMilli() - 3600000
	var newRequestTimes []int64
	var newRequestDurations []int64

	for i, t := range rl.requestTimes {
		if t > oneHourAgo {
			newRequestTimes = append(newRequestTimes, t)
			newRequestDurations = append(newRequestDurations, rl.requestDurations[i])
		}
	}

	rl.requestTimes = newRequestTimes
	rl.requestDurations = newRequestDurations
}

// GetMetrics returns metrics about the rate limiter
func (rl *RateLimiter) GetMetrics() RateLimiterMetrics {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	now := time.Now().UnixMilli()
	oneHourAgo := now - 3600000
	recentRequests := 0

	for _, t := range rl.requestTimes {
		if t > oneHourAgo {
			recentRequests++
		}
	}

	var avgRequestTime int64 = 0
	if len(rl.requestDurations) > 0 {
		var sum int64 = 0
		for _, d := range rl.requestDurations {
			sum += d
		}
		avgRequestTime = sum / int64(len(rl.requestDurations))
	}

	return RateLimiterMetrics{
		TotalRequests:      len(rl.requestTimes),
		RequestsInLastHour: recentRequests,
		AverageRequestTime: avgRequestTime,
		QueueLength:        len(rl.queue),
		LastRequestTime:    rl.lastRequestTime,
	}
}

// Helper function to format operation name for logging
func formatOperation(operation string) string {
	if operation != "" {
		return " for " + operation
	}
	return ""
}

```

--------------------------------------------------------------------------------
/pkg/tools/milestone_tools.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"
	"strings"

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

var GetMilestoneTool = mcp.NewTool("linear_get_milestone",
	mcp.WithDescription("Get a single project milestone by its ID or name."),
	mcp.WithString("milestone", mcp.Required(), mcp.Description("The ID or name of the project milestone to get.")),
)

func GetMilestoneHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		milestoneIdentifier, err := request.RequireString("milestone")
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
		}

		milestone, err := linearClient.GetMilestone(milestoneIdentifier)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get milestone: %v", err)}}}, nil
		}

		resultText := FormatMilestone(*milestone)

		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
	}
}

func FormatMilestone(milestone linear.ProjectMilestone) string {
	var builder strings.Builder
	builder.WriteString(fmt.Sprintf("Milestone: %s\n", milestone.Name))
	builder.WriteString(fmt.Sprintf("  ID: %s\n", milestone.ID))
	if milestone.Description != "" {
		builder.WriteString(fmt.Sprintf("  Description: %s\n", milestone.Description))
	}
	if milestone.TargetDate != nil {
		builder.WriteString(fmt.Sprintf("  Target Date: %s\n", *milestone.TargetDate))
	}
	if milestone.Project != nil {
		builder.WriteString(fmt.Sprintf("  Project: %s (%s)\n", milestone.Project.Name, milestone.Project.ID))
	}
	return builder.String()
}

var UpdateMilestoneTool = mcp.NewTool("linear_update_milestone",
	mcp.WithDescription("Update an existing project milestone."),
	mcp.WithString("milestone", mcp.Required(), mcp.Description("The ID or name of the milestone to update.")),
	mcp.WithString("name", mcp.Description("The new name of the milestone.")),
	mcp.WithString("description", mcp.Description("The new description of the milestone.")),
	mcp.WithString("targetDate", mcp.Description("The new target date of the milestone (YYYY-MM-DD).")),
)

func UpdateMilestoneHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		milestoneIdentifier, err := request.RequireString("milestone")
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
		}

		// Get the milestone first to get its ID
		mil, err := linearClient.GetMilestone(milestoneIdentifier)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get milestone: %v", err)}}}, nil
		}

		name := request.GetString("name", "")
		description := request.GetString("description", "")
		targetDate := request.GetString("targetDate", "")

		input := linear.ProjectMilestoneUpdateInput{
			Name:        name,
			Description: description,
			TargetDate:  targetDate,
		}

		milestone, err := linearClient.UpdateMilestone(mil.ID, input)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to update milestone: %v", err)}}}, nil
		}

		resultText := FormatMilestone(*milestone)

		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
	}
}

var CreateMilestoneTool = mcp.NewTool("linear_create_milestone",
	mcp.WithDescription("Create a new project milestone."),
	mcp.WithString("name", mcp.Required(), mcp.Description("The name of the milestone.")),
	mcp.WithString("projectId", mcp.Required(), mcp.Description("The ID of the project to create the milestone in.")),
	mcp.WithString("description", mcp.Description("The description of the milestone.")),
	mcp.WithString("targetDate", mcp.Description("The target date of the milestone (YYYY-MM-DD).")),
)

func CreateMilestoneHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		name, err := request.RequireString("name")
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
		}

		projectID, err := request.RequireString("projectId")
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
		}

		description := request.GetString("description", "")
		targetDate := request.GetString("targetDate", "")

		input := linear.ProjectMilestoneCreateInput{
			Name:        name,
			ProjectID:   projectID,
			Description: description,
			TargetDate:  targetDate,
		}

		milestone, err := linearClient.CreateMilestone(input)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to create milestone: %v", err)}}}, nil
		}

		resultText := FormatMilestone(*milestone)

		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
	}
}

```

--------------------------------------------------------------------------------
/testdata/fixtures/add_comment_handler_Reply with shorthand.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 322
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n\t\t\tissues(filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } }, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"number":10,"teamKey":"TEST"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 1
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 255
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetCommentByHash($hash: String!) {\n\t\t\tcomment(hash: $hash) {\n\t\t\t\tid\n\t\t\t\tbody\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tuser {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"hash":"ae3d62d6"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"comment":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","body":"Updated comment text via hash","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-ae3d62d6","createdAt":"2025-03-30T13:37:20.666Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"13e-dbWT1UL53WBZl2BVydYcoWd4NlQ"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 2
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 566
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tmutation AddComment($input: CommentCreateInput!) {\n\t\t\tcommentCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tcomment {\n\t\t\t\t\tid\n\t\t\t\t\tbody\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tuser {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tissue {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tidentifier\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\turl\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"body":"Reply using shorthand","issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f","parentId":"ae3d62d6-3f40-4990-867b-5c97dd265a40"}}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"commentCreate":{"success":true,"comment":{"id":"c414a6fe-c32c-40bf-a57e-07f032330bc3","body":"Reply using shorthand","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-c414a6fe","createdAt":"2025-10-07T16:13:08.292Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue"}}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"210-nCgczohGn3/AmxaCQPEau4rHXRE"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/add_comment_handler_Reply with URL.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 322
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n\t\t\tissues(filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } }, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"number":10,"teamKey":"TEST"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 1
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 255
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetCommentByHash($hash: String!) {\n\t\t\tcomment(hash: $hash) {\n\t\t\t\tid\n\t\t\t\tbody\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tuser {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"hash":"ae3d62d6"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"comment":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","body":"Updated comment text via hash","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-ae3d62d6","createdAt":"2025-03-30T13:37:20.666Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"13e-dbWT1UL53WBZl2BVydYcoWd4NlQ"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 2
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 568
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tmutation AddComment($input: CommentCreateInput!) {\n\t\t\tcommentCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tcomment {\n\t\t\t\t\tid\n\t\t\t\t\tbody\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tuser {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tissue {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tidentifier\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\turl\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"body":"Reply using comment URL","issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f","parentId":"ae3d62d6-3f40-4990-867b-5c97dd265a40"}}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"commentCreate":{"success":true,"comment":{"id":"3c4e53f0-6fef-4d32-a03d-388333aa0157","body":"Reply using comment URL","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-3c4e53f0","createdAt":"2025-10-07T16:13:07.925Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue"}}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"212-gMY3ZFQiv9fC2AkRJXlMJ63RLWo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/reply_to_comment_handler_Reply with URL.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 255
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetCommentByHash($hash: String!) {\n\t\t\tcomment(hash: $hash) {\n\t\t\t\tid\n\t\t\t\tbody\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tuser {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"hash":"ae3d62d6"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"comment":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","body":"Updated comment text via hash","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-ae3d62d6","createdAt":"2025-03-30T13:37:20.666Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"13e-dbWT1UL53WBZl2BVydYcoWd4NlQ"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 1
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 333
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetComment($id: String!) {\n\t\t\tcomment(id: $id) {\n\t\t\t\tid\n\t\t\t\tbody\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tuser {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"comment":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","body":"Updated comment text via hash","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-ae3d62d6","createdAt":"2025-03-30T13:37:20.666Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10"}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"18b-JtoAyZ6gspXHMOEbvedWimpK4Y0"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 2
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 578
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tmutation AddComment($input: CommentCreateInput!) {\n\t\t\tcommentCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tcomment {\n\t\t\t\t\tid\n\t\t\t\t\tbody\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tuser {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tissue {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tidentifier\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\turl\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"body":"Reply using URL in dedicated tool","issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f","parentId":"ae3d62d6-3f40-4990-867b-5c97dd265a40"}}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"commentCreate":{"success":true,"comment":{"id":"9f06f784-4132-4fae-bf2c-9065365759e3","body":"Reply using URL in dedicated tool","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-9f06f784","createdAt":"2025-10-07T13:55:14.826Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue"}}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"21c-IIg5wnDeEvhS6hopc1qa1TAVMgM"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/docs/prd/001-api-refresher.md:
--------------------------------------------------------------------------------

```markdown
# PRD: Enhance `linear_create_issue` Tool

**Version:** 1.2
**Date:** 2025-03-30
**Status:** Proposed

## 1. Introduction

This document outlines the requirements for completing the implementation of enhancements to the `linear_create_issue` MCP tool within the Linear MCP Server project. This includes adding support for more user-friendly label and parent issue identifiers.

## 2. Goals

-   Enable users to create sub-issues by specifying a parent issue ID or identifier (e.g., "TEAM-123").
-   Enable users to assign labels during issue creation by specifying label IDs or names.
-   Ensure the underlying Linear API client supports these new parameters.
-   Maintain consistency with existing tool design and project patterns.
-   Provide comprehensive test coverage for the new functionality.

## 3. Requirements / Implementation Plan

The following steps are required to complete the implementation:

1.  **Modify Linear Client Data Structures (`pkg/linear/models.go`):**
    *   No changes needed.

2.  **Update Linear Client API Call (`pkg/linear/client.go`):**
    *   No changes needed.

3.  **Enhance MCP Tool Definition (`pkg/server/tools.go`):**
    *   In `RegisterTools`, update the `linear_create_issue` tool definition:
        *   Update `parentIssue` description to: "Optional parent issue ID or identifier (e.g., 'TEAM-123') to create a sub-issue".
        *   Update `labels` description to: "Optional comma-separated list of label IDs or names to assign".

4.  **Implement Label Name Resolution (`pkg/server/tools.go`):**
    *   Create a `resolveLabelIdentifiers` function that:
        *   Takes a Linear client, team ID, and comma-separated string of label names/UUIDs
        *   Splits the string into individual identifiers
        *   For each identifier:
            *   If it's a valid UUID, add it directly to the result list
            *   Otherwise, query the Linear API for labels with the given name
            *   If a label is not found, return an error
        *   Returns a slice of label UUIDs
    *   GraphQL query for finding labels by name:
    ```graphql
    query GetLabelsByName($teamId: ID!, $names: [String!]!) {
      team(id: $teamId) {
        labels(filter: { name: { in: $names } }) {
          nodes {
            id
            name
          }
        }
      }
    }
    ```

5.  **Implement Parent Issue Identifier Resolution (`pkg/server/tools.go`):**
    *   Create a `resolveParentIssueIdentifier` function that:
        *   Takes a Linear client and parent issue identifier (UUID or "TEAM-123")
        *   If it's a valid UUID, returns it directly
        *   Otherwise, queries the Linear API for an issue with that identifier
        *   If an issue is found, returns its UUID
        *   If an issue is not found, returns an error
    *   GraphQL query for finding an issue by identifier:
    ```graphql
    query GetIssueByIdentifier($identifier: String!) {
      issueSearch(filter: { identifier: { eq: $identifier } }, first: 1) {
        nodes {
          id
        }
      }
    }
    ```

6.  **Update MCP Tool Handler (`pkg/server/tools.go`):**
    *   In `CreateIssueHandler`:
        *   Extract optional `parentIssue` and `labels` arguments
        *   If `parentIssue` is present, use `resolveParentIssueIdentifier` to get the parent issue UUID
        *   If `labels` is present, use `resolveLabelIdentifiers` to get the label UUIDs
        *   Update error handling to provide clear messages when resolution fails
        *   Populate the `ParentID` and `LabelIDs` fields in the `linear.CreateIssueInput` struct

7.  **Expand Test Coverage (`pkg/server/tools_test.go`):**
    *   Add new test cases for `create_issue`:
        *   Test creating a sub-issue using the issue identifier (e.g., "TEST-123")
        *   Test creating an issue with labels using label names
        *   Test creating a sub-issue with labels using a mix of label names and UUIDs
    *   Update existing test names and potentially arguments if needed for clarity

8.  **Update Test Fixtures (`testdata/fixtures/` & `testdata/golden/`):**
    *   Re-record VCR fixtures for new/modified `create_issue_handler` tests using `go test -v -recordWrites=true ./...`
        *   Requires a valid `LINEAR_API_KEY`
        *   Requires appropriate parent issues and labels in the test workspace
    *   Update corresponding `.golden` files with expected output

9.  **Documentation (`README.md`, `memory-bank/`):**
    *   Update `README.md` usage examples for `linear_create_issue`
    *   Update `memory-bank/progress.md` upon completion
    *   Update `.clinerules` if new patterns emerge

## 4. Resolution Workflows

### Label Resolution Workflow

```mermaid
flowchart TD
    A[Input: Comma-separated label names/UUIDs] --> B[Split into individual identifiers]
    B --> C{For each identifier}
    C --> D{Is valid UUID?}
    D -->|Yes| E[Add to result list]
    D -->|No| F[Query Linear API for label by name]
    F --> G{Label found?}
    G -->|Yes| H[Add label UUID to result list]
    G -->|No| I[Return error]
    H --> C
    E --> C
    C --> J[Return list of label UUIDs]
```

### Parent Issue Resolution Workflow

```mermaid
flowchart TD
    A[Input: Parent issue identifier] --> B{Is valid UUID?}
    B -->|Yes| C[Return UUID directly]
    B -->|No| D[Query Linear API for issue by identifier]
    D --> E{Issue found?}
    E -->|Yes| F[Return issue UUID]
    E -->|No| G[Return error]
```

## 5. Success Criteria

-   The `linear_create_issue` tool successfully creates sub-issues when `parentIssue` is provided as an ID or identifier.
-   The `linear_create_issue` tool successfully assigns labels when `labels` are provided as IDs or names.
-   All new test cases pass, including VCR playback.
-   Documentation accurately reflects the tool's updated capabilities.

## 6. Implementation Decisions

-   **Input Validation**: Delegate to Linear API for input validation as much as possible, rather than implementing extensive client-side validation.
-   **Label Limits**: No limits will be imposed on the number of labels that can be assigned at once.
-   **Error Handling**: Provide clear error messages when resolution fails, but rely on Linear API for validation of the resolved UUIDs.

```

--------------------------------------------------------------------------------
/pkg/tools/common.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"fmt"
	"strings"

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

// 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
}

// resolveParentIssueIdentifier is an alias for resolveIssueIdentifier for backward compatibility
func resolveParentIssueIdentifier(linearClient *linear.LinearClient, identifier string) (string, error) {
	return resolveIssueIdentifier(linearClient, identifier)
}

// 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
	// Get the organization to access all users
	org, err := linearClient.GetOrganization()
	if err != nil {
		return "", fmt.Errorf("failed to get organization: %v", err)
	}

	// First try exact match on name or email
	for _, user := range org.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 org.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)
}

// resolveLabelIdentifiers resolves a list of label identifiers (UUIDs or names) to UUIDs
func resolveLabelIdentifiers(linearClient *linear.LinearClient, teamID string, labelIdentifiers []string) ([]string, error) {
	// Separate UUIDs and names
	var labelUUIDs []string
	var labelNames []string

	for _, identifier := range labelIdentifiers {
		if isValidUUID(identifier) {
			labelUUIDs = append(labelUUIDs, identifier)
		} else {
			labelNames = append(labelNames, identifier)
		}
	}

	// If there are no names to resolve, return the UUIDs directly
	if len(labelNames) == 0 {
		return labelUUIDs, nil
	}

	// Get labels by name
	labels, err := linearClient.GetLabelsByName(teamID, labelNames)
	if err != nil {
		return nil, fmt.Errorf("failed to get labels by name: %v", err)
	}

	// Check if all label names were found
	if len(labels) < len(labelNames) {
		// Create a map of found label names for quick lookup
		foundLabels := make(map[string]bool)
		for _, label := range labels {
			foundLabels[label.Name] = true
		}

		// Find which label names were not found
		var missingLabels []string
		for _, name := range labelNames {
			if !foundLabels[name] {
				missingLabels = append(missingLabels, name)
			}
		}

		return nil, fmt.Errorf("label(s) not found: %s", strings.Join(missingLabels, ", "))
	}

	// Add the resolved label UUIDs to the result
	for _, label := range labels {
		labelUUIDs = append(labelUUIDs, label.ID)
	}

	return labelUUIDs, nil
}

// isValidUUID checks if a string is a valid UUID
func isValidUUID(uuidStr string) bool {
	return uuid.Validate(uuidStr) == nil
}

// extractCommentHashFromURL extracts the comment hash from various URL formats
// Supports:
//   - https://linear.app/.../issue/TEST-10/...#comment-abc123
//   - #comment-abc123
func extractCommentHashFromURL(identifier string) string {
	// Check if it contains a URL fragment with comment
	if strings.Contains(identifier, "#comment-") {
		// Extract everything after #comment-
		parts := strings.Split(identifier, "#comment-")
		if len(parts) >= 2 {
			return parts[1]
		}
	}
	return ""
}

// resolveCommentIdentifier resolves a comment identifier (UUID, URL, or shorthand like "comment-53099b37") to a UUID
func resolveCommentIdentifier(linearClient *linear.LinearClient, identifier string) (string, error) {
	// If it's a valid UUID, use it directly
	if isValidUUID(identifier) {
		return identifier, nil
	}

	// Try to extract hash from URL or fragment
	var hash string
	if strings.HasPrefix(identifier, "comment-") {
		hash = strings.TrimPrefix(identifier, "comment-")
	} else if strings.Contains(identifier, "linear.app") && strings.Contains(identifier, "#comment-") {
		hash = extractCommentHashFromURL(identifier)
	}

	if hash == "" {
		// Fallback: assume it's already just the hash part
		hash = identifier
	}
	comment, err := linearClient.GetCommentByHash(hash)
	if err != nil {
		return "", fmt.Errorf("failed to resolve comment identifier '%s': %v", identifier, err)
	}

	return comment.ID, nil
}

// resolveTeamIdentifier resolves a team identifier (UUID, name, or key) to a team ID
func resolveTeamIdentifier(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 team by name or key
	teams, err := linearClient.GetTeams("")
	if err != nil {
		return "", fmt.Errorf("failed to get teams: %v", err)
	}

	// First try exact match on name or key
	for _, team := range teams {
		if team.Name == identifier || team.Key == identifier {
			return team.ID, nil
		}
	}

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

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

// resolveProjectIdentifier resolves a project identifier (UUID, name, or slug) to a project ID
func resolveProjectIdentifier(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 get the project by identifier (name or slug)
	project, err := linearClient.GetProject(identifier)
	if err != nil {
		return "", fmt.Errorf("failed to resolve project identifier '%s': %v", identifier, err)
	}

	return project.ID, nil
}

```

--------------------------------------------------------------------------------
/memory-bank/techContext.md:
--------------------------------------------------------------------------------

```markdown
# Technical Context: Linear MCP Server

## Technologies Used

### Programming Language
- **Go**: Version 1.23.6
  - Chosen for its performance, strong typing, and concurrency support
  - Excellent standard library for HTTP requests and JSON handling

### Key Libraries
1. **github.com/mark3labs/mcp-go v0.8.5**
   - MCP protocol implementation for Go
   - Provides server, tool registration, and request/response handling

2. **github.com/spf13/cobra v1.9.1**
   - Command-line interface framework for Go
   - Provides subcommand support and flag handling

3. **gopkg.in/dnaeon/go-vcr.v4 v4.0.2**
   - HTTP interaction recording and playback for testing
   - Allows tests to run without actual API calls

### APIs
- **Linear API**
  - REST API for Linear issue tracking system
  - Requires API key authentication
  - Has rate limiting constraints

## Development Setup

### Prerequisites
- Go 1.23 or higher
- Linear API key

### Environment Variables
- `LINEAR_API_KEY`: Required for authentication with Linear API

### Build Process
```bash
# Build the server
go build

# Run the server in read-only mode (default)
./linear-mcp-go server

# Run the server with write operations enabled
./linear-mcp-go server --write-access

# Set up for Cline (default)
./linear-mcp-go setup --api-key=your_linear_api_key

# Set up with write access enabled
./linear-mcp-go setup --api-key=your_linear_api_key --write-access
```

### Command-Line Structure
- **Root Command**: Base command for the application
- **Subcommands**:
  - `server`: Starts the Linear MCP server
  - `setup`: Sets up the Linear MCP server for use with an AI assistant

### Command-Line Flags
- **Server Command**:
  - `--write-access`: Controls whether write operations are enabled (default: false)
    - When false, write tools (`linear_create_issue`, `linear_update_issue`, `linear_add_comment`) are disabled
    - When true, all tools are available

- **Setup Command**:
  - `--api-key`: Linear API key (required)
  - `--tool`: The AI assistant tool to set up for (default: cline)
  - `--write-access`: Enable write operations (default: false)

### Testing
```bash
# Run tests with existing recordings
go test -v ./...

# Re-record tests (requires LINEAR_API_KEY)
go test -v -record=true ./...

# Re-record all tests including state-changing ones
go test -v -recordWrites=true ./...
```

## Technical Constraints

### Linear API Limitations
1. **Rate Limiting**
   - Linear API has rate limits that must be respected
   - The server implements rate limiting to prevent quota exhaustion

2. **Authentication**
   - Requires API key passed via environment variable
   - No support for OAuth or other authentication methods

### MCP Protocol Constraints
1. **Communication Channel**
   - MCP server communicates via stdin/stdout
   - No HTTP or other network protocols for MCP communication

2. **Tool Schema**
   - Tools must define their parameters using MCP schema format
   - Parameters can be required or optional with descriptions

### Deployment Constraints
1. **Binary Distribution**
   - Server is distributed as a compiled binary
   - Binaries should be available for major platforms (Linux, macOS, Windows)

2. **Environment**
   - Requires environment variables to be set
   - No configuration file support currently

## Dependencies

### Direct Dependencies
```
github.com/mark3labs/mcp-go v0.8.5
github.com/spf13/cobra v1.9.1
gopkg.in/dnaeon/go-vcr.v4 v4.0.2
```

### Indirect Dependencies
```
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/inconshreveable/mousetrap v1.1.0
github.com/spf13/pflag v1.0.6
gopkg.in/yaml.v3 v3.0.1
```

## Version Information
- **Server Version**: 1.0.0 (defined in pkg/server/server.go)
- **Go Version**: 1.23.6 (defined in go.mod)
- **MCP SDK Version**: 0.8.5

## Build and Release Process
- GitHub Actions workflow for automated testing and releases
- Releases are created when tags matching "v*" are pushed
- Binaries are built for Linux, macOS, and Windows

## Code Style Guidelines

### Go Code Standards
- **Formatting**: Use standard Go formatting (gofmt) - all code must be properly formatted
- **Error Handling**: Follow Go best practices - return errors, don't panic in normal operation
- **Naming Conventions**: 
  - Use descriptive variable and function names that clearly indicate purpose
  - Follow Go naming conventions (camelCase for private, PascalCase for public)
- **Documentation**: Add comments for all exported functions and types
- **Code Organization**: Group related functionality together, separate concerns clearly

### Testing Patterns

#### go-vcr Usage
- **Test Fixtures**: All HTTP interactions are recorded using go-vcr
- **Fixture Storage**: Test fixtures stored in `testdata/fixtures/` directory
- **Replay Mode**: Tests run without Linear API key using recorded fixtures
- **Recording Flags**:
  - `-record=true`: Re-record tests (requires LINEAR_API_KEY)
  - `-recordWrites=true`: Re-record all tests including state-changing operations

#### Test Organization
- Each tool handler has comprehensive test coverage
- Test cases cover both success and error scenarios
- Golden files in `testdata/golden/` contain expected output
- Tests can run in isolation without external dependencies

### Development Commands

#### Building
```bash
# Build the server
go build

# Build with specific output name
go build -o linear-mcp-server
```

#### Testing
```bash
# Run tests with existing recordings
go test -v ./...

# Re-record tests (requires LINEAR_API_KEY)
go test -v -record=true ./...

# Re-record all tests including state-changing ones
go test -v -recordWrites=true ./...

# Run specific test
go test -v -run TestSpecificFunction ./...
```

#### Running
```bash
# Run server in read-only mode (default)
./linear-mcp-go server

# Run server with write operations enabled
./linear-mcp-go server --write-access

# Set up for AI assistant
./linear-mcp-go setup --api-key=your_linear_api_key
```

## Future Technical Considerations
1. **Configuration File Support**
   - Could add support for configuration files instead of just environment variables

2. **Additional Linear API Features**
   - More Linear API endpoints could be exposed as MCP tools

3. **Improved Error Handling**
   - More detailed error messages and recovery strategies

4. **Metrics and Logging**
   - Add structured logging and metrics collection

5. **Rate Limiting Enhancements**
   - Make rate limits configurable
   - Add more sophisticated rate limiting strategies

6. **Authentication Methods**
   - Support for OAuth or other authentication methods beyond API keys

```

--------------------------------------------------------------------------------
/docs/design/001-mcp-go-upgrade.md:
--------------------------------------------------------------------------------

```markdown
# Design Doc: `mcp-go` Library Upgrade

**Author:** Cline
**Date:** 2025-06-28
**Status:** Proposed

## 1. Abstract

This document outlines the plan to upgrade the `mcp-go` dependency from its current version (`v0.18.0`) to the latest version provided. The upgrade is necessary to leverage new features, performance improvements, and bug fixes in the library. This document details the scope, identifies breaking changes, and provides a phased implementation plan to ensure a smooth and successful migration.

## 2. Background and Motivation

The Linear MCP Server currently relies on `mcp-go v0.18.0`. A new version of the library is available and has been cloned to `context/mcp-go` for reference. Analysis of the new version reveals significant API improvements and breaking changes. Upgrading will align the project with the latest MCP specification, improve developer experience through a more robust API, and ensure long-term maintainability.

## 3. Key Breaking Changes

Analysis of the new `mcp-go` library reveals three primary areas of breaking changes that will drive the refactoring effort.

### 3.1. Tool Definition and Schema

The API for defining tool schemas has been completely redesigned from a monolithic `WithInputSchema` method to a more granular, fluent builder pattern.

-   **Old Approach:** A single `WithInputSchema` call with a nested `mcp.Object` map.
-   **New Approach:** A series of top-level builder functions (`mcp.WithString`, `mcp.WithNumber`, etc.) are passed directly to `mcp.NewTool`. Property attributes like descriptions and requirement constraints are now handled by `PropertyOption` functions (`mcp.Description`, `mcp.Required`).

### 3.2. Tool Argument Parsing

The new `mcp.CallToolRequest` struct introduces a suite of type-safe methods for accessing tool arguments, deprecating the need for manual map access and type assertions.

-   **Old Approach:** `request.GetArguments()` returned a `map[string]any`, requiring developers to perform manual key lookups and type assertions.
-   **New Approach:** Methods like `request.RequireString("key")`, `request.GetInt("key", 0)`, and `request.BindArguments(&myStruct)` provide robust, type-safe access to arguments.

### 3.3. Tool Result Construction

The mechanism for returning results from a tool handler has been fundamentally changed. The simple, single-type helper functions have been replaced by a more flexible, multi-content structure.

-   **Old Approach:** Helper functions like `mcp.NewToolResultText(...)` and `mcp.NewToolResultError(...)` were used to create the result.
-   **New Approach:** The `mcp.CallToolResult` struct now contains a `Content` slice. Successful results are returned by populating this slice with `mcp.Content` objects (e.g., `mcp.TextContent`). Errors are handled by populating the `Content` slice with an error message and setting the `IsError` boolean flag to `true`.

## 4. Proposed Implementation Plan

The upgrade will be performed in four distinct phases to ensure a controlled and verifiable migration.

### Phase 1: Dependency Update

The first step is to instruct the Go compiler to use the new version of the library.

1.  **Update `go.mod`:** A `replace` directive will be added to `go.mod` to point the `github.com/mark3labs/mcp-go` module to the local directory `context/mcp-go`.
2.  **Tidy Dependencies:** `go mod tidy` will be executed to resolve the new dependency tree and remove unused old dependencies.

### Phase 2: Refactor Tool Definitions

All tool registration sites will be updated to use the new schema definition API.

1.  **Locate Tool Registrations:** All calls to `server.AddTool` will be identified.
2.  **Rewrite Schemas:** The `WithInputSchema` calls will be replaced with the new fluent builder pattern (`mcp.WithString`, `mcp.WithNumber`, etc.).

**Example Transformation:**

```go
// OLD
mcp.NewTool("get_issue").
    WithDescription("...").
    WithInputSchema(mcp.Object{
        "issue": mcp.Required(mcp.String()).WithDescription("..."),
    })

// NEW
mcp.NewTool("get_issue",
    mcp.WithDescription("..."),
    mcp.WithString("issue",
        mcp.Required(),
        mcp.Description("..."),
    ),
)
```

### Phase 3: Refactor Tool Handlers

This is the most significant phase, requiring changes to the logic within every tool handler.

1.  **Update Argument Parsing:** All manual argument map access will be replaced with the new type-safe methods on `mcp.CallToolRequest`.
2.  **Update Result Construction:** All `return` statements will be refactored to construct the `mcp.CallToolResult` struct with the appropriate `Content` slice and `IsError` flag.

**Example Transformation (Success):**

```go
// OLD
return mcp.NewToolResultText("Success!"), nil

// NEW
return &mcp.CallToolResult{
    Content: []mcp.Content{
        mcp.TextContent{Type: "text", Text: "Success!"},
    },
}, nil
```

**Example Transformation (Error):**

```go
// OLD
return mcp.NewToolResultError("Error message"), nil

// NEW
return &mcp.CallToolResult{
    IsError: true,
    Content: []mcp.Content{
        mcp.TextContent{Type: "text", Text: "Error message"},
    },
}, nil
```

### Phase 4: Compilation and Testing

The final phase is to verify the correctness of the refactored code.

1.  **Compile Project:** The entire project will be compiled to ensure there are no build errors.
2.  **Update and Run Tests:** The existing test suite will be executed. It is anticipated that tests will fail due to the API changes. Test code, particularly mock object creation and result assertions, will be updated to align with the new library version. All tests must pass before the upgrade is considered complete.

## 5. Risks and Mitigation

-   **Risk:** Unforeseen breaking changes.
    -   **Mitigation:** The phased approach allows for isolating and addressing issues systematically. The initial file analysis was thorough, but compilation will be the ultimate verification.
-   **Risk:** Logic errors introduced during refactoring.
    -   **Mitigation:** The existing test suite provides a safety net. All tests will be run and updated to ensure existing functionality is preserved.

## 6. Success Criteria

-   The project successfully compiles against the new `mcp-go` library version.
-   All existing tests pass after being updated for the new API.
-   The server runs correctly and all tools function as expected.
-   The `go.mod` file correctly references the new library path.

## 7. Progress Tracking

-   [x] **Phase 1: Dependency Update**
    -   [x] Update `go.mod` to `v0.32.0`.
    -   [x] Run `go mod tidy`.
-   [x] **Phase 2: Refactor Tool Definitions**
    -   [x] Refactor `linear_create_issue`
    -   [x] Refactor `linear_update_issue`
    -   [x] Refactor `linear_add_comment`
    -   [x] Refactor `linear_get_issue`
    -   [x] Refactor `linear_get_issue_comments`
    -   [x] Refactor `linear_get_teams`
    -   [x] Refactor `linear_get_user_issues`
    -   [x] Refactor `linear_search_issues`
-   [x] **Phase 3: Refactor Tool Handlers**
    -   [x] Refactor `createIssueHandler`
    -   [x] Refactor `updateIssueHandler`
    -   [x] Refactor `addCommentHandler`
    -   [x] Refactor `getIssueHandler`
    -   [x] Refactor `getIssueCommentsHandler`
    -   [x] Refactor `getTeamsHandler`
    -   [x] Refactor `getUserIssuesHandler`
    -   [x] Refactor `searchIssuesHandler`
-   [x] **Phase 4: Compilation and Testing**
    -   [x] Compile project successfully.
    -   [x] Update and pass all tests.

```

--------------------------------------------------------------------------------
/docs/prd/002-tool-standardization.md:
--------------------------------------------------------------------------------

```markdown
# Product Requirements Document: Linear MCP Server Tool Standardization

## Overview
This document outlines the requirements for standardizing the Linear MCP Server tools according to a set of consistent rules. These rules aim to improve user experience, code maintainability, and consistency across all tools.

## Background
The Linear MCP Server currently provides several tools for interacting with the Linear API through the Model Context Protocol (MCP). While these tools are functional, they lack consistency in their descriptions, parameter handling, and result formatting. This inconsistency can lead to confusion for users and maintenance challenges for developers.

## Requirements

### 1. Concise Tool Descriptions
**Current State:** Tool descriptions are verbose and often contain parameter listings and result format explanations.

**Requirement:** Tool descriptions should be concise and focus only on the tool's purpose and functionality. They should not:
- List parameters (these are already defined in the schema)
- Explain the result format (this should be consistent across tools)

### 2. Flexible Object Identifier Resolution
**Current State:** Some tools (like `create_issue`) already support resolving different types of identifiers (UUID, name, key) to the underlying UUID, but this is not consistent across all tools.

**Requirement:** All input arguments that reference Linear objects should:
- Accept multiple forms of identification (UUID, name, key)
- Resolve these identifiers to the underlying UUID using appropriate resolution functions
- Use consistent resolution methods across all tools

### 3. Consistent Entity Rendering
**Current State:** Entity rendering in results varies across tools, with inconsistent formatting and information hierarchy.

**Requirement:** Tools fetching the same entities should:
- Emit results using the same format
- Add any tool-specific additional fields at the bottom of the result
- Use code reuse between different tools to ensure consistency

This requirement has two distinct parts:

1. **Full Entity Rendering**:
   - When displaying an entity as the primary subject of a response, use a consistent format with all required fields
   - Implement shared formatting functions (e.g., `formatIssue`, `formatTeam`) that include all necessary information
   - Example: When getting an issue with `linear_get_issue`, display the full issue details

2. **Entity Identifier Rendering**:
   - When referencing an entity from another entity, use a consistent, concise identifier format
   - Implement shared identifier formatting functions (e.g., `formatIssueIdentifier`, `formatTeamIdentifier`)
   - Always display entity identifiers in the format: "[Most descriptive field] (UUID: ...)"
     - For issues: "Issue: TEST-10 (UUID: ...)"
     - For teams: "Team: Test Team (UUID: ...)"

### 4. Field Superset for Retrieval Methods
**Current State:** Retrieval methods may not include all fields that can be set through create and update methods.

**Requirement:** The fields rendered on retrieval methods should follow these rules:

1. **Detail Retrieval Methods** (e.g., `linear_get_issue`):
   - Must include the complete superset of all fields that can be set through create and update methods
   - Ensures users can always view any field they can modify
   - Prevents "hidden" fields that can be set but not retrieved

2. **Overview Retrieval Methods** (e.g., `linear_search_issues`, `linear_get_user_issues`):
   - Only need to include key metadata fields (ID, title, status, priority, etc.)
   - Not required to display full content like descriptions or comments
   - Focus on providing sufficient information for selection and identification

This distinction ensures each tool provides an appropriate level of detail for its purpose while maintaining consistency.

For example:
- `linear_get_issue` must include all fields from `linear_create_issue` and `linear_update_issue`
- `linear_search_issues` only needs to show metadata fields, not full descriptions or comments

## Implementation Plan

### Phase 1: Analysis and Documentation
1. Review all existing tools to identify:
   - Current description formats
   - Object identifier resolution methods
   - Entity rendering patterns

2. Create a detailed tracking table for all tools, documenting:
   - Current state for each rule
   - Required changes
   - Implementation status

### Phase 2: Implementation
1. Create shared utility functions for:
   - Entity rendering
   - Identifier resolution (extending existing functions as needed)

2. Update each tool to:
   - Revise descriptions to be concise
   - Use shared identifier resolution functions
   - Implement consistent entity rendering

3. Update tests to verify:
   - Identifier resolution works correctly
   - Entity rendering is consistent

### Phase 3: Validation and Documentation
1. Verify all tools meet the new standards
2. Update documentation to reflect the changes
3. Create examples demonstrating the new consistent behavior

## Tool Standardization Tracking

| Tool | Rule 1: Concise Description | Rule 2: Flexible Identifiers | Rule 3: Consistent Rendering | Status |
|------|----------------------------|------------------------------|------------------------------|--------|
| linear_create_issue | ❌ Too verbose | ✅ Supports team, parent issue, and label resolution | ❌ Custom format | Not Started |
| linear_update_issue | ❌ Too verbose | ❌ Only accepts issue ID | ❌ Custom format | Not Started |
| linear_search_issues | ❌ Too verbose | ❌ Only accepts teamId | ❌ Custom format | Not Started |
| linear_get_user_issues | ❌ Too verbose | ❌ Only accepts userId | ❌ Custom format | Not Started |
| linear_get_issue | ❌ Too verbose | ❌ Only accepts issueId | ❌ Custom format | Not Started |
| linear_add_comment | ❌ Too verbose | ❌ Only accepts issueId | ❌ Custom format | Not Started |
| linear_get_teams | ❌ Too verbose | ✅ No identifiers needed | ❌ Custom format | Not Started |

## Implementation Details

### Common Identifier Resolution Functions
We'll need to create or extend the following resolution functions:
1. `resolveTeamIdentifier` (already exists)
2. `resolveIssueIdentifier` (extend from `resolveParentIssueIdentifier`)
3. `resolveUserIdentifier` (new)
4. `resolveLabelIdentifiers` (already exists)

### Common Entity Rendering Functions
We'll need to create the following rendering functions:
1. `formatIssue` - For consistent issue rendering
2. `formatTeam` - For consistent team rendering
3. `formatUser` - For consistent user rendering
4. `formatComment` - For consistent comment rendering

### Code Structure Changes
1. Move common functions to a shared package
2. Create a new `rendering.go` file for entity formatting functions
3. Update all tool handlers to use these shared functions

## Success Criteria
1. All tool descriptions are concise and focused on functionality
2. All tools that reference Linear objects accept multiple identifier types
3. All tools render entities in a consistent format
4. Code reuse is maximized through shared functions
5. All tests pass with the new implementation

## Next Steps
1. Begin with updating one tool to serve as a reference implementation
2. Review the reference implementation to ensure it meets all requirements
3. Apply the same patterns to all remaining tools
4. Update tests and documentation

## Conclusion
Implementing these standardization rules will improve the user experience, make the codebase more maintainable, and ensure consistency across all tools. This will make the Linear MCP Server more professional and easier to use.

```

--------------------------------------------------------------------------------
/testdata/fixtures/get_issue_comments_handler_Thread_with_pagination.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 322
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n\t\t\tissues(filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } }, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"number":10,"teamKey":"TEST"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 1
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 1316
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssue($id: String!) {\n\t\t\tissue(id: $id) {\n\t\t\t\tid\n\t\t\t\tidentifier\n\t\t\t\ttitle\n\t\t\t\tdescription\n\t\t\t\tpriority\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tstate {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tassignee {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tteam {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t}\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tprojectMilestone {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\trelations(first: 20) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttype\n\t\t\t\t\t\trelatedIssue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tidentifier\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinverseRelations(first: 20) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttype\n\t\t\t\t\t\tissue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tidentifier\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tattachments(first: 50) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\tsubtitle\n\t\t\t\t\t\turl\n\t\t\t\t\t\tsourceType\n\t\t\t\t\t\tmetadata\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue","createdAt":"2025-03-03T11:34:49.241Z","updatedAt":"2025-10-07T16:03:19.741Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"project":{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation"},"projectMilestone":{"id":"5214c4d9-9c2a-4ae7-b5e5-e33058b3e131","name":"M1: Gather potential resources to investigate"},"relations":{"nodes":[]},"inverseRelations":{"nodes":[]},"attachments":{"nodes":[]}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"36b-2tYg/a9gEbu6WHEn0vlp7gu8zgw"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 2
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 839
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssueComments($issueId: String!, $parentId: ID, $first: Int!, $after: String) {\n\t\t\tissue(id: $issueId) {\n\t\t\t\tcomments(\n\t\t\t\t\tfirst: $first,\n\t\t\t\t\tafter: $after,\n\t\t\t\t\tfilter: { parent: { id: { eq: $parentId } } }\n\t\t\t\t) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tbody\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tuser {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t\tparent {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchildren(first: 1) {\n\t\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpageInfo {\n\t\t\t\t\t\thasNextPage\n\t\t\t\t\t\tendCursor\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"first":2,"issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f","parentId":"ae3d62d6-3f40-4990-867b-5c97dd265a40"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issue":{"comments":{"nodes":[{"id":"6b337bfa-a7df-4b5c-9d6d-a0c8c6212301","body":"This is a reply to the comment","createdAt":"2025-10-07T16:03:19.784Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"1bc6bceb-1f2c-4a52-8f23-155aeb966ee2","body":"Reply using shorthand","createdAt":"2025-10-07T15:00:53.946Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}}],"pageInfo":{"hasNextPage":true,"endCursor":"1bc6bceb-1f2c-4a52-8f23-155aeb966ee2"}}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"2a8-ZLVxpKsJeNGYqj5ihN+Tbdu15Oo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/memory-bank/progress.md:
--------------------------------------------------------------------------------

```markdown
# Progress: Linear MCP Server

## What Works
1. **Command-Line Interface**:
   - Cobra-based command structure
   - Subcommand support (server, setup)
   - Consistent flag handling

2. **Core MCP Server**:
   - Server initialization and configuration
   - Tool registration and execution
   - Error handling and response formatting

3. **Linear API Integration**:
   - Authentication via API key
   - Rate limiting implementation
   - API request and response handling
   - Proper JSON structure handling for API responses
   - Correct GraphQL parameter types for API validation
   - Proper resolution of human-readable identifiers to UUIDs

4. **MCP Tools**:
   - `linear_create_issue`: Creating new Linear issues with support for:
     - Sub-issues using parent issue ID or human-readable identifier (e.g., "TEAM-123")
     - Label assignment using label IDs or names
     - Team specification using team ID, name, or key
   - `linear_update_issue`: Updating existing issues
   - `linear_search_issues`: Searching for issues with various criteria
   - `linear_get_user_issues`: Getting issues assigned to a user
   - `linear_get_issue`: Getting a single issue by ID
   - `linear_add_comment`: Adding comments to issues
   - `linear_get_teams`: Getting a list of teams

5. **Setup Automation**:
   - Binary discovery and download
   - Configuration file management
   - Support for Cline AI assistant

6. **Testing**:
   - Test fixtures for all tools
   - HTTP interaction recording and playback
   - Comprehensive test coverage for enhanced functionality

## What's Left to Build

### High Priority
1. **Setup Command Testing**:
   - Test on different platforms (Linux, macOS, Windows)
   - Verify configuration file creation
   - Test binary download functionality

### Medium Priority
1. **Additional AI Assistant Support**:
   - Research other AI assistants for Linear integration
   - Implement support for these assistants
   - Update documentation

2. **Documentation Improvements**:
   - Add CONTRIBUTING.md with development guidelines
   - Add examples of using the server with different AI assistants
   - Add troubleshooting section

3. **Additional Linear API Features**:
   - Support for managing issue attachments
   - Support for managing issue relationships

### Low Priority
1. **Infrastructure Improvements**:
   - Docker container support
   - Configuration file support for server
   - Improved logging and metrics
   - Automatic binary updates

## Current Status
- **Version**: 1.0.0
- **Stability**: Stable for core features
- **Test Coverage**: Good, all tools have test fixtures
- **Documentation**: Updated with new command structure, setup instructions, and tool standardization PRD
- **Release Process**: GitHub Actions workflow created
- **Security**: Write access control implemented (disabled by default)
- **User Experience**: Improved with setup command and user-friendly identifiers

## Known Issues
1. **API Key Management**:
   - API key validation only happens on first API request
   - No support for other authentication methods (OAuth, etc.)
   - LINEAR_API_KEY environment variable must be set manually

2. **Rate Limiting Constraints**:
   - Linear API has rate limits that must be respected
   - Current rate limiter implementation is basic (simple token bucket)
   - Rate limits are not configurable (hardcoded values)
   - No sophisticated backoff strategies for rate limit exceeded scenarios

3. **Error Handling**:
   - Some error messages could be more descriptive
   - No retry mechanism for transient errors
   - Network errors during setup could be handled better
   - GraphQL errors could be better formatted for end users

4. **Feature Limitations**:
   - Limited support for Linear API features
   - No support for webhooks or real-time updates
   - Limited AI assistant support (currently only Cline)
   - No configuration file support for server settings

5. **Authentication Limitations**:
   - Only supports API key authentication
   - No support for OAuth or other modern authentication methods
   - API key is not validated until first API request is made

## Recent Milestones
- ✅ Created comprehensive Tool Standardization PRD with implementation plan
- ✅ Implemented shared utility functions for entity rendering and identifier resolution
- ✅ Updated all tools to follow standardization rules:
  - Concise descriptions
  - Flexible identifier resolution
  - Consistent entity rendering
- ✅ Initial implementation of all core tools
- ✅ Test fixtures for all tools
- ✅ GitHub Actions workflow for testing and releases
- ✅ Write access control implementation (--write-access flag, default: false)
- ✅ Command-line interface with subcommands
- ✅ Setup command for AI assistants
- ✅ Enhanced `linear_create_issue` tool with support for sub-issues and labels
- ✅ Implemented user-friendly identifiers for parent issues and labels
- ✅ Fixed JSON unmarshaling issue with Labels field
- ✅ Added support for team key in issue creation
- ✅ Fixed label resolution issue with GraphQL parameter type
- ✅ Fixed parent issue identifier resolution for human-readable identifiers
- ✅ Enhanced Claude Code Support in Setup Command:
  - Register to all existing projects when no --project-path is specified
  - Support multiple project paths separated by commas
  - Comprehensive testing with 5 new test cases
  - Manual testing confirmed functionality

## Evolution of Project Decisions

### Initial Implementation Phase
- **Started with basic Linear API integration**: Focused on core functionality first
- **Implemented core MCP tools for issue management**: Prioritized the most common Linear operations
- **Added test fixtures for all tools**: Established testing foundation early
- **Decision Rationale**: Build a solid foundation before adding advanced features

### Command-Line Interface Evolution
- **Original**: Simple binary that only ran the server
- **Enhanced**: Added Cobra framework with subcommand structure
- **Current**: Full CLI with server and setup subcommands
- **Decision Rationale**: Better user experience and extensibility for future commands

### Setup Automation Development
- **Original**: Manual installation via bash script
- **Enhanced**: Integrated setup command within the main binary
- **Current**: Automated binary discovery, download, and configuration
- **Decision Rationale**: Reduce friction for users setting up the server

### Write Access Control Implementation
- **Original**: All tools available by default
- **Enhanced**: Added write access control with --write-access flag
- **Current**: Write operations disabled by default for security
- **Decision Rationale**: Prevent accidental modifications while maintaining functionality

### Tool Standardization Journey
- **Original**: Inconsistent tool descriptions and parameter naming
- **Enhanced**: Created comprehensive standardization PRD
- **Current**: All tools follow consistent patterns for descriptions, parameters, and output
- **Decision Rationale**: Improve user experience and maintainability

### Testing Strategy Evolution
- **Original**: Basic unit tests
- **Enhanced**: Added go-vcr for HTTP interaction recording
- **Current**: Comprehensive test coverage with fixtures for all scenarios
- **Decision Rationale**: Enable testing without API dependencies and ensure reliability

### Release Process Development
- **Original**: Manual releases
- **Enhanced**: Added GitHub Actions workflow
- **Current**: Automated testing and releases triggered by version tags
- **Decision Rationale**: Streamline release process and ensure quality

## Upcoming Milestones
- [x] Complete Tool Standardization testing
- [ ] Support for additional AI assistants
- [ ] Improved error handling and recovery
- [ ] Additional Linear API features
- [ ] Configuration file support for server

```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Create issue with project name.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 719
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetProject($id: String!) {\n\t\t\tproject(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\tslugId\n\t\t\t\tstate\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tlead {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tmembers {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tteams {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinitiatives(first: 10) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstartDate\n\t\t\t\ttargetDate\n\t\t\t}\n\t\t}\n\t","variables":{"id":"MCP tool investigation"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"errors":[{"message":"Entity not found: Project","path":["project"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced Project."}}],"data":null}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"113-pUQ9mkDn3KWYiQz0UBE51+d7gJ4"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 1
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 907
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetProjectByNameOrSlug($filter: ProjectFilter) {\n\t\t\tprojects(filter: $filter, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\tslugId\n\t\t\t\t\tstate\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tlead {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t\tmembers {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\temail\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tteams {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tinitiatives(first: 1) {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstartDate\n\t\t\t\t\ttargetDate\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"or":[{"name":{"eq":"MCP tool investigation"}},{"slugId":{"eq":""}}]}}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"projects":{"nodes":[{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation","description":"Summary text goes here","slugId":"ae44897e42a7","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/mcp-tool-investigation-ae44897e42a7","createdAt":"2025-06-28T18:06:47.606Z","updatedAt":"2025-06-28T18:07:51.899Z","lead":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"},"members":{"nodes":[{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"}]},"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}]},"initiatives":{"nodes":[{"id":"15e7c1bd-c0c5-4801-ac9a-8e98bf88ea7a","name":"Push for MCP"}]},"startDate":"2025-06-02","targetDate":"2025-06-30"}]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"355-Jji1j11utIAgJU/7ATKvhRyba4g"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 2
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 895
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tmutation CreateIssue($input: IssueCreateInput!) {\n\t\t\tissueCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tstate {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tteam {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t\tlabels {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tprojectMilestone {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"description":"","projectId":"01bff2dd-ab7f-4464-b425-97073862013f","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Issue with Project Name"}}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issueCreate":{"success":true,"issue":{"id":"5ab2381d-8838-442b-bbe7-bc1a10bd7866","identifier":"TEST-94","title":"Issue with Project Name","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-94/issue-with-project-name","createdAt":"2025-10-06T09:44:51.777Z","updatedAt":"2025-10-06T09:44:51.777Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[]},"project":{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation"},"projectMilestone":null}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"27d-P1WZhi2dq5MbxNwDZDRge46h1ZE"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Create issue with project slug.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 732
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetProject($id: String!) {\n\t\t\tproject(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\tslugId\n\t\t\t\tstate\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tlead {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tmembers {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tteams {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinitiatives(first: 10) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstartDate\n\t\t\t\ttargetDate\n\t\t\t}\n\t\t}\n\t","variables":{"id":"mcp-tool-investigation-ae44897e42a7"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"errors":[{"message":"Entity not found: Project","path":["project"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced Project."}}],"data":null}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"113-pUQ9mkDn3KWYiQz0UBE51+d7gJ4"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 1
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 932
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetProjectByNameOrSlug($filter: ProjectFilter) {\n\t\t\tprojects(filter: $filter, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\tslugId\n\t\t\t\t\tstate\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tlead {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t\tmembers {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\temail\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tteams {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tinitiatives(first: 1) {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstartDate\n\t\t\t\t\ttargetDate\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"or":[{"name":{"eq":"mcp-tool-investigation-ae44897e42a7"}},{"slugId":{"eq":"ae44897e42a7"}}]}}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"projects":{"nodes":[{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation","description":"Summary text goes here","slugId":"ae44897e42a7","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/mcp-tool-investigation-ae44897e42a7","createdAt":"2025-06-28T18:06:47.606Z","updatedAt":"2025-06-28T18:07:51.899Z","lead":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"},"members":{"nodes":[{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"}]},"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}]},"initiatives":{"nodes":[{"id":"15e7c1bd-c0c5-4801-ac9a-8e98bf88ea7a","name":"Push for MCP"}]},"startDate":"2025-06-02","targetDate":"2025-06-30"}]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"355-Jji1j11utIAgJU/7ATKvhRyba4g"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 2
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 895
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tmutation CreateIssue($input: IssueCreateInput!) {\n\t\t\tissueCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tstate {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tteam {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t\tlabels {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tprojectMilestone {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"description":"","projectId":"01bff2dd-ab7f-4464-b425-97073862013f","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Issue with Project Slug"}}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issueCreate":{"success":true,"issue":{"id":"d7bad82a-cab5-4514-859f-686da6a36a20","identifier":"TEST-95","title":"Issue with Project Slug","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-95/issue-with-project-slug","createdAt":"2025-10-06T09:44:55.631Z","updatedAt":"2025-10-06T09:44:55.631Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[]},"project":{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation"},"projectMilestone":null}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"27d-qyHETS4IDY0lD2ks/TgnkeoJyE4"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/get_issue_comments_handler_With limit.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 322
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n\t\t\tissues(filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } }, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"number":10,"teamKey":"TEST"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 1
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 1316
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssue($id: String!) {\n\t\t\tissue(id: $id) {\n\t\t\t\tid\n\t\t\t\tidentifier\n\t\t\t\ttitle\n\t\t\t\tdescription\n\t\t\t\tpriority\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tstate {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tassignee {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tteam {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t}\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tprojectMilestone {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\trelations(first: 20) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttype\n\t\t\t\t\t\trelatedIssue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tidentifier\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinverseRelations(first: 20) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttype\n\t\t\t\t\t\tissue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tidentifier\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tattachments(first: 50) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\tsubtitle\n\t\t\t\t\t\turl\n\t\t\t\t\t\tsourceType\n\t\t\t\t\t\tmetadata\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue","createdAt":"2025-03-03T11:34:49.241Z","updatedAt":"2025-10-07T16:03:19.741Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"project":{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation"},"projectMilestone":{"id":"5214c4d9-9c2a-4ae7-b5e5-e33058b3e131","name":"M1: Gather potential resources to investigate"},"relations":{"nodes":[]},"inverseRelations":{"nodes":[]},"attachments":{"nodes":[]}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"36b-2tYg/a9gEbu6WHEn0vlp7gu8zgw"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 2
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 789
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssueComments($issueId: String!, $parentId: ID, $first: Int!, $after: String) {\n\t\t\tissue(id: $issueId) {\n\t\t\t\tcomments(\n\t\t\t\t\tfirst: $first,\n\t\t\t\t\tafter: $after,\n\t\t\t\t\tfilter: { parent: { id: { eq: $parentId } } }\n\t\t\t\t) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tbody\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tuser {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t\tparent {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchildren(first: 1) {\n\t\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpageInfo {\n\t\t\t\t\t\thasNextPage\n\t\t\t\t\t\tendCursor\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"first":3,"issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issue":{"comments":{"nodes":[{"id":"6b337bfa-a7df-4b5c-9d6d-a0c8c6212301","body":"This is a reply to the comment","createdAt":"2025-10-07T16:03:19.784Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"1bc6bceb-1f2c-4a52-8f23-155aeb966ee2","body":"Reply using shorthand","createdAt":"2025-10-07T15:00:53.946Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"5c1c0a90-6778-41f4-94b4-fce69d894bb7","body":"Reply using comment URL","createdAt":"2025-10-07T15:00:53.396Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}}],"pageInfo":{"hasNextPage":true,"endCursor":"5c1c0a90-6778-41f4-94b4-fce69d894bb7"}}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"3ba-1ni/58B6SxP/Air8ViMneIjjN3E"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/pkg/tools/project_tools.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"
	"strings"

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

var GetProjectTool = mcp.NewTool("linear_get_project",
	mcp.WithDescription("Get a single project."),
	mcp.WithString("project", mcp.Required(), mcp.Description("The identifier of the project, either ID, name or slug.")),
)

func GetProjectHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		projectIdentifier, err := request.RequireString("project")
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
		}

		project, err := linearClient.GetProject(projectIdentifier)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get project: %v", err)}}}, nil
		}

		resultText := FormatProject(*project)

		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
	}
}

var SearchProjectsTool = mcp.NewTool("linear_search_projects",
	mcp.WithDescription("Search for projects."),
	mcp.WithString("query", mcp.Description("The search query.")),
)

func SearchProjectsHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		query := request.GetString("query", "")

		if query == "" {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: "Query parameter may not be empty."}}}, nil
		}

		projects, err := linearClient.SearchProjects(query)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to search projects: %v", err)}}}, nil
		}

		if len(projects) == 0 {
			return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: "No projects found."}}}, nil
		}

		var builder strings.Builder
		for _, project := range projects {
			builder.WriteString(FormatProject(project))
			builder.WriteString("\n")
		}

		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: builder.String()}}}, nil
	}
}

var CreateProjectTool = mcp.NewTool("linear_create_project",
	mcp.WithDescription("Create a new project."),
	mcp.WithString("name", mcp.Required(), mcp.Description("The name of the project.")),
	mcp.WithString("teamIds", mcp.Required(), mcp.Description("A comma-separated list of team IDs.")),
	mcp.WithString("description", mcp.Description("The description of the project.")),
	mcp.WithString("leadId", mcp.Description("The ID of the project lead.")),
	mcp.WithString("startDate", mcp.Description("The start date of the project (YYYY-MM-DD).")),
	mcp.WithString("targetDate", mcp.Description("The target date of the project (YYYY-MM-DD).")),
)

func CreateProjectHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		name, err := request.RequireString("name")
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
		}

		teamIDsStr, err := request.RequireString("teamIds")
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
		}
		teamIDs := strings.Split(teamIDsStr, ",")

		description := request.GetString("description", "")
		leadID := request.GetString("leadId", "")
		startDate := request.GetString("startDate", "")
		targetDate := request.GetString("targetDate", "")

		input := linear.ProjectCreateInput{
			Name:        name,
			TeamIDs:     teamIDs,
			Description: description,
			LeadID:      leadID,
			StartDate:   startDate,
			TargetDate:  targetDate,
		}

		project, err := linearClient.CreateProject(input)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to create project: %v", err)}}}, nil
		}

		resultText := FormatProject(*project)

		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
	}
}

var UpdateProjectTool = mcp.NewTool("linear_update_project",
	mcp.WithDescription("Update an existing project."),
	mcp.WithString("project", mcp.Required(), mcp.Description("The identifier of the project to update.")),
	mcp.WithString("name", mcp.Description("The new name of the project.")),
	mcp.WithString("description", mcp.Description("The new description of the project.")),
	mcp.WithString("leadId", mcp.Description("The ID of the project lead.")),
	mcp.WithString("startDate", mcp.Description("The start date of the project (YYYY-MM-DD).")),
	mcp.WithString("targetDate", mcp.Description("The target date of the project (YYYY-MM-DD).")),
	mcp.WithString("teamIds", mcp.Description("A comma-separated list of team IDs.")),
)

func UpdateProjectHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		projectIdentifier, err := request.RequireString("project")
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
		}

		// Get the project first to get its ID
		proj, err := linearClient.GetProject(projectIdentifier)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get project: %v", err)}}}, nil
		}

		name := request.GetString("name", "")
		description := request.GetString("description", "")
		leadID := request.GetString("leadId", "")
		startDate := request.GetString("startDate", "")
		targetDate := request.GetString("targetDate", "")
		teamIDsStr := request.GetString("teamIds", "")
		var teamIDs []string
		if teamIDsStr != "" {
			teamIDs = strings.Split(teamIDsStr, ",")
		}

		input := linear.ProjectUpdateInput{
			Name:        name,
			Description: description,
			LeadID:      leadID,
			StartDate:   startDate,
			TargetDate:  targetDate,
			TeamIDs:     teamIDs,
		}

		project, err := linearClient.UpdateProject(proj.ID, input)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to update project: %v", err)}}}, nil
		}

		resultText := FormatProject(*project)

		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
	}
}

func FormatProject(project linear.Project) string {
	var builder strings.Builder
	builder.WriteString(fmt.Sprintf("Project: %s\n", project.Name))
	builder.WriteString(fmt.Sprintf("  ID: %s\n", project.ID))
	builder.WriteString(fmt.Sprintf("  State: %s\n", project.State))
	builder.WriteString(fmt.Sprintf("  URL: %s\n", project.URL))
	if project.Description != "" {
		builder.WriteString(fmt.Sprintf("  Description: %s\n", project.Description))
	}
	if project.Lead == nil {
		builder.WriteString("  Lead: None\n")
	} else {
		builder.WriteString(fmt.Sprintf("  Lead: %s\n", project.Lead.Name))
	}
	if project.StartDate == nil {
		builder.WriteString("  Start Date: None\n")
	} else {
		builder.WriteString(fmt.Sprintf("  Start Date: %s\n", *project.StartDate))
	}
	if project.TargetDate == nil {
		builder.WriteString("  Target Date: None\n")
	} else {
		builder.WriteString(fmt.Sprintf("  Target Date: %s\n", *project.TargetDate))
	}
	if project.Initiatives != nil && len(project.Initiatives.Nodes) > 0 {
		builder.WriteString("  Initiatives:\n")
		for _, i := range project.Initiatives.Nodes {
			builder.WriteString(fmt.Sprintf("    - %s (ID: %s)\n", i.Name, i.ID))
		}
	} else {
		builder.WriteString("  Initiatives: None\n")
	}
	return builder.String()
}

```

--------------------------------------------------------------------------------
/memory-bank/developmentWorkflows.md:
--------------------------------------------------------------------------------

```markdown
# Development Workflows: Linear MCP Server

## Git Workflow

### Branch Management
- **Main Branch**: Protected branch that contains stable, production-ready code
- **Feature Branches**: Development happens in feature branches created from main
- **Branch Naming**: Use descriptive names (e.g., `feature/setup-command`, `fix/rate-limiting`)
- **Pull Requests**: All changes must go through pull request review process
- **Branch Protection**: Main branch requires PR approval before merging

### Version Control Practices
- **Commit Messages**: Use clear, descriptive commit messages
- **Atomic Commits**: Each commit should represent a single logical change
- **Code Review**: All PRs require review before merging
- **Testing**: All tests must pass before merging

## Release Process

### Semantic Versioning
- **Version Format**: Follow semantic versioning with "v" prefix (e.g., v1.0.0, v1.2.3)
- **Version Components**:
  - Major: Breaking changes
  - Minor: New features (backward compatible)
  - Patch: Bug fixes (backward compatible)

### Release Branch Workflow
Since the main branch is protected and requires PR approval, releases follow a branch-based workflow:

#### Phase 1: Prepare Release Branch
```bash
# Create release branch from current development branch
git checkout -b release/v{version}

# Update version in pkg/server/server.go
# Change ServerVersion constant to new version

# Commit version update
git add pkg/server/server.go
git commit -m "Bump version to v{version}"

# Push release branch
git push origin release/v{version}
```

#### Phase 2: Create Release Pull Request
```bash
# Create PR from release/v{version} to main
# PR Title: "Release v{version}"
# PR Description should include:
# - Summary of changes since last release
# - Breaking changes (if any)
# - New features added
# - Bug fixes included
# - Testing performed
```

#### Phase 3: Review and Merge
- **Code Review**: Release PR must be reviewed and approved
- **CI Checks**: All automated tests and checks must pass
- **Documentation**: Ensure README and docs are up to date
- **Final Testing**: Perform any additional manual testing if needed

#### Phase 4: Tag and Release
```bash
# After PR is merged to main
git checkout main
git pull origin main

# Create and push release tag
git tag v{version}
git push origin v{version}
```

#### Phase 5: Automated Release
GitHub Actions workflow automatically:
- Builds binaries for Linux, macOS, and Windows
- Runs full test suite
- Creates GitHub release with release notes
- Uploads release assets

### Release Branch Naming
- **Format**: `release/v{version}` (e.g., `release/v1.6.0`)
- **Purpose**: Isolate release preparation from ongoing development
- **Lifecycle**: Created for release prep, deleted after successful release

### Release PR Template
When creating a release PR, include:

```markdown
## Release v{version}

### Summary
Brief description of this release.

### Changes Since Last Release
- **New Features:**
  - Feature 1 description
  - Feature 2 description

- **Bug Fixes:**
  - Fix 1 description
  - Fix 2 description

- **Improvements:**
  - Improvement 1 description
  - Improvement 2 description

### Breaking Changes
- None / List any breaking changes

### Testing
- [ ] All automated tests pass
- [ ] Manual testing performed
- [ ] Setup command tested on target platforms

### Checklist
- [ ] Version updated in pkg/server/server.go
- [ ] CHANGELOG.md updated (if exists)
- [ ] Documentation updated
- [ ] All tests passing
- [ ] No merge conflicts with main
```

### Hotfix Process
For urgent fixes that need to bypass normal development flow:

```bash
# Create hotfix branch from main
git checkout main
git pull origin main
git checkout -b hotfix/v{version}

# Make necessary fixes
# Update version (patch increment)
# Commit changes

# Create PR to main (expedited review)
# After merge, tag immediately
```

### GitHub Actions Workflow
- **Trigger**: Activated when tags matching "v*" pattern are pushed
- **Build Matrix**: Builds for multiple platforms simultaneously
- **Testing**: Runs full test suite before creating release
- **Asset Upload**: Automatically uploads compiled binaries to GitHub releases

## Development Commands

### Project Setup
```bash
# Clone the repository
git clone <repository-url>
cd linear-mcp-go

# Install dependencies (Go modules)
go mod download

# Verify setup
go build
```

### Building
```bash
# Build for current platform
go build

# Build with custom output name
go build -o linear-mcp-server

# Build for specific platform
GOOS=linux GOARCH=amd64 go build -o linear-mcp-server-linux

# Build for all platforms (manual)
GOOS=linux GOARCH=amd64 go build -o dist/linear-mcp-server-linux
GOOS=darwin GOARCH=amd64 go build -o dist/linear-mcp-server-darwin
GOOS=windows GOARCH=amd64 go build -o dist/linear-mcp-server-windows.exe
```

### Testing

#### Running Tests
```bash
# Run all tests with existing recordings
go test -v ./...

# Run tests for specific package
go test -v ./pkg/server

# Run specific test function
go test -v -run TestCreateIssueHandler ./pkg/server

# Run tests with coverage
go test -v -cover ./...
```

#### Recording New Test Fixtures
```bash
# Re-record tests (requires LINEAR_API_KEY environment variable)
go test -v -record=true ./...

# Re-record all tests including state-changing operations
go test -v -recordWrites=true ./...

# Re-record specific test
LINEAR_API_KEY=your_key go test -v -record=true -run TestSpecificFunction ./...
```

#### Test Environment Setup
```bash
# Set up environment for recording tests
export LINEAR_API_KEY=your_linear_api_key

# Run tests that modify state (use with caution)
go test -v -recordWrites=true ./...
```

### Running the Server

#### Development Mode
```bash
# Run server in read-only mode (safe for development)
./linear-mcp-go server

# Run server with write access (use with caution)
./linear-mcp-go server --write-access

# Run with environment variable
LINEAR_API_KEY=your_key ./linear-mcp-go server
```

#### Setup for AI Assistants
```bash
# Set up for Cline (default)
./linear-mcp-go setup --api-key=your_linear_api_key

# Set up with write access enabled
./linear-mcp-go setup --api-key=your_linear_api_key --write-access

# Set up for specific AI assistant (future)
./linear-mcp-go setup --api-key=your_key --tool=assistant_name
```

### Code Quality

#### Formatting
```bash
# Format all Go code
go fmt ./...

# Check formatting
gofmt -l .

# Format specific file
go fmt pkg/server/server.go
```

#### Linting
```bash
# Run go vet (built-in static analysis)
go vet ./...

# Run golangci-lint (if installed)
golangci-lint run
```

#### Dependencies
```bash
# Update dependencies
go mod tidy

# Verify dependencies
go mod verify

# View dependency graph
go mod graph
```

## Debugging and Troubleshooting

### Common Issues

#### API Key Problems
```bash
# Verify API key is set
echo $LINEAR_API_KEY

# Test API key manually
curl -H "Authorization: Bearer $LINEAR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"query": "{ viewer { id name } }"}' \
     https://api.linear.app/graphql
```

#### Build Issues
```bash
# Clean build cache
go clean -cache

# Rebuild from scratch
go clean && go build
```

#### Test Issues
```bash
# Clean test cache
go clean -testcache

# Run tests with verbose output
go test -v -x ./...
```

### Development Tips

#### Working with Test Fixtures
- Test fixtures are stored in `testdata/fixtures/`
- Golden files (expected outputs) are in `testdata/golden/`
- Use `-record=true` flag sparingly to avoid API quota exhaustion
- Always review recorded fixtures before committing

#### MCP Tool Development
- Register new tools in `pkg/server/tools.go`
- Follow existing patterns for parameter validation
- Add comprehensive test coverage for new tools
- Update documentation when adding new tools

#### Linear API Integration
- All API calls go through the Linear client in `pkg/linear/client.go`
- Add new API methods to the client rather than calling API directly from tools
- Handle GraphQL errors consistently
- Respect rate limits in all API interactions

## Continuous Integration

### GitHub Actions
- **Workflow File**: `.github/workflows/release.yml`
- **Triggers**: Push to main branch, pull requests, version tags
- **Jobs**: Test, build, release (for tags)
- **Platforms**: Linux, macOS, Windows

### Quality Gates
- All tests must pass
- Code must be properly formatted
- No linting errors
- Build must succeed on all target platforms

```

--------------------------------------------------------------------------------
/docs/prd/004-tool-standardization-tracking.md:
--------------------------------------------------------------------------------

```markdown
# Tool Standardization Implementation Tracking

This document provides a detailed tracking sheet for the implementation of the standardization rules outlined in [002-tool-standardization.md](./002-tool-standardization.md) and [003-tool-standardization-implementation.md](./003-tool-standardization-implementation.md).

## Overall Progress

| Phase | Status | Progress | Notes |
|-------|--------|----------|-------|
| Phase 1: Create Shared Utility Functions | Completed | 100% | Created rendering.go and updated common.go |
| Phase 2: Update Tools | Completed | 100% | All rules implemented |
| Phase 3: Update Tests | Completed | 100% | Tests updated to reflect new formatting |
| **Overall** | **Completed** | **100%** | All rules implemented |

## Phase 1: Create Shared Utility Functions

| Task | Status | Assignee | Notes |
|------|--------|----------|-------|
| Refactor `resolveParentIssueIdentifier` to `resolveIssueIdentifier` | Completed | | Implemented in common.go |
| Create `resolveUserIdentifier` | Completed | | Implemented in common.go |
| Create `pkg/tools/rendering.go` | Completed | | Created with all formatting functions |
| Implement full entity rendering functions | Completed | | |
| - `formatIssue` | Completed | | Implemented in rendering.go |
| - `formatTeam` | Completed | | Implemented in rendering.go |
| - `formatUser` | Completed | | Implemented in rendering.go |
| - `formatComment` | Completed | | Implemented in rendering.go |
| Implement entity identifier rendering functions | Completed | | |
| - `formatIssueIdentifier` | Completed | | Implemented in rendering.go |
| - `formatTeamIdentifier` | Completed | | Implemented in rendering.go |
| - `formatUserIdentifier` | Completed | | Implemented in rendering.go |
| - `formatCommentIdentifier` | Completed | | Implemented in rendering.go |

## Phase 2: Update Tools

### linear_create_issue

| Task | Status | Assignee | Notes |
|------|--------|----------|-------|
| Update description to be concise | Completed | | Description simplified |
| Update result formatting to use `formatIssueIdentifier` | Completed | | Using formatIssueIdentifier for output |

### linear_update_issue

| Task | Status | Assignee | Notes |
|------|--------|----------|-------|
| Update description to be concise | Completed | | Description simplified |
| Update to use `resolveIssueIdentifier` | Completed | | Now accepts issue identifiers |
| Update result formatting to use `formatIssueIdentifier` | Completed | | Using formatIssueIdentifier for output |

### linear_search_issues

| Task | Status | Assignee | Notes |
|------|--------|----------|-------|
| Update description to be concise | Completed | | Description simplified |
| Update to use `resolveTeamIdentifier` for team | Completed | | Parameter renamed from teamId to team |
| Update to use `resolveUserIdentifier` for assignee | Completed | | Parameter renamed from assigneeId to assignee |
| Update result formatting to use `formatIssueIdentifier` | Completed | | Using formatIssueIdentifier for each issue |

### linear_get_user_issues

| Task | Status | Assignee | Notes |
|------|--------|----------|-------|
| Update description to be concise | Completed | | Description simplified |
| Update to use `resolveUserIdentifier` for user | Completed | | Parameter renamed from userId to user |
| Update result formatting to use `formatIssueIdentifier` | Completed | | Using formatIssueIdentifier for each issue |

### linear_get_issue

| Task | Status | Assignee | Notes |
|------|--------|----------|-------|
| Update description to be concise | Completed | | Description simplified |
| Update to use `resolveIssueIdentifier` | Completed | | Now accepts issue identifiers |
| Update result formatting to use formatting functions | Completed | | Using formatIssue, formatUserIdentifier, formatTeamIdentifier, and formatIssueIdentifier |

### linear_add_comment

| Task | Status | Assignee | Notes |
|------|--------|----------|-------|
| Update description to be concise | Completed | | Description simplified |
| Update to use `resolveIssueIdentifier` | Completed | | Now accepts issue identifiers |
| Update result formatting to use `formatIssueIdentifier` and `formatCommentIdentifier` | Completed | | Using both formatting functions |

### linear_get_teams

| Task | Status | Assignee | Notes |
|------|--------|----------|-------|
| Update description to be concise | Completed | | Description simplified |
| Update result formatting to use `formatTeamIdentifier` | Completed | | Using formatTeamIdentifier for each team |

## Phase 3: Update Tests

| Task | Status | Assignee | Notes |
|------|--------|----------|-------|
| Update test fixtures for `linear_create_issue` | Completed | | Updated with new standardized format |
| Update test fixtures for `linear_update_issue` | Completed | | Updated with new standardized format |
| Update test fixtures for `linear_search_issues` | Completed | | Updated with new standardized format |
| Update test fixtures for `linear_get_user_issues` | Completed | | Updated with new standardized format |
| Update test fixtures for `linear_get_issue` | Completed | | Updated with new standardized format |
| Update test fixtures for `linear_add_comment` | Completed | | Updated with new standardized format |
| Update test fixtures for `linear_get_teams` | Completed | | Updated with new standardized format |
| Update test cases for parameter name changes | Completed | | Updated parameter names in test cases |
| Run tests to verify changes | Completed | | All tests pass with new standardized format |

## Implementation Checklist

Use this checklist to track the implementation of each rule for each tool:

### Rule 1: Concise Tool Descriptions

- [x] linear_create_issue
- [x] linear_update_issue
- [x] linear_search_issues
- [x] linear_get_user_issues
- [x] linear_get_issue
- [x] linear_add_comment
- [x] linear_get_teams

### Rule 2: Flexible Object Identifier Resolution

- [x] linear_create_issue (already implemented)
- [x] linear_update_issue
- [x] linear_search_issues
- [x] linear_get_user_issues
- [x] linear_get_issue
- [x] linear_add_comment
- [x] linear_get_teams (no identifiers needed)

### Rule 3: Consistent Entity Rendering

#### Full Entity Rendering
- [x] linear_create_issue
- [x] linear_update_issue
- [x] linear_search_issues
- [x] linear_get_user_issues
- [x] linear_get_issue
- [x] linear_add_comment
- [x] linear_get_teams

#### Entity Identifier Rendering
- [x] linear_create_issue (for referenced entities)
- [x] linear_update_issue (for referenced entities)
- [x] linear_search_issues (for referenced entities)
- [x] linear_get_user_issues (for referenced entities)
- [x] linear_get_issue (for referenced entities)
- [x] linear_add_comment (for referenced entities)
- [x] linear_get_teams (for referenced entities)

### Rule 4: Field Superset for Retrieval Methods

#### Detail Retrieval Methods
- [x] linear_get_issue (must include all fields from create_issue and update_issue)
- [x] linear_get_issue (must include all necessary comment fields from add_comment)

#### Overview Retrieval Methods
- [x] linear_get_user_issues (must include key metadata fields)
- [x] linear_search_issues (must include key metadata fields)
- [x] linear_get_teams (must include all team fields that can be referenced)

## Notes and Issues

Use this section to track any issues or notes that arise during implementation:

1. The formatTeamIdentifier function expects a pointer to a Team, but the GetTeams function returns a slice of Team values. We had to create a pointer to each team in the loop.
2. Tests need to be updated to reflect the new formatting of results.
3. Parameter names have been updated to be more consistent with the entity they represent:
   - `id` → `issue` in linear_update_issue
   - `issueId` → `issue` in linear_get_issue and linear_add_comment
   - `teamId` → `team` in linear_search_issues (already done)
   - `userId` → `user` in linear_get_user_issues (already done)
   - `assigneeId` → `assignee` in linear_search_issues (already done)

## Next Steps

The following tasks have been completed for the Tool Standardization initiative:

1. **Rule 1: Concise Tool Descriptions** ✅
   - All tool descriptions have been updated to be concise and focused on functionality

2. **Rule 2: Flexible Object Identifier Resolution** ✅
   - All tools now accept multiple forms of identification for Linear objects

3. **Rule 3: Consistent Entity Rendering** ✅
   - All tools now use consistent formatting for entity rendering
   - Shared formatting functions have been implemented

4. **Rule 4: Field Superset for Retrieval Methods** ✅
   - Detail retrieval methods now include all fields that can be set in create/update methods
   - Overview retrieval methods include appropriate metadata fields
   - The displayIconURL parameter has been removed from add_comment

Future enhancements could include:

1. Adding more comprehensive tests for the resolution functions
2. Documenting the standardized approach in the project README
3. Applying similar standardization patterns to any new tools added in the future

```

--------------------------------------------------------------------------------
/testdata/fixtures/get_issue_comments_handler_Valid issue.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 322
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n\t\t\tissues(filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } }, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"number":10,"teamKey":"TEST"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 1
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 1316
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssue($id: String!) {\n\t\t\tissue(id: $id) {\n\t\t\t\tid\n\t\t\t\tidentifier\n\t\t\t\ttitle\n\t\t\t\tdescription\n\t\t\t\tpriority\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tstate {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tassignee {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tteam {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t}\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tprojectMilestone {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\trelations(first: 20) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttype\n\t\t\t\t\t\trelatedIssue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tidentifier\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinverseRelations(first: 20) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttype\n\t\t\t\t\t\tissue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tidentifier\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tattachments(first: 50) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\tsubtitle\n\t\t\t\t\t\turl\n\t\t\t\t\t\tsourceType\n\t\t\t\t\t\tmetadata\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue","createdAt":"2025-03-03T11:34:49.241Z","updatedAt":"2025-10-07T16:03:19.741Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"project":{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation"},"projectMilestone":{"id":"5214c4d9-9c2a-4ae7-b5e5-e33058b3e131","name":"M1: Gather potential resources to investigate"},"relations":{"nodes":[]},"inverseRelations":{"nodes":[]},"attachments":{"nodes":[]}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"36b-2tYg/a9gEbu6WHEn0vlp7gu8zgw"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 2
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 790
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssueComments($issueId: String!, $parentId: ID, $first: Int!, $after: String) {\n\t\t\tissue(id: $issueId) {\n\t\t\t\tcomments(\n\t\t\t\t\tfirst: $first,\n\t\t\t\t\tafter: $after,\n\t\t\t\t\tfilter: { parent: { id: { eq: $parentId } } }\n\t\t\t\t) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tbody\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tuser {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t\tparent {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchildren(first: 1) {\n\t\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpageInfo {\n\t\t\t\t\t\thasNextPage\n\t\t\t\t\t\tendCursor\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"first":10,"issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issue":{"comments":{"nodes":[{"id":"6b337bfa-a7df-4b5c-9d6d-a0c8c6212301","body":"This is a reply to the comment","createdAt":"2025-10-07T16:03:19.784Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"1bc6bceb-1f2c-4a52-8f23-155aeb966ee2","body":"Reply using shorthand","createdAt":"2025-10-07T15:00:53.946Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"5c1c0a90-6778-41f4-94b4-fce69d894bb7","body":"Reply using comment URL","createdAt":"2025-10-07T15:00:53.396Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"74e02d2a-5e18-48fc-b000-a4c12bdc3106","body":"This is a reply to the comment","createdAt":"2025-10-07T15:00:52.858Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"9f06f784-4132-4fae-bf2c-9065365759e3","body":"Reply using URL in dedicated tool","createdAt":"2025-10-07T13:55:14.826Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"243e8a79-e8cc-4617-848a-573758dcdfd5","body":"This is a reply using the dedicated tool","createdAt":"2025-10-07T13:55:14.349Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"c43cf7b9-574a-40fc-8b79-23f854bd4c48","body":"This is a reply to the comment","createdAt":"2025-10-07T13:50:15.195Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"272b238c-8065-4b61-975c-903b2fb9825a","body":"This is a reply to the comment","createdAt":"2025-03-30T14:16:58.457Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"e4668cd7-c87c-4305-bfc2-a2f0167435e9","body":"This is a reply to the comment","createdAt":"2025-03-30T14:15:49.931Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":null,"children":{"nodes":[]}},{"id":"9d24080c-b7d0-4a23-8b3a-5cd7fe1eafd9","body":"This is a reply to the comment","createdAt":"2025-03-30T14:11:59.567Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}}],"pageInfo":{"hasNextPage":true,"endCursor":"9d24080c-b7d0-4a23-8b3a-5cd7fe1eafd9"}}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"b4d-LzviRfsobJDoz23EC46pbZ8fUcE"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/get_issue_comments_handler_With_thread_parameter.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 322
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n\t\t\tissues(filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } }, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"number":10,"teamKey":"TEST"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 1
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 1316
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssue($id: String!) {\n\t\t\tissue(id: $id) {\n\t\t\t\tid\n\t\t\t\tidentifier\n\t\t\t\ttitle\n\t\t\t\tdescription\n\t\t\t\tpriority\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tstate {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tassignee {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tteam {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t}\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tprojectMilestone {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\trelations(first: 20) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttype\n\t\t\t\t\t\trelatedIssue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tidentifier\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinverseRelations(first: 20) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttype\n\t\t\t\t\t\tissue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tidentifier\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tattachments(first: 50) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\tsubtitle\n\t\t\t\t\t\turl\n\t\t\t\t\t\tsourceType\n\t\t\t\t\t\tmetadata\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue","createdAt":"2025-03-03T11:34:49.241Z","updatedAt":"2025-10-07T16:03:19.741Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"project":{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation"},"projectMilestone":{"id":"5214c4d9-9c2a-4ae7-b5e5-e33058b3e131","name":"M1: Gather potential resources to investigate"},"relations":{"nodes":[]},"inverseRelations":{"nodes":[]},"attachments":{"nodes":[]}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"36b-2tYg/a9gEbu6WHEn0vlp7gu8zgw"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s
    - id: 2
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 840
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetIssueComments($issueId: String!, $parentId: ID, $first: Int!, $after: String) {\n\t\t\tissue(id: $issueId) {\n\t\t\t\tcomments(\n\t\t\t\t\tfirst: $first,\n\t\t\t\t\tafter: $after,\n\t\t\t\t\tfilter: { parent: { id: { eq: $parentId } } }\n\t\t\t\t) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tbody\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tuser {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t\tparent {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchildren(first: 1) {\n\t\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpageInfo {\n\t\t\t\t\t\thasNextPage\n\t\t\t\t\t\tendCursor\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"first":10,"issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f","parentId":"ae3d62d6-3f40-4990-867b-5c97dd265a40"}}'
        form: {}
        headers:
            Content-Type:
                - application/json
        url: https://api.linear.app/graphql
        method: POST
      response:
        proto: HTTP/2.0
        proto_major: 2
        proto_minor: 0
        transfer_encoding: []
        trailer: {}
        content_length: -1
        uncompressed: true
        body: |
            {"data":{"issue":{"comments":{"nodes":[{"id":"6b337bfa-a7df-4b5c-9d6d-a0c8c6212301","body":"This is a reply to the comment","createdAt":"2025-10-07T16:03:19.784Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"1bc6bceb-1f2c-4a52-8f23-155aeb966ee2","body":"Reply using shorthand","createdAt":"2025-10-07T15:00:53.946Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"5c1c0a90-6778-41f4-94b4-fce69d894bb7","body":"Reply using comment URL","createdAt":"2025-10-07T15:00:53.396Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"74e02d2a-5e18-48fc-b000-a4c12bdc3106","body":"This is a reply to the comment","createdAt":"2025-10-07T15:00:52.858Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"9f06f784-4132-4fae-bf2c-9065365759e3","body":"Reply using URL in dedicated tool","createdAt":"2025-10-07T13:55:14.826Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"243e8a79-e8cc-4617-848a-573758dcdfd5","body":"This is a reply using the dedicated tool","createdAt":"2025-10-07T13:55:14.349Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"c43cf7b9-574a-40fc-8b79-23f854bd4c48","body":"This is a reply to the comment","createdAt":"2025-10-07T13:50:15.195Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"272b238c-8065-4b61-975c-903b2fb9825a","body":"This is a reply to the comment","createdAt":"2025-03-30T14:16:58.457Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"9d24080c-b7d0-4a23-8b3a-5cd7fe1eafd9","body":"This is a reply to the comment","createdAt":"2025-03-30T14:11:59.567Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"7539ff3c-1c61-4ac3-9203-bb51ec376c7e","body":"This is a reply to the comment","createdAt":"2025-03-30T13:41:41.052Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}}],"pageInfo":{"hasNextPage":false,"endCursor":"7539ff3c-1c61-4ac3-9203-bb51ec376c7e"}}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"b77-/Ngoki0KqN6T8N8XsVwy0CWbzYI"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/memory-bank/systemPatterns.md:
--------------------------------------------------------------------------------

```markdown
# System Patterns: Linear MCP Server

## System Architecture
The Linear MCP Server follows a modular architecture with clear separation of concerns:

```mermaid
flowchart TD
    Main[main.go] --> RootCmd[cmd/root.go]
    RootCmd --> ServerCmd[cmd/server.go]
    RootCmd --> SetupCmd[cmd/setup.go]
    ServerCmd --> |writeAccess| Server[pkg/server/server.go]
    Server --> |writeAccess| Tools[pkg/server/tools.go]
    Server --> LinearClient[pkg/linear/client.go]
    LinearClient --> RateLimiter[pkg/linear/rate_limiter.go]
    LinearClient --> Models[pkg/linear/models.go]
    Tools --> LinearClient
```

1. **Main Module** (`main.go`): Entry point that initializes the command structure.
2. **Command Module** (`cmd/`): Handles command-line interface and subcommands.
   - **Root Command** (`cmd/root.go`): Base command that serves as the entry point for all subcommands.
   - **Server Command** (`cmd/server.go`): Handles the server functionality.
   - **Setup Command** (`cmd/setup.go`): Handles the setup functionality for AI assistants.
3. **Server Module** (`pkg/server`): Handles MCP protocol implementation and tool registration.
4. **Linear Client Module** (`pkg/linear`): Manages communication with the Linear API.

## Key Technical Decisions

### 1. Command-Line Interface
- Uses the Cobra library for command-line handling.
- Implements a subcommand structure for different functionalities.
- Provides consistent flag handling across subcommands.

### 2. Write Access Control
- Implements write access control (default: disabled) to control access to write operations.
- Command-line flag `--write-access` determines whether write tools are registered.
- Write operations (`linear_create_issue`, `linear_update_issue`, `linear_add_comment`) are only available when write access is enabled.

### 3. Setup Automation
- Automates the installation and configuration process for AI assistants.
- Checks for existing binary before downloading.
- Merges new settings with existing settings to preserve user configuration.
- Supports multiple AI assistants (starting with Cline).

### 4. MCP Protocol Implementation
- Uses the `github.com/mark3labs/mcp-go` library for MCP server implementation.
- Implements the standard MCP protocol for tool registration and execution.

### 5. Linear API Integration
- Custom Linear client implementation in the `pkg/linear` package.
- Handles authentication, request formatting, and response parsing.

### 6. Rate Limiting
- Implements rate limiting to respect Linear API quotas.
- Uses a simple rate limiter to prevent API quota exhaustion.

### 7. Error Handling
- Consistent error handling patterns throughout the codebase.
- Errors are propagated up and formatted according to MCP specifications.

### 8. Testing Strategy
- Uses `go-vcr` for recording and replaying HTTP interactions in tests.
- Test fixtures stored in `testdata/fixtures/`.

## Design Patterns

### 1. Command Pattern
- The Cobra library implements the Command pattern for handling CLI commands.
- Each command is a separate object with its own run method.
- Commands can have subcommands, creating a hierarchical command structure.

### 2. Factory Pattern
- `NewLinearMCPServer()` and `NewLinearClientFromEnv()` functions create and initialize complex objects.

### 3. Dependency Injection
- The Linear client is injected into tool handlers, promoting testability and loose coupling.

### 4. Handler Pattern
- Each MCP tool has a dedicated handler function that processes requests and returns results.

### 5. Builder Pattern
- MCP tools are constructed using a builder-like pattern with the `mcp.NewTool()` function and various `With*` methods.

## MCP Tool Registration Patterns

### Tool Registration Process
- All tools are registered in the `RegisterTools` function in `pkg/server/tools.go`
- Tools are conditionally registered based on write access permissions
- Each tool follows a consistent registration pattern:

```go
server.AddTool(mcp.NewTool("tool_name").
    WithDescription("Tool description").
    WithInputSchema(mcp.Object{
        "param1": mcp.String().WithDescription("Parameter description"),
        "param2": mcp.Required(mcp.String()).WithDescription("Required parameter"),
    }).
    WithHandler(toolHandlerFunction))
```

### Tool Handler Structure
- Each tool has a dedicated handler function that processes MCP requests
- Handler functions follow a consistent pattern:
  1. Extract and validate parameters from the request
  2. Call appropriate Linear client methods
  3. Format and return the response
- Error handling is consistent across all handlers
- Response formatting follows MCP specifications

### Parameter Definition Patterns
- Tool parameters are defined using the MCP schema format
- Required parameters are marked with `mcp.Required()`
- Parameter types include: `mcp.String()`, `mcp.Number()`, `mcp.Boolean()`
- All parameters include descriptive help text
- Optional parameters have sensible defaults where applicable

### Write Access Control
- Write operations are controlled by the `writeAccess` flag
- Read-only tools are always registered
- Write tools (`linear_create_issue`, `linear_update_issue`, `linear_add_comment`) are only registered when write access is enabled
- This provides a security layer to prevent accidental modifications

## Linear API Integration Patterns

### Client Architecture
- The Linear client is implemented in `pkg/linear/client.go`
- Client provides high-level methods that abstract GraphQL complexity
- All API interactions go through the centralized client

### Authentication Pattern
- Authentication is handled via the `LINEAR_API_KEY` environment variable
- API key is validated on first API request, not at startup
- No support for other authentication methods currently
- Client includes the API key in all requests via Authorization header

### Rate Limiting Implementation
- Simple rate limiter implemented to respect Linear's API quotas
- Rate limiting is applied at the client level before making requests
- Current implementation uses a basic token bucket approach
- Rate limits are not configurable (hardcoded values)

### API Response Handling
- API responses are parsed into Go structs defined in `pkg/linear/models.go`
- JSON unmarshaling handles nested structures (e.g., `LabelConnection`)
- Error responses from Linear API are translated into user-friendly messages
- GraphQL errors are properly extracted and formatted

### GraphQL Query Patterns
- All Linear API interactions use GraphQL queries and mutations
- Queries are embedded as string literals in the client methods
- Parameter types in queries match Linear API expectations (e.g., `String!` vs `ID!`)
- Query structure follows Linear's API schema requirements

### Identifier Resolution Strategy
- Flexible identifier resolution allows users to specify entities by:
  - UUID (direct API identifier)
  - Human-readable identifiers (e.g., "TEAM-123" for issues)
  - Names (e.g., team names, label names)
- Resolution functions handle the translation from user input to API identifiers
- Error handling provides clear feedback when identifiers cannot be resolved

## Component Relationships

### Commands and Subcommands
- The root command serves as the entry point for all subcommands.
- Subcommands handle specific functionalities (server, setup).
- Each subcommand has its own flags and run method.

### Server and Tools
- The server registers tools during initialization.
- Each tool has a handler function that processes requests.
- Tools are defined with schemas that specify required and optional parameters.

### Linear Client and API
- The Linear client translates MCP tool calls into Linear API requests.
- It handles authentication, request formatting, and response parsing.
- The rate limiter ensures API quotas are respected.

## Data Flow

1. **Command Flow**:
   ```mermaid
   sequenceDiagram
       participant User
       participant Main as main.go
       participant Root as cmd/root.go
       participant Cmd as Subcommand
       participant Action as Command Action
       
       User->>Main: Execute Command
       Main->>Root: Execute Root Command
       Root->>Cmd: Parse and Execute Subcommand
       Cmd->>Action: Execute Command Action
       Action->>User: Return Result
   ```

2. **Setup Flow**:
   ```mermaid
   sequenceDiagram
       participant User
       participant Setup as cmd/setup.go
       participant Binary as Binary Management
       participant Config as Configuration Management
       
       User->>Setup: Execute Setup Command
       Setup->>Binary: Check for Existing Binary
       Binary-->>Setup: Binary Status
       alt Binary Not Found
           Setup->>Binary: Download Latest Release
           Binary-->>Setup: Binary Path
       end
       Setup->>Config: Create/Update Configuration
       Config-->>Setup: Configuration Status
       Setup->>User: Setup Complete
   ```

3. **Request Flow**:
   ```mermaid
   sequenceDiagram
       participant Client as MCP Client
       participant Server as MCP Server
       participant Tool as Tool Handler
       participant Linear as Linear Client
       participant API as Linear API
       
       Client->>Server: Call Tool Request
       Server->>Tool: Forward Request
       Tool->>Linear: Translate Request
       Linear->>API: API Request
       API->>Linear: API Response
       Linear->>Tool: Parsed Response
       Tool->>Server: Formatted Result
       Server->>Client: Tool Result
   ```

4. **Error Flow**:
   ```mermaid
   sequenceDiagram
       participant Client as MCP Client
       participant Server as MCP Server
       participant Tool as Tool Handler
       participant Linear as Linear Client
       participant API as Linear API
       
       Client->>Server: Call Tool Request
       Server->>Tool: Forward Request
       Tool->>Linear: Translate Request
       Linear->>API: API Request
       API->>Linear: Error Response
       Linear->>Tool: Error
       Tool->>Server: Error Result
       Server->>Client: Error Result
   ```

## Code Organization
- **main.go**: Entry point that initializes the command structure
- **cmd/root.go**: Root command that serves as the base for all subcommands
- **cmd/server.go**: Server command that handles the server functionality
- **cmd/setup.go**: Setup command that handles the setup functionality for AI assistants
- **pkg/server/server.go**: Server initialization and management
- **pkg/server/tools.go**: Tool definitions and handlers
- **pkg/linear/client.go**: Linear API client implementation
- **pkg/linear/models.go**: Data models for Linear API requests and responses
- **pkg/linear/rate_limiter.go**: Rate limiting implementation
- **pkg/linear/test_helpers.go**: Test utilities

```
Page 3/5FirstPrevNextLast