#
tokens: 47642/50000 14/150 files (page 4/6)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 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_issue_comments_handler_Thread_with_pagination.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: 1316
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         body: '{"query":"\n\t\tquery GetIssue($id: String!) {\n\t\t\tissue(id: $id) {\n\t\t\t\tid\n\t\t\t\tidentifier\n\t\t\t\ttitle\n\t\t\t\tdescription\n\t\t\t\tpriority\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tstate {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tassignee {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tteam {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t}\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tprojectMilestone {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\trelations(first: 20) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttype\n\t\t\t\t\t\trelatedIssue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tidentifier\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinverseRelations(first: 20) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttype\n\t\t\t\t\t\tissue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tidentifier\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tattachments(first: 50) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\tsubtitle\n\t\t\t\t\t\turl\n\t\t\t\t\t\tsourceType\n\t\t\t\t\t\tmetadata\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f"}}'
 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":{"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue","createdAt":"2025-03-03T11:34:49.241Z","updatedAt":"2025-10-07T16:03:19.741Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"project":{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation"},"projectMilestone":{"id":"5214c4d9-9c2a-4ae7-b5e5-e33058b3e131","name":"M1: Gather potential resources to investigate"},"relations":{"nodes":[]},"inverseRelations":{"nodes":[]},"attachments":{"nodes":[]}}}}
 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/"36b-2tYg/a9gEbu6WHEn0vlp7gu8zgw"
 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 |     - id: 2
101 |       request:
102 |         proto: HTTP/1.1
103 |         proto_major: 1
104 |         proto_minor: 1
105 |         content_length: 839
106 |         transfer_encoding: []
107 |         trailer: {}
108 |         host: api.linear.app
109 |         remote_addr: ""
110 |         request_uri: ""
111 |         body: '{"query":"\n\t\tquery GetIssueComments($issueId: String!, $parentId: ID, $first: Int!, $after: String) {\n\t\t\tissue(id: $issueId) {\n\t\t\t\tcomments(\n\t\t\t\t\tfirst: $first,\n\t\t\t\t\tafter: $after,\n\t\t\t\t\tfilter: { parent: { id: { eq: $parentId } } }\n\t\t\t\t) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tbody\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tuser {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t\tparent {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchildren(first: 1) {\n\t\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpageInfo {\n\t\t\t\t\t\thasNextPage\n\t\t\t\t\t\tendCursor\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"first":2,"issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f","parentId":"ae3d62d6-3f40-4990-867b-5c97dd265a40"}}'
112 |         form: {}
113 |         headers:
114 |             Content-Type:
115 |                 - application/json
116 |         url: https://api.linear.app/graphql
117 |         method: POST
118 |       response:
119 |         proto: HTTP/2.0
120 |         proto_major: 2
121 |         proto_minor: 0
122 |         transfer_encoding: []
123 |         trailer: {}
124 |         content_length: -1
125 |         uncompressed: true
126 |         body: |
127 |             {"data":{"issue":{"comments":{"nodes":[{"id":"6b337bfa-a7df-4b5c-9d6d-a0c8c6212301","body":"This is a reply to the comment","createdAt":"2025-10-07T16:03:19.784Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"1bc6bceb-1f2c-4a52-8f23-155aeb966ee2","body":"Reply using shorthand","createdAt":"2025-10-07T15:00:53.946Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}}],"pageInfo":{"hasNextPage":true,"endCursor":"1bc6bceb-1f2c-4a52-8f23-155aeb966ee2"}}}}}
128 |         headers:
129 |             Alt-Svc:
130 |                 - h3=":443"; ma=86400
131 |             Cache-Control:
132 |                 - no-store
133 |             Cf-Cache-Status:
134 |                 - DYNAMIC
135 |             Content-Type:
136 |                 - application/json; charset=utf-8
137 |             Etag:
138 |                 - W/"2a8-ZLVxpKsJeNGYqj5ihN+Tbdu15Oo"
139 |             Server:
140 |                 - cloudflare
141 |             Vary:
142 |                 - Accept-Encoding
143 |             Via:
144 |                 - 1.1 google
145 |         status: 200 OK
146 |         code: 200
147 |         duration: 0s
148 | 
```

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

```markdown
  1 | # Progress: Linear MCP Server
  2 | 
  3 | ## What Works
  4 | 1. **Command-Line Interface**:
  5 |    - Cobra-based command structure
  6 |    - Subcommand support (server, setup)
  7 |    - Consistent flag handling
  8 | 
  9 | 2. **Core MCP Server**:
 10 |    - Server initialization and configuration
 11 |    - Tool registration and execution
 12 |    - Error handling and response formatting
 13 | 
 14 | 3. **Linear API Integration**:
 15 |    - Authentication via API key
 16 |    - Rate limiting implementation
 17 |    - API request and response handling
 18 |    - Proper JSON structure handling for API responses
 19 |    - Correct GraphQL parameter types for API validation
 20 |    - Proper resolution of human-readable identifiers to UUIDs
 21 | 
 22 | 4. **MCP Tools**:
 23 |    - `linear_create_issue`: Creating new Linear issues with support for:
 24 |      - Sub-issues using parent issue ID or human-readable identifier (e.g., "TEAM-123")
 25 |      - Label assignment using label IDs or names
 26 |      - Team specification using team ID, name, or key
 27 |    - `linear_update_issue`: Updating existing issues
 28 |    - `linear_search_issues`: Searching for issues with various criteria
 29 |    - `linear_get_user_issues`: Getting issues assigned to a user
 30 |    - `linear_get_issue`: Getting a single issue by ID
 31 |    - `linear_add_comment`: Adding comments to issues
 32 |    - `linear_get_teams`: Getting a list of teams
 33 | 
 34 | 5. **Setup Automation**:
 35 |    - Binary discovery and download
 36 |    - Configuration file management
 37 |    - Support for Cline AI assistant
 38 | 
 39 | 6. **Testing**:
 40 |    - Test fixtures for all tools
 41 |    - HTTP interaction recording and playback
 42 |    - Comprehensive test coverage for enhanced functionality
 43 | 
 44 | ## What's Left to Build
 45 | 
 46 | ### High Priority
 47 | 1. **Setup Command Testing**:
 48 |    - Test on different platforms (Linux, macOS, Windows)
 49 |    - Verify configuration file creation
 50 |    - Test binary download functionality
 51 | 
 52 | ### Medium Priority
 53 | 1. **Additional AI Assistant Support**:
 54 |    - Research other AI assistants for Linear integration
 55 |    - Implement support for these assistants
 56 |    - Update documentation
 57 | 
 58 | 2. **Documentation Improvements**:
 59 |    - Add CONTRIBUTING.md with development guidelines
 60 |    - Add examples of using the server with different AI assistants
 61 |    - Add troubleshooting section
 62 | 
 63 | 3. **Additional Linear API Features**:
 64 |    - Support for managing issue attachments
 65 |    - Support for managing issue relationships
 66 | 
 67 | ### Low Priority
 68 | 1. **Infrastructure Improvements**:
 69 |    - Docker container support
 70 |    - Configuration file support for server
 71 |    - Improved logging and metrics
 72 |    - Automatic binary updates
 73 | 
 74 | ## Current Status
 75 | - **Version**: 1.0.0
 76 | - **Stability**: Stable for core features
 77 | - **Test Coverage**: Good, all tools have test fixtures
 78 | - **Documentation**: Updated with new command structure, setup instructions, and tool standardization PRD
 79 | - **Release Process**: GitHub Actions workflow created
 80 | - **Security**: Write access control implemented (disabled by default)
 81 | - **User Experience**: Improved with setup command and user-friendly identifiers
 82 | 
 83 | ## Known Issues
 84 | 1. **API Key Management**:
 85 |    - API key validation only happens on first API request
 86 |    - No support for other authentication methods (OAuth, etc.)
 87 |    - LINEAR_API_KEY environment variable must be set manually
 88 | 
 89 | 2. **Rate Limiting Constraints**:
 90 |    - Linear API has rate limits that must be respected
 91 |    - Current rate limiter implementation is basic (simple token bucket)
 92 |    - Rate limits are not configurable (hardcoded values)
 93 |    - No sophisticated backoff strategies for rate limit exceeded scenarios
 94 | 
 95 | 3. **Error Handling**:
 96 |    - Some error messages could be more descriptive
 97 |    - No retry mechanism for transient errors
 98 |    - Network errors during setup could be handled better
 99 |    - GraphQL errors could be better formatted for end users
100 | 
101 | 4. **Feature Limitations**:
102 |    - Limited support for Linear API features
103 |    - No support for webhooks or real-time updates
104 |    - Limited AI assistant support (currently only Cline)
105 |    - No configuration file support for server settings
106 | 
107 | 5. **Authentication Limitations**:
108 |    - Only supports API key authentication
109 |    - No support for OAuth or other modern authentication methods
110 |    - API key is not validated until first API request is made
111 | 
112 | ## Recent Milestones
113 | - ✅ Created comprehensive Tool Standardization PRD with implementation plan
114 | - ✅ Implemented shared utility functions for entity rendering and identifier resolution
115 | - ✅ Updated all tools to follow standardization rules:
116 |   - Concise descriptions
117 |   - Flexible identifier resolution
118 |   - Consistent entity rendering
119 | - ✅ Initial implementation of all core tools
120 | - ✅ Test fixtures for all tools
121 | - ✅ GitHub Actions workflow for testing and releases
122 | - ✅ Write access control implementation (--write-access flag, default: false)
123 | - ✅ Command-line interface with subcommands
124 | - ✅ Setup command for AI assistants
125 | - ✅ Enhanced `linear_create_issue` tool with support for sub-issues and labels
126 | - ✅ Implemented user-friendly identifiers for parent issues and labels
127 | - ✅ Fixed JSON unmarshaling issue with Labels field
128 | - ✅ Added support for team key in issue creation
129 | - ✅ Fixed label resolution issue with GraphQL parameter type
130 | - ✅ Fixed parent issue identifier resolution for human-readable identifiers
131 | - ✅ Enhanced Claude Code Support in Setup Command:
132 |   - Register to all existing projects when no --project-path is specified
133 |   - Support multiple project paths separated by commas
134 |   - Comprehensive testing with 5 new test cases
135 |   - Manual testing confirmed functionality
136 | 
137 | ## Evolution of Project Decisions
138 | 
139 | ### Initial Implementation Phase
140 | - **Started with basic Linear API integration**: Focused on core functionality first
141 | - **Implemented core MCP tools for issue management**: Prioritized the most common Linear operations
142 | - **Added test fixtures for all tools**: Established testing foundation early
143 | - **Decision Rationale**: Build a solid foundation before adding advanced features
144 | 
145 | ### Command-Line Interface Evolution
146 | - **Original**: Simple binary that only ran the server
147 | - **Enhanced**: Added Cobra framework with subcommand structure
148 | - **Current**: Full CLI with server and setup subcommands
149 | - **Decision Rationale**: Better user experience and extensibility for future commands
150 | 
151 | ### Setup Automation Development
152 | - **Original**: Manual installation via bash script
153 | - **Enhanced**: Integrated setup command within the main binary
154 | - **Current**: Automated binary discovery, download, and configuration
155 | - **Decision Rationale**: Reduce friction for users setting up the server
156 | 
157 | ### Write Access Control Implementation
158 | - **Original**: All tools available by default
159 | - **Enhanced**: Added write access control with --write-access flag
160 | - **Current**: Write operations disabled by default for security
161 | - **Decision Rationale**: Prevent accidental modifications while maintaining functionality
162 | 
163 | ### Tool Standardization Journey
164 | - **Original**: Inconsistent tool descriptions and parameter naming
165 | - **Enhanced**: Created comprehensive standardization PRD
166 | - **Current**: All tools follow consistent patterns for descriptions, parameters, and output
167 | - **Decision Rationale**: Improve user experience and maintainability
168 | 
169 | ### Testing Strategy Evolution
170 | - **Original**: Basic unit tests
171 | - **Enhanced**: Added go-vcr for HTTP interaction recording
172 | - **Current**: Comprehensive test coverage with fixtures for all scenarios
173 | - **Decision Rationale**: Enable testing without API dependencies and ensure reliability
174 | 
175 | ### Release Process Development
176 | - **Original**: Manual releases
177 | - **Enhanced**: Added GitHub Actions workflow
178 | - **Current**: Automated testing and releases triggered by version tags
179 | - **Decision Rationale**: Streamline release process and ensure quality
180 | 
181 | ## Upcoming Milestones
182 | - [x] Complete Tool Standardization testing
183 | - [ ] Support for additional AI assistants
184 | - [ ] Improved error handling and recovery
185 | - [ ] Additional Linear API features
186 | - [ ] Configuration file support for server
187 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Create issue with project 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: 719
 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":"MCP tool investigation"}}'
 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: 907
 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":"MCP tool investigation"}},{"slugId":{"eq":""}}]}}}'
 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":{"projects":{"nodes":[{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation","description":"Summary text goes here","slugId":"ae44897e42a7","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/mcp-tool-investigation-ae44897e42a7","createdAt":"2025-06-28T18:06:47.606Z","updatedAt":"2025-06-28T18:07:51.899Z","lead":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"},"members":{"nodes":[{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"}]},"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}]},"initiatives":{"nodes":[{"id":"15e7c1bd-c0c5-4801-ac9a-8e98bf88ea7a","name":"Push for MCP"}]},"startDate":"2025-06-02","targetDate":"2025-06-30"}]}}}
 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/"355-Jji1j11utIAgJU/7ATKvhRyba4g"
 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 |     - id: 2
