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

--------------------------------------------------------------------------------
/.clinerules/memory-bank.md:
--------------------------------------------------------------------------------

```markdown
# Cline's Memory Bank

I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.

## Memory Bank Structure

The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:

flowchart TD
    PB[projectbrief.md] --> PC[productContext.md]
    PB --> SP[systemPatterns.md]
    PB --> TC[techContext.md]

    PC --> AC[activeContext.md]
    SP --> AC
    TC --> AC
    
    TC --> DW[developmentWorkflows.md]
    SP --> DW

    AC --> P[progress.md]

### Core Files (Required)
1. `projectbrief.md`
   - Foundation document that shapes all other files
   - Created at project start if it doesn't exist
   - Defines core requirements and goals
   - Source of truth for project scope

2. `productContext.md`
   - Why this project exists
   - Problems it solves
   - How it should work
   - User experience goals

3. `activeContext.md`
   - Current work focus
   - Recent changes
   - Next steps
   - Active decisions and considerations
   - Important patterns and preferences
   - Learnings and project insights

4. `systemPatterns.md`
   - System architecture
   - Key technical decisions
   - Design patterns in use
   - Component relationships
   - Critical implementation paths

5. `techContext.md`
   - Technologies used
   - Development setup
   - Technical constraints
   - Dependencies
   - Tool usage patterns

6. `progress.md`
   - What works
   - What's left to build
   - Current status
   - Known issues
   - Evolution of project decisions

### Development-Specific Files (Critical for Development Tasks)
7. `developmentWorkflows.md`
   - Git workflow and branch management
   - Release process and versioning
   - Build, test, and deployment commands
   - Code quality standards and practices
   - Debugging and troubleshooting guides
   - **Essential for any development work**

### Additional Context
Create additional files/folders within memory-bank/ when they help organize:
- Complex feature documentation
- Integration specifications
- API documentation
- Testing strategies
- Deployment procedures

## Core Workflows

### Plan Mode
flowchart TD
    Start[Start] --> ReadFiles[Read Memory Bank]
    ReadFiles --> CheckFiles{Files Complete?}

    CheckFiles -->|No| Plan[Create Plan]
    Plan --> Document[Document in Chat]

    CheckFiles -->|Yes| Verify[Verify Context]
    Verify --> Strategy[Develop Strategy]
    Strategy --> Present[Present Approach]

### Act Mode
flowchart TD
    Start[Start] --> Context[Check Memory Bank]
    Context --> Update[Update Documentation]
    Update --> Execute[Execute Task]
    Execute --> Document[Document Changes]

## Documentation Updates

Memory Bank updates occur when:
1. Discovering new project patterns
2. After implementing significant changes
3. When user requests with **update memory bank** (MUST review ALL files)
4. When context needs clarification
5. When development processes change (update developmentWorkflows.md)

flowchart TD
    Start[Update Process]

    subgraph Process
        P1[Review ALL Files]
        P2[Document Current State]
        P3[Clarify Next Steps]
        P4[Document Insights & Patterns]

        P1 --> P2 --> P3 --> P4
    end

    Start --> Process

Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.

REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.

```

--------------------------------------------------------------------------------
/testdata/fixtures/add_comment_handler_Valid comment.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
            User-Agent:
                - linear-mcp-go/1.0.0
        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: 507
        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":"Test comment","issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f"}}}'
        form: {}
        headers:
            Content-Type:
                - application/json
            User-Agent:
                - linear-mcp-go/1.0.0
        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":"ae3d62d6-3f40-4990-867b-5c97dd265a40","body":"Test comment","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","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/"207-A36zaBZM2etAKuEB572ckko3PUw"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/update_comment_handler_Valid comment update with shorthand.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","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/"135-SJj8cT5t4cfk3yiLplqz0T3j/uE"
            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: 550
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tmutation UpdateComment($id: String!, $input: CommentUpdateInput!) {\n\t\t\tcommentUpdate(id: $id, 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":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","input":{"body":"Updated comment text via shorthand"}}}'
        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":{"commentUpdate":{"success":true,"comment":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","body":"Updated comment text via shorthand","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","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/"21d-cSmAgkHmAGU1qbZ3TOEPbZCgEx8"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/update_issue_handler_Valid update.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
            User-Agent:
                - linear-mcp-go/1.0.0
        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: 589
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tmutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n\t\t\tissueUpdate(id: $id, 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}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","input":{"title":"Updated Test Issue"}}}'
        form: {}
        headers:
            Content-Type:
                - application/json
            User-Agent:
                - linear-mcp-go/1.0.0
        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":{"issueUpdate":{"success":true,"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-03-30T10:09:33.866Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"1ed-NQYcphbrRBp9Qc2UkRq4wmgK4Us"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/update_comment_handler_Valid comment update with hash only.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 shorthand","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/"143-m1nvHd82UihGhurKSTyMAF0whhw"
            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: 545
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tmutation UpdateComment($id: String!, $input: CommentUpdateInput!) {\n\t\t\tcommentUpdate(id: $id, 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":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","input":{"body":"Updated comment text via hash"}}}'
        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":{"commentUpdate":{"success":true,"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","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/"218-JZAC2Y8E9AKSYWGafAFwxnfBleE"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/resource_TeamResourceHandler_Fetch By Key.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 310
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetTeams($filter: TeamFilter) {\n\t\t\tteams(filter: $filter) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t\tdescription\n\t\t\t\t\tstates {\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}\n\t\t\t}\n\t\t}\n\t"}'
        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":{"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST","description":null,"states":{"nodes":[{"id":"d4caa373-1a02-431c-bd3f-1bbb67318617","name":"Done"},{"id":"cffb8999-f10e-447d-9672-8faf5b06ac67","name":"Todo"},{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},{"id":"2d26ea57-c1f7-43ae-ba30-3f828ac8edb6","name":"Canceled"},{"id":"2a939ee1-65a1-445c-8e5d-18239e5f64bc","name":"Duplicate"},{"id":"12bb7f66-d9be-4faa-800f-49b8e3b38a3f","name":"In Progress"}]}}]}}}
        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-+ISnhlSrm6Gd7LWWbqn3eOeSXhw"
            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: 310
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetTeams($filter: TeamFilter) {\n\t\t\tteams(filter: $filter) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t\tdescription\n\t\t\t\t\tstates {\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}\n\t\t\t}\n\t\t}\n\t"}'
        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":{"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST","description":null,"states":{"nodes":[{"id":"d4caa373-1a02-431c-bd3f-1bbb67318617","name":"Done"},{"id":"cffb8999-f10e-447d-9672-8faf5b06ac67","name":"Todo"},{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},{"id":"2d26ea57-c1f7-43ae-ba30-3f828ac8edb6","name":"Canceled"},{"id":"2a939ee1-65a1-445c-8e5d-18239e5f64bc","name":"Duplicate"},{"id":"12bb7f66-d9be-4faa-800f-49b8e3b38a3f","name":"In Progress"}]}}]}}}
        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-+ISnhlSrm6Gd7LWWbqn3eOeSXhw"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/resource_TeamResourceHandler_Fetch By Name.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 310
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetTeams($filter: TeamFilter) {\n\t\t\tteams(filter: $filter) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t\tdescription\n\t\t\t\t\tstates {\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}\n\t\t\t}\n\t\t}\n\t"}'
        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":{"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST","description":null,"states":{"nodes":[{"id":"d4caa373-1a02-431c-bd3f-1bbb67318617","name":"Done"},{"id":"cffb8999-f10e-447d-9672-8faf5b06ac67","name":"Todo"},{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},{"id":"2d26ea57-c1f7-43ae-ba30-3f828ac8edb6","name":"Canceled"},{"id":"2a939ee1-65a1-445c-8e5d-18239e5f64bc","name":"Duplicate"},{"id":"12bb7f66-d9be-4faa-800f-49b8e3b38a3f","name":"In Progress"}]}}]}}}
        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-+ISnhlSrm6Gd7LWWbqn3eOeSXhw"
            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: 310
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetTeams($filter: TeamFilter) {\n\t\t\tteams(filter: $filter) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t\tdescription\n\t\t\t\t\tstates {\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}\n\t\t\t}\n\t\t}\n\t"}'
        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":{"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST","description":null,"states":{"nodes":[{"id":"d4caa373-1a02-431c-bd3f-1bbb67318617","name":"Done"},{"id":"cffb8999-f10e-447d-9672-8faf5b06ac67","name":"Todo"},{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},{"id":"2d26ea57-c1f7-43ae-ba30-3f828ac8edb6","name":"Canceled"},{"id":"2a939ee1-65a1-445c-8e5d-18239e5f64bc","name":"Duplicate"},{"id":"12bb7f66-d9be-4faa-800f-49b8e3b38a3f","name":"In Progress"}]}}]}}}
        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-+ISnhlSrm6Gd7LWWbqn3eOeSXhw"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/pkg/tools/get_issue_comments.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"

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

