#
tokens: 49203/50000 30/150 files (page 2/6)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 6. Use http://codebase.md/geropl/linear-mcp-go?lines=true&page={x} to view the full context.

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/testdata/fixtures/get_milestone_handler_Non-existent milestone.yaml:
--------------------------------------------------------------------------------

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 282
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         body: '{"query":"\n\t\tquery ProjectMilestone($id: String!) {\n\t\t\tprojectMilestone(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\ttargetDate\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}\n\t\t}\n\t","variables":{"id":"non-existent-milestone"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"errors":[{"message":"Entity not found: ProjectMilestone","path":["projectMilestone"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced ProjectMilestone."}}],"data":null}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"12e-0KhOKt5R3marKFxUi49Nm51rMnE"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 386
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         body: '{"query":"\n\t\tquery GetMilestoneByName($filter: ProjectMilestoneFilter) {\n\t\t\tprojectMilestones(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\ttargetDate\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}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"name":{"eq":"non-existent-milestone"}}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: 44
 77 |         uncompressed: false
 78 |         body: |
 79 |             {"data":{"projectMilestones":{"nodes":[]}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Length:
 88 |                 - "44"
 89 |             Content-Type:
 90 |                 - application/json; charset=utf-8
 91 |             Etag:
 92 |                 - W/"2c-CfwOLPDRtN/upofCH3tMCaygMOE"
 93 |             Server:
 94 |                 - cloudflare
 95 |             Vary:
 96 |                 - Accept-Encoding
 97 |             Via:
 98 |                 - 1.1 google
 99 |         status: 200 OK
100 |         code: 200
101 |         duration: 0s
102 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/update_milestone_handler_Non-existent milestone.yaml:
--------------------------------------------------------------------------------

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 282
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         body: '{"query":"\n\t\tquery ProjectMilestone($id: String!) {\n\t\t\tprojectMilestone(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\ttargetDate\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}\n\t\t}\n\t","variables":{"id":"non-existent-milestone"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"errors":[{"message":"Entity not found: ProjectMilestone","path":["projectMilestone"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced ProjectMilestone."}}],"data":null}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"12e-0KhOKt5R3marKFxUi49Nm51rMnE"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 386
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         body: '{"query":"\n\t\tquery GetMilestoneByName($filter: ProjectMilestoneFilter) {\n\t\t\tprojectMilestones(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\ttargetDate\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}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"name":{"eq":"non-existent-milestone"}}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: 44
 77 |         uncompressed: false
 78 |         body: |
 79 |             {"data":{"projectMilestones":{"nodes":[]}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Length:
 88 |                 - "44"
 89 |             Content-Type:
 90 |                 - application/json; charset=utf-8
 91 |             Etag:
 92 |                 - W/"2c-CfwOLPDRtN/upofCH3tMCaygMOE"
 93 |             Server:
 94 |                 - cloudflare
 95 |             Vary:
 96 |                 - Accept-Encoding
 97 |             Via:
 98 |                 - 1.1 google
 99 |         status: 200 OK
100 |         code: 200
101 |         duration: 0s
102 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 220
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         body: '{"query":"\n\t\tquery GetInitiative($id: String!) {\n\t\t\tinitiative(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\turl\n\t\t\t}\n\t\t}\n\t","variables":{"id":"c6a7dd0c-cbe2-4101-906d-ddd97acb2241"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"data":{"initiative":{"id":"c6a7dd0c-cbe2-4101-906d-ddd97acb2241","name":"Created Test Initiative 2","description":"Test Description","url":"https://linear.app/linear-mcp-go-test/initiative/created-test-initiative-2-e209008074dc"}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"ea-uo1CNA8fgECA7BrV1BpztW43pw4"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 416
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         body: '{"query":"\n\t\tmutation InitiativeUpdate($id: String!, $input: InitiativeUpdateInput!) {\n\t\t\tinitiativeUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tinitiative {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\turl\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"c6a7dd0c-cbe2-4101-906d-ddd97acb2241","input":{"name":"Updated Initiative Name","description":"Updated Description"}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"data":{"initiativeUpdate":{"success":true,"initiative":{"id":"c6a7dd0c-cbe2-4101-906d-ddd97acb2241","name":"Updated Initiative Name","description":"Updated Description","url":"https://linear.app/linear-mcp-go-test/initiative/updated-initiative-name-e209008074dc"}}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"10d-JARpP5QUsJRs1w/2NBEsL4A+GRY"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/get_milestone_handler_By name.yaml:
--------------------------------------------------------------------------------

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 276
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         body: '{"query":"\n\t\tquery ProjectMilestone($id: String!) {\n\t\t\tprojectMilestone(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\ttargetDate\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}\n\t\t}\n\t","variables":{"id":"Test Milestone 2"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"errors":[{"message":"Entity not found: ProjectMilestone","path":["projectMilestone"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced ProjectMilestone."}}],"data":null}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"12e-0KhOKt5R3marKFxUi49Nm51rMnE"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 380
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         body: '{"query":"\n\t\tquery GetMilestoneByName($filter: ProjectMilestoneFilter) {\n\t\t\tprojectMilestones(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\ttargetDate\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}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"name":{"eq":"Test Milestone 2"}}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"data":{"projectMilestones":{"nodes":[{"id":"67ae1d0a-107b-42af-b063-c31a3fa05fd1","name":"Test Milestone 2","description":"Test Description","targetDate":"2024-12-31","project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2"}}]}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"106-9HbnUOf8b5eDeaQSxg5wvPdcXPI"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
  1 | name: Build and Release
  2 | 
  3 | on:
  4 |   push:
  5 |     branches: [ main ]
  6 |     tags: [ 'v*' ]
  7 |   pull_request:
  8 |     branches: [ main ]
  9 | 
 10 | jobs:
 11 |   build-and-test:
 12 |     name: Build and Test
 13 |     runs-on: ubuntu-latest
 14 |     steps:
 15 |       - name: Checkout code
 16 |         uses: actions/checkout@v4
 17 | 
 18 |       - name: Set up Go
 19 |         uses: actions/setup-go@v5
 20 |         with:
 21 |           go-version: '1.23.x'
 22 |           check-latest: true
 23 | 
 24 |       - name: Build
 25 |         run: go build -ldflags "-X github.com/geropl/linear-mcp-go/cmd.GitCommit=${{ github.sha }} -X github.com/geropl/linear-mcp-go/cmd.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -v ./...
 26 | 
 27 |       - name: Test
 28 |         run: go test -v ./...
 29 | 
 30 |   create-release:
 31 |     name: Create Release
 32 |     needs: build-and-test
 33 |     if: startsWith(github.ref, 'refs/tags/v')
 34 |     runs-on: ubuntu-latest
 35 |     permissions:
 36 |       contents: write  # This gives permission to create releases and upload assets
 37 |     steps:
 38 |       - name: Checkout code
 39 |         uses: actions/checkout@v4
 40 | 
 41 |       - name: Set up Go
 42 |         uses: actions/setup-go@v5
 43 |         with:
 44 |           go-version: '1.23.x'
 45 |           check-latest: true
 46 | 
 47 |       - name: Get version from tag
 48 |         id: get_version
 49 |         run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
 50 | 
 51 |       - name: Build for Linux
 52 |         run: |
 53 |           GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/geropl/linear-mcp-go/cmd.GitCommit=${{ github.sha }} -X github.com/geropl/linear-mcp-go/cmd.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o linear-mcp-go-linux-amd64 -v .
 54 |           chmod +x linear-mcp-go-linux-amd64
 55 | 
 56 |       - name: Build for macOS
 57 |         run: |
 58 |           GOOS=darwin GOARCH=amd64 go build -ldflags "-X github.com/geropl/linear-mcp-go/cmd.GitCommit=${{ github.sha }} -X github.com/geropl/linear-mcp-go/cmd.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o linear-mcp-go-darwin-amd64 -v .
 59 |           chmod +x linear-mcp-go-darwin-amd64
 60 | 
 61 |       - name: Build for Windows
 62 |         run: |
 63 |           GOOS=windows GOARCH=amd64 go build -ldflags "-X github.com/geropl/linear-mcp-go/cmd.GitCommit=${{ github.sha }} -X github.com/geropl/linear-mcp-go/cmd.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o linear-mcp-go-windows-amd64.exe -v .
 64 | 
 65 |       - name: Create Release
 66 |         id: create_release
 67 |         uses: actions/create-release@v1
 68 |         env:
 69 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 70 |         with:
 71 |           tag_name: ${{ github.ref }}
 72 |           release_name: Linear MCP Server ${{ steps.get_version.outputs.VERSION }}
 73 |           draft: false
 74 |           prerelease: false
 75 | 
 76 |       - name: Upload Linux Binary
 77 |         uses: actions/upload-release-asset@v1
 78 |         env:
 79 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 80 |         with:
 81 |           upload_url: ${{ steps.create_release.outputs.upload_url }}
 82 |           asset_path: ./linear-mcp-go-linux-amd64
 83 |           asset_name: linear-mcp-go-linux-amd64
 84 |           asset_content_type: application/octet-stream
 85 | 
 86 |       - name: Upload macOS Binary
 87 |         uses: actions/upload-release-asset@v1
 88 |         env:
 89 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 90 |         with:
 91 |           upload_url: ${{ steps.create_release.outputs.upload_url }}
 92 |           asset_path: ./linear-mcp-go-darwin-amd64
 93 |           asset_name: linear-mcp-go-darwin-amd64
 94 |           asset_content_type: application/octet-stream
 95 | 
 96 |       - name: Upload Windows Binary
 97 |         uses: actions/upload-release-asset@v1
 98 |         env:
 99 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
100 |         with:
101 |           upload_url: ${{ steps.create_release.outputs.upload_url }}
102 |           asset_path: ./linear-mcp-go-windows-amd64.exe
103 |           asset_name: linear-mcp-go-windows-amd64.exe
104 |           asset_content_type: application/octet-stream
105 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/search_issues_handler_Search by team.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | ---
 2 | version: 2
 3 | interactions:
 4 |     - id: 0
 5 |       request:
 6 |         proto: HTTP/1.1
 7 |         proto_major: 1
 8 |         proto_minor: 1
 9 |         content_length: 703
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tquery SearchIssues($filter: IssueFilter, $first: Int, $includeArchived: Boolean) {\n\t\t\tissues(filter: $filter, first: $first, includeArchived: $includeArchived) {\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\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\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\tassignee {\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\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}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"team":{"id":{"eq":"234c5451-a839-4c8f-98d9-da00973f1060"}}},"first":5,"includeArchived":false}}'
16 |         form: {}
17 |         headers:
18 |             Content-Type:
19 |                 - application/json
20 |         url: https://api.linear.app/graphql
21 |         method: POST
22 |       response:
23 |         proto: HTTP/2.0
24 |         proto_major: 2
25 |         proto_minor: 0
26 |         transfer_encoding: []
27 |         trailer: {}
28 |         content_length: -1
29 |         uncompressed: true
30 |         body: |
31 |             {"data":{"issues":{"nodes":[{"id":"acdeb5e4-bf7e-4281-9a15-ffea27aa5965","identifier":"TEST-71","title":"Sub Issue with Labels","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-71/sub-issue-with-labels","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":null,"labels":{"nodes":[{"id":"fcd49e32-5043-4bfd-88a5-2bbe3c95124a","name":"ws-label 2"},{"id":"94087865-ce6c-470b-896c-4d1d2c7456b8","name":"Feature"}]}},{"id":"2486653d-f073-4bdc-a94d-eab0e34587c9","identifier":"TEST-70","title":"Issue with Labels","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-70/issue-with-labels","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":null,"labels":{"nodes":[{"id":"37e1cdc8-a696-4412-8ad7-8ba8435ba0f4","name":"team label 1"}]}},{"id":"de74de85-b597-444e-abdd-c75052e72f37","identifier":"TEST-69","title":"Sub Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-69/sub-issue","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":null,"labels":{"nodes":[]}},{"id":"88460755-1c87-4a8a-927b-6a729275c9c7","identifier":"TEST-68","title":"Sub Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-68/sub-issue","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":null,"labels":{"nodes":[]}},{"id":"8a2c9f4a-88ce-4f02-9b42-81c257d7a4f7","identifier":"TEST-67","title":"Test Issue with team key","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-67/test-issue-with-team-key","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":null,"labels":{"nodes":[]}}]}}}
32 |         headers:
33 |             Alt-Svc:
34 |                 - h3=":443"; ma=86400
35 |             Cache-Control:
36 |                 - no-store
37 |             Cf-Cache-Status:
38 |                 - DYNAMIC
39 |             Content-Type:
40 |                 - application/json; charset=utf-8
41 |             Etag:
42 |                 - W/"705-mKrlAmxVi/v3PBWaz/5XCTzocI0"
43 |             Server:
44 |                 - cloudflare
45 |             Vary:
46 |                 - Accept-Encoding
47 |             Via:
48 |                 - 1.1 google
49 |         status: 200 OK
50 |         code: 200
51 |         duration: 0s
52 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 296
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         body: '{"query":"\n\t\tquery ProjectMilestone($id: String!) {\n\t\t\tprojectMilestone(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\ttargetDate\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}\n\t\t}\n\t","variables":{"id":"2d95299d-1341-484b-ab00-5cb587f2cc67"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"data":{"projectMilestone":{"id":"2d95299d-1341-484b-ab00-5cb587f2cc67","name":"Test Milestone 2.2","description":null,"targetDate":null,"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2"}}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"e5-h5yNN+ZdI94WBaZJFpZUdcwi9uA"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 543
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         body: '{"query":"\n\t\tmutation ProjectMilestoneUpdate($id: String!, $input: ProjectMilestoneUpdateInput!) {\n\t\t\tprojectMilestoneUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tprojectMilestone {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\ttargetDate\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}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"2d95299d-1341-484b-ab00-5cb587f2cc67","input":{"name":"Updated Milestone Name 22","description":"Updated Description","targetDate":"2025-01-01"}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"data":{"projectMilestoneUpdate":{"success":true,"projectMilestone":{"id":"2d95299d-1341-484b-ab00-5cb587f2cc67","name":"Updated Milestone Name 22","description":"Updated Description","targetDate":"2025-01-01","project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2"}}}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"12f-fyPVeE08hVwySnKgSCGm3ErLoSE"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/add_comment_handler_Reply_to_comment.yaml:
--------------------------------------------------------------------------------

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 322
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 575
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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 to the comment","issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f","parentId":"ae3d62d6-3f40-4990-867b-5c97dd265a40"}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"data":{"commentCreate":{"success":true,"comment":{"id":"6a2b6ab6-e3b8-41bd-bdd6-6891a8d6d86f","body":"This is a reply to the comment","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-6a2b6ab6","createdAt":"2025-10-07T16:13:07.481Z","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"}}}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"219-613DOqbo34flC1F2tWNw/F01f4k"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

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

```markdown
  1 | # Cline's Memory Bank
  2 | 
  3 | 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.
  4 | 
  5 | ## Memory Bank Structure
  6 | 
  7 | The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
  8 | 
  9 | flowchart TD
 10 |     PB[projectbrief.md] --> PC[productContext.md]
 11 |     PB --> SP[systemPatterns.md]
 12 |     PB --> TC[techContext.md]
 13 | 
 14 |     PC --> AC[activeContext.md]
 15 |     SP --> AC
 16 |     TC --> AC
 17 |     
 18 |     TC --> DW[developmentWorkflows.md]
 19 |     SP --> DW
 20 | 
 21 |     AC --> P[progress.md]
 22 | 
 23 | ### Core Files (Required)
 24 | 1. `projectbrief.md`
 25 |    - Foundation document that shapes all other files
 26 |    - Created at project start if it doesn't exist
 27 |    - Defines core requirements and goals
 28 |    - Source of truth for project scope
 29 | 
 30 | 2. `productContext.md`
 31 |    - Why this project exists
 32 |    - Problems it solves
 33 |    - How it should work
 34 |    - User experience goals
 35 | 
 36 | 3. `activeContext.md`
 37 |    - Current work focus
 38 |    - Recent changes
 39 |    - Next steps
 40 |    - Active decisions and considerations
 41 |    - Important patterns and preferences
 42 |    - Learnings and project insights
 43 | 
 44 | 4. `systemPatterns.md`
 45 |    - System architecture
 46 |    - Key technical decisions
 47 |    - Design patterns in use
 48 |    - Component relationships
 49 |    - Critical implementation paths
 50 | 
 51 | 5. `techContext.md`
 52 |    - Technologies used
 53 |    - Development setup
 54 |    - Technical constraints
 55 |    - Dependencies
 56 |    - Tool usage patterns
 57 | 
 58 | 6. `progress.md`
 59 |    - What works
 60 |    - What's left to build
 61 |    - Current status
 62 |    - Known issues
 63 |    - Evolution of project decisions
 64 | 
 65 | ### Development-Specific Files (Critical for Development Tasks)
 66 | 7. `developmentWorkflows.md`
 67 |    - Git workflow and branch management
 68 |    - Release process and versioning
 69 |    - Build, test, and deployment commands
 70 |    - Code quality standards and practices
 71 |    - Debugging and troubleshooting guides
 72 |    - **Essential for any development work**
 73 | 
 74 | ### Additional Context
 75 | Create additional files/folders within memory-bank/ when they help organize:
 76 | - Complex feature documentation
 77 | - Integration specifications
 78 | - API documentation
 79 | - Testing strategies
 80 | - Deployment procedures
 81 | 
 82 | ## Core Workflows
 83 | 
 84 | ### Plan Mode
 85 | flowchart TD
 86 |     Start[Start] --> ReadFiles[Read Memory Bank]
 87 |     ReadFiles --> CheckFiles{Files Complete?}
 88 | 
 89 |     CheckFiles -->|No| Plan[Create Plan]
 90 |     Plan --> Document[Document in Chat]
 91 | 
 92 |     CheckFiles -->|Yes| Verify[Verify Context]
 93 |     Verify --> Strategy[Develop Strategy]
 94 |     Strategy --> Present[Present Approach]
 95 | 
 96 | ### Act Mode
 97 | flowchart TD
 98 |     Start[Start] --> Context[Check Memory Bank]
 99 |     Context --> Update[Update Documentation]
100 |     Update --> Execute[Execute Task]
101 |     Execute --> Document[Document Changes]
102 | 
103 | ## Documentation Updates
104 | 
105 | Memory Bank updates occur when:
106 | 1. Discovering new project patterns
107 | 2. After implementing significant changes
108 | 3. When user requests with **update memory bank** (MUST review ALL files)
109 | 4. When context needs clarification
110 | 5. When development processes change (update developmentWorkflows.md)
111 | 
112 | flowchart TD
113 |     Start[Update Process]
114 | 
115 |     subgraph Process
116 |         P1[Review ALL Files]
117 |         P2[Document Current State]
118 |         P3[Clarify Next Steps]
119 |         P4[Document Insights & Patterns]
120 | 
121 |         P1 --> P2 --> P3 --> P4
122 |     end
123 | 
124 |     Start --> Process
125 | 
126 | 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.
127 | 
128 | 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.
129 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/add_comment_handler_Valid comment.yaml:
--------------------------------------------------------------------------------

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 322
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |             User-Agent:
 21 |                 - linear-mcp-go/1.0.0
 22 |         url: https://api.linear.app/graphql
 23 |         method: POST
 24 |       response:
 25 |         proto: HTTP/2.0
 26 |         proto_major: 2
 27 |         proto_minor: 0
 28 |         transfer_encoding: []
 29 |         trailer: {}
 30 |         content_length: -1
 31 |         uncompressed: true
 32 |         body: |
 33 |             {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}}
 34 |         headers:
 35 |             Alt-Svc:
 36 |                 - h3=":443"; ma=86400
 37 |             Cache-Control:
 38 |                 - no-store
 39 |             Cf-Cache-Status:
 40 |                 - DYNAMIC
 41 |             Content-Type:
 42 |                 - application/json; charset=utf-8
 43 |             Etag:
 44 |                 - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo"
 45 |             Server:
 46 |                 - cloudflare
 47 |             Vary:
 48 |                 - Accept-Encoding
 49 |             Via:
 50 |                 - 1.1 google
 51 |         status: 200 OK
 52 |         code: 200
 53 |         duration: 0s
 54 |     - id: 1
 55 |       request:
 56 |         proto: HTTP/1.1
 57 |         proto_major: 1
 58 |         proto_minor: 1
 59 |         content_length: 507
 60 |         transfer_encoding: []
 61 |         trailer: {}
 62 |         host: api.linear.app
 63 |         remote_addr: ""
 64 |         request_uri: ""
 65 |         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"}}}'
 66 |         form: {}
 67 |         headers:
 68 |             Content-Type:
 69 |                 - application/json
 70 |             User-Agent:
 71 |                 - linear-mcp-go/1.0.0
 72 |         url: https://api.linear.app/graphql
 73 |         method: POST
 74 |       response:
 75 |         proto: HTTP/2.0
 76 |         proto_major: 2
 77 |         proto_minor: 0
 78 |         transfer_encoding: []
 79 |         trailer: {}
 80 |         content_length: -1
 81 |         uncompressed: true
 82 |         body: |
 83 |             {"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"}}}}}
 84 |         headers:
 85 |             Alt-Svc:
 86 |                 - h3=":443"; ma=86400
 87 |             Cache-Control:
 88 |                 - no-store
 89 |             Cf-Cache-Status:
 90 |                 - DYNAMIC
 91 |             Content-Type:
 92 |                 - application/json; charset=utf-8
 93 |             Etag:
 94 |                 - W/"207-A36zaBZM2etAKuEB572ckko3PUw"
 95 |             Server:
 96 |                 - cloudflare
 97 |             Vary:
 98 |                 - Accept-Encoding
 99 |             Via:
100 |                 - 1.1 google
101 |         status: 200 OK
102 |         code: 200
103 |         duration: 0s
104 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/update_comment_handler_Valid comment update with shorthand.yaml:
--------------------------------------------------------------------------------

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 255
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"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"}}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"135-SJj8cT5t4cfk3yiLplqz0T3j/uE"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 550
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"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"}}}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"21d-cSmAgkHmAGU1qbZ3TOEPbZCgEx8"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 322
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |             User-Agent:
 21 |                 - linear-mcp-go/1.0.0
 22 |         url: https://api.linear.app/graphql
 23 |         method: POST
 24 |       response:
 25 |         proto: HTTP/2.0
 26 |         proto_major: 2
 27 |         proto_minor: 0
 28 |         transfer_encoding: []
 29 |         trailer: {}
 30 |         content_length: -1
 31 |         uncompressed: true
 32 |         body: |
 33 |             {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}}
 34 |         headers:
 35 |             Alt-Svc:
 36 |                 - h3=":443"; ma=86400
 37 |             Cache-Control:
 38 |                 - no-store
 39 |             Cf-Cache-Status:
 40 |                 - DYNAMIC
 41 |             Content-Type:
 42 |                 - application/json; charset=utf-8
 43 |             Etag:
 44 |                 - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo"
 45 |             Server:
 46 |                 - cloudflare
 47 |             Vary:
 48 |                 - Accept-Encoding
 49 |             Via:
 50 |                 - 1.1 google
 51 |         status: 200 OK
 52 |         code: 200
 53 |         duration: 0s
 54 |     - id: 1
 55 |       request:
 56 |         proto: HTTP/1.1
 57 |         proto_major: 1
 58 |         proto_minor: 1
 59 |         content_length: 589
 60 |         transfer_encoding: []
 61 |         trailer: {}
 62 |         host: api.linear.app
 63 |         remote_addr: ""
 64 |         request_uri: ""
 65 |         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"}}}'
 66 |         form: {}
 67 |         headers:
 68 |             Content-Type:
 69 |                 - application/json
 70 |             User-Agent:
 71 |                 - linear-mcp-go/1.0.0
 72 |         url: https://api.linear.app/graphql
 73 |         method: POST
 74 |       response:
 75 |         proto: HTTP/2.0
 76 |         proto_major: 2
 77 |         proto_minor: 0
 78 |         transfer_encoding: []
 79 |         trailer: {}
 80 |         content_length: -1
 81 |         uncompressed: true
 82 |         body: |
 83 |             {"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"}}}}}
 84 |         headers:
 85 |             Alt-Svc:
 86 |                 - h3=":443"; ma=86400
 87 |             Cache-Control:
 88 |                 - no-store
 89 |             Cf-Cache-Status:
 90 |                 - DYNAMIC
 91 |             Content-Type:
 92 |                 - application/json; charset=utf-8
 93 |             Etag:
 94 |                 - W/"1ed-NQYcphbrRBp9Qc2UkRq4wmgK4Us"
 95 |             Server:
 96 |                 - cloudflare
 97 |             Vary:
 98 |                 - Accept-Encoding
 99 |             Via:
100 |                 - 1.1 google
101 |         status: 200 OK
102 |         code: 200
103 |         duration: 0s
104 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/update_comment_handler_Valid comment update with hash only.yaml:
--------------------------------------------------------------------------------

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 255
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"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"}}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"143-m1nvHd82UihGhurKSTyMAF0whhw"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 545
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"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"}}}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"218-JZAC2Y8E9AKSYWGafAFwxnfBleE"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 310
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"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"}]}}]}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"210-+ISnhlSrm6Gd7LWWbqn3eOeSXhw"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 310
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"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"}]}}]}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"210-+ISnhlSrm6Gd7LWWbqn3eOeSXhw"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 310
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"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"}]}}]}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"210-+ISnhlSrm6Gd7LWWbqn3eOeSXhw"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 310
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"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"}]}}]}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"210-+ISnhlSrm6Gd7LWWbqn3eOeSXhw"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

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

```go
  1 | package tools
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 
  7 | 	"github.com/geropl/linear-mcp-go/pkg/linear"
  8 | 	"github.com/mark3labs/mcp-go/mcp"
  9 | )
 10 | 
 11 | // GetIssueCommentsTool is the tool definition for getting paginated comments for an issue
 12 | var GetIssueCommentsTool = mcp.NewTool("linear_get_issue_comments",
 13 | 	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."),
 14 | 	mcp.WithString("issue", mcp.Required(), mcp.Description("issue identifier (e.g., 'TEAM-123')")),
 15 | 	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.")),
 16 | 	mcp.WithNumber("limit", mcp.Description("Maximum number of comments to return (default: 10)")),
 17 | 	mcp.WithString("after", mcp.Description("Cursor for pagination, to get comments after this point")),
 18 | )
 19 | 
 20 | // GetIssueCommentsHandler handles the linear_get_issue_comments tool
 21 | func GetIssueCommentsHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 22 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 23 | 		// Extract arguments
 24 | 		issueIdentifier, err := request.RequireString("issue")
 25 | 		if err != nil {
 26 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
 27 | 		}
 28 | 
 29 | 		// Extract optional arguments
 30 | 		parentID := request.GetString("thread", "")
 31 | 		limit := request.GetInt("limit", 10)
 32 | 		afterCursor := request.GetString("after", "")
 33 | 
 34 | 		// Resolve issue identifier to a UUID
 35 | 		issueID, err := resolveIssueIdentifier(linearClient, issueIdentifier)
 36 | 		if err != nil {
 37 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve issue: %v", err)}}}, nil
 38 | 		}
 39 | 
 40 | 		// Get the issue for basic information
 41 | 		issue, err := linearClient.GetIssue(issueID)
 42 | 		if err != nil {
 43 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get issue: %v", err)}}}, nil
 44 | 		}
 45 | 
 46 | 		// Get the comments
 47 | 		commentsInput := linear.GetIssueCommentsInput{
 48 | 			IssueID:     issueID,
 49 | 			ParentID:    parentID,
 50 | 			Limit:       limit,
 51 | 			AfterCursor: afterCursor,
 52 | 		}
 53 | 
 54 | 		comments, err := linearClient.GetIssueComments(commentsInput)
 55 | 		if err != nil {
 56 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get comments: %v", err)}}}, nil
 57 | 		}
 58 | 
 59 | 		// Format the result
 60 | 		var resultText string
 61 | 
 62 | 		// Add issue information
 63 | 		resultText += formatIssueIdentifier(issue) + "\n"
 64 | 
 65 | 		// Add thread information
 66 | 		if parentID == "" {
 67 | 			resultText += "Thread: none (top-level comments)\n"
 68 | 		} else {
 69 | 			resultText += fmt.Sprintf("Thread: %s (replies to comment)\n", parentID)
 70 | 		}
 71 | 
 72 | 		resultText += "\n"
 73 | 
 74 | 		// Add comments
 75 | 		if len(comments.Nodes) > 0 {
 76 | 			resultText += "Comments:\n"
 77 | 
 78 | 			for _, comment := range comments.Nodes {
 79 | 				createdAt := comment.CreatedAt.Format("2006-01-02 15:04:05")
 80 | 				hasReplies := false
 81 | 				if comment.Children != nil && len(comment.Children.Nodes) > 0 {
 82 | 					hasReplies = true
 83 | 				}
 84 | 
 85 | 				resultText += fmt.Sprintf("- Comment: %s\n  %s\n  CreatedAt: %s\n  HasReplies: %s\n  Body: %s\n",
 86 | 					comment.ID,
 87 | 					formatUserIdentifier(comment.User),
 88 | 					createdAt,
 89 | 					formatBool(hasReplies),
 90 | 					comment.Body)
 91 | 			}
 92 | 		} else {
 93 | 			resultText += "Comments: None\n"
 94 | 		}
 95 | 
 96 | 		// Add pagination information
 97 | 		resultText += "\nPagination:\n"
 98 | 		resultText += fmt.Sprintf("Has more comments: %s\n", formatBool(comments.PageInfo.HasNextPage))
 99 | 
100 | 		if comments.PageInfo.HasNextPage {
101 | 			resultText += fmt.Sprintf("Next cursor: %s\n", comments.PageInfo.EndCursor)
102 | 		}
103 | 
104 | 		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
105 | 	}
106 | }
107 | 
108 | // formatBool formats a boolean value as "yes" or "no"
109 | func formatBool(value bool) string {
110 | 	if value {
111 | 		return "yes"
112 | 	}
113 | 	return "no"
114 | }
115 | 
```

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

```go
  1 | package tools
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 
  7 | 	"github.com/geropl/linear-mcp-go/pkg/linear"
  8 | 	"github.com/mark3labs/mcp-go/mcp"
  9 | )
 10 | 
 11 | // GetIssueTool is the tool definition for getting an issue
 12 | var GetIssueTool = mcp.NewTool("linear_get_issue",
 13 | 	mcp.WithDescription("Retrieves a single Linear issue."),
 14 | 	mcp.WithString("issue", mcp.Required(), mcp.Description("ID or identifier (e.g., 'TEAM-123') of the issue to retrieve")),
 15 | )
 16 | 
 17 | // GetIssueHandler handles the linear_get_issue tool
 18 | func GetIssueHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 19 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 20 | 		// Extract arguments
 21 | 		issueIdentifier, err := request.RequireString("issue")
 22 | 		if err != nil {
 23 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
 24 | 		}
 25 | 
 26 | 		// Resolve issue identifier to a UUID
 27 | 		issueID, err := resolveIssueIdentifier(linearClient, issueIdentifier)
 28 | 		if err != nil {
 29 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve issue: %v", err)}}}, nil
 30 | 		}
 31 | 
 32 | 		// Get the issue
 33 | 		issue, err := linearClient.GetIssue(issueID)
 34 | 		if err != nil {
 35 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get issue: %v", err)}}}, nil
 36 | 		}
 37 | 
 38 | 		// Format the result using the full issue formatting
 39 | 		resultText := formatIssue(issue)
 40 | 
 41 | 		// Add assignee and team information using identifier formatting
 42 | 		if issue.Assignee != nil {
 43 | 			resultText += fmt.Sprintf("Assignee: %s\n", formatUserIdentifier(issue.Assignee))
 44 | 		} else {
 45 | 			resultText += "Assignee: None\n"
 46 | 		}
 47 | 
 48 | 		resultText += fmt.Sprintf("%s\n", formatTeamIdentifier(issue.Team))
 49 | 
 50 | 		if issue.Project != nil {
 51 | 			resultText += fmt.Sprintf("Project: %s (%s)\n", issue.Project.Name, issue.Project.ID)
 52 | 		} else {
 53 | 			resultText += "Project: None\n"
 54 | 		}
 55 | 
 56 | 		if issue.ProjectMilestone != nil {
 57 | 			resultText += fmt.Sprintf("Milestone: %s (%s)\n", issue.ProjectMilestone.Name, issue.ProjectMilestone.ID)
 58 | 		} else {
 59 | 			resultText += "Milestone: None\n"
 60 | 		}
 61 | 
 62 | 		// Add attachments section if there are attachments
 63 | 		if issue.Attachments != nil && len(issue.Attachments.Nodes) > 0 {
 64 | 			resultText += "\nAttachments:\n"
 65 | 
 66 | 			// Display all attachments in a simple list without grouping by source type
 67 | 			for _, attachment := range issue.Attachments.Nodes {
 68 | 				resultText += fmt.Sprintf("- %s: %s\n", attachment.Title, attachment.URL)
 69 | 				if attachment.Subtitle != "" {
 70 | 					resultText += fmt.Sprintf("  %s\n", attachment.Subtitle)
 71 | 				}
 72 | 			}
 73 | 		} else {
 74 | 			resultText += "\nAttachments: None\n"
 75 | 		}
 76 | 
 77 | 		// Add related issues section
 78 | 		if (issue.Relations != nil && len(issue.Relations.Nodes) > 0) ||
 79 | 			(issue.InverseRelations != nil && len(issue.InverseRelations.Nodes) > 0) {
 80 | 			resultText += "\nRelated Issues:\n"
 81 | 
 82 | 			// Add direct relations
 83 | 			if issue.Relations != nil && len(issue.Relations.Nodes) > 0 {
 84 | 				for _, relation := range issue.Relations.Nodes {
 85 | 					if relation.RelatedIssue != nil {
 86 | 						resultText += fmt.Sprintf("- %s\n  Title: %s\n  RelationType: %s\n  URL: %s\n",
 87 | 							formatIssueIdentifier(relation.RelatedIssue),
 88 | 							relation.RelatedIssue.Title,
 89 | 							relation.Type,
 90 | 							relation.RelatedIssue.URL)
 91 | 					}
 92 | 				}
 93 | 			}
 94 | 
 95 | 			// Add inverse relations
 96 | 			if issue.InverseRelations != nil && len(issue.InverseRelations.Nodes) > 0 {
 97 | 				for _, relation := range issue.InverseRelations.Nodes {
 98 | 					if relation.Issue != nil {
 99 | 						resultText += fmt.Sprintf("- %s\n  Title: %s\n  RelationType: %s (inverse)\n  URL: %s\n",
100 | 							formatIssueIdentifier(relation.Issue),
101 | 							relation.Issue.Title,
102 | 							relation.Type,
103 | 							relation.Issue.URL)
104 | 					}
105 | 				}
106 | 			}
107 | 		} else {
108 | 			resultText += "\nRelated Issues: None\n"
109 | 		}
110 | 
111 | 		// Note about comments
112 | 		resultText += "\nComments: Use the linear_get_issue_comments tool to retrieve comments for this issue.\n"
113 | 
114 | 		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
115 | 	}
116 | }
117 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 333
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"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"}}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"18b-JtoAyZ6gspXHMOEbvedWimpK4Y0"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 585
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"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"}}}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"223-qLAwFJpu6uPxkTsxKQkgHklk+m4"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Create sub issue from identifier.yaml:
--------------------------------------------------------------------------------

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 322
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 880
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"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}}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"218-0Svxf7NuXjSFfPU18wFj97GbalA"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 342
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"data":{"team":{"labels":{"nodes":[{"id":"37e1cdc8-a696-4412-8ad7-8ba8435ba0f4","name":"team label 1"}]}}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"6d-NtoFPWMa8e+cqurX/zzYmekk2Dg"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 890
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"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}}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"26b-Kh3ayVGqSTqbO3LB/W4HMT1nF4s"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 733
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"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}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"221-5dHPmIHEC1rBzEVJqPrdCyJ2NbY"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 400
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"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"}}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"12d-wnpLXmtIiIJiE+9jOexlH0g7wGY"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 714
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"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}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"113-pUQ9mkDn3KWYiQz0UBE51+d7gJ4"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 906
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}]}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: 35
 77 |         uncompressed: false
 78 |         body: |
 79 |             {"data":{"projects":{"nodes":[]}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Length:
 88 |                 - "35"
 89 |             Content-Type:
 90 |                 - application/json; charset=utf-8
 91 |             Etag:
 92 |                 - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo"
 93 |             Server:
 94 |                 - cloudflare
 95 |             Vary:
 96 |                 - Accept-Encoding
 97 |             Via:
 98 |                 - 1.1 google
 99 |         status: 200 OK
100 |         code: 200
101 |         duration: 0s
102 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 716
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"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}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"113-pUQ9mkDn3KWYiQz0UBE51+d7gJ4"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 911
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}]}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: 35
 77 |         uncompressed: false
 78 |         body: |
 79 |             {"data":{"projects":{"nodes":[]}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Length:
 88 |                 - "35"
 89 |             Content-Type:
 90 |                 - application/json; charset=utf-8
 91 |             Etag:
 92 |                 - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo"
 93 |             Server:
 94 |                 - cloudflare
 95 |             Vary:
 96 |                 - Accept-Encoding
 97 |             Via:
 98 |                 - 1.1 google
 99 |         status: 200 OK
100 |         code: 200
101 |         duration: 0s
102 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 733
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"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}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"21c-zZT5JHii4UiJPH+GXG9r9OBmr2A"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 411
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"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"}}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"131-exvq876nrApQ/gFGvqKIsgVXXo0"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 717
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"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}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"113-pUQ9mkDn3KWYiQz0UBE51+d7gJ4"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 912
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}]}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: 35
 77 |         uncompressed: false
 78 |         body: |
 79 |             {"data":{"projects":{"nodes":[]}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Length:
 88 |                 - "35"
 89 |             Content-Type:
 90 |                 - application/json; charset=utf-8
 91 |             Etag:
 92 |                 - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo"
 93 |             Server:
 94 |                 - cloudflare
 95 |             Vary:
 96 |                 - Accept-Encoding
 97 |             Via:
 98 |                 - 1.1 google
 99 |         status: 200 OK
100 |         code: 200
101 |         duration: 0s
102 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 717
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"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}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"113-pUQ9mkDn3KWYiQz0UBE51+d7gJ4"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 912
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}]}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: 35
 77 |         uncompressed: false
 78 |         body: |
 79 |             {"data":{"projects":{"nodes":[]}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Length:
 88 |                 - "35"
 89 |             Content-Type:
 90 |                 - application/json; charset=utf-8
 91 |             Etag:
 92 |                 - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo"
 93 |             Server:
 94 |                 - cloudflare
 95 |             Vary:
 96 |                 - Accept-Encoding
 97 |             Via:
 98 |                 - 1.1 google
 99 |         status: 200 OK
100 |         code: 200
101 |         duration: 0s
102 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 733
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"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}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"21d-jg2vG6vT5da0XDzGnJVlIPQka7E"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 438
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"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"}}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"12c-qm99WOFVQtdIlEMxVa4cu3vvOAo"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```

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

```go
  1 | package server
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"os"
  6 | 
  7 | 	"github.com/geropl/linear-mcp-go/pkg/linear"
  8 | 	"github.com/geropl/linear-mcp-go/pkg/tools"
  9 | 	"github.com/mark3labs/mcp-go/mcp"
 10 | 	mcpserver "github.com/mark3labs/mcp-go/server"
 11 | )
 12 | 
 13 | const (
 14 | 	// ServerName is the name of the MCP server
 15 | 	ServerName = "Linear MCP Server"
 16 | 	// ServerVersion is the version of the MCP server
 17 | 	ServerVersion = "1.15.0"
 18 | )
 19 | 
 20 | // LinearMCPServer represents the Linear MCP server
 21 | type LinearMCPServer struct {
 22 | 	mcpServer    *mcpserver.MCPServer
 23 | 	linearClient *linear.LinearClient
 24 | 	writeAccess  bool // Controls whether write operations are enabled
 25 | }
 26 | 
 27 | // NewLinearMCPServer creates a new Linear MCP server
 28 | func NewLinearMCPServer(writeAccess bool) (*LinearMCPServer, error) {
 29 | 	// Create the Linear client
 30 | 	linearClient, err := linear.NewLinearClientFromEnv(ServerVersion)
 31 | 	if err != nil {
 32 | 		return nil, fmt.Errorf("failed to create Linear client: %w", err)
 33 | 	}
 34 | 
 35 | 	// Create the MCP server
 36 | 	mcpServer := mcpserver.NewMCPServer(ServerName, ServerVersion)
 37 | 
 38 | 	// Create the Linear MCP server
 39 | 	server := &LinearMCPServer{
 40 | 		mcpServer:    mcpServer,
 41 | 		linearClient: linearClient,
 42 | 		writeAccess:  writeAccess,
 43 | 	}
 44 | 
 45 | 	// Register tools
 46 | 	RegisterTools(mcpServer, linearClient, writeAccess)
 47 | 
 48 | 	// Register resources
 49 | 	RegisterResources(mcpServer, linearClient)
 50 | 
 51 | 	return server, nil
 52 | }
 53 | 
 54 | // Start starts the Linear MCP server
 55 | func (s *LinearMCPServer) Start() error {
 56 | 	// Check if the Linear API key is set
 57 | 	if os.Getenv("LINEAR_API_KEY") == "" {
 58 | 		return fmt.Errorf("LINEAR_API_KEY environment variable is required")
 59 | 	}
 60 | 
 61 | 	// Start the server
 62 | 	fmt.Printf("Starting %s v%s\n", ServerName, ServerVersion)
 63 | 	return mcpserver.ServeStdio(s.mcpServer)
 64 | }
 65 | 
 66 | // GetMCPServer returns the underlying MCP server
 67 | func (s *LinearMCPServer) GetMCPServer() *mcpserver.MCPServer {
 68 | 	return s.mcpServer
 69 | }
 70 | 
 71 | // GetLinearClient returns the Linear client
 72 | func (s *LinearMCPServer) GetLinearClient() *linear.LinearClient {
 73 | 	return s.linearClient
 74 | }
 75 | 
 76 | // GetReadOnlyToolNames returns the names of all read-only tools
 77 | func GetReadOnlyToolNames() map[string]bool {
 78 | 	return map[string]bool{
 79 | 		"linear_search_issues":       true,
 80 | 		"linear_get_user_issues":     true,
 81 | 		"linear_get_issue":           true,
 82 | 		"linear_get_issue_comments":  true,
 83 | 		"linear_get_teams":           true,
 84 | 		"linear_get_project":         true,
 85 | 		"linear_search_projects":     true,
 86 | 		"linear_get_milestone":       true,
 87 | 		"linear_get_initiative":      true,
 88 | 	}
 89 | }
 90 | 
 91 | // RegisterTools registers all Linear tools with the MCP server
 92 | func RegisterTools(s *mcpserver.MCPServer, linearClient *linear.LinearClient, writeAccess bool) {
 93 | 	// Register tools, based on writeAccess
 94 | 	addTool := func(tool mcp.Tool, handler mcpserver.ToolHandlerFunc) {
 95 | 		if !writeAccess {
 96 | 			if readOnly := GetReadOnlyToolNames()[tool.Name]; !readOnly {
 97 | 				// Skip registering write tools if write access is disabled
 98 | 				return
 99 | 			}
100 | 		}
101 | 		s.AddTool(tool, handler)
102 | 	}
103 | 
104 | 	// Register each tool
105 | 	addTool(tools.SearchIssuesTool, tools.SearchIssuesHandler(linearClient))
106 | 	addTool(tools.GetUserIssuesTool, tools.GetUserIssuesHandler(linearClient))
107 | 	addTool(tools.GetIssueTool, tools.GetIssueHandler(linearClient))
108 | 	addTool(tools.GetIssueCommentsTool, tools.GetIssueCommentsHandler(linearClient))
109 | 	addTool(tools.GetTeamsTool, tools.GetTeamsHandler(linearClient))
110 | 	addTool(tools.GetProjectTool, tools.GetProjectHandler(linearClient))
111 | 	addTool(tools.SearchProjectsTool, tools.SearchProjectsHandler(linearClient))
112 | 	addTool(tools.CreateProjectTool, tools.CreateProjectHandler(linearClient))
113 | 	addTool(tools.UpdateProjectTool, tools.UpdateProjectHandler(linearClient))
114 | 	addTool(tools.GetMilestoneTool, tools.GetMilestoneHandler(linearClient))
115 | 	addTool(tools.CreateMilestoneTool, tools.CreateMilestoneHandler(linearClient))
116 | 	addTool(tools.UpdateMilestoneTool, tools.UpdateMilestoneHandler(linearClient))
117 | 	addTool(tools.GetInitiativeTool, tools.GetInitiativeHandler(linearClient))
118 | 	addTool(tools.CreateInitiativeTool, tools.CreateInitiativeHandler(linearClient))
119 | 	addTool(tools.UpdateInitiativeTool, tools.UpdateInitiativeHandler(linearClient))
120 | 	addTool(tools.CreateIssueTool, tools.CreateIssueHandler(linearClient))
121 | 	addTool(tools.UpdateIssueTool, tools.UpdateIssueHandler(linearClient))
122 | 	addTool(tools.AddCommentTool, tools.AddCommentHandler(linearClient))
123 | 	addTool(tools.ReplyToCommentTool, tools.ReplyToCommentHandler(linearClient))
124 | 	addTool(tools.UpdateCommentTool, tools.UpdateCommentHandler(linearClient))
125 | }
126 | 
```

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

```yaml
 1 | ---
 2 | version: 2
 3 | interactions:
 4 |     - id: 0
 5 |       request:
 6 |         proto: HTTP/1.1
 7 |         proto_major: 1
 8 |         proto_minor: 1
 9 |         content_length: 88
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tquery GetCurrentUser {\n\t\t\tviewer {\n\t\t\t\tid\n\t\t\t}\n\t\t}\n\t"}'
16 |         form: {}
17 |         headers:
18 |             Content-Type:
19 |                 - application/json
20 |         url: https://api.linear.app/graphql
21 |         method: POST
22 |       response:
23 |         proto: HTTP/2.0
24 |         proto_major: 2
25 |         proto_minor: 0
26 |         transfer_encoding: []
27 |         trailer: {}
28 |         content_length: -1
29 |         uncompressed: true
30 |         body: |
31 |             {"data":{"viewer":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85"}}}
32 |         headers:
33 |             Alt-Svc:
34 |                 - h3=":443"; ma=86400
35 |             Cache-Control:
36 |                 - no-store
37 |             Cf-Cache-Status:
38 |                 - DYNAMIC
39 |             Content-Type:
40 |                 - application/json; charset=utf-8
41 |             Etag:
42 |                 - W/"42-6PvRWCcs8jOXFygoL3FhQP+PK3o"
43 |             Server:
44 |                 - cloudflare
45 |             Vary:
46 |                 - Accept-Encoding
47 |             Via:
48 |                 - 1.1 google
49 |         status: 200 OK
50 |         code: 200
51 |         duration: 0s
52 |     - id: 1
53 |       request:
54 |         proto: HTTP/1.1
55 |         proto_major: 1
56 |         proto_minor: 1
57 |         content_length: 556
58 |         transfer_encoding: []
59 |         trailer: {}
60 |         host: api.linear.app
61 |         remote_addr: ""
62 |         request_uri: ""
63 |         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"}}'
64 |         form: {}
65 |         headers:
66 |             Content-Type:
67 |                 - application/json
68 |         url: https://api.linear.app/graphql
69 |         method: POST
70 |       response:
71 |         proto: HTTP/2.0
72 |         proto_major: 2
73 |         proto_minor: 0
74 |         transfer_encoding: []
75 |         trailer: {}
76 |         content_length: -1
77 |         uncompressed: true
78 |         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"
79 |         headers:
80 |             Alt-Svc:
81 |                 - h3=":443"; ma=86400
82 |             Cache-Control:
83 |                 - no-store
84 |             Cf-Cache-Status:
85 |                 - DYNAMIC
86 |             Content-Type:
87 |                 - application/json; charset=utf-8
88 |             Etag:
89 |                 - W/"52b-iwXdi0Au8LYVFWrptXXwpSz7HVA"
90 |             Server:
91 |                 - cloudflare
92 |             Vary:
93 |                 - Accept-Encoding
94 |             Via:
95 |                 - 1.1 google
96 |         status: 200 OK
97 |         code: 200
98 |         duration: 0s
99 | 
```

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

```yaml
  1 | ---
  2 | version: 2
  3 | interactions:
  4 |     - id: 0
  5 |       request:
  6 |         proto: HTTP/1.1
  7 |         proto_major: 1
  8 |         proto_minor: 1
  9 |         content_length: 350
 10 |         transfer_encoding: []
 11 |         trailer: {}
 12 |         host: api.linear.app
 13 |         remote_addr: ""
 14 |         request_uri: ""
 15 |         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"}}'
 16 |         form: {}
 17 |         headers:
 18 |             Content-Type:
 19 |                 - application/json
 20 |         url: https://api.linear.app/graphql
 21 |         method: POST
 22 |       response:
 23 |         proto: HTTP/2.0
 24 |         proto_major: 2
 25 |         proto_minor: 0
 26 |         transfer_encoding: []
 27 |         trailer: {}
 28 |         content_length: -1
 29 |         uncompressed: true
 30 |         body: |
 31 |             {"data":{"team":{"labels":{"nodes":[{"id":"fcd49e32-5043-4bfd-88a5-2bbe3c95124a","name":"ws-label 2"},{"id":"94087865-ce6c-470b-896c-4d1d2c7456b8","name":"Feature"}]}}}}
 32 |         headers:
 33 |             Alt-Svc:
 34 |                 - h3=":443"; ma=86400
 35 |             Cache-Control:
 36 |                 - no-store
 37 |             Cf-Cache-Status:
 38 |                 - DYNAMIC
 39 |             Content-Type:
 40 |                 - application/json; charset=utf-8
 41 |             Etag:
 42 |                 - W/"aa-fC3D/tel+q+/3XRkoHnKGoa70sg"
 43 |             Server:
 44 |                 - cloudflare
 45 |             Vary:
 46 |                 - Accept-Encoding
 47 |             Via:
 48 |                 - 1.1 google
 49 |         status: 200 OK
 50 |         code: 200
 51 |         duration: 0s
 52 |     - id: 1
 53 |       request:
 54 |         proto: HTTP/1.1
 55 |         proto_major: 1
 56 |         proto_minor: 1
 57 |         content_length: 983
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         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"}}}'
 64 |         form: {}
 65 |         headers:
 66 |             Content-Type:
 67 |                 - application/json
 68 |         url: https://api.linear.app/graphql
 69 |         method: POST
 70 |       response:
 71 |         proto: HTTP/2.0
 72 |         proto_major: 2
 73 |         proto_minor: 0
 74 |         transfer_encoding: []
 75 |         trailer: {}
 76 |         content_length: -1
 77 |         uncompressed: true
 78 |         body: |
 79 |             {"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}}}}
 80 |         headers:
 81 |             Alt-Svc:
 82 |                 - h3=":443"; ma=86400
 83 |             Cache-Control:
 84 |                 - no-store
 85 |             Cf-Cache-Status:
 86 |                 - DYNAMIC
 87 |             Content-Type:
 88 |                 - application/json; charset=utf-8
 89 |             Etag:
 90 |                 - W/"2b0-QsIb1mXyrKfU1pD5SHlQtNywn7k"
 91 |             Server:
 92 |                 - cloudflare
 93 |             Vary:
 94 |                 - Accept-Encoding
 95 |             Via:
 96 |                 - 1.1 google
 97 |         status: 200 OK
 98 |         code: 200
 99 |         duration: 0s
100 | 
```
Page 2/6FirstPrevNextLast