101 |       request:
102 |         proto: HTTP/1.1
103 |         proto_major: 1
104 |         proto_minor: 1
105 |         content_length: 895
106 |         transfer_encoding: []
107 |         trailer: {}
108 |         host: api.linear.app
109 |         remote_addr: ""
110 |         request_uri: ""
111 |         body: '{"query":"\n\t\tmutation CreateIssue($input: IssueCreateInput!) {\n\t\t\tissueCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tstate {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tteam {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t\tlabels {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tprojectMilestone {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"description":"","projectId":"01bff2dd-ab7f-4464-b425-97073862013f","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Issue with Project Name"}}}'
112 |         form: {}
113 |         headers:
114 |             Content-Type:
115 |                 - application/json
116 |         url: https://api.linear.app/graphql
117 |         method: POST
118 |       response:
119 |         proto: HTTP/2.0
120 |         proto_major: 2
121 |         proto_minor: 0
122 |         transfer_encoding: []
123 |         trailer: {}
124 |         content_length: -1
125 |         uncompressed: true
126 |         body: |
127 |             {"data":{"issueCreate":{"success":true,"issue":{"id":"5ab2381d-8838-442b-bbe7-bc1a10bd7866","identifier":"TEST-94","title":"Issue with Project Name","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-94/issue-with-project-name","createdAt":"2025-10-06T09:44:51.777Z","updatedAt":"2025-10-06T09:44:51.777Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[]},"project":{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation"},"projectMilestone":null}}}}
128 |         headers:
129 |             Alt-Svc:
130 |                 - h3=":443"; ma=86400
131 |             Cache-Control:
132 |                 - no-store
133 |             Cf-Cache-Status:
134 |                 - DYNAMIC
135 |             Content-Type:
136 |                 - application/json; charset=utf-8
137 |             Etag:
138 |                 - W/"27d-P1WZhi2dq5MbxNwDZDRge46h1ZE"
139 |             Server:
140 |                 - cloudflare
141 |             Vary:
142 |                 - Accept-Encoding
143 |             Via:
144 |                 - 1.1 google
145 |         status: 200 OK
146 |         code: 200
147 |         duration: 0s
148 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Create issue with project 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: 732
 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":"mcp-tool-investigation-ae44897e42a7"}}'
 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: 932
 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":"mcp-tool-investigation-ae44897e42a7"}},{"slugId":{"eq":"ae44897e42a7"}}]}}}'
 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":{"projects":{"nodes":[{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation","description":"Summary text goes here","slugId":"ae44897e42a7","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/mcp-tool-investigation-ae44897e42a7","createdAt":"2025-06-28T18:06:47.606Z","updatedAt":"2025-06-28T18:07:51.899Z","lead":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"},"members":{"nodes":[{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"}]},"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}]},"initiatives":{"nodes":[{"id":"15e7c1bd-c0c5-4801-ac9a-8e98bf88ea7a","name":"Push for MCP"}]},"startDate":"2025-06-02","targetDate":"2025-06-30"}]}}}
 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/"355-Jji1j11utIAgJU/7ATKvhRyba4g"
 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 |     - id: 2