// GetIssueCommentsTool is the tool definition for getting paginated comments for an issue
var GetIssueCommentsTool = mcp.NewTool("linear_get_issue_comments",
	mcp.WithDescription("Retrieves a flat list of comments for a Linear issue and thread. Use to list all replies in a thread or all top-level comments."),
	mcp.WithString("issue", mcp.Required(), mcp.Description("issue identifier (e.g., 'TEAM-123')")),
	mcp.WithString("thread", mcp.Description("Optional thread identifier. Accepts: full URL, UUID, shorthand (comment-abc123), or hash (abc123). If not provided, all top-level comments are returned.")),
	mcp.WithNumber("limit", mcp.Description("Maximum number of comments to return (default: 10)")),
	mcp.WithString("after", mcp.Description("Cursor for pagination, to get comments after this point")),
)

// GetIssueCommentsHandler handles the linear_get_issue_comments tool
func GetIssueCommentsHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		// Extract arguments
		issueIdentifier, err := request.RequireString("issue")
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
		}

		// Extract optional arguments
		parentID := request.GetString("thread", "")
		limit := request.GetInt("limit", 10)
		afterCursor := request.GetString("after", "")

		// Resolve issue identifier to a UUID
		issueID, err := resolveIssueIdentifier(linearClient, issueIdentifier)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve issue: %v", err)}}}, nil
		}

		// Get the issue for basic information
		issue, err := linearClient.GetIssue(issueID)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get issue: %v", err)}}}, nil
		}

		// Get the comments
		commentsInput := linear.GetIssueCommentsInput{
			IssueID:     issueID,
			ParentID:    parentID,
			Limit:       limit,
			AfterCursor: afterCursor,
		}

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

		// Format the result
		var resultText string

		// Add issue information
		resultText += formatIssueIdentifier(issue) + "\n"

		// Add thread information
		if parentID == "" {
			resultText += "Thread: none (top-level comments)\n"
		} else {
			resultText += fmt.Sprintf("Thread: %s (replies to comment)\n", parentID)
		}

		resultText += "\n"

		// Add comments
		if len(comments.Nodes) > 0 {
			resultText += "Comments:\n"

			for _, comment := range comments.Nodes {
				createdAt := comment.CreatedAt.Format("2006-01-02 15:04:05")
				hasReplies := false
				if comment.Children != nil && len(comment.Children.Nodes) > 0 {
					hasReplies = true
				}

				resultText += fmt.Sprintf("- Comment: %s\n  %s\n  CreatedAt: %s\n  HasReplies: %s\n  Body: %s\n",
					comment.ID,
					formatUserIdentifier(comment.User),
					createdAt,
					formatBool(hasReplies),
					comment.Body)
			}
		} else {
			resultText += "Comments: None\n"
		}

		// Add pagination information
		resultText += "\nPagination:\n"
		resultText += fmt.Sprintf("Has more comments: %s\n", formatBool(comments.PageInfo.HasNextPage))

		if comments.PageInfo.HasNextPage {
			resultText += fmt.Sprintf("Next cursor: %s\n", comments.PageInfo.EndCursor)
		}

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

// formatBool formats a boolean value as "yes" or "no"
func formatBool(value bool) string {
	if value {
		return "yes"
	}
	return "no"
}

```

--------------------------------------------------------------------------------
/pkg/tools/get_issue.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"

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

// GetIssueTool is the tool definition for getting an issue
var GetIssueTool = mcp.NewTool("linear_get_issue",
	mcp.WithDescription("Retrieves a single Linear issue."),
	mcp.WithString("issue", mcp.Required(), mcp.Description("ID or identifier (e.g., 'TEAM-123') of the issue to retrieve")),
)

// GetIssueHandler handles the linear_get_issue tool
func GetIssueHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		// Extract arguments
		issueIdentifier, err := request.RequireString("issue")
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
		}

		// Resolve issue identifier to a UUID
		issueID, err := resolveIssueIdentifier(linearClient, issueIdentifier)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve issue: %v", err)}}}, nil
		}

		// Get the issue
		issue, err := linearClient.GetIssue(issueID)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get issue: %v", err)}}}, nil
		}

		// Format the result using the full issue formatting
		resultText := formatIssue(issue)

		// Add assignee and team information using identifier formatting
		if issue.Assignee != nil {
			resultText += fmt.Sprintf("Assignee: %s\n", formatUserIdentifier(issue.Assignee))
		} else {
			resultText += "Assignee: None\n"
		}

		resultText += fmt.Sprintf("%s\n", formatTeamIdentifier(issue.Team))

		if issue.Project != nil {
			resultText += fmt.Sprintf("Project: %s (%s)\n", issue.Project.Name, issue.Project.ID)
		} else {
			resultText += "Project: None\n"
		}

		if issue.ProjectMilestone != nil {
			resultText += fmt.Sprintf("Milestone: %s (%s)\n", issue.ProjectMilestone.Name, issue.ProjectMilestone.ID)
		} else {
			resultText += "Milestone: None\n"
		}

		// Add attachments section if there are attachments
		if issue.Attachments != nil && len(issue.Attachments.Nodes) > 0 {
			resultText += "\nAttachments:\n"

			// Display all attachments in a simple list without grouping by source type
			for _, attachment := range issue.Attachments.Nodes {
				resultText += fmt.Sprintf("- %s: %s\n", attachment.Title, attachment.URL)
				if attachment.Subtitle != "" {
					resultText += fmt.Sprintf("  %s\n", attachment.Subtitle)
				}
			}
		} else {
			resultText += "\nAttachments: None\n"
		}

		// Add related issues section
		if (issue.Relations != nil && len(issue.Relations.Nodes) > 0) ||
			(issue.InverseRelations != nil && len(issue.InverseRelations.Nodes) > 0) {
			resultText += "\nRelated Issues:\n"

			// Add direct relations
			if issue.Relations != nil && len(issue.Relations.Nodes) > 0 {
				for _, relation := range issue.Relations.Nodes {
					if relation.RelatedIssue != nil {
						resultText += fmt.Sprintf("- %s\n  Title: %s\n  RelationType: %s\n  URL: %s\n",
							formatIssueIdentifier(relation.RelatedIssue),
							relation.RelatedIssue.Title,
							relation.Type,
							relation.RelatedIssue.URL)
					}
				}
			}

			// Add inverse relations
			if issue.InverseRelations != nil && len(issue.InverseRelations.Nodes) > 0 {
				for _, relation := range issue.InverseRelations.Nodes {
					if relation.Issue != nil {
						resultText += fmt.Sprintf("- %s\n  Title: %s\n  RelationType: %s (inverse)\n  URL: %s\n",
							formatIssueIdentifier(relation.Issue),
							relation.Issue.Title,
							relation.Type,
							relation.Issue.URL)
					}
				}
			}
		} else {
			resultText += "\nRelated Issues: None\n"
		}

		// Note about comments
		resultText += "\nComments: Use the linear_get_issue_comments tool to retrieve comments for this issue.\n"

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

```

