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 | ```