101 |       request:
102 |         proto: HTTP/1.1
103 |         proto_major: 1
104 |         proto_minor: 1
105 |         content_length: 895
106 |         transfer_encoding: []
107 |         trailer: {}
108 |         host: api.linear.app
109 |         remote_addr: ""
110 |         request_uri: ""
111 |         body: '{"query":"\n\t\tmutation CreateIssue($input: IssueCreateInput!) {\n\t\t\tissueCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tstate {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tteam {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t\tlabels {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tprojectMilestone {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"description":"","projectId":"01bff2dd-ab7f-4464-b425-97073862013f","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Issue with Project Slug"}}}'
112 |         form: {}
113 |         headers:
114 |             Content-Type:
115 |                 - application/json
116 |         url: https://api.linear.app/graphql
117 |         method: POST
118 |       response:
119 |         proto: HTTP/2.0
120 |         proto_major: 2
121 |         proto_minor: 0
122 |         transfer_encoding: []
123 |         trailer: {}
124 |         content_length: -1
125 |         uncompressed: true
126 |         body: |
127 |             {"data":{"issueCreate":{"success":true,"issue":{"id":"d7bad82a-cab5-4514-859f-686da6a36a20","identifier":"TEST-95","title":"Issue with Project Slug","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-95/issue-with-project-slug","createdAt":"2025-10-06T09:44:55.631Z","updatedAt":"2025-10-06T09:44:55.631Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[]},"project":{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation"},"projectMilestone":null}}}}
128 |         headers:
129 |             Alt-Svc:
130 |                 - h3=":443"; ma=86400
131 |             Cache-Control:
132 |                 - no-store
133 |             Cf-Cache-Status:
134 |                 - DYNAMIC
135 |             Content-Type:
136 |                 - application/json; charset=utf-8
137 |             Etag:
138 |                 - W/"27d-qyHETS4IDY0lD2ks/TgnkeoJyE4"
139 |             Server:
140 |                 - cloudflare
141 |             Vary:
142 |                 - Accept-Encoding
143 |             Via:
144 |                 - 1.1 google
145 |         status: 200 OK
146 |         code: 200
147 |         duration: 0s
148 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/get_issue_comments_handler_With limit.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: 1316
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         body: '{"query":"\n\t\tquery GetIssue($id: String!) {\n\t\t\tissue(id: $id) {\n\t\t\t\tid\n\t\t\t\tidentifier\n\t\t\t\ttitle\n\t\t\t\tdescription\n\t\t\t\tpriority\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tstate {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tassignee {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tteam {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t}\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tprojectMilestone {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\trelations(first: 20) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttype\n\t\t\t\t\t\trelatedIssue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tidentifier\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinverseRelations(first: 20) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttype\n\t\t\t\t\t\tissue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tidentifier\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tattachments(first: 50) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\tsubtitle\n\t\t\t\t\t\turl\n\t\t\t\t\t\tsourceType\n\t\t\t\t\t\tmetadata\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f"}}'
 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":{"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue","createdAt":"2025-03-03T11:34:49.241Z","updatedAt":"2025-10-07T16:03:19.741Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"project":{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation"},"projectMilestone":{"id":"5214c4d9-9c2a-4ae7-b5e5-e33058b3e131","name":"M1: Gather potential resources to investigate"},"relations":{"nodes":[]},"inverseRelations":{"nodes":[]},"attachments":{"nodes":[]}}}}
 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/"36b-2tYg/a9gEbu6WHEn0vlp7gu8zgw"
 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 |     - id: 2
101 |       request:
102 |         proto: HTTP/1.1
103 |         proto_major: 1
104 |         proto_minor: 1
105 |         content_length: 789
106 |         transfer_encoding: []
107 |         trailer: {}
108 |         host: api.linear.app
109 |         remote_addr: ""
110 |         request_uri: ""
111 |         body: '{"query":"\n\t\tquery GetIssueComments($issueId: String!, $parentId: ID, $first: Int!, $after: String) {\n\t\t\tissue(id: $issueId) {\n\t\t\t\tcomments(\n\t\t\t\t\tfirst: $first,\n\t\t\t\t\tafter: $after,\n\t\t\t\t\tfilter: { parent: { id: { eq: $parentId } } }\n\t\t\t\t) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tbody\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tuser {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t\tparent {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchildren(first: 1) {\n\t\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpageInfo {\n\t\t\t\t\t\thasNextPage\n\t\t\t\t\t\tendCursor\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"first":3,"issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f"}}'
112 |         form: {}
113 |         headers:
114 |             Content-Type:
115 |                 - application/json
116 |         url: https://api.linear.app/graphql
117 |         method: POST
118 |       response:
119 |         proto: HTTP/2.0
120 |         proto_major: 2
121 |         proto_minor: 0
122 |         transfer_encoding: []
123 |         trailer: {}
124 |         content_length: -1
125 |         uncompressed: true
126 |         body: |
127 |             {"data":{"issue":{"comments":{"nodes":[{"id":"6b337bfa-a7df-4b5c-9d6d-a0c8c6212301","body":"This is a reply to the comment","createdAt":"2025-10-07T16:03:19.784Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"1bc6bceb-1f2c-4a52-8f23-155aeb966ee2","body":"Reply using shorthand","createdAt":"2025-10-07T15:00:53.946Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}},{"id":"5c1c0a90-6778-41f4-94b4-fce69d894bb7","body":"Reply using comment URL","createdAt":"2025-10-07T15:00:53.396Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"parent":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"},"children":{"nodes":[]}}],"pageInfo":{"hasNextPage":true,"endCursor":"5c1c0a90-6778-41f4-94b4-fce69d894bb7"}}}}}
128 |         headers:
129 |             Alt-Svc:
130 |                 - h3=":443"; ma=86400
131 |             Cache-Control:
132 |                 - no-store
133 |             Cf-Cache-Status:
134 |                 - DYNAMIC
135 |             Content-Type:
136 |                 - application/json; charset=utf-8
137 |             Etag:
138 |                 - W/"3ba-1ni/58B6SxP/Air8ViMneIjjN3E"
139 |             Server:
140 |                 - cloudflare
141 |             Vary:
142 |                 - Accept-Encoding
143 |             Via:
144 |                 - 1.1 google
145 |         status: 200 OK
146 |         code: 200
147 |         duration: 0s
148 | 
```

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

```go
  1 | package tools
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"strings"
  7 | 
  8 | 	"github.com/geropl/linear-mcp-go/pkg/linear"
  9 | 	"github.com/mark3labs/mcp-go/mcp"
 10 | )
 11 | 
 12 | var GetProjectTool = mcp.NewTool("linear_get_project",
 13 | 	mcp.WithDescription("Get a single project."),
 14 | 	mcp.WithString("project", mcp.Required(), mcp.Description("The identifier of the project, either ID, name or slug.")),
 15 | )
 16 | 
 17 | func GetProjectHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 18 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 19 | 		projectIdentifier, err := request.RequireString("project")
 20 | 		if err != nil {
 21 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
 22 | 		}
 23 | 
 24 | 		project, err := linearClient.GetProject(projectIdentifier)
 25 | 		if err != nil {
 26 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get project: %v", err)}}}, nil
 27 | 		}
 28 | 
 29 | 		resultText := FormatProject(*project)
 30 | 
 31 | 		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
 32 | 	}
 33 | }
 34 | 
 35 | var SearchProjectsTool = mcp.NewTool("linear_search_projects",
 36 | 	mcp.WithDescription("Search for projects."),
 37 | 	mcp.WithString("query", mcp.Description("The search query.")),
 38 | )
 39 | 
 40 | func SearchProjectsHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 41 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 42 | 		query := request.GetString("query", "")
 43 | 
 44 | 		if query == "" {
 45 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: "Query parameter may not be empty."}}}, nil
 46 | 		}
 47 | 
 48 | 		projects, err := linearClient.SearchProjects(query)
 49 | 		if err != nil {
 50 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to search projects: %v", err)}}}, nil
 51 | 		}
 52 | 
 53 | 		if len(projects) == 0 {
 54 | 			return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: "No projects found."}}}, nil
 55 | 		}
 56 | 
 57 | 		var builder strings.Builder
 58 | 		for _, project := range projects {
 59 | 			builder.WriteString(FormatProject(project))
 60 | 			builder.WriteString("\n")
 61 | 		}
 62 | 
 63 | 		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: builder.String()}}}, nil
 64 | 	}
 65 | }
 66 | 
 67 | var CreateProjectTool = mcp.NewTool("linear_create_project",
 68 | 	mcp.WithDescription("Create a new project."),
 69 | 	mcp.WithString("name", mcp.Required(), mcp.Description("The name of the project.")),
 70 | 	mcp.WithString("teamIds", mcp.Required(), mcp.Description("A comma-separated list of team IDs.")),
 71 | 	mcp.WithString("description", mcp.Description("The description of the project.")),
 72 | 	mcp.WithString("leadId", mcp.Description("The ID of the project lead.")),
 73 | 	mcp.WithString("startDate", mcp.Description("The start date of the project (YYYY-MM-DD).")),
 74 | 	mcp.WithString("targetDate", mcp.Description("The target date of the project (YYYY-MM-DD).")),
 75 | )
 76 | 
 77 | func CreateProjectHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 78 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 79 | 		name, err := request.RequireString("name")
 80 | 		if err != nil {
 81 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
 82 | 		}
 83 | 
 84 | 		teamIDsStr, err := request.RequireString("teamIds")
 85 | 		if err != nil {
 86 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
 87 | 		}
 88 | 		teamIDs := strings.Split(teamIDsStr, ",")
 89 | 
 90 | 		description := request.GetString("description", "")
 91 | 		leadID := request.GetString("leadId", "")
 92 | 		startDate := request.GetString("startDate", "")
 93 | 		targetDate := request.GetString("targetDate", "")
 94 | 
 95 | 		input := linear.ProjectCreateInput{
 96 | 			Name:        name,
 97 | 			TeamIDs:     teamIDs,
 98 | 			Description: description,
 99 | 			LeadID:      leadID,
100 | 			StartDate:   startDate,
101 | 			TargetDate:  targetDate,
102 | 		}
103 | 
104 | 		project, err := linearClient.CreateProject(input)
105 | 		if err != nil {
106 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to create project: %v", err)}}}, nil
107 | 		}
108 | 
109 | 		resultText := FormatProject(*project)
110 | 
111 | 		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
112 | 	}
113 | }
114 | 
115 | var UpdateProjectTool = mcp.NewTool("linear_update_project",
116 | 	mcp.WithDescription("Update an existing project."),
117 | 	mcp.WithString("project", mcp.Required(), mcp.Description("The identifier of the project to update.")),
118 | 	mcp.WithString("name", mcp.Description("The new name of the project.")),
119 | 	mcp.WithString("description", mcp.Description("The new description of the project.")),
120 | 	mcp.WithString("leadId", mcp.Description("The ID of the project lead.")),
121 | 	mcp.WithString("startDate", mcp.Description("The start date of the project (YYYY-MM-DD).")),
122 | 	mcp.WithString("targetDate", mcp.Description("The target date of the project (YYYY-MM-DD).")),
123 | 	mcp.WithString("teamIds", mcp.Description("A comma-separated list of team IDs.")),
124 | )
125 | 
126 | func UpdateProjectHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
127 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
128 | 		projectIdentifier, err := request.RequireString("project")
129 | 		if err != nil {
130 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
131 | 		}
132 | 
133 | 		// Get the project first to get its ID
134 | 		proj, err := linearClient.GetProject(projectIdentifier)
135 | 		if err != nil {
136 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get project: %v", err)}}}, nil
137 | 		}
138 | 
139 | 		name := request.GetString("name", "")
140 | 		description := request.GetString("description", "")
141 | 		leadID := request.GetString("leadId", "")
142 | 		startDate := request.GetString("startDate", "")
143 | 		targetDate := request.GetString("targetDate", "")
144 | 		teamIDsStr := request.GetString("teamIds", "")
145 | 		var teamIDs []string
146 | 		if teamIDsStr != "" {
147 | 			teamIDs = strings.Split(teamIDsStr, ",")
148 | 		}
149 | 
150 | 		input := linear.ProjectUpdateInput{
151 | 			Name:        name,
152 | 			Description: description,
153 | 			LeadID:      leadID,
154 | 			StartDate:   startDate,
155 | 			TargetDate:  targetDate,
156 | 			TeamIDs:     teamIDs,
157 | 		}
158 | 
159 | 		project, err := linearClient.UpdateProject(proj.ID, input)
160 | 		if err != nil {
161 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to update project: %v", err)}}}, nil
162 | 		}
163 | 
164 | 		resultText := FormatProject(*project)
165 | 
166 | 		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
167 | 	}
168 | }
169 | 
170 | func FormatProject(project linear.Project) string {
171 | 	var builder strings.Builder
172 | 	builder.WriteString(fmt.Sprintf("Project: %s\n", project.Name))
173 | 	builder.WriteString(fmt.Sprintf("  ID: %s\n", project.ID))
174 | 	builder.WriteString(fmt.Sprintf("  State: %s\n", project.State))
175 | 	builder.WriteString(fmt.Sprintf("  URL: %s\n", project.URL))
176 | 	if project.Description != "" {
177 | 		builder.WriteString(fmt.Sprintf("  Description: %s\n", project.Description))
178 | 	}
179 | 	if project.Lead == nil {
180 | 		builder.WriteString("  Lead: None\n")
181 | 	} else {
182 | 		builder.WriteString(fmt.Sprintf("  Lead: %s\n", project.Lead.Name))
183 | 	}
184 | 	if project.StartDate == nil {
185 | 		builder.WriteString("  Start Date: None\n")
186 | 	} else {
187 | 		builder.WriteString(fmt.Sprintf("  Start Date: %s\n", *project.StartDate))
188 | 	}
189 | 	if project.TargetDate == nil {
190 | 		builder.WriteString("  Target Date: None\n")
191 | 	} else {
192 | 		builder.WriteString(fmt.Sprintf("  Target Date: %s\n", *project.TargetDate))
193 | 	}
194 | 	if project.Initiatives != nil && len(project.Initiatives.Nodes) > 0 {
195 | 		builder.WriteString("  Initiatives:\n")
196 | 		for _, i := range project.Initiatives.Nodes {
197 | 			builder.WriteString(fmt.Sprintf("    - %s (ID: %s)\n", i.Name, i.ID))
198 | 		}
199 | 	} else {
200 | 		builder.WriteString("  Initiatives: None\n")
201 | 	}
202 | 	return builder.String()
203 | }
204 | 
```

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

```markdown
  1 | # Development Workflows: Linear MCP Server
  2 | 
  3 | ## Git Workflow
  4 | 
  5 | ### Branch Management
  6 | - **Main Branch**: Protected branch that contains stable, production-ready code
  7 | - **Feature Branches**: Development happens in feature branches created from main
  8 | - **Branch Naming**: Use descriptive names (e.g., `feature/setup-command`, `fix/rate-limiting`)
  9 | - **Pull Requests**: All changes must go through pull request review process
 10 | - **Branch Protection**: Main branch requires PR approval before merging
 11 | 
 12 | ### Version Control Practices
 13 | - **Commit Messages**: Use clear, descriptive commit messages
 14 | - **Atomic Commits**: Each commit should represent a single logical change
 15 | - **Code Review**: All PRs require review before merging
 16 | - **Testing**: All tests must pass before merging
 17 | 
 18 | ## Release Process
 19 | 
 20 | ### Semantic Versioning
 21 | - **Version Format**: Follow semantic versioning with "v" prefix (e.g., v1.0.0, v1.2.3)
 22 | - **Version Components**:
 23 |   - Major: Breaking changes
 24 |   - Minor: New features (backward compatible)
 25 |   - Patch: Bug fixes (backward compatible)
 26 | 
 27 | ### Release Branch Workflow
 28 | Since the main branch is protected and requires PR approval, releases follow a branch-based workflow:
 29 | 
 30 | #### Phase 1: Prepare Release Branch
 31 | ```bash
 32 | # Create release branch from current development branch
 33 | git checkout -b release/v{version}
 34 | 
 35 | # Update version in pkg/server/server.go
 36 | # Change ServerVersion constant to new version
 37 | 
 38 | # Commit version update
 39 | git add pkg/server/server.go
 40 | git commit -m "Bump version to v{version}"
 41 | 
 42 | # Push release branch
 43 | git push origin release/v{version}
 44 | ```
 45 | 
 46 | #### Phase 2: Create Release Pull Request
 47 | ```bash
 48 | # Create PR from release/v{version} to main
 49 | # PR Title: "Release v{version}"
 50 | # PR Description should include:
 51 | # - Summary of changes since last release
 52 | # - Breaking changes (if any)
 53 | # - New features added
 54 | # - Bug fixes included
 55 | # - Testing performed
 56 | ```
 57 | 
 58 | #### Phase 3: Review and Merge
 59 | - **Code Review**: Release PR must be reviewed and approved
 60 | - **CI Checks**: All automated tests and checks must pass
 61 | - **Documentation**: Ensure README and docs are up to date
 62 | - **Final Testing**: Perform any additional manual testing if needed
 63 | 
 64 | #### Phase 4: Tag and Release
 65 | ```bash
 66 | # After PR is merged to main
 67 | git checkout main
 68 | git pull origin main
 69 | 
 70 | # Create and push release tag
 71 | git tag v{version}
 72 | git push origin v{version}
 73 | ```
 74 | 
 75 | #### Phase 5: Automated Release
 76 | GitHub Actions workflow automatically:
 77 | - Builds binaries for Linux, macOS, and Windows
 78 | - Runs full test suite
 79 | - Creates GitHub release with release notes
 80 | - Uploads release assets
 81 | 
 82 | ### Release Branch Naming
 83 | - **Format**: `release/v{version}` (e.g., `release/v1.6.0`)
 84 | - **Purpose**: Isolate release preparation from ongoing development
 85 | - **Lifecycle**: Created for release prep, deleted after successful release
 86 | 
 87 | ### Release PR Template
 88 | When creating a release PR, include:
 89 | 
 90 | ```markdown
 91 | ## Release v{version}
 92 | 
 93 | ### Summary
 94 | Brief description of this release.
 95 | 
 96 | ### Changes Since Last Release
 97 | - **New Features:**
 98 |   - Feature 1 description
 99 |   - Feature 2 description
100 | 
101 | - **Bug Fixes:**
102 |   - Fix 1 description
103 |   - Fix 2 description
104 | 
105 | - **Improvements:**
106 |   - Improvement 1 description
107 |   - Improvement 2 description
108 | 
109 | ### Breaking Changes
110 | - None / List any breaking changes
111 | 
112 | ### Testing
113 | - [ ] All automated tests pass
114 | - [ ] Manual testing performed
115 | - [ ] Setup command tested on target platforms
116 | 
117 | ### Checklist
118 | - [ ] Version updated in pkg/server/server.go
119 | - [ ] CHANGELOG.md updated (if exists)
120 | - [ ] Documentation updated
121 | - [ ] All tests passing
122 | - [ ] No merge conflicts with main
123 | ```
124 | 
125 | ### Hotfix Process
126 | For urgent fixes that need to bypass normal development flow:
127 | 
128 | ```bash
129 | # Create hotfix branch from main
130 | git checkout main
131 | git pull origin main
132 | git checkout -b hotfix/v{version}
133 | 
134 | # Make necessary fixes
135 | # Update version (patch increment)
136 | # Commit changes
137 | 
138 | # Create PR to main (expedited review)
139 | # After merge, tag immediately
140 | ```
141 | 
142 | ### GitHub Actions Workflow
143 | - **Trigger**: Activated when tags matching "v*" pattern are pushed
144 | - **Build Matrix**: Builds for multiple platforms simultaneously
145 | - **Testing**: Runs full test suite before creating release
146 | - **Asset Upload**: Automatically uploads compiled binaries to GitHub releases
147 | 
148 | ## Development Commands
149 | 
150 | ### Project Setup
151 | ```bash
152 | # Clone the repository
153 | git clone <repository-url>
154 | cd linear-mcp-go
155 | 
156 | # Install dependencies (Go modules)
157 | go mod download
158 | 
159 | # Verify setup
160 | go build
161 | ```
162 | 
163 | ### Building
164 | ```bash
165 | # Build for current platform
166 | go build
167 | 
168 | # Build with custom output name
169 | go build -o linear-mcp-server
170 | 
171 | # Build for specific platform
172 | GOOS=linux GOARCH=amd64 go build -o linear-mcp-server-linux
173 | 
174 | # Build for all platforms (manual)
175 | GOOS=linux GOARCH=amd64 go build -o dist/linear-mcp-server-linux
176 | GOOS=darwin GOARCH=amd64 go build -o dist/linear-mcp-server-darwin
177 | GOOS=windows GOARCH=amd64 go build -o dist/linear-mcp-server-windows.exe
178 | ```
179 | 
180 | ### Testing
181 | 
182 | #### Running Tests
183 | ```bash
184 | # Run all tests with existing recordings
185 | go test -v ./...
186 | 
187 | # Run tests for specific package
188 | go test -v ./pkg/server
189 | 
190 | # Run specific test function
191 | go test -v -run TestCreateIssueHandler ./pkg/server
192 | 
193 | # Run tests with coverage
194 | go test -v -cover ./...
195 | ```
196 | 
197 | #### Recording New Test Fixtures
198 | ```bash
199 | # Re-record tests (requires LINEAR_API_KEY environment variable)
200 | go test -v -record=true ./...
201 | 
202 | # Re-record all tests including state-changing operations
203 | go test -v -recordWrites=true ./...
204 | 
205 | # Re-record specific test
206 | LINEAR_API_KEY=your_key go test -v -record=true -run TestSpecificFunction ./...
207 | ```
208 | 
209 | #### Test Environment Setup
210 | ```bash
211 | # Set up environment for recording tests
212 | export LINEAR_API_KEY=your_linear_api_key
213 | 
214 | # Run tests that modify state (use with caution)
215 | go test -v -recordWrites=true ./...
216 | ```
217 | 
218 | ### Running the Server
219 | 
220 | #### Development Mode
221 | ```bash
222 | # Run server in read-only mode (safe for development)
223 | ./linear-mcp-go server
224 | 
225 | # Run server with write access (use with caution)
226 | ./linear-mcp-go server --write-access
227 | 
228 | # Run with environment variable
229 | LINEAR_API_KEY=your_key ./linear-mcp-go server
230 | ```
231 | 
232 | #### Setup for AI Assistants
233 | ```bash
234 | # Set up for Cline (default)
235 | ./linear-mcp-go setup --api-key=your_linear_api_key
236 | 
237 | # Set up with write access enabled
238 | ./linear-mcp-go setup --api-key=your_linear_api_key --write-access
239 | 
240 | # Set up for specific AI assistant (future)
241 | ./linear-mcp-go setup --api-key=your_key --tool=assistant_name
242 | ```
243 | 
244 | ### Code Quality
245 | 
246 | #### Formatting
247 | ```bash
248 | # Format all Go code
249 | go fmt ./...
250 | 
251 | # Check formatting
252 | gofmt -l .
253 | 
254 | # Format specific file
255 | go fmt pkg/server/server.go
256 | ```
257 | 
258 | #### Linting
259 | ```bash
260 | # Run go vet (built-in static analysis)
261 | go vet ./...
262 | 
263 | # Run golangci-lint (if installed)
264 | golangci-lint run
265 | ```
266 | 
267 | #### Dependencies
268 | ```bash
269 | # Update dependencies
270 | go mod tidy
271 | 
272 | # Verify dependencies
273 | go mod verify
274 | 
275 | # View dependency graph
276 | go mod graph
277 | ```
278 | 
279 | ## Debugging and Troubleshooting
280 | 
281 | ### Common Issues
282 | 
283 | #### API Key Problems
284 | ```bash
285 | # Verify API key is set
286 | echo $LINEAR_API_KEY
287 | 
288 | # Test API key manually
289 | curl -H "Authorization: Bearer $LINEAR_API_KEY" \
290 |      -H "Content-Type: application/json" \
291 |      -d '{"query": "{ viewer { id name } }"}' \
292 |      https://api.linear.app/graphql
293 | ```
294 | 
295 | #### Build Issues
296 | ```bash
297 | # Clean build cache
298 | go clean -cache
299 | 
300 | # Rebuild from scratch
301 | go clean && go build
302 | ```
303 | 
304 | #### Test Issues
305 | ```bash
306 | # Clean test cache
307 | go clean -testcache
308 | 
309 | # Run tests with verbose output
310 | go test -v -x ./...
311 | ```
312 | 
313 | ### Development Tips
314 | 
315 | #### Working with Test Fixtures
316 | - Test fixtures are stored in `testdata/fixtures/`
317 | - Golden files (expected outputs) are in `testdata/golden/`
318 | - Use `-record=true` flag sparingly to avoid API quota exhaustion
319 | - Always review recorded fixtures before committing
320 | 
321 | #### MCP Tool Development
322 | - Register new tools in `pkg/server/tools.go`
323 | - Follow existing patterns for parameter validation
324 | - Add comprehensive test coverage for new tools
325 | - Update documentation when adding new tools
326 | 
327 | #### Linear API Integration
328 | - All API calls go through the Linear client in `pkg/linear/client.go`
329 | - Add new API methods to the client rather than calling API directly from tools
330 | - Handle GraphQL errors consistently
331 | - Respect rate limits in all API interactions
332 | 
333 | ## Continuous Integration
334 | 
335 | ### GitHub Actions
336 | - **Workflow File**: `.github/workflows/release.yml`
337 | - **Triggers**: Push to main branch, pull requests, version tags
338 | - **Jobs**: Test, build, release (for tags)
339 | - **Platforms**: Linux, macOS, Windows
340 | 
341 | ### Quality Gates
342 | - All tests must pass
343 | - Code must be properly formatted
344 | - No linting errors
345 | - Build must succeed on all target platforms
346 | 
```

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

```markdown
  1 | # Tool Standardization Implementation Tracking
  2 | 
  3 | This document provides a detailed tracking sheet for the implementation of the standardization rules outlined in [002-tool-standardization.md](./002-tool-standardization.md) and [003-tool-standardization-implementation.md](./003-tool-standardization-implementation.md).
  4 | 
  5 | ## Overall Progress
  6 | 
  7 | | Phase | Status | Progress | Notes |
  8 | |-------|--------|----------|-------|
  9 | | Phase 1: Create Shared Utility Functions | Completed | 100% | Created rendering.go and updated common.go |
 10 | | Phase 2: Update Tools | Completed | 100% | All rules implemented |
 11 | | Phase 3: Update Tests | Completed | 100% | Tests updated to reflect new formatting |
 12 | | **Overall** | **Completed** | **100%** | All rules implemented |
 13 | 
 14 | ## Phase 1: Create Shared Utility Functions
 15 | 
 16 | | Task | Status | Assignee | Notes |
 17 | |------|--------|----------|-------|
 18 | | Refactor `resolveParentIssueIdentifier` to `resolveIssueIdentifier` | Completed | | Implemented in common.go |
 19 | | Create `resolveUserIdentifier` | Completed | | Implemented in common.go |
 20 | | Create `pkg/tools/rendering.go` | Completed | | Created with all formatting functions |
 21 | | Implement full entity rendering functions | Completed | | |
 22 | | - `formatIssue` | Completed | | Implemented in rendering.go |
 23 | | - `formatTeam` | Completed | | Implemented in rendering.go |
 24 | | - `formatUser` | Completed | | Implemented in rendering.go |
 25 | | - `formatComment` | Completed | | Implemented in rendering.go |
 26 | | Implement entity identifier rendering functions | Completed | | |
 27 | | - `formatIssueIdentifier` | Completed | | Implemented in rendering.go |
 28 | | - `formatTeamIdentifier` | Completed | | Implemented in rendering.go |
 29 | | - `formatUserIdentifier` | Completed | | Implemented in rendering.go |
 30 | | - `formatCommentIdentifier` | Completed | | Implemented in rendering.go |
 31 | 
 32 | ## Phase 2: Update Tools
 33 | 
 34 | ### linear_create_issue
 35 | 
 36 | | Task | Status | Assignee | Notes |
 37 | |------|--------|----------|-------|
 38 | | Update description to be concise | Completed | | Description simplified |
 39 | | Update result formatting to use `formatIssueIdentifier` | Completed | | Using formatIssueIdentifier for output |
 40 | 
 41 | ### linear_update_issue
 42 | 
 43 | | Task | Status | Assignee | Notes |
 44 | |------|--------|----------|-------|
 45 | | Update description to be concise | Completed | | Description simplified |
 46 | | Update to use `resolveIssueIdentifier` | Completed | | Now accepts issue identifiers |
 47 | | Update result formatting to use `formatIssueIdentifier` | Completed | | Using formatIssueIdentifier for output |
 48 | 
 49 | ### linear_search_issues
 50 | 
 51 | | Task | Status | Assignee | Notes |
 52 | |------|--------|----------|-------|
 53 | | Update description to be concise | Completed | | Description simplified |
 54 | | Update to use `resolveTeamIdentifier` for team | Completed | | Parameter renamed from teamId to team |
 55 | | Update to use `resolveUserIdentifier` for assignee | Completed | | Parameter renamed from assigneeId to assignee |
 56 | | Update result formatting to use `formatIssueIdentifier` | Completed | | Using formatIssueIdentifier for each issue |
 57 | 
 58 | ### linear_get_user_issues
 59 | 
 60 | | Task | Status | Assignee | Notes |
 61 | |------|--------|----------|-------|
 62 | | Update description to be concise | Completed | | Description simplified |
 63 | | Update to use `resolveUserIdentifier` for user | Completed | | Parameter renamed from userId to user |
 64 | | Update result formatting to use `formatIssueIdentifier` | Completed | | Using formatIssueIdentifier for each issue |
 65 | 
 66 | ### linear_get_issue
 67 | 
 68 | | Task | Status | Assignee | Notes |
 69 | |------|--------|----------|-------|
 70 | | Update description to be concise | Completed | | Description simplified |
 71 | | Update to use `resolveIssueIdentifier` | Completed | | Now accepts issue identifiers |
 72 | | Update result formatting to use formatting functions | Completed | | Using formatIssue, formatUserIdentifier, formatTeamIdentifier, and formatIssueIdentifier |
 73 | 
 74 | ### linear_add_comment
 75 | 
 76 | | Task | Status | Assignee | Notes |
 77 | |------|--------|----------|-------|
 78 | | Update description to be concise | Completed | | Description simplified |
 79 | | Update to use `resolveIssueIdentifier` | Completed | | Now accepts issue identifiers |
 80 | | Update result formatting to use `formatIssueIdentifier` and `formatCommentIdentifier` | Completed | | Using both formatting functions |
 81 | 
 82 | ### linear_get_teams
 83 | 
 84 | | Task | Status | Assignee | Notes |
 85 | |------|--------|----------|-------|
 86 | | Update description to be concise | Completed | | Description simplified |
 87 | | Update result formatting to use `formatTeamIdentifier` | Completed | | Using formatTeamIdentifier for each team |
 88 | 
 89 | ## Phase 3: Update Tests
 90 | 
 91 | | Task | Status | Assignee | Notes |
 92 | |------|--------|----------|-------|
 93 | | Update test fixtures for `linear_create_issue` | Completed | | Updated with new standardized format |
 94 | | Update test fixtures for `linear_update_issue` | Completed | | Updated with new standardized format |
 95 | | Update test fixtures for `linear_search_issues` | Completed | | Updated with new standardized format |
 96 | | Update test fixtures for `linear_get_user_issues` | Completed | | Updated with new standardized format |
 97 | | Update test fixtures for `linear_get_issue` | Completed | | Updated with new standardized format |
 98 | | Update test fixtures for `linear_add_comment` | Completed | | Updated with new standardized format |
 99 | | Update test fixtures for `linear_get_teams` | Completed | | Updated with new standardized format |