--------------------------------------------------------------------------------
/testdata/fixtures/reply_to_comment_handler_Valid reply.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      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: 1
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 585
        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":"This is a reply using the 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":"243e8a79-e8cc-4617-848a-573758dcdfd5","body":"This is a reply using the dedicated tool","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-243e8a79","createdAt":"2025-10-07T13:55:14.349Z","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/"223-qLAwFJpu6uPxkTsxKQkgHklk+m4"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Create sub issue from identifier.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: 880
        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":"","parentId":"1c2de93f-4321-4015-bfde-ee893ef7976f","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Sub Issue"}}}'
        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":"3671ad80-a3c1-4783-b5cd-a0fa24e4ff9e","identifier":"TEST-90","title":"Sub Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-90/sub-issue","createdAt":"2025-10-06T09:44:32.400Z","updatedAt":"2025-10-06T09:44:32.400Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[]},"project":null,"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/"218-0Svxf7NuXjSFfPU18wFj97GbalA"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Create issue with labels.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 342
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetLabelsByName($teamId: String!, $names: [String!]!) {\n\t\t\tteam(id: $teamId) {\n\t\t\t\tlabels(filter: { name: { in: $names } }) {\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}\n\t\t}\n\t","variables":{"names":["team label 1"],"teamId":"234c5451-a839-4c8f-98d9-da00973f1060"}}'
        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":{"team":{"labels":{"nodes":[{"id":"37e1cdc8-a696-4412-8ad7-8ba8435ba0f4","name":"team label 1"}]}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"6d-NtoFPWMa8e+cqurX/zzYmekk2Dg"
            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: 890
        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":"","labelIds":["37e1cdc8-a696-4412-8ad7-8ba8435ba0f4"],"teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Issue with Labels"}}}'
        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":"640b14f2-643e-4010-a194-c4f1c2f6177b","identifier":"TEST-91","title":"Issue with Labels","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-91/issue-with-labels","createdAt":"2025-10-06T09:44:37.052Z","updatedAt":"2025-10-06T09:44:37.052Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[{"id":"37e1cdc8-a696-4412-8ad7-8ba8435ba0f4","name":"team label 1"}]},"project":null,"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/"26b-Kh3ayVGqSTqbO3LB/W4HMT1nF4s"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/update_project_handler_Valid update.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 733
        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":"bfa49864-16c9-44db-994e-a11ba2b386f1"}}'
        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":{"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2","description":"Updated Description Only","slugId":"e1153169a428","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/updated-project-name-2-e1153169a428","createdAt":"2025-06-28T18:42:20.223Z","updatedAt":"2025-06-28T18:56:53.580Z","lead":null,"members":{"nodes":[]},"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}]},"initiatives":{"nodes":[]},"startDate":null,"targetDate":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/"221-5dHPmIHEC1rBzEVJqPrdCyJ2NbY"
            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: 400
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tmutation ProjectUpdate($id: String!, $input: ProjectUpdateInput!) {\n\t\t\tprojectUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tproject {\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}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","input":{"name":"Updated 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":{"projectUpdate":{"success":true,"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name","description":"Updated Description Only","slugId":"e1153169a428","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/updated-project-name-e1153169a428"}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"12d-wnpLXmtIiIJiE+9jOexlH0g7wGY"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/get_project_handler_Non-existent slug.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 714
        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":"non-existent-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: |
            {"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: 906
        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":"non-existent-slug"}},{"slugId":{"eq":"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: 35
        uncompressed: false
        body: |
            {"data":{"projects":{"nodes":[]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Length:
                - "35"
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/get_project_handler_Invalid project.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 716
        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":"NONEXISTENT-PROJECT"}}'
        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: 911
        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":"NONEXISTENT-PROJECT"}},{"slugId":{"eq":"PROJECT"}}]}}}'
        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: 35
        uncompressed: false
        body: |
            {"data":{"projects":{"nodes":[]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Length:
                - "35"
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/update_project_handler_Update only description.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 733
        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":"bfa49864-16c9-44db-994e-a11ba2b386f1"}}'
        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":{"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2","description":"Updated Description","slugId":"e1153169a428","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/updated-project-name-2-e1153169a428","createdAt":"2025-06-28T18:42:20.223Z","updatedAt":"2025-06-28T21:44:56.564Z","lead":null,"members":{"nodes":[]},"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}]},"initiatives":{"nodes":[]},"startDate":null,"targetDate":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/"21c-zZT5JHii4UiJPH+GXG9r9OBmr2A"
            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: 411
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tmutation ProjectUpdate($id: String!, $input: ProjectUpdateInput!) {\n\t\t\tprojectUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tproject {\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}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","input":{"description":"Updated Description Only"}}}'
        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":{"projectUpdate":{"success":true,"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2","description":"Updated Description Only","slugId":"e1153169a428","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/updated-project-name-2-e1153169a428"}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"131-exvq876nrApQ/gFGvqKIsgVXXo0"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

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

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 717
        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":"non-existent-project"}}'
        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: 912
        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":"non-existent-project"}},{"slugId":{"eq":"project"}}]}}}'
        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: 35
        uncompressed: false
        body: |
            {"data":{"projects":{"nodes":[]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Length:
                - "35"
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/update_project_handler_Non-existent project.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 717
        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":"non-existent-project"}}'
        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: 912
        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":"non-existent-project"}},{"slugId":{"eq":"project"}}]}}}'
        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: 35
        uncompressed: false
        body: |
            {"data":{"projects":{"nodes":[]}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Length:
                - "35"
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/update_project_handler_Update name and description.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 733
        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":"bfa49864-16c9-44db-994e-a11ba2b386f1"}}'
        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":{"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name","description":"Updated Description Only","slugId":"e1153169a428","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/updated-project-name-e1153169a428","createdAt":"2025-06-28T18:42:20.223Z","updatedAt":"2025-06-28T21:44:56.396Z","lead":null,"members":{"nodes":[]},"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}]},"initiatives":{"nodes":[]},"startDate":null,"targetDate":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/"21d-jg2vG6vT5da0XDzGnJVlIPQka7E"
            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: 438
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tmutation ProjectUpdate($id: String!, $input: ProjectUpdateInput!) {\n\t\t\tprojectUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tproject {\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}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","input":{"name":"Updated Project Name 2","description":"Updated Description"}}}'
        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":{"projectUpdate":{"success":true,"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2","description":"Updated Description","slugId":"e1153169a428","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/updated-project-name-2-e1153169a428"}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"12c-qm99WOFVQtdIlEMxVa4cu3vvOAo"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

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

```go
package server

import (
	"fmt"
	"os"

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

const (
	// ServerName is the name of the MCP server
	ServerName = "Linear MCP Server"
	// ServerVersion is the version of the MCP server
	ServerVersion = "1.15.0"
)

// LinearMCPServer represents the Linear MCP server
type LinearMCPServer struct {
	mcpServer    *mcpserver.MCPServer
	linearClient *linear.LinearClient
	writeAccess  bool // Controls whether write operations are enabled
}

// NewLinearMCPServer creates a new Linear MCP server
func NewLinearMCPServer(writeAccess bool) (*LinearMCPServer, error) {
	// Create the Linear client
	linearClient, err := linear.NewLinearClientFromEnv(ServerVersion)
	if err != nil {
		return nil, fmt.Errorf("failed to create Linear client: %w", err)
	}

	// Create the MCP server
	mcpServer := mcpserver.NewMCPServer(ServerName, ServerVersion)

	// Create the Linear MCP server
	server := &LinearMCPServer{
		mcpServer:    mcpServer,
		linearClient: linearClient,
		writeAccess:  writeAccess,
	}

	// Register tools
	RegisterTools(mcpServer, linearClient, writeAccess)

	// Register resources
	RegisterResources(mcpServer, linearClient)

	return server, nil
}

// Start starts the Linear MCP server
func (s *LinearMCPServer) Start() error {
	// Check if the Linear API key is set
	if os.Getenv("LINEAR_API_KEY") == "" {
		return fmt.Errorf("LINEAR_API_KEY environment variable is required")
	}

	// Start the server
	fmt.Printf("Starting %s v%s\n", ServerName, ServerVersion)
	return mcpserver.ServeStdio(s.mcpServer)
}

// GetMCPServer returns the underlying MCP server
func (s *LinearMCPServer) GetMCPServer() *mcpserver.MCPServer {
	return s.mcpServer
}

// GetLinearClient returns the Linear client
func (s *LinearMCPServer) GetLinearClient() *linear.LinearClient {
	return s.linearClient
}

// GetReadOnlyToolNames returns the names of all read-only tools
func GetReadOnlyToolNames() map[string]bool {
	return map[string]bool{
		"linear_search_issues":       true,
		"linear_get_user_issues":     true,
		"linear_get_issue":           true,
		"linear_get_issue_comments":  true,
		"linear_get_teams":           true,
		"linear_get_project":         true,
		"linear_search_projects":     true,
		"linear_get_milestone":       true,
		"linear_get_initiative":      true,
	}
}

// RegisterTools registers all Linear tools with the MCP server
func RegisterTools(s *mcpserver.MCPServer, linearClient *linear.LinearClient, writeAccess bool) {
	// Register tools, based on writeAccess
	addTool := func(tool mcp.Tool, handler mcpserver.ToolHandlerFunc) {
		if !writeAccess {
			if readOnly := GetReadOnlyToolNames()[tool.Name]; !readOnly {
				// Skip registering write tools if write access is disabled
				return
			}
		}
		s.AddTool(tool, handler)
	}

	// Register each tool
	addTool(tools.SearchIssuesTool, tools.SearchIssuesHandler(linearClient))
	addTool(tools.GetUserIssuesTool, tools.GetUserIssuesHandler(linearClient))
	addTool(tools.GetIssueTool, tools.GetIssueHandler(linearClient))
	addTool(tools.GetIssueCommentsTool, tools.GetIssueCommentsHandler(linearClient))
	addTool(tools.GetTeamsTool, tools.GetTeamsHandler(linearClient))
	addTool(tools.GetProjectTool, tools.GetProjectHandler(linearClient))
	addTool(tools.SearchProjectsTool, tools.SearchProjectsHandler(linearClient))
	addTool(tools.CreateProjectTool, tools.CreateProjectHandler(linearClient))
	addTool(tools.UpdateProjectTool, tools.UpdateProjectHandler(linearClient))
	addTool(tools.GetMilestoneTool, tools.GetMilestoneHandler(linearClient))
	addTool(tools.CreateMilestoneTool, tools.CreateMilestoneHandler(linearClient))
	addTool(tools.UpdateMilestoneTool, tools.UpdateMilestoneHandler(linearClient))
	addTool(tools.GetInitiativeTool, tools.GetInitiativeHandler(linearClient))
	addTool(tools.CreateInitiativeTool, tools.CreateInitiativeHandler(linearClient))
	addTool(tools.UpdateInitiativeTool, tools.UpdateInitiativeHandler(linearClient))
	addTool(tools.CreateIssueTool, tools.CreateIssueHandler(linearClient))
	addTool(tools.UpdateIssueTool, tools.UpdateIssueHandler(linearClient))
	addTool(tools.AddCommentTool, tools.AddCommentHandler(linearClient))
	addTool(tools.ReplyToCommentTool, tools.ReplyToCommentHandler(linearClient))
	addTool(tools.UpdateCommentTool, tools.UpdateCommentHandler(linearClient))
}

```

--------------------------------------------------------------------------------
/testdata/fixtures/get_user_issues_handler_Current user issues.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 88
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetCurrentUser {\n\t\t\tviewer {\n\t\t\t\tid\n\t\t\t}\n\t\t}\n\t"}'
        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":{"viewer":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85"}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"42-6PvRWCcs8jOXFygoL3FhQP+PK3o"
            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: 556
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetUserIssues($userId: String!, $first: Int, $includeArchived: Boolean) {\n\t\t\tuser(id: $userId) {\n\t\t\t\tassignedIssues(first: $first, includeArchived: $includeArchived) {\n\t\t\t\t\tnodes {\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\tdescription\n\t\t\t\t\t\tpriority\n\t\t\t\t\t\turl\n\t\t\t\t\t\tstate {\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}\n\t\t\t}\n\t\t}\n\t","variables":{"first":5,"includeArchived":false,"userId":"cc24eee4-9edc-4bfe-b91b-fedde125ba85"}}'
        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\":{\"user\":{\"assignedIssues\":{\"nodes\":[{\"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\",\"state\":{\"id\":\"42f7ad15-fca3-4d11-b349-0e3c1385c256\",\"name\":\"Backlog\"}},{\"id\":\"c58953c5-a31d-4c5a-9427-6d6ebd9a1a4e\",\"identifier\":\"TEST-1\",\"title\":\"Welcome to Linear \U0001F44B\",\"description\":\"Hi there. Complete these issues to learn how to use Linear and discover ✨**ProTips.** When you're done, delete them or move them to another team for others to view.\\n\\n### **To start, type** `C` to **create your first issue.**\\n\\nCreate issues from any view using `C` or by clicking the `New issue` button.\\n\\n \\n\\n[1189b618-97f2-4e2c-ae25-4f25467679e7](https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/532d146d-bcd6-4602-bf1f-83f674b70fff/1189b618-97f2-4e2c-ae25-4f25467679e7)\\n\\nOur issue editor and comments support Markdown. You can also: \\n\\n* @mention a teammate\\n* Drag & drop images or video (Loom & YouTube embed automatically)\\n* Use emoji ✅\\n* Type `/` to bring up more formatting options\",\"priority\":2,\"url\":\"https://linear.app/linear-mcp-go-test/issue/TEST-1/welcome-to-linear\",\"state\":{\"id\":\"cffb8999-f10e-447d-9672-8faf5b06ac67\",\"name\":\"Todo\"}}]}}}}\n"
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"52b-iwXdi0Au8LYVFWrptXXwpSz7HVA"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Create sub issue with labels.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 350
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetLabelsByName($teamId: String!, $names: [String!]!) {\n\t\t\tteam(id: $teamId) {\n\t\t\t\tlabels(filter: { name: { in: $names } }) {\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}\n\t\t}\n\t","variables":{"names":["ws-label 2","Feature"],"teamId":"234c5451-a839-4c8f-98d9-da00973f1060"}}'
        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":{"team":{"labels":{"nodes":[{"id":"fcd49e32-5043-4bfd-88a5-2bbe3c95124a","name":"ws-label 2"},{"id":"94087865-ce6c-470b-896c-4d1d2c7456b8","name":"Feature"}]}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"aa-fC3D/tel+q+/3XRkoHnKGoa70sg"
            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: 983
        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":"","labelIds":["fcd49e32-5043-4bfd-88a5-2bbe3c95124a","94087865-ce6c-470b-896c-4d1d2c7456b8"],"parentId":"1c2de93f-4321-4015-bfde-ee893ef7976f","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Sub Issue with Labels"}}}'
        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":"bff52c02-bc32-422b-ba31-719c9b0f525f","identifier":"TEST-92","title":"Sub Issue with Labels","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-92/sub-issue-with-labels","createdAt":"2025-10-06T09:44:41.506Z","updatedAt":"2025-10-06T09:44:41.506Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[{"id":"fcd49e32-5043-4bfd-88a5-2bbe3c95124a","name":"ws-label 2"},{"id":"94087865-ce6c-470b-896c-4d1d2c7456b8","name":"Feature"}]},"project":null,"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/"2b0-QsIb1mXyrKfU1pD5SHlQtNywn7k"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/docs/prd/006-issue-comments-pagination.md:
--------------------------------------------------------------------------------

```markdown
# Product Requirements Document: Issue Comments Pagination

## Overview

This document outlines the requirements for splitting the comment functionality from the `get_issue` tool and implementing a new `get_issue_comments` tool with proper pagination support.

## Background

Currently, the `get_issue` tool returns all comments for an issue as part of its response. This approach has several limitations:

1. For issues with many comments, the response can become very large
2. There's no way to paginate through comments
3. There's no way to specifically retrieve replies to a particular comment
4. The client receives more data than might be needed if they're only interested in the issue details

## Requirements

### Functional Requirements

1. Create a new tool named `linear_get_issue_comments` that retrieves comments for a Linear issue
2. Implement pagination for comments to handle potentially long conversations
3. Support retrieving comments from specific threads (top-level or replies to a specific comment)
4. Modify the existing `get_issue` tool to remove the comments section and add a reference to the new tool
5. Ensure backward compatibility by maintaining the same comment formatting style

### Technical Requirements

1. The new tool should accept the following parameters:
   - `issue`: (required) ID or identifier of the issue to retrieve comments for
   - `thread`: (optional) UUID of the parent comment to retrieve replies for
   - `limit`: (optional) Maximum number of comments to return (default: 10)
   - `after`: (optional) Cursor for pagination to get comments after this point

2. The response should include:
   - Basic issue information (identifier, UUID)
   - Thread information (root or parent comment UUID)
   - List of comments with user, timestamp, and content
   - Pagination information (has more comments, next cursor)
   - Indication if comments have replies

3. Update the Linear client to support the new functionality:
   - Add a `GetIssueComments` method that supports pagination and thread filtering
   - Define a `GetIssueCommentsInput` struct for the parameters
   - Define a `PaginatedCommentConnection` struct for the response

4. Register the new tool in the server and add it to the read-only tools list

## Implementation Details

### API Changes

The Linear GraphQL API already supports pagination and filtering for comments. We'll use the following query structure:

```graphql
query GetIssueComments($issueId: String!, $parentId: String, $first: Int!, $after: String) {
  issue(id: $issueId) {
    comments(
      first: $first,
      after: $after,
      filter: { parent: { id: { eq: $parentId } } }
    ) {
      nodes {
        id
        body
        createdAt
        user {
          id
          name
        }
        childCount
      }
      pageInfo {
        hasNextPage
        endCursor
      }
      totalCount
    }
  }
}
```

### Implementation Steps

| Step | Description | Status |
|------|-------------|--------|
| 1 | Add `GetIssueCommentsInput` and `PaginatedCommentConnection` structs to models.go | ✅ |
| 2 | Implement `GetIssueComments` method in the Linear client | ✅ |
| 3 | Create the `get_issue_comments.go` file with tool definition and handler | ✅ |
| 4 | Update `get_issue.go` to remove comments section and add reference to new tool | ✅ |
| 5 | Register the new tool in server.go | ✅ |
| 6 | Add the new tool to the read-only tools list | ✅ |
| 7 | Test the implementation | ✅ |

## Usage Examples

### Example 1: Get top-level comments for an issue

```json
{
  "issue": "TEAM-123",
  "limit": 5
}
```

### Example 2: Get replies to a specific comment

```json
{
  "issue": "TEAM-123",
  "thread": "comment-uuid-here",
  "limit": 10
}
```

### Example 3: Paginate through comments

```json
{
  "issue": "TEAM-123",
  "after": "cursor-from-previous-response",
  "limit": 10
}
```

## Benefits

1. **Improved Performance**: Clients can request only the comments they need, reducing payload size
2. **Better User Experience**: Support for pagination allows handling large comment threads efficiently
3. **More Flexibility**: Ability to navigate through specific comment threads
4. **Cleaner API**: Separation of concerns between issue details and comments

## Conclusion

The implementation of the `linear_get_issue_comments` tool with pagination support will significantly improve the handling of issue comments, especially for issues with extensive discussions. This change aligns with best practices for API design by providing more granular control over data retrieval and reducing unnecessary data transfer.

```

--------------------------------------------------------------------------------
/pkg/server/resources_test.go:
--------------------------------------------------------------------------------

```go
package server

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

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

func TestResourceHandlers(t *testing.T) {
	// Define test cases
	tests := []struct {
		handlerName string
		name        string
		uri         string
		handlerFunc func(*linear.LinearClient) mcpserver.ResourceHandlerFunc
	}{
		// TeamsResourceHandler test cases
		{
			handlerName: "TeamsResourceHandler",
			name:        "List All",
			uri:         "linear://teams",
			handlerFunc: TeamsResourceHandler,
		},
		// TeamResourceHandler test cases
		{
			handlerName: "TeamResourceHandler",
			name:        "Fetch By ID",
			uri:         "linear://team/" + TEAM_ID,
			handlerFunc: TeamResourceHandler,
		},
		{
			handlerName: "TeamResourceHandler",
			name:        "Fetch By Name",
			uri:         "linear://team/" + TEAM_NAME,
			handlerFunc: TeamResourceHandler,
		},
		{
			handlerName: "TeamResourceHandler",
			name:        "Fetch By Key",
			uri:         "linear://team/" + TEAM_KEY,
			handlerFunc: TeamResourceHandler,
		},
		{
			handlerName: "TeamResourceHandler",
			name:        "Invalid ID",
			uri:         "linear://team/invalid-identifier-does-not-exist", // Use a clearly invalid identifier
			handlerFunc: TeamResourceHandler,
		},
		{
			handlerName: "TeamResourceHandler",
			name:        "Missing ID",
			uri:         "linear://team/", // Test case where ID is missing from URI path
			handlerFunc: TeamResourceHandler,
		},
	}

	for _, tt := range tests {
		t.Run(tt.handlerName+"_"+tt.name, func(t *testing.T) {
			// Generate fixture and golden file paths
			fixtureName := "resource_" + tt.handlerName + "_" + tt.name
			goldenPath := filepath.Join("../../testdata/golden", fixtureName+".golden")

			// Create test client with VCR
			// Use distinct flags for resource tests to avoid conflicts
			client, cleanup := linear.NewTestClient(t, fixtureName, *record || *recordWrites)
			defer cleanup()

			// Get the handler function
			handler := tt.handlerFunc(client)

			// Create the request
			request := mcp.ReadResourceRequest{}
			request.Params.URI = tt.uri

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

			// Extract the actual output and error
			var actualOutput, actualErr string
			if err != nil {
				actualErr = err.Error()
			} else {
				// Marshal the contents to JSON for comparison
				jsonBytes, jsonErr := json.MarshalIndent(contents, "", "  ") // Use indent for readability
				if jsonErr != nil {
					t.Fatalf("Failed to marshal resource contents to JSON: %v", jsonErr)
				}
				actualOutput = string(jsonBytes)
			}

			// If goldenResource flag is set, update the golden file
			if *golden {
				writeGoldenFile(t, goldenPath, expectation{
					Err:    actualErr,
					Output: actualOutput,
				})
				// Also update the VCR recording implicitly by running the test
				t.Logf("Updated golden file: %s", goldenPath)
				// We might need to re-run the test without the golden flag
				// after recording to ensure the comparison passes.
				// However, for now, just writing the golden file is the goal.
				return // Skip comparison when updating golden files
			}

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

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

			// Compare output (only if no error is expected)
			if expected.Err == "" && actualErr == "" {
				// Compare JSON strings directly
				if diff := cmp.Diff(expected.Output, actualOutput); diff != "" {
					// To make diffs easier to read, unmarshal and compare structures
					var expectedContents []mcp.ResourceContents
					var actualContents []mcp.ResourceContents
					json.Unmarshal([]byte(expected.Output), &expectedContents) // Ignore error for diffing
					json.Unmarshal([]byte(actualOutput), &actualContents)      // Ignore error for diffing
					t.Errorf("Output mismatch (-want +got):\n%s", cmp.Diff(expectedContents, actualContents))
					t.Logf("Expected JSON:\n%s", expected.Output)
					t.Logf("Actual JSON:\n%s", actualOutput)
				}
			} else if expected.Err == "" && actualErr != "" {
				t.Errorf("Expected no error, but got: %s", actualErr)
			} else if expected.Err != "" && actualErr == "" {
				t.Errorf("Expected error '%s', but got none", expected.Err)
			}
		})
	}
}

// readGoldenFile and writeGoldenFile are defined in test_helpers.go

```

--------------------------------------------------------------------------------
/pkg/server/resources.go:
--------------------------------------------------------------------------------

```go
package server

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

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

// TeamsResource is the resource definition for Linear teams
var TeamsResource = mcp.NewResource(
	"linear://teams",
	"Linear Teams",
	mcp.WithResourceDescription("List of teams in Linear"),
	mcp.WithMIMEType("application/json"),
)

// TeamResource is the resource definition for a specific Linear team
var TeamResource = mcp.NewResource(
	"linear://team/{id}",
	"Linear Team",
	mcp.WithResourceDescription("Details of a specific team in Linear"),
	mcp.WithMIMEType("application/json"),
)

// RegisterResources registers all Linear resources with the MCP server
func RegisterResources(s *mcpserver.MCPServer, linearClient *linear.LinearClient) {
	// Register Teams resource
	s.AddResource(TeamsResource, TeamsResourceHandler(linearClient))

	// Register Team resource
	s.AddResource(TeamResource, TeamResourceHandler(linearClient))
}

// TeamsResourceHandler handles the linear://teams resource
func TeamsResourceHandler(linearClient *linear.LinearClient) mcpserver.ResourceHandlerFunc {
	return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
		// Get teams from Linear
		teams, err := linearClient.GetTeams("")
		if err != nil {
			return nil, fmt.Errorf("failed to get teams: %v", err)
		}

		// Create resource content
		results := []mcp.ResourceContents{}
		for _, t := range teams {
			teamJSON, err := json.Marshal(t)
			if err != nil {
				return nil, fmt.Errorf("failed to marshal team: %v", err)
			}

			results = append(results, mcp.TextResourceContents{
				URI:      fmt.Sprintf("linear://team/%s", t.ID),
				MIMEType: "application/json",
				Text:     string(teamJSON),
			})
		}

		return results, nil
	}
}

// TeamResourceHandler handles the linear://team/{id} resource
func TeamResourceHandler(linearClient *linear.LinearClient) mcpserver.ResourceHandlerFunc {
	return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
		// Extract team ID from URI
		uri := request.Params.URI
		if !strings.HasPrefix(uri, "linear://team/") {
			return nil, fmt.Errorf("invalid team URI: %s", uri)
		}

		teamID := uri[len("linear://team/"):]
		if teamID == "" {
			return nil, fmt.Errorf("team ID is required")
		}

		// Resolve team ID (could be UUID, name, or key)
		resolvedTeamID, err := resolveTeamIdentifier(linearClient, teamID)
		if err != nil {
			return nil, fmt.Errorf("failed to resolve team identifier: %v", err)
		}

		// Get all teams and find the matching one
		teams, err := linearClient.GetTeams("")
		if err != nil {
			return nil, fmt.Errorf("failed to get teams: %v", err)
		}

		var team *linear.Team
		for i, t := range teams {
			if t.ID == resolvedTeamID {
				team = &teams[i]
				break
			}
		}

		if team == nil {
			return nil, fmt.Errorf("team not found: %s", teamID)
		}

		// Format team as JSON
		teamJSON, err := json.Marshal(team)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal team: %v", err)
		}

		// Create resource content
		return []mcp.ResourceContents{
			mcp.TextResourceContents{
				URI:      fmt.Sprintf("linear://team/%s", team.ID),
				MIMEType: "application/json",
				Text:     string(teamJSON),
			},
		}, 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)
}

// isValidUUID checks if a string is a valid UUID
func isValidUUID(uuidStr string) bool {
	// Simple UUID validation - check if it has the correct format
	// This is a simplified version and doesn't validate the UUID fully
	return len(uuidStr) == 36 && strings.Count(uuidStr, "-") == 4
}

```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Valid issue with team key.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 310
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetTeams($filter: TeamFilter) {\n\t\t\tteams(filter: $filter) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t\tdescription\n\t\t\t\t\tstates {\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}\n\t\t\t}\n\t\t}\n\t"}'
        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":{"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST","description":null,"states":{"nodes":[{"id":"d4caa373-1a02-431c-bd3f-1bbb67318617","name":"Done"},{"id":"cffb8999-f10e-447d-9672-8faf5b06ac67","name":"Todo"},{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},{"id":"2d26ea57-c1f7-43ae-ba30-3f828ac8edb6","name":"Canceled"},{"id":"2a939ee1-65a1-445c-8e5d-18239e5f64bc","name":"Duplicate"},{"id":"12bb7f66-d9be-4faa-800f-49b8e3b38a3f","name":"In Progress"}]}}]}}}
        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-+ISnhlSrm6Gd7LWWbqn3eOeSXhw"
            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: 845
        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":"","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Test Issue with team key"}}}'
        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":"bf164a25-82c9-4cd6-aaeb-c83c8dbf09b0","identifier":"TEST-88","title":"Test Issue with team key","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-88/test-issue-with-team-key","createdAt":"2025-10-06T09:44:21.430Z","updatedAt":"2025-10-06T09:44:21.430Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[]},"project":null,"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/"236-rqBxky5hsQ9s0ISmkxV2klYfYBg"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Valid issue with team name.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: 2
interactions:
    - id: 0
      request:
        proto: HTTP/1.1
        proto_major: 1
        proto_minor: 1
        content_length: 310
        transfer_encoding: []
        trailer: {}
        host: api.linear.app
        remote_addr: ""
        request_uri: ""
        body: '{"query":"\n\t\tquery GetTeams($filter: TeamFilter) {\n\t\t\tteams(filter: $filter) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t\tdescription\n\t\t\t\t\tstates {\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}\n\t\t\t}\n\t\t}\n\t"}'
        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":{"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST","description":null,"states":{"nodes":[{"id":"d4caa373-1a02-431c-bd3f-1bbb67318617","name":"Done"},{"id":"cffb8999-f10e-447d-9672-8faf5b06ac67","name":"Todo"},{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},{"id":"2d26ea57-c1f7-43ae-ba30-3f828ac8edb6","name":"Canceled"},{"id":"2a939ee1-65a1-445c-8e5d-18239e5f64bc","name":"Duplicate"},{"id":"12bb7f66-d9be-4faa-800f-49b8e3b38a3f","name":"In Progress"}]}}]}}}
        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-+ISnhlSrm6Gd7LWWbqn3eOeSXhw"
            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: 846
        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":"","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Test Issue with team 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":"aaf77828-2cd0-413f-977d-1e66968de5f6","identifier":"TEST-87","title":"Test Issue with team name","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-87/test-issue-with-team-name","createdAt":"2025-10-06T09:44:13.623Z","updatedAt":"2025-10-06T09:44:13.623Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[]},"project":null,"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/"238-a41QzB6Eb98+OtI+B5Rg9E9GPa0"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/docs/prd/000-tool-standardization-overview.md:
--------------------------------------------------------------------------------

```markdown
# Linear MCP Server Tool Standardization

## Executive Summary

This document provides an overview of the Linear MCP Server Tool Standardization initiative. The goal is to establish and implement consistent rules across all tools in the Linear MCP Server, improving user experience, code maintainability, and overall quality.

## Documents in this Series

1. [**000-tool-standardization-overview.md**](./000-tool-standardization-overview.md) (this document)
   - Executive summary and overview of the standardization effort

2. [**002-tool-standardization.md**](./002-tool-standardization.md)
   - Detailed requirements and rationale for the standardization rules
   - Analysis of current state and implementation plan

3. [**003-tool-standardization-implementation.md**](./003-tool-standardization-implementation.md)
   - Detailed implementation guide with specific tasks for each tool
   - Example implementations and code structure changes

4. [**004-tool-standardization-tracking.md**](./004-tool-standardization-tracking.md)
   - Tracking sheet for implementation progress
   - Detailed task breakdown and status tracking

5. [**005-sample-implementation.md**](./005-sample-implementation.md)
   - Sample code for key components
   - Testing examples and implementation strategy

## Standardization Rules

### Rule 1: Concise Tool Descriptions
Tool descriptions should be concise and focus only on the tool's purpose and functionality, without listing parameters or explaining the result format.

### Rule 2: Flexible Object Identifier Resolution
Input arguments that reference Linear objects should accept multiple forms of identification (UUID, name, key) and resolve them to the underlying UUID using consistent resolution methods.

### Rule 3: Consistent Entity Rendering
Tools fetching the same entities should emit results using the same format, with additional fields added at the bottom. This includes:

1. **Full Entity Rendering**: When displaying an entity as the primary subject of a response, use a consistent format with all required fields.
2. **Entity Identifier Rendering**: When referencing an entity from another entity, use a consistent, concise identifier format.

### Rule 4: Field Superset for Retrieval Methods
The fields rendered on retrieval methods should follow a consistent pattern:
- **Detail retrieval methods** must include all fields that can be set through create/update methods
- **Overview retrieval methods** only need to include key metadata fields
This ensures appropriate level of detail while maintaining consistency across the API.

## Benefits

1. **Improved User Experience**
   - Consistent behavior across all tools
   - More flexible parameter handling
   - Standardized output format

2. **Enhanced Code Maintainability**
   - Shared utility functions for common operations
   - Reduced code duplication
   - Consistent patterns across the codebase

3. **Better Quality**
   - Standardized error handling
   - Consistent validation
   - Comprehensive testing

## Implementation Approach

The implementation will follow a phased approach:

1. **Phase 1: Create Shared Utility Functions**
   - Develop common identifier resolution functions
   - Create entity rendering functions
   - Establish consistent patterns

2. **Phase 2: Update Tools**
   - Update each tool to follow the standardization rules
   - Start with one tool as a reference implementation
   - Apply the same patterns to all remaining tools

3. **Phase 3: Update Tests**
   - Update test fixtures to reflect the new formatting
   - Add tests for the new utility functions
   - Verify all tests pass with the new implementation

## Timeline and Resources

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

## Success Criteria

The standardization effort will be considered successful when:

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. Retrieval methods include all fields that can be set in create/update methods
5. Code reuse is maximized through shared functions
6. All tests pass with the new implementation

## Next Steps

1. Review and approve the standardization requirements
2. Begin implementation of Phase 1 (Shared Utility Functions)
3. Select a tool to serve as the reference implementation
4. Implement changes for all tools
5. Update tests and verify functionality

```

--------------------------------------------------------------------------------
/pkg/tools/initiative_tools.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"
	"strings"

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

var GetInitiativeTool = mcp.NewTool("linear_get_initiative",
	mcp.WithDescription("Get a single initiative by its identifier (ID or name)."),
	mcp.WithString("initiative", mcp.Required(), mcp.Description("The identifier of the initiative to get. Can be the initiative's ID or name.")),
)

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

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

		resultText := FormatInitiative(*initiative)

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

func FormatInitiative(initiative linear.Initiative) string {
	var builder strings.Builder
	builder.WriteString(fmt.Sprintf("Initiative: %s\n", initiative.Name))
	builder.WriteString(fmt.Sprintf("  ID: %s\n", initiative.ID))
	if initiative.Description != "" {
		builder.WriteString(fmt.Sprintf("  Description: %s\n", initiative.Description))
	}
	builder.WriteString(fmt.Sprintf("  URL: %s\n", initiative.URL))
	return builder.String()
}

var CreateInitiativeTool = mcp.NewTool("linear_create_initiative",
	mcp.WithDescription("Create a new initiative."),
	mcp.WithString("name", mcp.Required(), mcp.Description("The name of the initiative.")),
	mcp.WithString("description", mcp.Description("The description of the initiative.")),
)

func CreateInitiativeHandler(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
		}

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

		input := linear.InitiativeCreateInput{
			Name:        name,
			Description: description,
		}

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

		resultText := FormatInitiative(*initiative)

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

var UpdateInitiativeTool = mcp.NewTool("linear_update_initiative",
	mcp.WithDescription("Update an existing initiative."),
	mcp.WithString("initiative", mcp.Required(), mcp.Description("The ID or name of the initiative to update.")),
	mcp.WithString("name", mcp.Description("The new name of the initiative.")),
	mcp.WithString("description", mcp.Description("The new description of the initiative.")),
)

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

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

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

		input := linear.InitiativeUpdateInput{
			Name:        name,
			Description: description,
		}

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

		resultText := FormatInitiative(*initiative)

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

```

--------------------------------------------------------------------------------
/pkg/tools/rendering.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"fmt"
	"strings"

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

// Full Entity Rendering Functions

// formatIssue returns a consistently formatted full representation of an issue
func formatIssue(issue *linear.Issue) string {
	if issue == nil {
		return "Issue: Unknown"
	}
	
	var result strings.Builder
	result.WriteString(fmt.Sprintf("Issue: %s (UUID: %s)\n", issue.Identifier, issue.ID))
	result.WriteString(fmt.Sprintf("Title: %s\n", issue.Title))
	result.WriteString(fmt.Sprintf("URL: %s\n", issue.URL))
	
	result.WriteString(fmt.Sprintf("Priority: %s\n", priorityToString(issue.Priority)))
	
	statusStr := "None"
	if issue.Status != "" {
		statusStr = issue.Status
	} else if issue.State != nil {
		statusStr = issue.State.Name
	}
	result.WriteString(fmt.Sprintf("Status: %s\n", statusStr))
	
	// Include labels if available
	if issue.Labels != nil && len(issue.Labels.Nodes) > 0 {
		labelNames := make([]string, 0, len(issue.Labels.Nodes))
		for _, label := range issue.Labels.Nodes {
			labelNames = append(labelNames, label.Name)
		}
		result.WriteString(fmt.Sprintf("Labels: %s\n", strings.Join(labelNames, ", ")))
	} else {
		result.WriteString("Labels: None\n")
	}
	
	// Include description
	if issue.Description != "" {
		result.WriteString(fmt.Sprintf("Description: %s\n", issue.Description))
	} else {
		result.WriteString("Description: None\n")
	}
	
	return result.String()
}

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

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

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

// Entity Identifier Rendering Functions

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

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

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

// formatCommentIdentifier returns a consistently formatted identifier for a comment
func formatCommentIdentifier(comment *linear.Comment) string {
	if comment == nil {
		return "Unknown"
	}
	
	return fmt.Sprintf(comment.ID)
}

// formatNewComment returns a consistently formatted response for a newly created comment
// parentID should be the UUID of the parent comment if this is a reply, empty string otherwise
func formatNewComment(comment *linear.Comment, issue *linear.Issue, parentID string) string {
	if comment == nil || issue == nil {
		return "Error: Invalid comment or issue"
	}
	
	var result strings.Builder
	
	// Format based on whether this is a reply or top-level comment
	if parentID != "" {
		// This is a reply
		result.WriteString(fmt.Sprintf("Replied with Comment: %s\n", comment.ID))
		result.WriteString(fmt.Sprintf("to Thread: %s\n", parentID))
		result.WriteString(fmt.Sprintf("on %s\n", formatIssueIdentifier(issue)))
	} else {
		// This is a top-level comment
		result.WriteString(fmt.Sprintf("Added Comment: %s\n", comment.ID))
		result.WriteString(fmt.Sprintf("to %s\n", formatIssueIdentifier(issue)))
	}
	
	result.WriteString(fmt.Sprintf("URL: %s", comment.URL))
	
	return result.String()
}

```

--------------------------------------------------------------------------------
/pkg/tools/search_issues.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"
	"strings"

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

// SearchIssuesTool is the tool definition for searching issues
var SearchIssuesTool = mcp.NewTool("linear_search_issues",
	mcp.WithDescription("Searches Linear issues."),
	mcp.WithString("query", mcp.Description("Optional text to search in title and description")),
	mcp.WithString("team", mcp.Description("Filter by team identifier (UUID, name, or key)")),
	mcp.WithString("status", mcp.Description("Filter by status name (e.g., 'In Progress', 'Done')")),
	mcp.WithString("assignee", mcp.Description("Filter by assignee identifier (UUID, name, or email)")),
	mcp.WithString("labels", mcp.Description("Filter by label names (comma-separated)")),
	mcp.WithString("priority", getPriorityOptions()...),
	mcp.WithNumber("estimate", mcp.Description("Filter by estimate points")),
	mcp.WithBoolean("includeArchived", mcp.Description("Include archived issues in results (default: false)")),
	mcp.WithNumber("limit", mcp.Description("Max results to return (default: 10)")),
)

// SearchIssuesHandler handles the linear_search_issues tool
func SearchIssuesHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		// Build search input
		input := linear.SearchIssuesInput{}

		input.Query = request.GetString("query", "")

		if team, err := request.RequireString("team"); err == nil && team != "" {
			// Resolve team identifier to a team ID
			teamID, err := resolveTeamIdentifier(linearClient, team)
			if err != nil {
				return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve team: %v", err)}}}, nil
			}
			input.TeamID = teamID
		}

		input.Status = request.GetString("status", "")

		if assignee, err := request.RequireString("assignee"); err == nil && assignee != "" {
			// Resolve assignee identifier to a user ID
			assigneeID, err := resolveUserIdentifier(linearClient, assignee)
			if err != nil {
				return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve assignee: %v", err)}}}, nil
			}
			input.AssigneeID = assigneeID
		}

		if labelsStr, err := request.RequireString("labels"); err == nil && labelsStr != "" {
			// Split comma-separated labels
			labels := []string{}
			for _, label := range strings.Split(labelsStr, ",") {
				trimmedLabel := strings.TrimSpace(label)
				if trimmedLabel != "" {
					labels = append(labels, trimmedLabel)
				}
			}
			input.Labels = labels
		}

		if priorityStr, err := request.RequireString("priority"); err == nil && priorityStr != "" {
			priority, err := parsePriority(priorityStr)
			if err != nil {
				return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Invalid priority: %v", err)}}}, nil
			}
			input.Priority = &priority
		}

		if estimate, err := request.RequireFloat("estimate"); err == nil {
			input.Estimate = &estimate
		}

		input.IncludeArchived = request.GetBool("includeArchived", false)
		input.Limit = request.GetInt("limit", 10)

		// Search for issues
		issues, err := linearClient.SearchIssues(input)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to search issues: %v", err)}}}, nil
		}

		// Format the result
		resultText := fmt.Sprintf("Found %d issues:\n", len(issues))
		for _, issue := range issues {
			// Create a temporary Issue object to use with formatIssueIdentifier
			tempIssue := &linear.Issue{
				ID:         issue.ID,
				Identifier: issue.Identifier,
			}

			priorityStr := priorityToString(issue.Priority)

			statusStr := "None"
			if issue.Status != "" {
				statusStr = issue.Status
			} else if issue.StateName != "" {
				statusStr = issue.StateName
			}

			resultText += fmt.Sprintf("- %s\n", formatIssueIdentifier(tempIssue))
			resultText += fmt.Sprintf("  Title: %s\n", issue.Title)
			resultText += fmt.Sprintf("  Priority: %s\n", priorityStr)
			resultText += fmt.Sprintf("  Status: %s\n", statusStr)
			if issue.Project != nil {
				resultText += fmt.Sprintf("  Project: %s (%s)\n", issue.Project.Name, issue.Project.ID)
			} else {
				resultText += "  Project: None\n"
			}
			if issue.ProjectMilestone != nil {
				resultText += fmt.Sprintf("  Milestone: %s (%s)\n", issue.ProjectMilestone.Name, issue.ProjectMilestone.ID)
			} else {
				resultText += "  Milestone: None\n"
			}
			resultText += fmt.Sprintf("  URL: %s\n", issue.URL)
		}

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

```

--------------------------------------------------------------------------------
/testdata/fixtures/get_issue_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-06-28T19:53:27.855Z","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-tillFLIUMm8VXol85JbmMotLYUg"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/get_issue_handler_Get comment 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":12,"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":"9407c793-5fd8-4730-9280-0e17ffddf320","identifier":"TEST-12","title":"Comments 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/"7e-a2LOPkL8PZhOQop7X2YpU+ZF/Y8"
            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":"9407c793-5fd8-4730-9280-0e17ffddf320"}}'
        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":"9407c793-5fd8-4730-9280-0e17ffddf320","identifier":"TEST-12","title":"Comments issue","description":"This is the description","priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-12/comments-issue","createdAt":"2025-03-04T08:40:53.877Z","updatedAt":"2025-03-04T08:43:37.989Z","state":{"id":"cffb8999-f10e-447d-9672-8faf5b06ac67","name":"Todo"},"assignee":null,"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"project":null,"projectMilestone":null,"relations":{"nodes":[]},"inverseRelations":{"nodes":[]},"attachments":{"nodes":[{"id":"cf677e8d-955f-430e-b281-4ee9bde7df79","title":"[docs] Getting Started","subtitle":"Gitpod Documentation: Learn how to start your first Gitpod workspace for free, set up a gitpod.yml configuration file and enable Prebuilds.","url":"https://www.gitpod.io/docs/introduction/getting-started","sourceType":"api","metadata":{},"createdAt":"2025-03-04T08:43:37.989Z"}]}}}}
        headers:
            Alt-Svc:
                - h3=":443"; ma=86400
            Cache-Control:
                - no-store
            Cf-Cache-Status:
                - DYNAMIC
            Content-Type:
                - application/json; charset=utf-8
            Etag:
                - W/"3d2-y7Op6fHSC2Lvc4f+0aw4k03LArM"
            Server:
                - cloudflare
            Vary:
                - Accept-Encoding
            Via:
                - 1.1 google
        status: 200 OK
        code: 200
        duration: 0s

```

--------------------------------------------------------------------------------
/testdata/fixtures/get_project_handler_By 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

```

--------------------------------------------------------------------------------
/pkg/tools/create_issue.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"context"
	"fmt"
	"strings"

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

// CreateIssueTool is the tool definition for creating issues
var CreateIssueTool = mcp.NewTool("linear_create_issue",
	mcp.WithDescription("Creates a new Linear issue."),
	mcp.WithString("title", mcp.Required(), mcp.Description("Issue title")),
	mcp.WithString("team", mcp.Required(), mcp.Description("Team identifier (key, UUID or name)")),
	mcp.WithString("description", mcp.Description("Issue description")),
	mcp.WithString("priority", getPriorityOptions()...),
	mcp.WithString("status", mcp.Description("Issue status")),
	mcp.WithString("makeSubissueOf", mcp.Description("Makes this issue a sub-issue of the specified parent. Accepts issue ID (UUID) or identifier (e.g., 'TEAM-123'). Creates a parent-child relationship in Linear.")),
	mcp.WithString("labels", mcp.Description("Optional comma-separated list of label IDs or names to assign")),
	mcp.WithString("project", mcp.Description("Optional project identifier (ID, name, or slug) to assign the issue to")),
)

// CreateIssueHandler handles the linear_create_issue tool
func CreateIssueHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		// Extract arguments
		title, err := request.RequireString("title")
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
		}

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

		// Resolve team identifier to a team ID
		teamId, err := resolveTeamIdentifier(linearClient, team)
		if err != nil {
			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve team: %v", err)}}}, nil
		}

		// Extract optional arguments
		description := request.GetString("description", "")

		var priority *int
		if priorityStr, err := request.RequireString("priority"); err == nil && priorityStr != "" {
			p, err := parsePriority(priorityStr)
			if err != nil {
				return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Invalid priority: %v", err)}}}, nil
			}
			priority = &p
		}

		status := request.GetString("status", "")

		// Extract makeSubissueOf parameter and resolve it if needed
		var parentID *string
		if parentIssue, err := request.RequireString("makeSubissueOf"); err == nil && parentIssue != "" {
			resolvedParentID, err := resolveIssueIdentifier(linearClient, parentIssue)
			if err != nil {
				return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve parent issue: %v", err)}}}, nil
			}
			parentID = &resolvedParentID
		}

		// Extract labels parameter and resolve them if needed
		var labelIDs []string
		if labelsStr, err := request.RequireString("labels"); err == nil && labelsStr != "" {
			// Split comma-separated labels
			var labelIdentifiers []string
			for _, label := range strings.Split(labelsStr, ",") {
				trimmedLabel := strings.TrimSpace(label)
				if trimmedLabel != "" {
					labelIdentifiers = append(labelIdentifiers, trimmedLabel)
				}
			}

			// Resolve label identifiers to UUIDs
			if len(labelIdentifiers) > 0 {
				resolvedLabelIDs, err := resolveLabelIdentifiers(linearClient, teamId, labelIdentifiers)
				if err != nil {
					return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve labels: %v", err)}}}, nil
				}
				labelIDs = resolvedLabelIDs
			}
		}

		// Extract project parameter and resolve it if needed
		var projectID string
		if project, err := request.RequireString("project"); err == nil && project != "" {
			resolvedProjectID, err := resolveProjectIdentifier(linearClient, project)
			if err != nil {
				return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve project: %v", err)}}}, nil
			}
			projectID = resolvedProjectID
		}

		// Create the issue
		input := linear.CreateIssueInput{
			Title:       title,
			TeamID:      teamId,
			Description: description,
			Priority:    priority,
			Status:      status,
			ParentID:    parentID,
			LabelIDs:    labelIDs,
			ProjectID:   projectID,
		}

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

		// Return the result
		resultText := fmt.Sprintf("Created %s", formatIssueIdentifier(issue))
		resultText += fmt.Sprintf("\nTitle: %s", issue.Title)
		resultText += fmt.Sprintf("\nURL: %s", issue.URL)
		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
	}
}

```
Page 2/5FirstPrevNextLast