100 | | Update test cases for parameter name changes | Completed | | Updated parameter names in test cases |
101 | | Run tests to verify changes | Completed | | All tests pass with new standardized format |
102 | 
103 | ## Implementation Checklist
104 | 
105 | Use this checklist to track the implementation of each rule for each tool:
106 | 
107 | ### Rule 1: Concise Tool Descriptions
108 | 
109 | - [x] linear_create_issue
110 | - [x] linear_update_issue
111 | - [x] linear_search_issues
112 | - [x] linear_get_user_issues
113 | - [x] linear_get_issue
114 | - [x] linear_add_comment
115 | - [x] linear_get_teams
116 | 
117 | ### Rule 2: Flexible Object Identifier Resolution
118 | 
119 | - [x] linear_create_issue (already implemented)
120 | - [x] linear_update_issue
121 | - [x] linear_search_issues
122 | - [x] linear_get_user_issues
123 | - [x] linear_get_issue
124 | - [x] linear_add_comment
125 | - [x] linear_get_teams (no identifiers needed)
126 | 
127 | ### Rule 3: Consistent Entity Rendering
128 | 
129 | #### Full Entity Rendering
130 | - [x] linear_create_issue
131 | - [x] linear_update_issue
132 | - [x] linear_search_issues
133 | - [x] linear_get_user_issues
134 | - [x] linear_get_issue
135 | - [x] linear_add_comment
136 | - [x] linear_get_teams
137 | 
138 | #### Entity Identifier Rendering
139 | - [x] linear_create_issue (for referenced entities)
140 | - [x] linear_update_issue (for referenced entities)
141 | - [x] linear_search_issues (for referenced entities)
142 | - [x] linear_get_user_issues (for referenced entities)
143 | - [x] linear_get_issue (for referenced entities)
144 | - [x] linear_add_comment (for referenced entities)
145 | - [x] linear_get_teams (for referenced entities)
146 | 
147 | ### Rule 4: Field Superset for Retrieval Methods
148 | 
149 | #### Detail Retrieval Methods
150 | - [x] linear_get_issue (must include all fields from create_issue and update_issue)
151 | - [x] linear_get_issue (must include all necessary comment fields from add_comment)
152 | 
153 | #### Overview Retrieval Methods
154 | - [x] linear_get_user_issues (must include key metadata fields)
155 | - [x] linear_search_issues (must include key metadata fields)
156 | - [x] linear_get_teams (must include all team fields that can be referenced)
157 | 
158 | ## Notes and Issues
159 | 
160 | Use this section to track any issues or notes that arise during implementation:
161 | 
162 | 1. The formatTeamIdentifier function expects a pointer to a Team, but the GetTeams function returns a slice of Team values. We had to create a pointer to each team in the loop.
163 | 2. Tests need to be updated to reflect the new formatting of results.
164 | 3. Parameter names have been updated to be more consistent with the entity they represent:
165 |    - `id` → `issue` in linear_update_issue
166 |    - `issueId` → `issue` in linear_get_issue and linear_add_comment
167 |    - `teamId` → `team` in linear_search_issues (already done)
168 |    - `userId` → `user` in linear_get_user_issues (already done)
169 |    - `assigneeId` → `assignee` in linear_search_issues (already done)
170 | 
171 | ## Next Steps
172 | 
173 | The following tasks have been completed for the Tool Standardization initiative:
174 | 
175 | 1. **Rule 1: Concise Tool Descriptions** ✅
176 |    - All tool descriptions have been updated to be concise and focused on functionality
177 | 
178 | 2. **Rule 2: Flexible Object Identifier Resolution** ✅
179 |    - All tools now accept multiple forms of identification for Linear objects
180 | 
181 | 3. **Rule 3: Consistent Entity Rendering** ✅
182 |    - All tools now use consistent formatting for entity rendering
183 |    - Shared formatting functions have been implemented
184 | 
185 | 4. **Rule 4: Field Superset for Retrieval Methods** ✅
186 |    - Detail retrieval methods now include all fields that can be set in create/update methods
187 |    - Overview retrieval methods include appropriate metadata fields
188 |    - The displayIconURL parameter has been removed from add_comment
189 | 
190 | Future enhancements could include:
191 | 
192 | 1. Adding more comprehensive tests for the resolution functions
193 | 2. Documenting the standardized approach in the project README
194 | 3. Applying similar standardization patterns to any new tools added in the future
195 | 
```

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

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

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

```markdown
  1 | # System Patterns: Linear MCP Server
  2 | 
  3 | ## System Architecture
  4 | The Linear MCP Server follows a modular architecture with clear separation of concerns:
  5 | 
  6 | ```mermaid
  7 | flowchart TD
  8 |     Main[main.go] --> RootCmd[cmd/root.go]
  9 |     RootCmd --> ServerCmd[cmd/server.go]
 10 |     RootCmd --> SetupCmd[cmd/setup.go]
 11 |     ServerCmd --> |writeAccess| Server[pkg/server/server.go]
 12 |     Server --> |writeAccess| Tools[pkg/server/tools.go]
 13 |     Server --> LinearClient[pkg/linear/client.go]
 14 |     LinearClient --> RateLimiter[pkg/linear/rate_limiter.go]
 15 |     LinearClient --> Models[pkg/linear/models.go]
 16 |     Tools --> LinearClient
 17 | ```
 18 | 
 19 | 1. **Main Module** (`main.go`): Entry point that initializes the command structure.
 20 | 2. **Command Module** (`cmd/`): Handles command-line interface and subcommands.
 21 |    - **Root Command** (`cmd/root.go`): Base command that serves as the entry point for all subcommands.
 22 |    - **Server Command** (`cmd/server.go`): Handles the server functionality.
 23 |    - **Setup Command** (`cmd/setup.go`): Handles the setup functionality for AI assistants.
 24 | 3. **Server Module** (`pkg/server`): Handles MCP protocol implementation and tool registration.
 25 | 4. **Linear Client Module** (`pkg/linear`): Manages communication with the Linear API.
 26 | 
 27 | ## Key Technical Decisions
 28 | 
 29 | ### 1. Command-Line Interface
 30 | - Uses the Cobra library for command-line handling.
 31 | - Implements a subcommand structure for different functionalities.
 32 | - Provides consistent flag handling across subcommands.
 33 | 
 34 | ### 2. Write Access Control
 35 | - Implements write access control (default: disabled) to control access to write operations.
 36 | - Command-line flag `--write-access` determines whether write tools are registered.
 37 | - Write operations (`linear_create_issue`, `linear_update_issue`, `linear_add_comment`) are only available when write access is enabled.
 38 | 
 39 | ### 3. Setup Automation
 40 | - Automates the installation and configuration process for AI assistants.
 41 | - Checks for existing binary before downloading.
 42 | - Merges new settings with existing settings to preserve user configuration.
 43 | - Supports multiple AI assistants (starting with Cline).
 44 | 
 45 | ### 4. MCP Protocol Implementation
 46 | - Uses the `github.com/mark3labs/mcp-go` library for MCP server implementation.
 47 | - Implements the standard MCP protocol for tool registration and execution.
 48 | 
 49 | ### 5. Linear API Integration
 50 | - Custom Linear client implementation in the `pkg/linear` package.
 51 | - Handles authentication, request formatting, and response parsing.
 52 | 
 53 | ### 6. Rate Limiting
 54 | - Implements rate limiting to respect Linear API quotas.
 55 | - Uses a simple rate limiter to prevent API quota exhaustion.
 56 | 
 57 | ### 7. Error Handling
 58 | - Consistent error handling patterns throughout the codebase.
 59 | - Errors are propagated up and formatted according to MCP specifications.
 60 | 
 61 | ### 8. Testing Strategy
 62 | - Uses `go-vcr` for recording and replaying HTTP interactions in tests.
 63 | - Test fixtures stored in `testdata/fixtures/`.
 64 | 
 65 | ## Design Patterns
 66 | 
 67 | ### 1. Command Pattern
 68 | - The Cobra library implements the Command pattern for handling CLI commands.
 69 | - Each command is a separate object with its own run method.
 70 | - Commands can have subcommands, creating a hierarchical command structure.
 71 | 
 72 | ### 2. Factory Pattern
 73 | - `NewLinearMCPServer()` and `NewLinearClientFromEnv()` functions create and initialize complex objects.
 74 | 
 75 | ### 3. Dependency Injection
 76 | - The Linear client is injected into tool handlers, promoting testability and loose coupling.
 77 | 
 78 | ### 4. Handler Pattern
 79 | - Each MCP tool has a dedicated handler function that processes requests and returns results.
 80 | 
 81 | ### 5. Builder Pattern
 82 | - MCP tools are constructed using a builder-like pattern with the `mcp.NewTool()` function and various `With*` methods.
 83 | 
 84 | ## MCP Tool Registration Patterns
 85 | 
 86 | ### Tool Registration Process
 87 | - All tools are registered in the `RegisterTools` function in `pkg/server/tools.go`
 88 | - Tools are conditionally registered based on write access permissions
 89 | - Each tool follows a consistent registration pattern:
 90 | 
 91 | ```go
 92 | server.AddTool(mcp.NewTool("tool_name").
 93 |     WithDescription("Tool description").
 94 |     WithInputSchema(mcp.Object{
 95 |         "param1": mcp.String().WithDescription("Parameter description"),
 96 |         "param2": mcp.Required(mcp.String()).WithDescription("Required parameter"),
 97 |     }).
 98 |     WithHandler(toolHandlerFunction))
 99 | ```
100 | 
101 | ### Tool Handler Structure
102 | - Each tool has a dedicated handler function that processes MCP requests
103 | - Handler functions follow a consistent pattern:
104 |   1. Extract and validate parameters from the request
105 |   2. Call appropriate Linear client methods
106 |   3. Format and return the response
107 | - Error handling is consistent across all handlers
108 | - Response formatting follows MCP specifications
109 | 
110 | ### Parameter Definition Patterns
111 | - Tool parameters are defined using the MCP schema format
112 | - Required parameters are marked with `mcp.Required()`
113 | - Parameter types include: `mcp.String()`, `mcp.Number()`, `mcp.Boolean()`
114 | - All parameters include descriptive help text
115 | - Optional parameters have sensible defaults where applicable
116 | 
117 | ### Write Access Control
118 | - Write operations are controlled by the `writeAccess` flag
119 | - Read-only tools are always registered
120 | - Write tools (`linear_create_issue`, `linear_update_issue`, `linear_add_comment`) are only registered when write access is enabled
121 | - This provides a security layer to prevent accidental modifications
122 | 
123 | ## Linear API Integration Patterns
124 | 
125 | ### Client Architecture
126 | - The Linear client is implemented in `pkg/linear/client.go`
127 | - Client provides high-level methods that abstract GraphQL complexity
128 | - All API interactions go through the centralized client
129 | 
130 | ### Authentication Pattern
131 | - Authentication is handled via the `LINEAR_API_KEY` environment variable
132 | - API key is validated on first API request, not at startup
133 | - No support for other authentication methods currently
134 | - Client includes the API key in all requests via Authorization header
135 | 
136 | ### Rate Limiting Implementation
137 | - Simple rate limiter implemented to respect Linear's API quotas
138 | - Rate limiting is applied at the client level before making requests
139 | - Current implementation uses a basic token bucket approach
140 | - Rate limits are not configurable (hardcoded values)
141 | 
142 | ### API Response Handling
143 | - API responses are parsed into Go structs defined in `pkg/linear/models.go`
144 | - JSON unmarshaling handles nested structures (e.g., `LabelConnection`)
145 | - Error responses from Linear API are translated into user-friendly messages
146 | - GraphQL errors are properly extracted and formatted
147 | 
148 | ### GraphQL Query Patterns
149 | - All Linear API interactions use GraphQL queries and mutations
150 | - Queries are embedded as string literals in the client methods
151 | - Parameter types in queries match Linear API expectations (e.g., `String!` vs `ID!`)
152 | - Query structure follows Linear's API schema requirements
153 | 
154 | ### Identifier Resolution Strategy
155 | - Flexible identifier resolution allows users to specify entities by:
156 |   - UUID (direct API identifier)
157 |   - Human-readable identifiers (e.g., "TEAM-123" for issues)
158 |   - Names (e.g., team names, label names)
159 | - Resolution functions handle the translation from user input to API identifiers
160 | - Error handling provides clear feedback when identifiers cannot be resolved
161 | 
162 | ## Component Relationships
163 | 
164 | ### Commands and Subcommands
165 | - The root command serves as the entry point for all subcommands.
166 | - Subcommands handle specific functionalities (server, setup).
167 | - Each subcommand has its own flags and run method.
168 | 
169 | ### Server and Tools
170 | - The server registers tools during initialization.
171 | - Each tool has a handler function that processes requests.
172 | - Tools are defined with schemas that specify required and optional parameters.
173 | 
174 | ### Linear Client and API
175 | - The Linear client translates MCP tool calls into Linear API requests.
176 | - It handles authentication, request formatting, and response parsing.
177 | - The rate limiter ensures API quotas are respected.
178 | 
179 | ## Data Flow
180 | 
181 | 1. **Command Flow**:
182 |    ```mermaid
183 |    sequenceDiagram
184 |        participant User
185 |        participant Main as main.go
186 |        participant Root as cmd/root.go
187 |        participant Cmd as Subcommand
188 |        participant Action as Command Action
189 |        
190 |        User->>Main: Execute Command
191 |        Main->>Root: Execute Root Command
192 |        Root->>Cmd: Parse and Execute Subcommand
193 |        Cmd->>Action: Execute Command Action
194 |        Action->>User: Return Result
195 |    ```
196 | 
197 | 2. **Setup Flow**:
198 |    ```mermaid
199 |    sequenceDiagram
200 |        participant User
201 |        participant Setup as cmd/setup.go
202 |        participant Binary as Binary Management
203 |        participant Config as Configuration Management
204 |        
205 |        User->>Setup: Execute Setup Command
206 |        Setup->>Binary: Check for Existing Binary
207 |        Binary-->>Setup: Binary Status
208 |        alt Binary Not Found
209 |            Setup->>Binary: Download Latest Release
210 |            Binary-->>Setup: Binary Path
211 |        end
212 |        Setup->>Config: Create/Update Configuration
213 |        Config-->>Setup: Configuration Status
214 |        Setup->>User: Setup Complete
215 |    ```
216 | 
217 | 3. **Request Flow**:
218 |    ```mermaid
219 |    sequenceDiagram
220 |        participant Client as MCP Client
221 |        participant Server as MCP Server
222 |        participant Tool as Tool Handler
223 |        participant Linear as Linear Client
224 |        participant API as Linear API
225 |        
226 |        Client->>Server: Call Tool Request
227 |        Server->>Tool: Forward Request
228 |        Tool->>Linear: Translate Request
229 |        Linear->>API: API Request
230 |        API->>Linear: API Response
231 |        Linear->>Tool: Parsed Response
232 |        Tool->>Server: Formatted Result
233 |        Server->>Client: Tool Result
234 |    ```
235 | 
236 | 4. **Error Flow**:
237 |    ```mermaid
238 |    sequenceDiagram
239 |        participant Client as MCP Client
240 |        participant Server as MCP Server
241 |        participant Tool as Tool Handler
242 |        participant Linear as Linear Client
243 |        participant API as Linear API
244 |        
245 |        Client->>Server: Call Tool Request
246 |        Server->>Tool: Forward Request
247 |        Tool->>Linear: Translate Request
248 |        Linear->>API: API Request
249 |        API->>Linear: Error Response
250 |        Linear->>Tool: Error
251 |        Tool->>Server: Error Result
252 |        Server->>Client: Error Result
253 |    ```
254 | 
255 | ## Code Organization
256 | - **main.go**: Entry point that initializes the command structure
257 | - **cmd/root.go**: Root command that serves as the base for all subcommands
258 | - **cmd/server.go**: Server command that handles the server functionality
259 | - **cmd/setup.go**: Setup command that handles the setup functionality for AI assistants
260 | - **pkg/server/server.go**: Server initialization and management
261 | - **pkg/server/tools.go**: Tool definitions and handlers
262 | - **pkg/linear/client.go**: Linear API client implementation
263 | - **pkg/linear/models.go**: Data models for Linear API requests and responses
264 | - **pkg/linear/rate_limiter.go**: Rate limiting implementation
265 | - **pkg/linear/test_helpers.go**: Test utilities
266 | 
```

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

```go
  1 | package linear
  2 | 
  3 | import "time"
  4 | 
  5 | // Issue represents a Linear issue
  6 | type Issue struct {
  7 | 	ID               string                   `json:"id"`
  8 | 	Identifier       string                   `json:"identifier"`
  9 | 	Title            string                   `json:"title"`
 10 | 	Description      string                   `json:"description"`
 11 | 	Priority         int                      `json:"priority"`
 12 | 	Status           string                   `json:"status"`
 13 | 	Assignee         *User                    `json:"assignee,omitempty"`
 14 | 	Team             *Team                    `json:"team,omitempty"`
 15 | 	Project          *Project                 `json:"project,omitempty"`
 16 | 	ProjectMilestone *ProjectMilestone        `json:"projectMilestone,omitempty"`
 17 | 	URL              string                   `json:"url"`
 18 | 	CreatedAt        time.Time                `json:"createdAt"`
 19 | 	UpdatedAt        time.Time                `json:"updatedAt"`
 20 | 	Labels           *LabelConnection         `json:"labels,omitempty"`
 21 | 	State            *State                   `json:"state,omitempty"`
 22 | 	Estimate         *float64                 `json:"estimate,omitempty"`
 23 | 	Comments         *CommentConnection       `json:"comments,omitempty"`
 24 | 	Relations        *IssueRelationConnection `json:"relations,omitempty"`
 25 | 	InverseRelations *IssueRelationConnection `json:"inverseRelations,omitempty"`
 26 | 	Attachments      *AttachmentConnection    `json:"attachments,omitempty"`
 27 | }
 28 | 
 29 | // User represents a Linear user
 30 | type User struct {
 31 | 	ID    string `json:"id"`
 32 | 	Name  string `json:"name"`
 33 | 	Email string `json:"email"`
 34 | 	Admin bool   `json:"admin"`
 35 | }
 36 | 
 37 | // Team represents a Linear team
 38 | type Team struct {
 39 | 	ID   string `json:"id"`
 40 | 	Name string `json:"name"`
 41 | 	Key  string `json:"key"`
 42 | }
 43 | 
 44 | // Project represents a Linear project
 45 | type Project struct {
 46 | 	ID          string `json:"id"`
 47 | 	Name        string `json:"name"`
 48 | 	Description string `json:"description"`
 49 | 	SlugID      string `json:"slugId"`
 50 | 	State       string `json:"state"`
 51 | 	Creator     *User  `json:"creator,omitempty"`
 52 | 	Lead        *User  `json:"lead,omitempty"`
 53 | 	// Members     *UserConnection `json:"members,omitempty"`
 54 | 	// Teams       *TeamConnection `json:"teams,omitempty"`
 55 | 	Initiatives *InitiativeConnection `json:"initiatives,omitempty"`
 56 | 	StartDate   *string               `json:"startDate,omitempty"`
 57 | 	TargetDate  *string               `json:"targetDate,omitempty"`
 58 | 	Color       string                `json:"color"`
 59 | 	Icon        string                `json:"icon,omitempty"`
 60 | 	URL         string                `json:"url"`
 61 | }
 62 | 
 63 | // ProjectConnection represents a connection of projects
 64 | type ProjectConnection struct {
 65 | 	Nodes []Project `json:"nodes"`
 66 | }
 67 | 
 68 | // ProjectMilestoneConnection represents a connection of project milestones.
 69 | type ProjectMilestoneConnection struct {
 70 | 	Nodes []ProjectMilestone `json:"nodes"`
 71 | }
 72 | 
 73 | // ProjectMilestone represents a Linear project milestone
 74 | type ProjectMilestone struct {
 75 | 	ID          string   `json:"id"`
 76 | 	Name        string   `json:"name"`
 77 | 	Description string   `json:"description,omitempty"`
 78 | 	TargetDate  *string  `json:"targetDate,omitempty"`
 79 | 	Project     *Project `json:"project,omitempty"`
 80 | 	SortOrder   float64  `json:"sortOrder"`
 81 | }
 82 | 
 83 | // InitiativeConnection represents a connection of initiatives.
 84 | type InitiativeConnection struct {
 85 | 	Nodes []Initiative `json:"nodes"`
 86 | }
 87 | 
 88 | // Initiative represents a Linear initiative
 89 | type Initiative struct {
 90 | 	ID          string `json:"id"`
 91 | 	Name        string `json:"name"`
 92 | 	Description string `json:"description,omitempty"`
 93 | 	Owner       *User  `json:"owner,omitempty"`
 94 | 	Color       string `json:"color,omitempty"`
 95 | 	Icon        string `json:"icon,omitempty"`
 96 | 	SlugID      string `json:"slugId"`
 97 | 	URL         string `json:"url"`
 98 | }
 99 | 
100 | // State represents a workflow state in Linear
101 | type State struct {
102 | 	ID   string `json:"id"`
103 | 	Name string `json:"name"`
104 | }
105 | 
106 | // LabelConnection represents a connection of labels
107 | type LabelConnection struct {
108 | 	Nodes []Label `json:"nodes"`
109 | }
110 | 
111 | // Label represents a Linear issue label
112 | type Label struct {
113 | 	ID   string `json:"id"`
114 | 	Name string `json:"name"`
115 | }
116 | 
117 | // CommentConnection represents a connection of comments
118 | type CommentConnection struct {
119 | 	Nodes []Comment `json:"nodes"`
120 | }
121 | 
122 | // PageInfo represents pagination information
123 | type PageInfo struct {
124 | 	HasNextPage bool   `json:"hasNextPage"`
125 | 	EndCursor   string `json:"endCursor"`
126 | }
127 | 
128 | // PaginatedCommentConnection represents a paginated connection of comments
129 | type PaginatedCommentConnection struct {
130 | 	Nodes    []Comment `json:"nodes"`
131 | 	PageInfo PageInfo  `json:"pageInfo"`
132 | }
133 | 
134 | // Comment represents a comment on a Linear issue
135 | type Comment struct {
136 | 	ID        string             `json:"id"`
137 | 	Body      string             `json:"body"`
138 | 	User      *User              `json:"user,omitempty"`
139 | 	CreatedAt time.Time          `json:"createdAt"`
140 | 	URL       string             `json:"url,omitempty"`
141 | 	Parent    *Comment           `json:"parent,omitempty"`
142 | 	Children  *CommentConnection `json:"children,omitempty"`
143 | 	Issue     *Issue             `json:"issue,omitempty"`
144 | }
145 | 
146 | // IssueRelationConnection represents a connection of issue relations
147 | type IssueRelationConnection struct {
148 | 	Nodes []IssueRelation `json:"nodes"`
149 | }
150 | 
151 | // IssueRelation represents a relation between two issues
152 | type IssueRelation struct {
153 | 	ID           string `json:"id"`
154 | 	Type         string `json:"type"`
155 | 	RelatedIssue *Issue `json:"relatedIssue,omitempty"`
156 | 	Issue        *Issue `json:"issue,omitempty"`
157 | }
158 | 
159 | // AttachmentConnection represents a connection of attachments
160 | type AttachmentConnection struct {
161 | 	Nodes []Attachment `json:"nodes"`
162 | }
163 | 
164 | // Attachment represents an external resource linked to an issue
165 | type Attachment struct {
166 | 	ID         string                 `json:"id"`
167 | 	Title      string                 `json:"title"`
168 | 	Subtitle   string                 `json:"subtitle,omitempty"`
169 | 	URL        string                 `json:"url"`
170 | 	SourceType string                 `json:"sourceType,omitempty"`
171 | 	Metadata   map[string]interface{} `json:"metadata,omitempty"`
172 | 	CreatedAt  time.Time              `json:"createdAt"`
173 | }
174 | 
175 | // Organization represents a Linear organization
176 | type Organization struct {
177 | 	ID     string `json:"id"`
178 | 	Name   string `json:"name"`
179 | 	URLKey string `json:"urlKey"`
180 | 	Teams  []Team `json:"teams,omitempty"`
181 | 	Users  []User `json:"users,omitempty"`
182 | }
183 | 
184 | // LinearIssueResponse represents a simplified issue response
185 | type LinearIssueResponse struct {
186 | 	ID               string            `json:"id"`
187 | 	Identifier       string            `json:"identifier"`
188 | 	Title            string            `json:"title"`
189 | 	Priority         int               `json:"priority"`
190 | 	Status           string            `json:"status,omitempty"`
191 | 	StateName        string            `json:"stateName,omitempty"`
192 | 	URL              string            `json:"url"`
193 | 	Project          *Project          `json:"project,omitempty"`
194 | 	ProjectMilestone *ProjectMilestone `json:"projectMilestone,omitempty"`
195 | }
196 | 
197 | // APIMetrics represents metrics about API usage
198 | type APIMetrics struct {
199 | 	RequestsInLastHour int    `json:"requestsInLastHour"`
200 | 	RemainingRequests  int    `json:"remainingRequests"`
201 | 	AverageRequestTime string `json:"averageRequestTime"`
202 | 	QueueLength        int    `json:"queueLength"`
203 | 	LastRequestTime    string `json:"lastRequestTime"`
204 | }
205 | 
206 | // CreateIssueInput represents input for creating an issue
207 | type CreateIssueInput struct {
208 | 	Title       string   `json:"title"`
209 | 	TeamID      string   `json:"teamId"`
210 | 	Description string   `json:"description,omitempty"`
211 | 	Priority    *int     `json:"priority,omitempty"`
212 | 	Status      string   `json:"status,omitempty"`
213 | 	ParentID    *string  `json:"parentId,omitempty"`
214 | 	LabelIDs    []string `json:"labelIds,omitempty"`
215 | 	ProjectID   string   `json:"projectId,omitempty"`
216 | }
217 | 
218 | // UpdateIssueInput represents input for updating an issue
219 | type UpdateIssueInput struct {
220 | 	ID          string `json:"id"`
221 | 	Title       string `json:"title,omitempty"`
222 | 	Description string `json:"description,omitempty"`
223 | 	Priority    *int   `json:"priority,omitempty"`
224 | 	Status      string `json:"status,omitempty"`
225 | 	TeamID      string `json:"teamId,omitempty"`
226 | 	ProjectID   string `json:"projectId,omitempty"`
227 | 	MilestoneID string `json:"milestoneId,omitempty"`
228 | }
229 | 
230 | // SearchIssuesInput represents input for searching issues
231 | type SearchIssuesInput struct {
232 | 	Query           string   `json:"query,omitempty"`
233 | 	TeamID          string   `json:"teamId,omitempty"`
234 | 	Status          string   `json:"status,omitempty"`
235 | 	AssigneeID      string   `json:"assigneeId,omitempty"`
236 | 	Labels          []string `json:"labels,omitempty"`
237 | 	Priority        *int     `json:"priority,omitempty"`
238 | 	Estimate        *float64 `json:"estimate,omitempty"`
239 | 	IncludeArchived bool     `json:"includeArchived,omitempty"`
240 | 	Limit           int      `json:"limit,omitempty"`
241 | }
242 | 
243 | // GetUserIssuesInput represents input for getting user issues
244 | type GetUserIssuesInput struct {
245 | 	UserID          string `json:"userId,omitempty"`
246 | 	IncludeArchived bool   `json:"includeArchived,omitempty"`
247 | 	Limit           int    `json:"limit,omitempty"`
248 | }
249 | 
250 | // GetIssueCommentsInput represents input for getting issue comments
251 | type GetIssueCommentsInput struct {
252 | 	IssueID     string `json:"issueId"`
253 | 	ParentID    string `json:"parentId,omitempty"`
254 | 	Limit       int    `json:"limit,omitempty"`
255 | 	AfterCursor string `json:"afterCursor,omitempty"`
256 | }
257 | 
258 | // AddCommentInput represents input for adding a comment
259 | type AddCommentInput struct {
260 | 	IssueID      string `json:"issueId"`
261 | 	Body         string `json:"body"`
262 | 	CreateAsUser string `json:"createAsUser,omitempty"`
263 | 	ParentID     string `json:"parentId,omitempty"`
264 | }
265 | 
266 | // UpdateCommentInput represents the input for updating a comment
267 | type UpdateCommentInput struct {
268 | 	CommentID string `json:"commentId"`
269 | 	Body      string `json:"body"`
270 | }
271 | 
272 | // ProjectCreateInput represents the input for creating a project.
273 | type ProjectCreateInput struct {
274 | 	Name        string   `json:"name"`
275 | 	TeamIDs     []string `json:"teamIds"`
276 | 	Description string   `json:"description,omitempty"`
277 | 	LeadID      string   `json:"leadId,omitempty"`
278 | 	StartDate   string   `json:"startDate,omitempty"`
279 | 	TargetDate  string   `json:"targetDate,omitempty"`
280 | }
281 | 
282 | // ProjectUpdateInput represents the input for updating a project.
283 | type ProjectUpdateInput struct {
284 | 	Name        string   `json:"name,omitempty"`
285 | 	Description string   `json:"description,omitempty"`
286 | 	LeadID      string   `json:"leadId,omitempty"`
287 | 	StartDate   string   `json:"startDate,omitempty"`
288 | 	TargetDate  string   `json:"targetDate,omitempty"`
289 | 	TeamIDs     []string `json:"teamIds,omitempty"`
290 | }
291 | 
292 | // ProjectMilestoneCreateInput represents the input for creating a project milestone.
293 | type ProjectMilestoneCreateInput struct {
294 | 	Name        string `json:"name"`
295 | 	ProjectID   string `json:"projectId"`
296 | 	Description string `json:"description,omitempty"`
297 | 	TargetDate  string `json:"targetDate,omitempty"`
298 | }
299 | 
300 | // ProjectMilestoneUpdateInput represents the input for updating a project milestone.
301 | type ProjectMilestoneUpdateInput struct {
302 | 	Name        string `json:"name,omitempty"`
303 | 	Description string `json:"description,omitempty"`
304 | 	TargetDate  string `json:"targetDate,omitempty"`
305 | }
306 | 
307 | // InitiativeCreateInput represents the input for creating an initiative.
308 | type InitiativeCreateInput struct {
309 | 	Name        string `json:"name"`
310 | 	Description string `json:"description,omitempty"`
311 | }
312 | 
313 | // InitiativeUpdateInput represents the input for updating an initiative.
314 | type InitiativeUpdateInput struct {
315 | 	Name        string `json:"name,omitempty"`
316 | 	Description string `json:"description,omitempty"`
317 | }
318 | 
319 | // GraphQLRequest represents a GraphQL request
320 | type GraphQLRequest struct {
321 | 	Query     string                 `json:"query"`
322 | 	Variables map[string]interface{} `json:"variables,omitempty"`
323 | }
324 | 
325 | // GraphQLResponse represents a GraphQL response
326 | type GraphQLResponse struct {
327 | 	Data   map[string]interface{} `json:"data,omitempty"`
328 | 	Errors []GraphQLError         `json:"errors,omitempty"`
329 | }
330 | 
331 | // GraphQLError represents a GraphQL error
332 | type GraphQLError struct {
333 | 	Message string `json:"message"`
334 | }
335 | 
```

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

```markdown
  1 | # Sample Implementation
  2 | 
  3 | This document provides sample implementations for the key components of the tool standardization effort. These samples can be used as references when implementing the actual changes.
  4 | 
  5 | ## 1. Shared Utility Functions
  6 | 
  7 | ### rendering.go
  8 | 
  9 | ```go
 10 | package tools
 11 | 
 12 | import (
 13 | 	"fmt"
 14 | 	"strings"
 15 | 
 16 | 	"github.com/geropl/linear-mcp-go/pkg/linear"
 17 | )
 18 | 
 19 | // Full Entity Rendering Functions
 20 | 
 21 | // formatIssue returns a consistently formatted full representation of an issue
 22 | func formatIssue(issue *linear.Issue) string {
 23 | 	if issue == nil {
 24 | 		return "Issue: Unknown"
 25 | 	}
 26 | 	
 27 | 	var result strings.Builder
 28 | 	result.WriteString(fmt.Sprintf("Issue: %s (UUID: %s)\n", issue.Identifier, issue.ID))
 29 | 	result.WriteString(fmt.Sprintf("Title: %s\n", issue.Title))
 30 | 	result.WriteString(fmt.Sprintf("URL: %s\n", issue.URL))
 31 | 	
 32 | 	if issue.Description != "" {
 33 | 		result.WriteString(fmt.Sprintf("Description: %s\n", issue.Description))
 34 | 	}
 35 | 	
 36 | 	priorityStr := "None"
 37 | 	if issue.Priority > 0 {
 38 | 		priorityStr = fmt.Sprintf("%d", issue.Priority)
 39 | 	}
 40 | 	result.WriteString(fmt.Sprintf("Priority: %s\n", priorityStr))
 41 | 	
 42 | 	statusStr := "None"
 43 | 	if issue.Status != "" {
 44 | 		statusStr = issue.Status
 45 | 	} else if issue.State != nil {
 46 | 		statusStr = issue.State.Name
 47 | 	}
 48 | 	result.WriteString(fmt.Sprintf("Status: %s\n", statusStr))
 49 | 	
 50 | 	if issue.Assignee != nil {
 51 | 		result.WriteString(fmt.Sprintf("Assignee: %s\n", formatUserIdentifier(issue.Assignee)))
 52 | 	}
 53 | 	
 54 | 	if issue.Team != nil {
 55 | 		result.WriteString(fmt.Sprintf("Team: %s\n", formatTeamIdentifier(issue.Team)))
 56 | 	}
 57 | 	
 58 | 	return result.String()
 59 | }
 60 | 
 61 | // formatTeam returns a consistently formatted full representation of a team
 62 | func formatTeam(team *linear.Team) string {
 63 | 	if team == nil {
 64 | 		return "Team: Unknown"
 65 | 	}
 66 | 	
 67 | 	var result strings.Builder
 68 | 	result.WriteString(fmt.Sprintf("Team: %s (UUID: %s)\n", team.Name, team.ID))
 69 | 	result.WriteString(fmt.Sprintf("Key: %s\n", team.Key))
 70 | 	
 71 | 	return result.String()
 72 | }
 73 | 
 74 | // formatUser returns a consistently formatted full representation of a user
 75 | func formatUser(user *linear.User) string {
 76 | 	if user == nil {
 77 | 		return "User: Unknown"
 78 | 	}
 79 | 	
 80 | 	var result strings.Builder
 81 | 	result.WriteString(fmt.Sprintf("User: %s (UUID: %s)\n", user.Name, user.ID))
 82 | 	
 83 | 	if user.Email != "" {
 84 | 		result.WriteString(fmt.Sprintf("Email: %s\n", user.Email))
 85 | 	}
 86 | 	
 87 | 	return result.String()
 88 | }
 89 | 
 90 | // formatComment returns a consistently formatted full representation of a comment
 91 | func formatComment(comment *linear.Comment) string {
 92 | 	if comment == nil {
 93 | 		return "Comment: Unknown"
 94 | 	}
 95 | 	
 96 | 	userName := "Unknown"
 97 | 	if comment.User != nil {
 98 | 		userName = comment.User.Name
 99 | 	}
100 | 	
101 | 	var result strings.Builder
102 | 	result.WriteString(fmt.Sprintf("Comment by %s (UUID: %s)\n", userName, comment.ID))
103 | 	result.WriteString(fmt.Sprintf("Body: %s\n", comment.Body))
104 | 	
105 | 	if comment.CreatedAt != nil {
106 | 		result.WriteString(fmt.Sprintf("Created: %s\n", comment.CreatedAt.Format("2006-01-02 15:04:05")))
107 | 	}
108 | 	
109 | 	return result.String()
110 | }
111 | 
112 | // Entity Identifier Rendering Functions
113 | 
114 | // formatIssueIdentifier returns a consistently formatted identifier for an issue
115 | func formatIssueIdentifier(issue *linear.Issue) string {
116 | 	if issue == nil {
117 | 		return "Issue: Unknown"
118 | 	}
119 | 	return fmt.Sprintf("Issue: %s (UUID: %s)", issue.Identifier, issue.ID)
120 | }
121 | 
122 | // formatTeamIdentifier returns a consistently formatted identifier for a team
123 | func formatTeamIdentifier(team *linear.Team) string {
124 | 	if team == nil {
125 | 		return "Team: Unknown"
126 | 	}
127 | 	return fmt.Sprintf("Team: %s (UUID: %s)", team.Name, team.ID)
128 | }
129 | 
130 | // formatUserIdentifier returns a consistently formatted identifier for a user
131 | func formatUserIdentifier(user *linear.User) string {
132 | 	if user == nil {
133 | 		return "User: Unknown"
134 | 	}
135 | 	return fmt.Sprintf("User: %s (UUID: %s)", user.Name, user.ID)
136 | }
137 | 
138 | // formatCommentIdentifier returns a consistently formatted identifier for a comment
139 | func formatCommentIdentifier(comment *linear.Comment) string {
140 | 	if comment == nil {
141 | 		return "Comment: Unknown"
142 | 	}
143 | 	
144 | 	userName := "Unknown"
145 | 	if comment.User != nil {
146 | 		userName = comment.User.Name
147 | 	}
148 | 	
149 | 	return fmt.Sprintf("Comment by %s (UUID: %s)", userName, comment.ID)
150 | }
151 | ```
152 | 
153 | ### Updated common.go
154 | 
155 | ```go
156 | // resolveIssueIdentifier resolves an issue identifier (UUID or "TEAM-123") to a UUID
157 | func resolveIssueIdentifier(linearClient *linear.LinearClient, identifier string) (string, error) {
158 | 	// If it's a valid UUID, use it directly
159 | 	if isValidUUID(identifier) {
160 | 		return identifier, nil
161 | 	}
162 | 
163 | 	// Otherwise, try to find an issue by identifier
164 | 	issue, err := linearClient.GetIssueByIdentifier(identifier)
165 | 	if err != nil {
166 | 		return "", fmt.Errorf("failed to resolve issue identifier '%s': %v", identifier, err)
167 | 	}
168 | 
169 | 	return issue.ID, nil
170 | }
171 | 
172 | // resolveUserIdentifier resolves a user identifier (UUID, name, or email) to a UUID
173 | func resolveUserIdentifier(linearClient *linear.LinearClient, identifier string) (string, error) {
174 | 	// If it's a valid UUID, use it directly
175 | 	if isValidUUID(identifier) {
176 | 		return identifier, nil
177 | 	}
178 | 
179 | 	// Otherwise, try to find a user by name or email
180 | 	users, err := linearClient.GetUsers()
181 | 	if err != nil {
182 | 		return "", fmt.Errorf("failed to get users: %v", err)
183 | 	}
184 | 
185 | 	// First try exact match on name or email
186 | 	for _, user := range users {
187 | 		if user.Name == identifier || user.Email == identifier {
188 | 			return user.ID, nil
189 | 		}
190 | 	}
191 | 
192 | 	// If no exact match, try case-insensitive match
193 | 	identifierLower := strings.ToLower(identifier)
194 | 	for _, user := range users {
195 | 		if strings.ToLower(user.Name) == identifierLower || strings.ToLower(user.Email) == identifierLower {
196 | 			return user.ID, nil
197 | 		}
198 | 	}
199 | 
200 | 	return "", fmt.Errorf("no user found with identifier '%s'", identifier)
201 | }
202 | ```
203 | 
204 | ## 2. Updated Tool Examples
205 | 
206 | ### linear_create_issue
207 | 
208 | ```go
209 | // Before
210 | var CreateIssueTool = mcp.NewTool("linear_create_issue",
211 | 	mcp.WithDescription("Creates a new Linear issue with specified details. Use this to create tickets for tasks, bugs, or feature requests. Returns the created issue's identifier and URL. Supports creating sub-issues and assigning labels."),
212 | 	// ... parameters ...
213 | )
214 | 
215 | // After
216 | var CreateIssueTool = mcp.NewTool("linear_create_issue",
217 | 	mcp.WithDescription("Creates a new Linear issue."),
218 | 	// ... parameters ...
219 | )
220 | 
221 | // Before (result formatting)
222 | resultText := fmt.Sprintf("Created issue: %s\nTitle: %s\nURL: %s", issue.Identifier, issue.Title, issue.URL)
223 | 
224 | // After (result formatting)
225 | resultText := fmt.Sprintf("Created %s\nTitle: %s\nURL: %s", formatIssue(issue), issue.Title, issue.URL)
226 | ```
227 | 
228 | ### linear_update_issue
229 | 
230 | ```go
231 | // Before
232 | var UpdateIssueTool = mcp.NewTool("linear_update_issue",
233 | 	mcp.WithDescription("Updates an existing Linear issue's properties. Use this to modify issue details like title, description, priority, or status. Requires the issue ID and accepts any combination of updatable fields. Returns the updated issue's identifier and URL."),
234 | 	// ... parameters ...
235 | )
236 | 
237 | // After
238 | var UpdateIssueTool = mcp.NewTool("linear_update_issue",
239 | 	mcp.WithDescription("Updates an existing Linear issue."),
240 | 	// ... parameters ...
241 | )
242 | 
243 | // Before (parameter handling)
244 | id, ok := args["id"].(string)
245 | if !ok || id == "" {
246 | 	return mcp.NewToolResultError("id must be a non-empty string"), nil
247 | }
248 | 
249 | // After (parameter handling)
250 | issueID, ok := args["id"].(string)
251 | if !ok || issueID == "" {
252 | 	return mcp.NewToolResultError("id must be a non-empty string"), nil
253 | }
254 | 
255 | // Resolve the issue identifier
256 | id, err := resolveIssueIdentifier(linearClient, issueID)
257 | if err != nil {
258 | 	return mcp.NewToolResultError(fmt.Sprintf("Failed to resolve issue: %v", err)), nil
259 | }
260 | 
261 | // Before (result formatting)
262 | resultText := fmt.Sprintf("Updated issue %s\nURL: %s", issue.Identifier, issue.URL)
263 | 
264 | // After (result formatting)
265 | resultText := fmt.Sprintf("Updated %s\nURL: %s", formatIssue(issue), issue.URL)
266 | ```
267 | 
268 | ### linear_get_issue
269 | 
270 | ```go
271 | // Before
272 | var GetIssueTool = mcp.NewTool("linear_get_issue",
273 | 	mcp.WithDescription("Retrieves a single Linear issue by its ID. Returns detailed information about the issue including title, description, priority, status, assignee, team, full comment history (including nested comments), related issues, and all attachments (pull requests, design files, documents, etc.)."),
274 | 	// ... parameters ...
275 | )
276 | 
277 | // After
278 | var GetIssueTool = mcp.NewTool("linear_get_issue",
279 | 	mcp.WithDescription("Retrieves a single Linear issue."),
280 | 	// ... parameters ...
281 | )
282 | 
283 | // Before (parameter handling)
284 | issueID, ok := args["issueId"].(string)
285 | if !ok || issueID == "" {
286 | 	return mcp.NewToolResultError("issueId must be a non-empty string"), nil
287 | }
288 | 
289 | // After (parameter handling)
290 | issueIdentifier, ok := args["issueId"].(string)
291 | if !ok || issueIdentifier == "" {
292 | 	return mcp.NewToolResultError("issueId must be a non-empty string"), nil
293 | }
294 | 
295 | // Resolve the issue identifier
296 | issueID, err := resolveIssueIdentifier(linearClient, issueIdentifier)
297 | if err != nil {
298 | 	return mcp.NewToolResultError(fmt.Sprintf("Failed to resolve issue: %v", err)), nil
299 | }
300 | 
301 | // Before (result formatting)
302 | resultText := fmt.Sprintf("Issue %s: %s\n", issue.Identifier, issue.Title)
303 | // ... more formatting ...
304 | 
305 | // After (result formatting)
306 | // Use the full formatIssue function for the main entity
307 | resultText := formatIssue(issue)
308 | 
309 | // When referencing related entities, use the identifier formatting functions
310 | if issue.Assignee != nil {
311 |     resultText += fmt.Sprintf("Assignee: %s\n", formatUserIdentifier(issue.Assignee))
312 | }
313 | 
314 | if issue.Team != nil {
315 |     resultText += fmt.Sprintf("Team: %s\n", formatTeamIdentifier(issue.Team))
316 | }
317 | 
318 | // For related issues, use the identifier formatting
319 | if issue.Relations != nil && len(issue.Relations.Nodes) > 0 {
320 |     resultText += "\nRelated Issues:\n"
321 |     for _, relation := range issue.Relations.Nodes {
322 |         if relation.RelatedIssue != nil {
323 |             resultText += fmt.Sprintf("- %s\n  RelationType: %s\n", 
324 |                 formatIssueIdentifier(relation.RelatedIssue),
325 |                 relation.Type)
326 |         }
327 |     }
328 | }
329 | ```
330 | 
331 | ## 3. Testing Examples
332 | 
333 | ### Test for resolveIssueIdentifier
334 | 
335 | ```go
336 | func TestResolveIssueIdentifier(t *testing.T) {
337 | 	// Create test client
338 | 	client, cleanup := linear.NewTestClient(t, "resolve_issue_identifier", true)
339 | 	defer cleanup()
340 | 
341 | 	// Test cases
342 | 	tests := []struct {
343 | 		name       string
344 | 		identifier string
345 | 		wantErr    bool
346 | 	}{
347 | 		{
348 | 			name:       "Valid UUID",
349 | 			identifier: "1c2de93f-4321-4015-bfde-ee893ef7976f",
350 | 			wantErr:    false,
351 | 		},
352 | 		{
353 | 			name:       "Valid identifier",
354 | 			identifier: "TEST-10",
355 | 			wantErr:    false,
356 | 		},
357 | 		{
358 | 			name:       "Invalid identifier",
359 | 			identifier: "NONEXISTENT-123",
360 | 			wantErr:    true,
361 | 		},
362 | 	}
363 | 
364 | 	for _, tt := range tests {
365 | 		t.Run(tt.name, func(t *testing.T) {
366 | 			got, err := resolveIssueIdentifier(client, tt.identifier)
367 | 			if (err != nil) != tt.wantErr {
368 | 				t.Errorf("resolveIssueIdentifier() error = %v, wantErr %v", err, tt.wantErr)
369 | 				return
370 | 			}
371 | 			if !tt.wantErr && got == "" {
372 | 				t.Errorf("resolveIssueIdentifier() returned empty UUID")
373 | 			}
374 | 		})
375 | 	}
376 | }
377 | ```
378 | 
379 | ### Test for Formatting Functions
380 | 
381 | ```go
382 | func TestFormatIssue(t *testing.T) {
383 | 	tests := []struct {
384 | 		name  string
385 | 		issue *linear.Issue
386 | 		want  string
387 | 	}{
388 | 		{
389 | 			name: "Valid issue",
390 | 			issue: &linear.Issue{
391 | 				ID:         "1c2de93f-4321-4015-bfde-ee893ef7976f",
392 | 				Identifier: "TEST-10",
393 | 			},
394 | 			want: "Issue: TEST-10 (UUID: 1c2de93f-4321-4015-bfde-ee893ef7976f)",
395 | 		},
396 | 		{
397 | 			name:  "Nil issue",
398 | 			issue: nil,
399 | 			want:  "Issue: Unknown",
400 | 		},
401 | 	}
402 | 
403 | 	for _, tt := range tests {
404 | 		t.Run(tt.name, func(t *testing.T) {
405 | 			if got := formatIssue(tt.issue); got != tt.want {
406 | 				t.Errorf("formatIssue() = %v, want %v", got, tt.want)
407 | 			}
408 | 		})
409 | 	}
410 | }
411 | ```
412 | 
413 | ## 4. Implementation Strategy
414 | 
415 | 1. Start with creating the shared utility functions in `rendering.go` and updating `common.go`
416 | 2. Implement the changes for one tool (e.g., `linear_create_issue`) as a reference
417 | 3. Review the reference implementation to ensure it meets all requirements
418 | 4. Apply the same patterns to all remaining tools
419 | 5. Update tests to verify the changes
420 | 
421 | ## 5. Potential Challenges and Solutions
422 | 
423 | ### Challenge: Backward Compatibility
424 | Changing the output format of tools could break existing integrations that parse the output.
425 | 
426 | **Solution:** Consider versioning the API or providing a compatibility mode.
427 | 
428 | ### Challenge: Test Fixtures
429 | Updating the output format will require updating all test fixtures.
430 | 
431 | **Solution:** Use the `--golden` flag to update all golden files at once after implementing the changes.
432 | 
433 | ### Challenge: Consistent Implementation
434 | Ensuring consistency across all tools can be challenging.
435 | 
436 | **Solution:** Create a code review checklist to verify that each tool follows the same patterns.
437 | 
```

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

```markdown
  1 | # Tool Standardization Implementation Guide
  2 | 
  3 | This document provides a detailed implementation plan for standardizing the Linear MCP Server tools according to the requirements outlined in [002-tool-standardization.md](./002-tool-standardization.md).
  4 | 
  5 | ## Implementation Approach
  6 | 
  7 | We'll implement the standardization in phases, focusing on one rule at a time across all tools to ensure consistency:
  8 | 
  9 | 1. First, create the necessary shared utility functions
 10 | 2. Then, update each tool one by one, applying all three rules
 11 | 3. Finally, update tests to verify the changes
 12 | 
 13 | ## Shared Utility Functions
 14 | 
 15 | ### 1. Identifier Resolution Functions
 16 | 
 17 | Create or update the following functions in `pkg/tools/common.go`:
 18 | 
 19 | ```go
 20 | // resolveIssueIdentifier resolves an issue identifier (UUID or "TEAM-123") to a UUID
 21 | // This is an extension of the existing resolveParentIssueIdentifier function
 22 | func resolveIssueIdentifier(linearClient *linear.LinearClient, identifier string) (string, error) {
 23 |     // Implementation similar to resolveParentIssueIdentifier
 24 | }
 25 | 
 26 | // resolveUserIdentifier resolves a user identifier (UUID, name, or email) to a UUID
 27 | func resolveUserIdentifier(linearClient *linear.LinearClient, identifier string) (string, error) {
 28 |     // Implementation
 29 | }
 30 | ```
 31 | 
 32 | ### 2. Entity Rendering Functions
 33 | 
 34 | Create a new file `pkg/tools/rendering.go` with two types of formatting functions:
 35 | 
 36 | #### Full Entity Rendering Functions
 37 | 
 38 | ```go
 39 | // formatIssue returns a consistently formatted full representation of an issue
 40 | func formatIssue(issue *linear.Issue) string {
 41 |     if issue == nil {
 42 |         return "Issue: Unknown"
 43 |     }
 44 |     
 45 |     var result strings.Builder
 46 |     result.WriteString(fmt.Sprintf("Issue: %s (UUID: %s)\n", issue.Identifier, issue.ID))
 47 |     result.WriteString(fmt.Sprintf("Title: %s\n", issue.Title))
 48 |     result.WriteString(fmt.Sprintf("URL: %s\n", issue.URL))
 49 |     
 50 |     // Add other required fields
 51 |     
 52 |     return result.String()
 53 | }
 54 | 
 55 | // formatTeam returns a consistently formatted full representation of a team
 56 | func formatTeam(team *linear.Team) string {
 57 |     if team == nil {
 58 |         return "Team: Unknown"
 59 |     }
 60 |     
 61 |     var result strings.Builder
 62 |     result.WriteString(fmt.Sprintf("Team: %s (UUID: %s)\n", team.Name, team.ID))
 63 |     result.WriteString(fmt.Sprintf("Key: %s\n", team.Key))
 64 |     
 65 |     // Add other required fields
 66 |     
 67 |     return result.String()
 68 | }
 69 | 
 70 | // formatUser returns a consistently formatted full representation of a user
 71 | func formatUser(user *linear.User) string {
 72 |     if user == nil {
 73 |         return "User: Unknown"
 74 |     }
 75 |     
 76 |     var result strings.Builder
 77 |     result.WriteString(fmt.Sprintf("User: %s (UUID: %s)\n", user.Name, user.ID))
 78 |     
 79 |     // Add other required fields
 80 |     
 81 |     return result.String()
 82 | }
 83 | 
 84 | // formatComment returns a consistently formatted full representation of a comment
 85 | func formatComment(comment *linear.Comment) string {
 86 |     if comment == nil {
 87 |         return "Comment: Unknown"
 88 |     }
 89 |     
 90 |     userName := "Unknown"
 91 |     if comment.User != nil {
 92 |         userName = comment.User.Name
 93 |     }
 94 |     
 95 |     var result strings.Builder
 96 |     result.WriteString(fmt.Sprintf("Comment by %s (UUID: %s)\n", userName, comment.ID))
 97 |     result.WriteString(fmt.Sprintf("Body: %s\n", comment.Body))
 98 |     
 99 |     // Add other required fields
100 |     
101 |     return result.String()
102 | }
103 | ```
104 | 
105 | #### Entity Identifier Rendering Functions
106 | 
107 | ```go
108 | // formatIssueIdentifier returns a consistently formatted identifier for an issue
109 | func formatIssueIdentifier(issue *linear.Issue) string {
110 |     if issue == nil {
111 |         return "Issue: Unknown"
112 |     }
113 |     return fmt.Sprintf("Issue: %s (UUID: %s)", issue.Identifier, issue.ID)
114 | }
115 | 
116 | // formatTeamIdentifier returns a consistently formatted identifier for a team
117 | func formatTeamIdentifier(team *linear.Team) string {
118 |     if team == nil {
119 |         return "Team: Unknown"
120 |     }
121 |     return fmt.Sprintf("Team: %s (UUID: %s)", team.Name, team.ID)
122 | }
123 | 
124 | // formatUserIdentifier returns a consistently formatted identifier for a user
125 | func formatUserIdentifier(user *linear.User) string {
126 |     if user == nil {
127 |         return "User: Unknown"
128 |     }
129 |     return fmt.Sprintf("User: %s (UUID: %s)", user.Name, user.ID)
130 | }
131 | 
132 | // formatCommentIdentifier returns a consistently formatted identifier for a comment
133 | func formatCommentIdentifier(comment *linear.Comment) string {
134 |     if comment == nil {
135 |         return "Comment: Unknown"
136 |     }
137 |     
138 |     userName := "Unknown"
139 |     if comment.User != nil {
140 |         userName = comment.User.Name
141 |     }
142 |     
143 |     return fmt.Sprintf("Comment by %s (UUID: %s)", userName, comment.ID)
144 | }
145 | ```
146 | 
147 | ## Detailed Implementation Tasks
148 | 
149 | ### Phase 1: Create Shared Utility Functions
150 | 
151 | 1. Update `pkg/tools/common.go`:
152 |    - Refactor `resolveParentIssueIdentifier` to `resolveIssueIdentifier`
153 |    - Add `resolveUserIdentifier`
154 |    - Ensure all resolution functions follow the same pattern
155 | 
156 | 2. Create `pkg/tools/rendering.go`:
157 |    - Add formatting functions for each entity type
158 |    - Ensure consistent formatting across all entity types
159 | 
160 | ### Phase 2: Update Tools
161 | 
162 | For each tool, perform the following tasks:
163 | 
164 | 1. Update the tool description to be concise
165 | 2. Update parameter handling to use the appropriate resolution functions
166 | 3. Update result formatting to use the rendering functions
167 | 4. Ensure retrieval methods include all fields that can be set in create/update methods
168 | 
169 | #### Implementing Rule 4: Field Superset for Retrieval Methods
170 | 
171 | For each entity type, follow these steps:
172 | 
173 | 1. **Identify Modifiable Fields**:
174 |    - Review all create and update methods to identify fields that can be set or modified
175 |    - Create a comprehensive list of these fields for each entity type
176 | 
177 | 2. **Categorize Retrieval Methods**:
178 |    - **Detail Retrieval Methods** (e.g., `linear_get_issue`): Must include all fields
179 |    - **Overview Retrieval Methods** (e.g., `linear_search_issues`, `linear_get_user_issues`): Only need metadata fields
180 | 
181 | 3. **Update Detail Retrieval Methods**:
182 |    - Ensure they include all fields that can be set in create/update methods
183 |    - Modify the formatting functions to include all required fields
184 | 
185 | 4. **Update Overview Retrieval Methods**:
186 |    - Ensure they include key metadata fields (ID, title, status, priority, etc.)
187 |    - No need to include full content like descriptions or comments
188 | 
189 | 5. **Entity-Specific Considerations**:
190 |    - **Issues**: 
191 |      - `linear_get_issue` must include all fields from `linear_create_issue` and `linear_update_issue`
192 |      - `linear_search_issues` and `linear_get_user_issues` only need metadata fields
193 |    - **Comments**: 
194 |      - Comments returned in `linear_get_issue` must include all necessary fields from `linear_add_comment`
195 |      - Overview methods don't need to display comments
196 |    - **Teams**: 
197 |      - `linear_get_teams` should include all team fields that can be referenced
198 | 
199 | #### Tool-Specific Tasks
200 | 
201 | | Tool | Description Update | Identifier Resolution Update | Rendering Update |
202 | |------|-------------------|----------------------------|-----------------|
203 | | linear_create_issue | Remove parameter listing and result format explanation | Already uses resolution functions | Use formatIssue for result |
204 | | linear_update_issue | Remove parameter listing and result format explanation | Update to use resolveIssueIdentifier | Use formatIssue for result |
205 | | linear_search_issues | Remove parameter listing and result format explanation | Update to use resolveTeamIdentifier for teamId | Use formatIssue for each issue in results |
206 | | linear_get_user_issues | Remove parameter listing and result format explanation | Add resolveUserIdentifier for userId | Use formatIssue for each issue in results |
207 | | linear_get_issue | Remove parameter listing and result format explanation | Update to use resolveIssueIdentifier | Use formatIssue, formatTeam, formatUser, and formatComment |
208 | | linear_add_comment | Remove parameter listing and result format explanation | Update to use resolveIssueIdentifier | Use formatIssue and formatComment |
209 | | linear_get_teams | Remove parameter listing and result format explanation | No changes needed | Use formatTeam for each team in results |
210 | 
211 | ### Phase 3: Update Tests
212 | 
213 | 1. Update test fixtures to reflect the new formatting
214 | 2. Add tests for the new resolution functions
215 | 3. Verify that all tests pass with the new implementation
216 | 
217 | ## Detailed Tracking Table
218 | 
219 | | Tool | Task | Status | Notes |
220 | |------|------|--------|-------|
221 | | Shared | Create resolveIssueIdentifier | Not Started | Extend from resolveParentIssueIdentifier |
222 | | Shared | Create resolveUserIdentifier | Not Started | New function |
223 | | Shared | Create rendering.go with formatting functions | Not Started | New file |
224 | | linear_create_issue | Update description | Not Started | |
225 | | linear_create_issue | Update result formatting | Not Started | |
226 | | linear_update_issue | Update description | Not Started | |
227 | | linear_update_issue | Add issue identifier resolution | Not Started | |
228 | | linear_update_issue | Update result formatting | Not Started | |
229 | | linear_search_issues | Update description | Not Started | |
230 | | linear_search_issues | Add team identifier resolution | Not Started | |
231 | | linear_search_issues | Update result formatting | Not Started | |
232 | | linear_get_user_issues | Update description | Not Started | |
233 | | linear_get_user_issues | Add user identifier resolution | Not Started | |
234 | | linear_get_user_issues | Update result formatting | Not Started | |
235 | | linear_get_issue | Update description | Not Started | |
236 | | linear_get_issue | Add issue identifier resolution | Not Started | |
237 | | linear_get_issue | Update result formatting | Not Started | |
238 | | linear_get_issue | Ensure all fields from create/update are included | Not Started | Rule 4 implementation |
239 | | linear_get_issue | Ensure all comment fields are included | Not Started | Rule 4 implementation |
240 | | linear_get_user_issues | Ensure all relevant issue fields are included | Not Started | Rule 4 implementation |
241 | | linear_search_issues | Ensure all relevant issue fields are included | Not Started | Rule 4 implementation |
242 | | linear_get_teams | Ensure all team fields are included | Not Started | Rule 4 implementation |
243 | | linear_add_comment | Update description | Not Started | |
244 | | linear_add_comment | Add issue identifier resolution | Not Started | |
245 | | linear_add_comment | Update result formatting | Not Started | |
246 | | linear_get_teams | Update description | Not Started | |
247 | | linear_get_teams | Update result formatting | Not Started | |
248 | | Tests | Update test fixtures | Not Started | |
249 | | Tests | Add tests for new resolution functions | Not Started | |
250 | | Tests | Add tests for field superset compliance | Not Started | Rule 4 testing |
251 | 
252 | ## Example Implementation: linear_create_issue
253 | 
254 | Here's an example of how the `linear_create_issue` tool would be updated:
255 | 
256 | ### Before
257 | 
258 | ```go
259 | var CreateIssueTool = mcp.NewTool("linear_create_issue",
260 |     mcp.WithDescription("Creates a new Linear issue with specified details. Use this to create tickets for tasks, bugs, or feature requests. Returns the created issue's identifier and URL. Supports creating sub-issues and assigning labels."),
261 |     // ... parameters ...
262 | )
263 | 
264 | // CreateIssueHandler handles the linear_create_issue tool
265 | func CreateIssueHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
266 |     // ... implementation ...
267 |     
268 |     // Return the result
269 |     resultText := fmt.Sprintf("Created issue: %s\nTitle: %s\nURL: %s", issue.Identifier, issue.Title, issue.URL)
270 |     return mcp.NewToolResultText(resultText), nil
271 | }
272 | ```
273 | 
274 | ### After
275 | 
276 | ```go
277 | var CreateIssueTool = mcp.NewTool("linear_create_issue",
278 |     mcp.WithDescription("Creates a new Linear issue."),
279 |     // ... parameters ...
280 | )
281 | 
282 | // CreateIssueHandler handles the linear_create_issue tool
283 | func CreateIssueHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
284 |     // ... implementation ...
285 |     
286 |     // Return the result
287 |     resultText := fmt.Sprintf("%s\nTitle: %s\nURL: %s", formatIssue(issue), issue.Title, issue.URL)
288 |     return mcp.NewToolResultText(resultText), nil
289 | }
290 | ```
291 | 
292 | ## Timeline
293 | 
294 | | Phase | Estimated Duration | Dependencies |
295 | |-------|-------------------|--------------|
296 | | Phase 1: Create Shared Utility Functions | 1 day | None |
297 | | Phase 2: Update Tools | 3 days | Phase 1 |
298 | | Phase 3: Update Tests | 1 day | Phase 2 |
299 | 
300 | ## Risks and Mitigations
301 | 
302 | | Risk | Impact | Mitigation |
303 | |------|--------|------------|
304 | | Breaking changes to API | High | Ensure backward compatibility or version the API |
305 | | Test failures | Medium | Update test fixtures and add new tests |
306 | | Inconsistent implementation | Medium | Review each tool implementation for consistency |
307 | 
308 | ## Success Criteria
309 | 
310 | 1. All tool descriptions are concise
311 | 2. All tools that reference Linear objects accept multiple identifier types
312 | 3. All tools render entities in a consistent format
313 | 4. Retrieval methods include all fields that can be set in create/update methods
314 | 5. All tests pass with the new implementation
315 | 6. Code review confirms consistency across all tools
316 | 
```
Page 4/6FirstPrevNextLast