This is page 2 of 6. Use http://codebase.md/geropl/linear-mcp-go?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .clinerules │ └── memory-bank.md ├── .devcontainer │ ├── devcontainer.json │ └── Dockerfile ├── .github │ └── workflows │ └── release.yml ├── .gitignore ├── .gitpod.yml ├── cmd │ ├── root.go │ ├── serve.go │ ├── setup_test.go │ ├── setup.go │ └── version.go ├── docs │ ├── design │ │ ├── 001-mcp-go-upgrade.md │ │ └── 002-project-milestone-initiative.md │ └── prd │ ├── 000-tool-standardization-overview.md │ ├── 001-api-refresher.md │ ├── 002-tool-standardization.md │ ├── 003-tool-standardization-implementation.md │ ├── 004-tool-standardization-tracking.md │ ├── 005-sample-implementation.md │ ├── 006-issue-comments-pagination.md │ └── README.md ├── go.mod ├── go.sum ├── main.go ├── memory-bank │ ├── activeContext.md │ ├── developmentWorkflows.md │ ├── productContext.md │ ├── progress.md │ ├── projectbrief.md │ ├── systemPatterns.md │ └── techContext.md ├── pkg │ ├── linear │ │ ├── client.go │ │ ├── models.go │ │ ├── rate_limiter.go │ │ └── test_helpers.go │ ├── server │ │ ├── resources_test.go │ │ ├── resources.go │ │ ├── server.go │ │ ├── test_helpers.go │ │ └── tools_test.go │ └── tools │ ├── add_comment.go │ ├── common.go │ ├── create_issue.go │ ├── get_issue_comments.go │ ├── get_issue.go │ ├── get_teams.go │ ├── get_user_issues.go │ ├── initiative_tools.go │ ├── milestone_tools.go │ ├── priority_test.go │ ├── priority.go │ ├── project_tools.go │ ├── rendering.go │ ├── reply_to_comment.go │ ├── search_issues.go │ ├── update_issue_comment.go │ └── update_issue.go ├── README.md ├── scripts │ └── register-cline.sh └── testdata ├── fixtures │ ├── add_comment_handler_Missing body.yaml │ ├── add_comment_handler_Missing issue.yaml │ ├── add_comment_handler_Missing issueId.yaml │ ├── add_comment_handler_Reply with shorthand.yaml │ ├── add_comment_handler_Reply with URL.yaml │ ├── add_comment_handler_Reply_to_comment.yaml │ ├── add_comment_handler_Valid comment.yaml │ ├── create_initiative_handler_Missing name.yaml │ ├── create_initiative_handler_Valid initiative.yaml │ ├── create_initiative_handler_With description.yaml │ ├── create_issue_handler_Create issue with invalid project.yaml │ ├── create_issue_handler_Create issue with labels.yaml │ ├── create_issue_handler_Create issue with project ID.yaml │ ├── create_issue_handler_Create issue with project name.yaml │ ├── create_issue_handler_Create issue with project slug.yaml │ ├── create_issue_handler_Create sub issue from identifier.yaml │ ├── create_issue_handler_Create sub issue with labels.yaml │ ├── create_issue_handler_Create sub issue.yaml │ ├── create_issue_handler_Invalid team.yaml │ ├── create_issue_handler_Missing team.yaml │ ├── create_issue_handler_Missing teamId.yaml │ ├── create_issue_handler_Missing title.yaml │ ├── create_issue_handler_Valid issue with team key.yaml │ ├── create_issue_handler_Valid issue with team name.yaml │ ├── create_issue_handler_Valid issue with team UUID.yaml │ ├── create_issue_handler_Valid issue with team.yaml │ ├── create_issue_handler_Valid issue with teamId.yaml │ ├── create_issue_handler_Valid issue.yaml │ ├── create_milestone_handler_Invalid project ID.yaml │ ├── create_milestone_handler_Missing name.yaml │ ├── create_milestone_handler_Valid milestone.yaml │ ├── create_milestone_handler_With all optional fields.yaml │ ├── create_project_handler_Invalid team ID.yaml │ ├── create_project_handler_Missing name.yaml │ ├── create_project_handler_Valid project.yaml │ ├── create_project_handler_With all optional fields.yaml │ ├── get_initiative_handler_By name.yaml │ ├── get_initiative_handler_Non-existent name.yaml │ ├── get_initiative_handler_Valid initiative.yaml │ ├── get_issue_comments_handler_Invalid issue.yaml │ ├── get_issue_comments_handler_Missing issue.yaml │ ├── get_issue_comments_handler_Thread_with_pagination.yaml │ ├── get_issue_comments_handler_Valid issue.yaml │ ├── get_issue_comments_handler_With limit.yaml │ ├── get_issue_comments_handler_With_thread_parameter.yaml │ ├── get_issue_handler_Get comment issue.yaml │ ├── get_issue_handler_Missing issue.yaml │ ├── get_issue_handler_Missing issueId.yaml │ ├── get_issue_handler_Valid issue.yaml │ ├── get_milestone_handler_By name.yaml │ ├── get_milestone_handler_Non-existent milestone.yaml │ ├── get_milestone_handler_Valid milestone.yaml │ ├── get_project_handler_By ID.yaml │ ├── get_project_handler_By name.yaml │ ├── get_project_handler_By slug.yaml │ ├── get_project_handler_Invalid project.yaml │ ├── get_project_handler_Missing project param.yaml │ ├── get_project_handler_Non-existent slug.yaml │ ├── get_teams_handler_Get Teams.yaml │ ├── get_user_issues_handler_Current user issues.yaml │ ├── get_user_issues_handler_Specific user issues.yaml │ ├── reply_to_comment_handler_Missing body.yaml │ ├── reply_to_comment_handler_Missing thread.yaml │ ├── reply_to_comment_handler_Reply with URL.yaml │ ├── reply_to_comment_handler_Valid reply.yaml │ ├── resource_TeamResourceHandler_Fetch By ID.yaml │ ├── resource_TeamResourceHandler_Fetch By Key.yaml │ ├── resource_TeamResourceHandler_Fetch By Name.yaml │ ├── resource_TeamResourceHandler_Invalid ID.yaml │ ├── resource_TeamResourceHandler_Missing ID.yaml │ ├── resource_TeamsResourceHandler_List All.yaml │ ├── search_issues_handler_Search by query.yaml │ ├── search_issues_handler_Search by team.yaml │ ├── search_projects_handler_Empty query.yaml │ ├── search_projects_handler_Multiple results.yaml │ ├── search_projects_handler_No results.yaml │ ├── search_projects_handler_Search by query.yaml │ ├── update_comment_handler_Invalid comment identifier.yaml │ ├── update_comment_handler_Missing body.yaml │ ├── update_comment_handler_Missing comment.yaml │ ├── update_comment_handler_Valid comment update with hash only.yaml │ ├── update_comment_handler_Valid comment update with shorthand.yaml │ ├── update_comment_handler_Valid comment update.yaml │ ├── update_initiative_handler_Non-existent initiative.yaml │ ├── update_initiative_handler_Valid update.yaml │ ├── update_issue_handler_Missing id.yaml │ ├── update_issue_handler_Valid update.yaml │ ├── update_milestone_handler_Non-existent milestone.yaml │ ├── update_milestone_handler_Valid update.yaml │ ├── update_project_handler_Non-existent project.yaml │ ├── update_project_handler_Update name and description.yaml │ ├── update_project_handler_Update only description.yaml │ └── update_project_handler_Valid update.yaml └── golden ├── add_comment_handler_Missing body.golden ├── add_comment_handler_Missing issue.golden ├── add_comment_handler_Missing issueId.golden ├── add_comment_handler_Reply with shorthand.golden ├── add_comment_handler_Reply with URL.golden ├── add_comment_handler_Reply_to_comment.golden ├── add_comment_handler_Valid comment.golden ├── create_initiative_handler_Missing name.golden ├── create_initiative_handler_Valid initiative.golden ├── create_initiative_handler_With description.golden ├── create_issue_handler_Create issue with invalid project.golden ├── create_issue_handler_Create issue with labels.golden ├── create_issue_handler_Create issue with project ID.golden ├── create_issue_handler_Create issue with project name.golden ├── create_issue_handler_Create issue with project slug.golden ├── create_issue_handler_Create sub issue from identifier.golden ├── create_issue_handler_Create sub issue with labels.golden ├── create_issue_handler_Create sub issue.golden ├── create_issue_handler_Invalid team.golden ├── create_issue_handler_Missing team.golden ├── create_issue_handler_Missing teamId.golden ├── create_issue_handler_Missing title.golden ├── create_issue_handler_Valid issue with team key.golden ├── create_issue_handler_Valid issue with team name.golden ├── create_issue_handler_Valid issue with team UUID.golden ├── create_issue_handler_Valid issue with team.golden ├── create_issue_handler_Valid issue with teamId.golden ├── create_issue_handler_Valid issue.golden ├── create_milestone_handler_Invalid project ID.golden ├── create_milestone_handler_Missing name.golden ├── create_milestone_handler_Valid milestone.golden ├── create_milestone_handler_With all optional fields.golden ├── create_project_handler_Invalid team ID.golden ├── create_project_handler_Missing name.golden ├── create_project_handler_Valid project.golden ├── create_project_handler_With all optional fields.golden ├── get_initiative_handler_By name.golden ├── get_initiative_handler_Non-existent name.golden ├── get_initiative_handler_Valid initiative.golden ├── get_issue_comments_handler_Invalid issue.golden ├── get_issue_comments_handler_Missing issue.golden ├── get_issue_comments_handler_Thread_with_pagination.golden ├── get_issue_comments_handler_Valid issue.golden ├── get_issue_comments_handler_With limit.golden ├── get_issue_comments_handler_With_thread_parameter.golden ├── get_issue_handler_Get comment issue.golden ├── get_issue_handler_Missing issue.golden ├── get_issue_handler_Missing issueId.golden ├── get_issue_handler_Valid issue.golden ├── get_milestone_handler_By name.golden ├── get_milestone_handler_Non-existent milestone.golden ├── get_milestone_handler_Valid milestone.golden ├── get_project_handler_By ID.golden ├── get_project_handler_By name.golden ├── get_project_handler_By slug.golden ├── get_project_handler_Invalid project.golden ├── get_project_handler_Missing project param.golden ├── get_project_handler_Non-existent slug.golden ├── get_teams_handler_Get Teams.golden ├── get_user_issues_handler_Current user issues.golden ├── get_user_issues_handler_Specific user issues.golden ├── reply_to_comment_handler_Missing body.golden ├── reply_to_comment_handler_Missing thread.golden ├── reply_to_comment_handler_Reply with URL.golden ├── reply_to_comment_handler_Valid reply.golden ├── resource_TeamResourceHandler_Fetch By ID.golden ├── resource_TeamResourceHandler_Fetch By Key.golden ├── resource_TeamResourceHandler_Fetch By Name.golden ├── resource_TeamResourceHandler_Invalid ID.golden ├── resource_TeamResourceHandler_Missing ID.golden ├── resource_TeamsResourceHandler_List All.golden ├── search_issues_handler_Search by query.golden ├── search_issues_handler_Search by team.golden ├── search_projects_handler_Empty query.golden ├── search_projects_handler_Multiple results.golden ├── search_projects_handler_No results.golden ├── search_projects_handler_Search by query.golden ├── update_comment_handler_Invalid comment identifier.golden ├── update_comment_handler_Missing body.golden ├── update_comment_handler_Missing comment.golden ├── update_comment_handler_Valid comment update with hash only.golden ├── update_comment_handler_Valid comment update with shorthand.golden ├── update_comment_handler_Valid comment update.golden ├── update_initiative_handler_Non-existent initiative.golden ├── update_initiative_handler_Valid update.golden ├── update_issue_handler_Missing id.golden ├── update_issue_handler_Valid update.golden ├── update_milestone_handler_Non-existent milestone.golden ├── update_milestone_handler_Valid update.golden ├── update_project_handler_Non-existent project.golden ├── update_project_handler_Update name and description.golden ├── update_project_handler_Update only description.golden └── update_project_handler_Valid update.golden ``` # Files -------------------------------------------------------------------------------- /testdata/fixtures/get_milestone_handler_Non-existent milestone.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 282 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery ProjectMilestone($id: String!) {\n\t\t\tprojectMilestone(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\ttargetDate\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"non-existent-milestone"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"errors":[{"message":"Entity not found: ProjectMilestone","path":["projectMilestone"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced ProjectMilestone."}}],"data":null} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"12e-0KhOKt5R3marKFxUi49Nm51rMnE" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 386 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tquery GetMilestoneByName($filter: ProjectMilestoneFilter) {\n\t\t\tprojectMilestones(filter: $filter, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\ttargetDate\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"name":{"eq":"non-existent-milestone"}}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: 44 77 | uncompressed: false 78 | body: | 79 | {"data":{"projectMilestones":{"nodes":[]}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Length: 88 | - "44" 89 | Content-Type: 90 | - application/json; charset=utf-8 91 | Etag: 92 | - W/"2c-CfwOLPDRtN/upofCH3tMCaygMOE" 93 | Server: 94 | - cloudflare 95 | Vary: 96 | - Accept-Encoding 97 | Via: 98 | - 1.1 google 99 | status: 200 OK 100 | code: 200 101 | duration: 0s 102 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/update_milestone_handler_Non-existent milestone.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 282 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery ProjectMilestone($id: String!) {\n\t\t\tprojectMilestone(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\ttargetDate\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"non-existent-milestone"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"errors":[{"message":"Entity not found: ProjectMilestone","path":["projectMilestone"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced ProjectMilestone."}}],"data":null} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"12e-0KhOKt5R3marKFxUi49Nm51rMnE" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 386 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tquery GetMilestoneByName($filter: ProjectMilestoneFilter) {\n\t\t\tprojectMilestones(filter: $filter, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\ttargetDate\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"name":{"eq":"non-existent-milestone"}}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: 44 77 | uncompressed: false 78 | body: | 79 | {"data":{"projectMilestones":{"nodes":[]}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Length: 88 | - "44" 89 | Content-Type: 90 | - application/json; charset=utf-8 91 | Etag: 92 | - W/"2c-CfwOLPDRtN/upofCH3tMCaygMOE" 93 | Server: 94 | - cloudflare 95 | Vary: 96 | - Accept-Encoding 97 | Via: 98 | - 1.1 google 99 | status: 200 OK 100 | code: 200 101 | duration: 0s 102 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/update_initiative_handler_Valid update.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 220 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetInitiative($id: String!) {\n\t\t\tinitiative(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\turl\n\t\t\t}\n\t\t}\n\t","variables":{"id":"c6a7dd0c-cbe2-4101-906d-ddd97acb2241"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"initiative":{"id":"c6a7dd0c-cbe2-4101-906d-ddd97acb2241","name":"Created Test Initiative 2","description":"Test Description","url":"https://linear.app/linear-mcp-go-test/initiative/created-test-initiative-2-e209008074dc"}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"ea-uo1CNA8fgECA7BrV1BpztW43pw4" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 416 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tmutation InitiativeUpdate($id: String!, $input: InitiativeUpdateInput!) {\n\t\t\tinitiativeUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tinitiative {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\turl\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"c6a7dd0c-cbe2-4101-906d-ddd97acb2241","input":{"name":"Updated Initiative Name","description":"Updated Description"}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"initiativeUpdate":{"success":true,"initiative":{"id":"c6a7dd0c-cbe2-4101-906d-ddd97acb2241","name":"Updated Initiative Name","description":"Updated Description","url":"https://linear.app/linear-mcp-go-test/initiative/updated-initiative-name-e209008074dc"}}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"10d-JARpP5QUsJRs1w/2NBEsL4A+GRY" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/get_milestone_handler_By name.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 276 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery ProjectMilestone($id: String!) {\n\t\t\tprojectMilestone(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\ttargetDate\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"Test Milestone 2"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"errors":[{"message":"Entity not found: ProjectMilestone","path":["projectMilestone"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced ProjectMilestone."}}],"data":null} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"12e-0KhOKt5R3marKFxUi49Nm51rMnE" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 380 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tquery GetMilestoneByName($filter: ProjectMilestoneFilter) {\n\t\t\tprojectMilestones(filter: $filter, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\ttargetDate\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"name":{"eq":"Test Milestone 2"}}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"projectMilestones":{"nodes":[{"id":"67ae1d0a-107b-42af-b063-c31a3fa05fd1","name":"Test Milestone 2","description":"Test Description","targetDate":"2024-12-31","project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2"}}]}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"106-9HbnUOf8b5eDeaQSxg5wvPdcXPI" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags: [ 'v*' ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build-and-test: 12 | name: Build and Test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: '1.23.x' 22 | check-latest: true 23 | 24 | - name: Build 25 | run: go build -ldflags "-X github.com/geropl/linear-mcp-go/cmd.GitCommit=${{ github.sha }} -X github.com/geropl/linear-mcp-go/cmd.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | 30 | create-release: 31 | name: Create Release 32 | needs: build-and-test 33 | if: startsWith(github.ref, 'refs/tags/v') 34 | runs-on: ubuntu-latest 35 | permissions: 36 | contents: write # This gives permission to create releases and upload assets 37 | steps: 38 | - name: Checkout code 39 | uses: actions/checkout@v4 40 | 41 | - name: Set up Go 42 | uses: actions/setup-go@v5 43 | with: 44 | go-version: '1.23.x' 45 | check-latest: true 46 | 47 | - name: Get version from tag 48 | id: get_version 49 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 50 | 51 | - name: Build for Linux 52 | run: | 53 | GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/geropl/linear-mcp-go/cmd.GitCommit=${{ github.sha }} -X github.com/geropl/linear-mcp-go/cmd.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o linear-mcp-go-linux-amd64 -v . 54 | chmod +x linear-mcp-go-linux-amd64 55 | 56 | - name: Build for macOS 57 | run: | 58 | GOOS=darwin GOARCH=amd64 go build -ldflags "-X github.com/geropl/linear-mcp-go/cmd.GitCommit=${{ github.sha }} -X github.com/geropl/linear-mcp-go/cmd.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o linear-mcp-go-darwin-amd64 -v . 59 | chmod +x linear-mcp-go-darwin-amd64 60 | 61 | - name: Build for Windows 62 | run: | 63 | GOOS=windows GOARCH=amd64 go build -ldflags "-X github.com/geropl/linear-mcp-go/cmd.GitCommit=${{ github.sha }} -X github.com/geropl/linear-mcp-go/cmd.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o linear-mcp-go-windows-amd64.exe -v . 64 | 65 | - name: Create Release 66 | id: create_release 67 | uses: actions/create-release@v1 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | with: 71 | tag_name: ${{ github.ref }} 72 | release_name: Linear MCP Server ${{ steps.get_version.outputs.VERSION }} 73 | draft: false 74 | prerelease: false 75 | 76 | - name: Upload Linux Binary 77 | uses: actions/upload-release-asset@v1 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | with: 81 | upload_url: ${{ steps.create_release.outputs.upload_url }} 82 | asset_path: ./linear-mcp-go-linux-amd64 83 | asset_name: linear-mcp-go-linux-amd64 84 | asset_content_type: application/octet-stream 85 | 86 | - name: Upload macOS Binary 87 | uses: actions/upload-release-asset@v1 88 | env: 89 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 90 | with: 91 | upload_url: ${{ steps.create_release.outputs.upload_url }} 92 | asset_path: ./linear-mcp-go-darwin-amd64 93 | asset_name: linear-mcp-go-darwin-amd64 94 | asset_content_type: application/octet-stream 95 | 96 | - name: Upload Windows Binary 97 | uses: actions/upload-release-asset@v1 98 | env: 99 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 100 | with: 101 | upload_url: ${{ steps.create_release.outputs.upload_url }} 102 | asset_path: ./linear-mcp-go-windows-amd64.exe 103 | asset_name: linear-mcp-go-windows-amd64.exe 104 | asset_content_type: application/octet-stream 105 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/search_issues_handler_Search by team.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 703 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery SearchIssues($filter: IssueFilter, $first: Int, $includeArchived: Boolean) {\n\t\t\tissues(filter: $filter, first: $first, includeArchived: $includeArchived) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\n\t\t\t\t\tstate {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tassignee {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tlabels {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"team":{"id":{"eq":"234c5451-a839-4c8f-98d9-da00973f1060"}}},"first":5,"includeArchived":false}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"issues":{"nodes":[{"id":"acdeb5e4-bf7e-4281-9a15-ffea27aa5965","identifier":"TEST-71","title":"Sub Issue with Labels","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-71/sub-issue-with-labels","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":null,"labels":{"nodes":[{"id":"fcd49e32-5043-4bfd-88a5-2bbe3c95124a","name":"ws-label 2"},{"id":"94087865-ce6c-470b-896c-4d1d2c7456b8","name":"Feature"}]}},{"id":"2486653d-f073-4bdc-a94d-eab0e34587c9","identifier":"TEST-70","title":"Issue with Labels","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-70/issue-with-labels","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":null,"labels":{"nodes":[{"id":"37e1cdc8-a696-4412-8ad7-8ba8435ba0f4","name":"team label 1"}]}},{"id":"de74de85-b597-444e-abdd-c75052e72f37","identifier":"TEST-69","title":"Sub Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-69/sub-issue","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":null,"labels":{"nodes":[]}},{"id":"88460755-1c87-4a8a-927b-6a729275c9c7","identifier":"TEST-68","title":"Sub Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-68/sub-issue","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":null,"labels":{"nodes":[]}},{"id":"8a2c9f4a-88ce-4f02-9b42-81c257d7a4f7","identifier":"TEST-67","title":"Test Issue with team key","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-67/test-issue-with-team-key","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"assignee":null,"labels":{"nodes":[]}}]}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"705-mKrlAmxVi/v3PBWaz/5XCTzocI0" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/update_milestone_handler_Valid update.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 296 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery ProjectMilestone($id: String!) {\n\t\t\tprojectMilestone(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\ttargetDate\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"2d95299d-1341-484b-ab00-5cb587f2cc67"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"projectMilestone":{"id":"2d95299d-1341-484b-ab00-5cb587f2cc67","name":"Test Milestone 2.2","description":null,"targetDate":null,"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2"}}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"e5-h5yNN+ZdI94WBaZJFpZUdcwi9uA" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 543 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tmutation ProjectMilestoneUpdate($id: String!, $input: ProjectMilestoneUpdateInput!) {\n\t\t\tprojectMilestoneUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tprojectMilestone {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\ttargetDate\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"2d95299d-1341-484b-ab00-5cb587f2cc67","input":{"name":"Updated Milestone Name 22","description":"Updated Description","targetDate":"2025-01-01"}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"projectMilestoneUpdate":{"success":true,"projectMilestone":{"id":"2d95299d-1341-484b-ab00-5cb587f2cc67","name":"Updated Milestone Name 22","description":"Updated Description","targetDate":"2025-01-01","project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2"}}}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"12f-fyPVeE08hVwySnKgSCGm3ErLoSE" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/add_comment_handler_Reply_to_comment.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 322 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n\t\t\tissues(filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } }, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"number":10,"teamKey":"TEST"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 575 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tmutation AddComment($input: CommentCreateInput!) {\n\t\t\tcommentCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tcomment {\n\t\t\t\t\tid\n\t\t\t\t\tbody\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tuser {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tissue {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tidentifier\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\turl\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"body":"This is a reply to the comment","issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f","parentId":"ae3d62d6-3f40-4990-867b-5c97dd265a40"}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"commentCreate":{"success":true,"comment":{"id":"6a2b6ab6-e3b8-41bd-bdd6-6891a8d6d86f","body":"This is a reply to the comment","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-6a2b6ab6","createdAt":"2025-10-07T16:13:07.481Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue"}}}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"219-613DOqbo34flC1F2tWNw/F01f4k" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /.clinerules/memory-bank.md: -------------------------------------------------------------------------------- ```markdown 1 | # Cline's Memory Bank 2 | 3 | I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. 4 | 5 | ## Memory Bank Structure 6 | 7 | The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy: 8 | 9 | flowchart TD 10 | PB[projectbrief.md] --> PC[productContext.md] 11 | PB --> SP[systemPatterns.md] 12 | PB --> TC[techContext.md] 13 | 14 | PC --> AC[activeContext.md] 15 | SP --> AC 16 | TC --> AC 17 | 18 | TC --> DW[developmentWorkflows.md] 19 | SP --> DW 20 | 21 | AC --> P[progress.md] 22 | 23 | ### Core Files (Required) 24 | 1. `projectbrief.md` 25 | - Foundation document that shapes all other files 26 | - Created at project start if it doesn't exist 27 | - Defines core requirements and goals 28 | - Source of truth for project scope 29 | 30 | 2. `productContext.md` 31 | - Why this project exists 32 | - Problems it solves 33 | - How it should work 34 | - User experience goals 35 | 36 | 3. `activeContext.md` 37 | - Current work focus 38 | - Recent changes 39 | - Next steps 40 | - Active decisions and considerations 41 | - Important patterns and preferences 42 | - Learnings and project insights 43 | 44 | 4. `systemPatterns.md` 45 | - System architecture 46 | - Key technical decisions 47 | - Design patterns in use 48 | - Component relationships 49 | - Critical implementation paths 50 | 51 | 5. `techContext.md` 52 | - Technologies used 53 | - Development setup 54 | - Technical constraints 55 | - Dependencies 56 | - Tool usage patterns 57 | 58 | 6. `progress.md` 59 | - What works 60 | - What's left to build 61 | - Current status 62 | - Known issues 63 | - Evolution of project decisions 64 | 65 | ### Development-Specific Files (Critical for Development Tasks) 66 | 7. `developmentWorkflows.md` 67 | - Git workflow and branch management 68 | - Release process and versioning 69 | - Build, test, and deployment commands 70 | - Code quality standards and practices 71 | - Debugging and troubleshooting guides 72 | - **Essential for any development work** 73 | 74 | ### Additional Context 75 | Create additional files/folders within memory-bank/ when they help organize: 76 | - Complex feature documentation 77 | - Integration specifications 78 | - API documentation 79 | - Testing strategies 80 | - Deployment procedures 81 | 82 | ## Core Workflows 83 | 84 | ### Plan Mode 85 | flowchart TD 86 | Start[Start] --> ReadFiles[Read Memory Bank] 87 | ReadFiles --> CheckFiles{Files Complete?} 88 | 89 | CheckFiles -->|No| Plan[Create Plan] 90 | Plan --> Document[Document in Chat] 91 | 92 | CheckFiles -->|Yes| Verify[Verify Context] 93 | Verify --> Strategy[Develop Strategy] 94 | Strategy --> Present[Present Approach] 95 | 96 | ### Act Mode 97 | flowchart TD 98 | Start[Start] --> Context[Check Memory Bank] 99 | Context --> Update[Update Documentation] 100 | Update --> Execute[Execute Task] 101 | Execute --> Document[Document Changes] 102 | 103 | ## Documentation Updates 104 | 105 | Memory Bank updates occur when: 106 | 1. Discovering new project patterns 107 | 2. After implementing significant changes 108 | 3. When user requests with **update memory bank** (MUST review ALL files) 109 | 4. When context needs clarification 110 | 5. When development processes change (update developmentWorkflows.md) 111 | 112 | flowchart TD 113 | Start[Update Process] 114 | 115 | subgraph Process 116 | P1[Review ALL Files] 117 | P2[Document Current State] 118 | P3[Clarify Next Steps] 119 | P4[Document Insights & Patterns] 120 | 121 | P1 --> P2 --> P3 --> P4 122 | end 123 | 124 | Start --> Process 125 | 126 | Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state. 127 | 128 | REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy. 129 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/add_comment_handler_Valid comment.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 322 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n\t\t\tissues(filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } }, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"number":10,"teamKey":"TEST"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | User-Agent: 21 | - linear-mcp-go/1.0.0 22 | url: https://api.linear.app/graphql 23 | method: POST 24 | response: 25 | proto: HTTP/2.0 26 | proto_major: 2 27 | proto_minor: 0 28 | transfer_encoding: [] 29 | trailer: {} 30 | content_length: -1 31 | uncompressed: true 32 | body: | 33 | {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}} 34 | headers: 35 | Alt-Svc: 36 | - h3=":443"; ma=86400 37 | Cache-Control: 38 | - no-store 39 | Cf-Cache-Status: 40 | - DYNAMIC 41 | Content-Type: 42 | - application/json; charset=utf-8 43 | Etag: 44 | - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo" 45 | Server: 46 | - cloudflare 47 | Vary: 48 | - Accept-Encoding 49 | Via: 50 | - 1.1 google 51 | status: 200 OK 52 | code: 200 53 | duration: 0s 54 | - id: 1 55 | request: 56 | proto: HTTP/1.1 57 | proto_major: 1 58 | proto_minor: 1 59 | content_length: 507 60 | transfer_encoding: [] 61 | trailer: {} 62 | host: api.linear.app 63 | remote_addr: "" 64 | request_uri: "" 65 | body: '{"query":"\n\t\tmutation AddComment($input: CommentCreateInput!) {\n\t\t\tcommentCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tcomment {\n\t\t\t\t\tid\n\t\t\t\t\tbody\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tuser {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tissue {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tidentifier\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\turl\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"body":"Test comment","issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f"}}}' 66 | form: {} 67 | headers: 68 | Content-Type: 69 | - application/json 70 | User-Agent: 71 | - linear-mcp-go/1.0.0 72 | url: https://api.linear.app/graphql 73 | method: POST 74 | response: 75 | proto: HTTP/2.0 76 | proto_major: 2 77 | proto_minor: 0 78 | transfer_encoding: [] 79 | trailer: {} 80 | content_length: -1 81 | uncompressed: true 82 | body: | 83 | {"data":{"commentCreate":{"success":true,"comment":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","body":"Test comment","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-ae3d62d6","createdAt":"2025-03-30T13:37:20.666Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue"}}}}} 84 | headers: 85 | Alt-Svc: 86 | - h3=":443"; ma=86400 87 | Cache-Control: 88 | - no-store 89 | Cf-Cache-Status: 90 | - DYNAMIC 91 | Content-Type: 92 | - application/json; charset=utf-8 93 | Etag: 94 | - W/"207-A36zaBZM2etAKuEB572ckko3PUw" 95 | Server: 96 | - cloudflare 97 | Vary: 98 | - Accept-Encoding 99 | Via: 100 | - 1.1 google 101 | status: 200 OK 102 | code: 200 103 | duration: 0s 104 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/update_comment_handler_Valid comment update with shorthand.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 255 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetCommentByHash($hash: String!) {\n\t\t\tcomment(hash: $hash) {\n\t\t\t\tid\n\t\t\t\tbody\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tuser {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"hash":"ae3d62d6"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"comment":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","body":"Updated comment text","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-ae3d62d6","createdAt":"2025-03-30T13:37:20.666Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"}}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"135-SJj8cT5t4cfk3yiLplqz0T3j/uE" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 550 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tmutation UpdateComment($id: String!, $input: CommentUpdateInput!) {\n\t\t\tcommentUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tcomment {\n\t\t\t\t\tid\n\t\t\t\t\tbody\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tuser {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tissue {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tidentifier\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\turl\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","input":{"body":"Updated comment text via shorthand"}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"commentUpdate":{"success":true,"comment":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","body":"Updated comment text via shorthand","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-ae3d62d6","createdAt":"2025-03-30T13:37:20.666Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue"}}}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"21d-cSmAgkHmAGU1qbZ3TOEPbZCgEx8" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/update_issue_handler_Valid update.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 322 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n\t\t\tissues(filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } }, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"number":10,"teamKey":"TEST"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | User-Agent: 21 | - linear-mcp-go/1.0.0 22 | url: https://api.linear.app/graphql 23 | method: POST 24 | response: 25 | proto: HTTP/2.0 26 | proto_major: 2 27 | proto_minor: 0 28 | transfer_encoding: [] 29 | trailer: {} 30 | content_length: -1 31 | uncompressed: true 32 | body: | 33 | {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}} 34 | headers: 35 | Alt-Svc: 36 | - h3=":443"; ma=86400 37 | Cache-Control: 38 | - no-store 39 | Cf-Cache-Status: 40 | - DYNAMIC 41 | Content-Type: 42 | - application/json; charset=utf-8 43 | Etag: 44 | - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo" 45 | Server: 46 | - cloudflare 47 | Vary: 48 | - Accept-Encoding 49 | Via: 50 | - 1.1 google 51 | status: 200 OK 52 | code: 200 53 | duration: 0s 54 | - id: 1 55 | request: 56 | proto: HTTP/1.1 57 | proto_major: 1 58 | proto_minor: 1 59 | content_length: 589 60 | transfer_encoding: [] 61 | trailer: {} 62 | host: api.linear.app 63 | remote_addr: "" 64 | request_uri: "" 65 | body: '{"query":"\n\t\tmutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n\t\t\tissueUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tstate {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tteam {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","input":{"title":"Updated Test Issue"}}}' 66 | form: {} 67 | headers: 68 | Content-Type: 69 | - application/json 70 | User-Agent: 71 | - linear-mcp-go/1.0.0 72 | url: https://api.linear.app/graphql 73 | method: POST 74 | response: 75 | proto: HTTP/2.0 76 | proto_major: 2 77 | proto_minor: 0 78 | transfer_encoding: [] 79 | trailer: {} 80 | content_length: -1 81 | uncompressed: true 82 | body: | 83 | {"data":{"issueUpdate":{"success":true,"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue","createdAt":"2025-03-03T11:34:49.241Z","updatedAt":"2025-03-30T10:09:33.866Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}}}}} 84 | headers: 85 | Alt-Svc: 86 | - h3=":443"; ma=86400 87 | Cache-Control: 88 | - no-store 89 | Cf-Cache-Status: 90 | - DYNAMIC 91 | Content-Type: 92 | - application/json; charset=utf-8 93 | Etag: 94 | - W/"1ed-NQYcphbrRBp9Qc2UkRq4wmgK4Us" 95 | Server: 96 | - cloudflare 97 | Vary: 98 | - Accept-Encoding 99 | Via: 100 | - 1.1 google 101 | status: 200 OK 102 | code: 200 103 | duration: 0s 104 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/update_comment_handler_Valid comment update with hash only.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 255 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetCommentByHash($hash: String!) {\n\t\t\tcomment(hash: $hash) {\n\t\t\t\tid\n\t\t\t\tbody\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tuser {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"hash":"ae3d62d6"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"comment":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","body":"Updated comment text via shorthand","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-ae3d62d6","createdAt":"2025-03-30T13:37:20.666Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"}}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"143-m1nvHd82UihGhurKSTyMAF0whhw" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 545 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tmutation UpdateComment($id: String!, $input: CommentUpdateInput!) {\n\t\t\tcommentUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tcomment {\n\t\t\t\t\tid\n\t\t\t\t\tbody\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tuser {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tissue {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tidentifier\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\turl\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","input":{"body":"Updated comment text via hash"}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"commentUpdate":{"success":true,"comment":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","body":"Updated comment text via hash","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-ae3d62d6","createdAt":"2025-03-30T13:37:20.666Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue"}}}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"218-JZAC2Y8E9AKSYWGafAFwxnfBleE" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/resource_TeamResourceHandler_Fetch By Key.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 310 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetTeams($filter: TeamFilter) {\n\t\t\tteams(filter: $filter) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t\tdescription\n\t\t\t\t\tstates {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t"}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST","description":null,"states":{"nodes":[{"id":"d4caa373-1a02-431c-bd3f-1bbb67318617","name":"Done"},{"id":"cffb8999-f10e-447d-9672-8faf5b06ac67","name":"Todo"},{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},{"id":"2d26ea57-c1f7-43ae-ba30-3f828ac8edb6","name":"Canceled"},{"id":"2a939ee1-65a1-445c-8e5d-18239e5f64bc","name":"Duplicate"},{"id":"12bb7f66-d9be-4faa-800f-49b8e3b38a3f","name":"In Progress"}]}}]}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"210-+ISnhlSrm6Gd7LWWbqn3eOeSXhw" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 310 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tquery GetTeams($filter: TeamFilter) {\n\t\t\tteams(filter: $filter) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t\tdescription\n\t\t\t\t\tstates {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t"}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST","description":null,"states":{"nodes":[{"id":"d4caa373-1a02-431c-bd3f-1bbb67318617","name":"Done"},{"id":"cffb8999-f10e-447d-9672-8faf5b06ac67","name":"Todo"},{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},{"id":"2d26ea57-c1f7-43ae-ba30-3f828ac8edb6","name":"Canceled"},{"id":"2a939ee1-65a1-445c-8e5d-18239e5f64bc","name":"Duplicate"},{"id":"12bb7f66-d9be-4faa-800f-49b8e3b38a3f","name":"In Progress"}]}}]}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"210-+ISnhlSrm6Gd7LWWbqn3eOeSXhw" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/resource_TeamResourceHandler_Fetch By Name.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 310 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetTeams($filter: TeamFilter) {\n\t\t\tteams(filter: $filter) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t\tdescription\n\t\t\t\t\tstates {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t"}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST","description":null,"states":{"nodes":[{"id":"d4caa373-1a02-431c-bd3f-1bbb67318617","name":"Done"},{"id":"cffb8999-f10e-447d-9672-8faf5b06ac67","name":"Todo"},{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},{"id":"2d26ea57-c1f7-43ae-ba30-3f828ac8edb6","name":"Canceled"},{"id":"2a939ee1-65a1-445c-8e5d-18239e5f64bc","name":"Duplicate"},{"id":"12bb7f66-d9be-4faa-800f-49b8e3b38a3f","name":"In Progress"}]}}]}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"210-+ISnhlSrm6Gd7LWWbqn3eOeSXhw" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 310 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tquery GetTeams($filter: TeamFilter) {\n\t\t\tteams(filter: $filter) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tkey\n\t\t\t\t\tdescription\n\t\t\t\t\tstates {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t"}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST","description":null,"states":{"nodes":[{"id":"d4caa373-1a02-431c-bd3f-1bbb67318617","name":"Done"},{"id":"cffb8999-f10e-447d-9672-8faf5b06ac67","name":"Todo"},{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},{"id":"2d26ea57-c1f7-43ae-ba30-3f828ac8edb6","name":"Canceled"},{"id":"2a939ee1-65a1-445c-8e5d-18239e5f64bc","name":"Duplicate"},{"id":"12bb7f66-d9be-4faa-800f-49b8e3b38a3f","name":"In Progress"}]}}]}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"210-+ISnhlSrm6Gd7LWWbqn3eOeSXhw" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /pkg/tools/get_issue_comments.go: -------------------------------------------------------------------------------- ```go 1 | package tools 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/geropl/linear-mcp-go/pkg/linear" 8 | "github.com/mark3labs/mcp-go/mcp" 9 | ) 10 | 11 | // GetIssueCommentsTool is the tool definition for getting paginated comments for an issue 12 | var GetIssueCommentsTool = mcp.NewTool("linear_get_issue_comments", 13 | mcp.WithDescription("Retrieves a flat list of comments for a Linear issue and thread. Use to list all replies in a thread or all top-level comments."), 14 | mcp.WithString("issue", mcp.Required(), mcp.Description("issue identifier (e.g., 'TEAM-123')")), 15 | mcp.WithString("thread", mcp.Description("Optional thread identifier. Accepts: full URL, UUID, shorthand (comment-abc123), or hash (abc123). If not provided, all top-level comments are returned.")), 16 | mcp.WithNumber("limit", mcp.Description("Maximum number of comments to return (default: 10)")), 17 | mcp.WithString("after", mcp.Description("Cursor for pagination, to get comments after this point")), 18 | ) 19 | 20 | // GetIssueCommentsHandler handles the linear_get_issue_comments tool 21 | func GetIssueCommentsHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 22 | return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 23 | // Extract arguments 24 | issueIdentifier, err := request.RequireString("issue") 25 | if err != nil { 26 | return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil 27 | } 28 | 29 | // Extract optional arguments 30 | parentID := request.GetString("thread", "") 31 | limit := request.GetInt("limit", 10) 32 | afterCursor := request.GetString("after", "") 33 | 34 | // Resolve issue identifier to a UUID 35 | issueID, err := resolveIssueIdentifier(linearClient, issueIdentifier) 36 | if err != nil { 37 | return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve issue: %v", err)}}}, nil 38 | } 39 | 40 | // Get the issue for basic information 41 | issue, err := linearClient.GetIssue(issueID) 42 | if err != nil { 43 | return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get issue: %v", err)}}}, nil 44 | } 45 | 46 | // Get the comments 47 | commentsInput := linear.GetIssueCommentsInput{ 48 | IssueID: issueID, 49 | ParentID: parentID, 50 | Limit: limit, 51 | AfterCursor: afterCursor, 52 | } 53 | 54 | comments, err := linearClient.GetIssueComments(commentsInput) 55 | if err != nil { 56 | return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get comments: %v", err)}}}, nil 57 | } 58 | 59 | // Format the result 60 | var resultText string 61 | 62 | // Add issue information 63 | resultText += formatIssueIdentifier(issue) + "\n" 64 | 65 | // Add thread information 66 | if parentID == "" { 67 | resultText += "Thread: none (top-level comments)\n" 68 | } else { 69 | resultText += fmt.Sprintf("Thread: %s (replies to comment)\n", parentID) 70 | } 71 | 72 | resultText += "\n" 73 | 74 | // Add comments 75 | if len(comments.Nodes) > 0 { 76 | resultText += "Comments:\n" 77 | 78 | for _, comment := range comments.Nodes { 79 | createdAt := comment.CreatedAt.Format("2006-01-02 15:04:05") 80 | hasReplies := false 81 | if comment.Children != nil && len(comment.Children.Nodes) > 0 { 82 | hasReplies = true 83 | } 84 | 85 | resultText += fmt.Sprintf("- Comment: %s\n %s\n CreatedAt: %s\n HasReplies: %s\n Body: %s\n", 86 | comment.ID, 87 | formatUserIdentifier(comment.User), 88 | createdAt, 89 | formatBool(hasReplies), 90 | comment.Body) 91 | } 92 | } else { 93 | resultText += "Comments: None\n" 94 | } 95 | 96 | // Add pagination information 97 | resultText += "\nPagination:\n" 98 | resultText += fmt.Sprintf("Has more comments: %s\n", formatBool(comments.PageInfo.HasNextPage)) 99 | 100 | if comments.PageInfo.HasNextPage { 101 | resultText += fmt.Sprintf("Next cursor: %s\n", comments.PageInfo.EndCursor) 102 | } 103 | 104 | return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil 105 | } 106 | } 107 | 108 | // formatBool formats a boolean value as "yes" or "no" 109 | func formatBool(value bool) string { 110 | if value { 111 | return "yes" 112 | } 113 | return "no" 114 | } 115 | ``` -------------------------------------------------------------------------------- /pkg/tools/get_issue.go: -------------------------------------------------------------------------------- ```go 1 | package tools 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/geropl/linear-mcp-go/pkg/linear" 8 | "github.com/mark3labs/mcp-go/mcp" 9 | ) 10 | 11 | // GetIssueTool is the tool definition for getting an issue 12 | var GetIssueTool = mcp.NewTool("linear_get_issue", 13 | mcp.WithDescription("Retrieves a single Linear issue."), 14 | mcp.WithString("issue", mcp.Required(), mcp.Description("ID or identifier (e.g., 'TEAM-123') of the issue to retrieve")), 15 | ) 16 | 17 | // GetIssueHandler handles the linear_get_issue tool 18 | func GetIssueHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 19 | return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { 20 | // Extract arguments 21 | issueIdentifier, err := request.RequireString("issue") 22 | if err != nil { 23 | return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil 24 | } 25 | 26 | // Resolve issue identifier to a UUID 27 | issueID, err := resolveIssueIdentifier(linearClient, issueIdentifier) 28 | if err != nil { 29 | return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve issue: %v", err)}}}, nil 30 | } 31 | 32 | // Get the issue 33 | issue, err := linearClient.GetIssue(issueID) 34 | if err != nil { 35 | return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get issue: %v", err)}}}, nil 36 | } 37 | 38 | // Format the result using the full issue formatting 39 | resultText := formatIssue(issue) 40 | 41 | // Add assignee and team information using identifier formatting 42 | if issue.Assignee != nil { 43 | resultText += fmt.Sprintf("Assignee: %s\n", formatUserIdentifier(issue.Assignee)) 44 | } else { 45 | resultText += "Assignee: None\n" 46 | } 47 | 48 | resultText += fmt.Sprintf("%s\n", formatTeamIdentifier(issue.Team)) 49 | 50 | if issue.Project != nil { 51 | resultText += fmt.Sprintf("Project: %s (%s)\n", issue.Project.Name, issue.Project.ID) 52 | } else { 53 | resultText += "Project: None\n" 54 | } 55 | 56 | if issue.ProjectMilestone != nil { 57 | resultText += fmt.Sprintf("Milestone: %s (%s)\n", issue.ProjectMilestone.Name, issue.ProjectMilestone.ID) 58 | } else { 59 | resultText += "Milestone: None\n" 60 | } 61 | 62 | // Add attachments section if there are attachments 63 | if issue.Attachments != nil && len(issue.Attachments.Nodes) > 0 { 64 | resultText += "\nAttachments:\n" 65 | 66 | // Display all attachments in a simple list without grouping by source type 67 | for _, attachment := range issue.Attachments.Nodes { 68 | resultText += fmt.Sprintf("- %s: %s\n", attachment.Title, attachment.URL) 69 | if attachment.Subtitle != "" { 70 | resultText += fmt.Sprintf(" %s\n", attachment.Subtitle) 71 | } 72 | } 73 | } else { 74 | resultText += "\nAttachments: None\n" 75 | } 76 | 77 | // Add related issues section 78 | if (issue.Relations != nil && len(issue.Relations.Nodes) > 0) || 79 | (issue.InverseRelations != nil && len(issue.InverseRelations.Nodes) > 0) { 80 | resultText += "\nRelated Issues:\n" 81 | 82 | // Add direct relations 83 | if issue.Relations != nil && len(issue.Relations.Nodes) > 0 { 84 | for _, relation := range issue.Relations.Nodes { 85 | if relation.RelatedIssue != nil { 86 | resultText += fmt.Sprintf("- %s\n Title: %s\n RelationType: %s\n URL: %s\n", 87 | formatIssueIdentifier(relation.RelatedIssue), 88 | relation.RelatedIssue.Title, 89 | relation.Type, 90 | relation.RelatedIssue.URL) 91 | } 92 | } 93 | } 94 | 95 | // Add inverse relations 96 | if issue.InverseRelations != nil && len(issue.InverseRelations.Nodes) > 0 { 97 | for _, relation := range issue.InverseRelations.Nodes { 98 | if relation.Issue != nil { 99 | resultText += fmt.Sprintf("- %s\n Title: %s\n RelationType: %s (inverse)\n URL: %s\n", 100 | formatIssueIdentifier(relation.Issue), 101 | relation.Issue.Title, 102 | relation.Type, 103 | relation.Issue.URL) 104 | } 105 | } 106 | } 107 | } else { 108 | resultText += "\nRelated Issues: None\n" 109 | } 110 | 111 | // Note about comments 112 | resultText += "\nComments: Use the linear_get_issue_comments tool to retrieve comments for this issue.\n" 113 | 114 | return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil 115 | } 116 | } 117 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/reply_to_comment_handler_Valid reply.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 333 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetComment($id: String!) {\n\t\t\tcomment(id: $id) {\n\t\t\t\tid\n\t\t\t\tbody\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tuser {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"comment":{"id":"ae3d62d6-3f40-4990-867b-5c97dd265a40","body":"Updated comment text via hash","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-ae3d62d6","createdAt":"2025-03-30T13:37:20.666Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10"}}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"18b-JtoAyZ6gspXHMOEbvedWimpK4Y0" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 585 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tmutation AddComment($input: CommentCreateInput!) {\n\t\t\tcommentCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tcomment {\n\t\t\t\t\tid\n\t\t\t\t\tbody\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tuser {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tissue {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tidentifier\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\turl\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"body":"This is a reply using the dedicated tool","issueId":"1c2de93f-4321-4015-bfde-ee893ef7976f","parentId":"ae3d62d6-3f40-4990-867b-5c97dd265a40"}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"commentCreate":{"success":true,"comment":{"id":"243e8a79-e8cc-4617-848a-573758dcdfd5","body":"This is a reply using the dedicated tool","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue#comment-243e8a79","createdAt":"2025-10-07T13:55:14.349Z","user":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue","url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue"}}}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"223-qLAwFJpu6uPxkTsxKQkgHklk+m4" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/create_issue_handler_Create sub issue from identifier.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 322 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n\t\t\tissues(filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } }, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"number":10,"teamKey":"TEST"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"issues":{"nodes":[{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Updated Test Issue"}]}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"82-w0K/VnjlqJtYAurPyBwU/9QgAFo" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 880 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tmutation CreateIssue($input: IssueCreateInput!) {\n\t\t\tissueCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tstate {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tteam {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t\tlabels {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tprojectMilestone {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"description":"","parentId":"1c2de93f-4321-4015-bfde-ee893ef7976f","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Sub Issue"}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"issueCreate":{"success":true,"issue":{"id":"3671ad80-a3c1-4783-b5cd-a0fa24e4ff9e","identifier":"TEST-90","title":"Sub Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-90/sub-issue","createdAt":"2025-10-06T09:44:32.400Z","updatedAt":"2025-10-06T09:44:32.400Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[]},"project":null,"projectMilestone":null}}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"218-0Svxf7NuXjSFfPU18wFj97GbalA" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/create_issue_handler_Create issue with labels.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 342 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetLabelsByName($teamId: String!, $names: [String!]!) {\n\t\t\tteam(id: $teamId) {\n\t\t\t\tlabels(filter: { name: { in: $names } }) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"names":["team label 1"],"teamId":"234c5451-a839-4c8f-98d9-da00973f1060"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"team":{"labels":{"nodes":[{"id":"37e1cdc8-a696-4412-8ad7-8ba8435ba0f4","name":"team label 1"}]}}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"6d-NtoFPWMa8e+cqurX/zzYmekk2Dg" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 890 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tmutation CreateIssue($input: IssueCreateInput!) {\n\t\t\tissueCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tstate {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tteam {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t\tlabels {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tprojectMilestone {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"description":"","labelIds":["37e1cdc8-a696-4412-8ad7-8ba8435ba0f4"],"teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Issue with Labels"}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"issueCreate":{"success":true,"issue":{"id":"640b14f2-643e-4010-a194-c4f1c2f6177b","identifier":"TEST-91","title":"Issue with Labels","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-91/issue-with-labels","createdAt":"2025-10-06T09:44:37.052Z","updatedAt":"2025-10-06T09:44:37.052Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[{"id":"37e1cdc8-a696-4412-8ad7-8ba8435ba0f4","name":"team label 1"}]},"project":null,"projectMilestone":null}}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"26b-Kh3ayVGqSTqbO3LB/W4HMT1nF4s" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/update_project_handler_Valid update.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 733 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetProject($id: String!) {\n\t\t\tproject(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\tslugId\n\t\t\t\tstate\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tlead {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tmembers {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tteams {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinitiatives(first: 10) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstartDate\n\t\t\t\ttargetDate\n\t\t\t}\n\t\t}\n\t","variables":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2","description":"Updated Description Only","slugId":"e1153169a428","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/updated-project-name-2-e1153169a428","createdAt":"2025-06-28T18:42:20.223Z","updatedAt":"2025-06-28T18:56:53.580Z","lead":null,"members":{"nodes":[]},"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}]},"initiatives":{"nodes":[]},"startDate":null,"targetDate":null}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"221-5dHPmIHEC1rBzEVJqPrdCyJ2NbY" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 400 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tmutation ProjectUpdate($id: String!, $input: ProjectUpdateInput!) {\n\t\t\tprojectUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\tslugId\n\t\t\t\t\tstate\n\t\t\t\t\turl\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","input":{"name":"Updated Project Name"}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"projectUpdate":{"success":true,"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name","description":"Updated Description Only","slugId":"e1153169a428","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/updated-project-name-e1153169a428"}}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"12d-wnpLXmtIiIJiE+9jOexlH0g7wGY" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/get_project_handler_Non-existent slug.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 714 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetProject($id: String!) {\n\t\t\tproject(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\tslugId\n\t\t\t\tstate\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tlead {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tmembers {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tteams {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinitiatives(first: 10) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstartDate\n\t\t\t\ttargetDate\n\t\t\t}\n\t\t}\n\t","variables":{"id":"non-existent-slug"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"errors":[{"message":"Entity not found: Project","path":["project"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced Project."}}],"data":null} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"113-pUQ9mkDn3KWYiQz0UBE51+d7gJ4" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 906 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tquery GetProjectByNameOrSlug($filter: ProjectFilter) {\n\t\t\tprojects(filter: $filter, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\tslugId\n\t\t\t\t\tstate\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tlead {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t\tmembers {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\temail\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tteams {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tinitiatives(first: 1) {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstartDate\n\t\t\t\t\ttargetDate\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"or":[{"name":{"eq":"non-existent-slug"}},{"slugId":{"eq":"slug"}}]}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: 35 77 | uncompressed: false 78 | body: | 79 | {"data":{"projects":{"nodes":[]}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Length: 88 | - "35" 89 | Content-Type: 90 | - application/json; charset=utf-8 91 | Etag: 92 | - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo" 93 | Server: 94 | - cloudflare 95 | Vary: 96 | - Accept-Encoding 97 | Via: 98 | - 1.1 google 99 | status: 200 OK 100 | code: 200 101 | duration: 0s 102 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/get_project_handler_Invalid project.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 716 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetProject($id: String!) {\n\t\t\tproject(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\tslugId\n\t\t\t\tstate\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tlead {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tmembers {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tteams {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinitiatives(first: 10) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstartDate\n\t\t\t\ttargetDate\n\t\t\t}\n\t\t}\n\t","variables":{"id":"NONEXISTENT-PROJECT"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"errors":[{"message":"Entity not found: Project","path":["project"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced Project."}}],"data":null} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"113-pUQ9mkDn3KWYiQz0UBE51+d7gJ4" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 911 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tquery GetProjectByNameOrSlug($filter: ProjectFilter) {\n\t\t\tprojects(filter: $filter, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\tslugId\n\t\t\t\t\tstate\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tlead {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t\tmembers {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\temail\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tteams {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tinitiatives(first: 1) {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstartDate\n\t\t\t\t\ttargetDate\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"or":[{"name":{"eq":"NONEXISTENT-PROJECT"}},{"slugId":{"eq":"PROJECT"}}]}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: 35 77 | uncompressed: false 78 | body: | 79 | {"data":{"projects":{"nodes":[]}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Length: 88 | - "35" 89 | Content-Type: 90 | - application/json; charset=utf-8 91 | Etag: 92 | - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo" 93 | Server: 94 | - cloudflare 95 | Vary: 96 | - Accept-Encoding 97 | Via: 98 | - 1.1 google 99 | status: 200 OK 100 | code: 200 101 | duration: 0s 102 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/update_project_handler_Update only description.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 733 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetProject($id: String!) {\n\t\t\tproject(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\tslugId\n\t\t\t\tstate\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tlead {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tmembers {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tteams {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinitiatives(first: 10) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstartDate\n\t\t\t\ttargetDate\n\t\t\t}\n\t\t}\n\t","variables":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2","description":"Updated Description","slugId":"e1153169a428","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/updated-project-name-2-e1153169a428","createdAt":"2025-06-28T18:42:20.223Z","updatedAt":"2025-06-28T21:44:56.564Z","lead":null,"members":{"nodes":[]},"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}]},"initiatives":{"nodes":[]},"startDate":null,"targetDate":null}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"21c-zZT5JHii4UiJPH+GXG9r9OBmr2A" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 411 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tmutation ProjectUpdate($id: String!, $input: ProjectUpdateInput!) {\n\t\t\tprojectUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\tslugId\n\t\t\t\t\tstate\n\t\t\t\t\turl\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","input":{"description":"Updated Description Only"}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"projectUpdate":{"success":true,"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2","description":"Updated Description Only","slugId":"e1153169a428","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/updated-project-name-2-e1153169a428"}}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"131-exvq876nrApQ/gFGvqKIsgVXXo0" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/create_issue_handler_Create issue with invalid project.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 717 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetProject($id: String!) {\n\t\t\tproject(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\tslugId\n\t\t\t\tstate\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tlead {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tmembers {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tteams {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinitiatives(first: 10) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstartDate\n\t\t\t\ttargetDate\n\t\t\t}\n\t\t}\n\t","variables":{"id":"non-existent-project"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"errors":[{"message":"Entity not found: Project","path":["project"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced Project."}}],"data":null} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"113-pUQ9mkDn3KWYiQz0UBE51+d7gJ4" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 912 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tquery GetProjectByNameOrSlug($filter: ProjectFilter) {\n\t\t\tprojects(filter: $filter, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\tslugId\n\t\t\t\t\tstate\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tlead {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t\tmembers {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\temail\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tteams {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tinitiatives(first: 1) {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstartDate\n\t\t\t\t\ttargetDate\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"or":[{"name":{"eq":"non-existent-project"}},{"slugId":{"eq":"project"}}]}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: 35 77 | uncompressed: false 78 | body: | 79 | {"data":{"projects":{"nodes":[]}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Length: 88 | - "35" 89 | Content-Type: 90 | - application/json; charset=utf-8 91 | Etag: 92 | - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo" 93 | Server: 94 | - cloudflare 95 | Vary: 96 | - Accept-Encoding 97 | Via: 98 | - 1.1 google 99 | status: 200 OK 100 | code: 200 101 | duration: 0s 102 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/update_project_handler_Non-existent project.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 717 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetProject($id: String!) {\n\t\t\tproject(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\tslugId\n\t\t\t\tstate\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tlead {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tmembers {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tteams {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinitiatives(first: 10) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstartDate\n\t\t\t\ttargetDate\n\t\t\t}\n\t\t}\n\t","variables":{"id":"non-existent-project"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"errors":[{"message":"Entity not found: Project","path":["project"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced Project."}}],"data":null} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"113-pUQ9mkDn3KWYiQz0UBE51+d7gJ4" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 912 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tquery GetProjectByNameOrSlug($filter: ProjectFilter) {\n\t\t\tprojects(filter: $filter, first: 1) {\n\t\t\t\tnodes {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\tslugId\n\t\t\t\t\tstate\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tlead {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t\tmembers {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\temail\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tteams {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tinitiatives(first: 1) {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstartDate\n\t\t\t\t\ttargetDate\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"or":[{"name":{"eq":"non-existent-project"}},{"slugId":{"eq":"project"}}]}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: 35 77 | uncompressed: false 78 | body: | 79 | {"data":{"projects":{"nodes":[]}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Length: 88 | - "35" 89 | Content-Type: 90 | - application/json; charset=utf-8 91 | Etag: 92 | - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo" 93 | Server: 94 | - cloudflare 95 | Vary: 96 | - Accept-Encoding 97 | Via: 98 | - 1.1 google 99 | status: 200 OK 100 | code: 200 101 | duration: 0s 102 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/update_project_handler_Update name and description.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 733 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetProject($id: String!) {\n\t\t\tproject(id: $id) {\n\t\t\t\tid\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\tslugId\n\t\t\t\tstate\n\t\t\t\turl\n\t\t\t\tcreatedAt\n\t\t\t\tupdatedAt\n\t\t\t\tlead {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tmembers {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\temail\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tteams {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinitiatives(first: 10) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstartDate\n\t\t\t\ttargetDate\n\t\t\t}\n\t\t}\n\t","variables":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name","description":"Updated Description Only","slugId":"e1153169a428","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/updated-project-name-e1153169a428","createdAt":"2025-06-28T18:42:20.223Z","updatedAt":"2025-06-28T21:44:56.396Z","lead":null,"members":{"nodes":[]},"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}]},"initiatives":{"nodes":[]},"startDate":null,"targetDate":null}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"21d-jg2vG6vT5da0XDzGnJVlIPQka7E" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 438 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tmutation ProjectUpdate($id: String!, $input: ProjectUpdateInput!) {\n\t\t\tprojectUpdate(id: $id, input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tproject {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t\tdescription\n\t\t\t\t\tslugId\n\t\t\t\t\tstate\n\t\t\t\t\turl\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","input":{"name":"Updated Project Name 2","description":"Updated Description"}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"projectUpdate":{"success":true,"project":{"id":"bfa49864-16c9-44db-994e-a11ba2b386f1","name":"Updated Project Name 2","description":"Updated Description","slugId":"e1153169a428","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/updated-project-name-2-e1153169a428"}}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"12c-qm99WOFVQtdIlEMxVa4cu3vvOAo" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ``` -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- ```go 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/geropl/linear-mcp-go/pkg/linear" 8 | "github.com/geropl/linear-mcp-go/pkg/tools" 9 | "github.com/mark3labs/mcp-go/mcp" 10 | mcpserver "github.com/mark3labs/mcp-go/server" 11 | ) 12 | 13 | const ( 14 | // ServerName is the name of the MCP server 15 | ServerName = "Linear MCP Server" 16 | // ServerVersion is the version of the MCP server 17 | ServerVersion = "1.15.0" 18 | ) 19 | 20 | // LinearMCPServer represents the Linear MCP server 21 | type LinearMCPServer struct { 22 | mcpServer *mcpserver.MCPServer 23 | linearClient *linear.LinearClient 24 | writeAccess bool // Controls whether write operations are enabled 25 | } 26 | 27 | // NewLinearMCPServer creates a new Linear MCP server 28 | func NewLinearMCPServer(writeAccess bool) (*LinearMCPServer, error) { 29 | // Create the Linear client 30 | linearClient, err := linear.NewLinearClientFromEnv(ServerVersion) 31 | if err != nil { 32 | return nil, fmt.Errorf("failed to create Linear client: %w", err) 33 | } 34 | 35 | // Create the MCP server 36 | mcpServer := mcpserver.NewMCPServer(ServerName, ServerVersion) 37 | 38 | // Create the Linear MCP server 39 | server := &LinearMCPServer{ 40 | mcpServer: mcpServer, 41 | linearClient: linearClient, 42 | writeAccess: writeAccess, 43 | } 44 | 45 | // Register tools 46 | RegisterTools(mcpServer, linearClient, writeAccess) 47 | 48 | // Register resources 49 | RegisterResources(mcpServer, linearClient) 50 | 51 | return server, nil 52 | } 53 | 54 | // Start starts the Linear MCP server 55 | func (s *LinearMCPServer) Start() error { 56 | // Check if the Linear API key is set 57 | if os.Getenv("LINEAR_API_KEY") == "" { 58 | return fmt.Errorf("LINEAR_API_KEY environment variable is required") 59 | } 60 | 61 | // Start the server 62 | fmt.Printf("Starting %s v%s\n", ServerName, ServerVersion) 63 | return mcpserver.ServeStdio(s.mcpServer) 64 | } 65 | 66 | // GetMCPServer returns the underlying MCP server 67 | func (s *LinearMCPServer) GetMCPServer() *mcpserver.MCPServer { 68 | return s.mcpServer 69 | } 70 | 71 | // GetLinearClient returns the Linear client 72 | func (s *LinearMCPServer) GetLinearClient() *linear.LinearClient { 73 | return s.linearClient 74 | } 75 | 76 | // GetReadOnlyToolNames returns the names of all read-only tools 77 | func GetReadOnlyToolNames() map[string]bool { 78 | return map[string]bool{ 79 | "linear_search_issues": true, 80 | "linear_get_user_issues": true, 81 | "linear_get_issue": true, 82 | "linear_get_issue_comments": true, 83 | "linear_get_teams": true, 84 | "linear_get_project": true, 85 | "linear_search_projects": true, 86 | "linear_get_milestone": true, 87 | "linear_get_initiative": true, 88 | } 89 | } 90 | 91 | // RegisterTools registers all Linear tools with the MCP server 92 | func RegisterTools(s *mcpserver.MCPServer, linearClient *linear.LinearClient, writeAccess bool) { 93 | // Register tools, based on writeAccess 94 | addTool := func(tool mcp.Tool, handler mcpserver.ToolHandlerFunc) { 95 | if !writeAccess { 96 | if readOnly := GetReadOnlyToolNames()[tool.Name]; !readOnly { 97 | // Skip registering write tools if write access is disabled 98 | return 99 | } 100 | } 101 | s.AddTool(tool, handler) 102 | } 103 | 104 | // Register each tool 105 | addTool(tools.SearchIssuesTool, tools.SearchIssuesHandler(linearClient)) 106 | addTool(tools.GetUserIssuesTool, tools.GetUserIssuesHandler(linearClient)) 107 | addTool(tools.GetIssueTool, tools.GetIssueHandler(linearClient)) 108 | addTool(tools.GetIssueCommentsTool, tools.GetIssueCommentsHandler(linearClient)) 109 | addTool(tools.GetTeamsTool, tools.GetTeamsHandler(linearClient)) 110 | addTool(tools.GetProjectTool, tools.GetProjectHandler(linearClient)) 111 | addTool(tools.SearchProjectsTool, tools.SearchProjectsHandler(linearClient)) 112 | addTool(tools.CreateProjectTool, tools.CreateProjectHandler(linearClient)) 113 | addTool(tools.UpdateProjectTool, tools.UpdateProjectHandler(linearClient)) 114 | addTool(tools.GetMilestoneTool, tools.GetMilestoneHandler(linearClient)) 115 | addTool(tools.CreateMilestoneTool, tools.CreateMilestoneHandler(linearClient)) 116 | addTool(tools.UpdateMilestoneTool, tools.UpdateMilestoneHandler(linearClient)) 117 | addTool(tools.GetInitiativeTool, tools.GetInitiativeHandler(linearClient)) 118 | addTool(tools.CreateInitiativeTool, tools.CreateInitiativeHandler(linearClient)) 119 | addTool(tools.UpdateInitiativeTool, tools.UpdateInitiativeHandler(linearClient)) 120 | addTool(tools.CreateIssueTool, tools.CreateIssueHandler(linearClient)) 121 | addTool(tools.UpdateIssueTool, tools.UpdateIssueHandler(linearClient)) 122 | addTool(tools.AddCommentTool, tools.AddCommentHandler(linearClient)) 123 | addTool(tools.ReplyToCommentTool, tools.ReplyToCommentHandler(linearClient)) 124 | addTool(tools.UpdateCommentTool, tools.UpdateCommentHandler(linearClient)) 125 | } 126 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/get_user_issues_handler_Current user issues.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 88 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetCurrentUser {\n\t\t\tviewer {\n\t\t\t\tid\n\t\t\t}\n\t\t}\n\t"}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"viewer":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85"}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"42-6PvRWCcs8jOXFygoL3FhQP+PK3o" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 556 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tquery GetUserIssues($userId: String!, $first: Int, $includeArchived: Boolean) {\n\t\t\tuser(id: $userId) {\n\t\t\t\tassignedIssues(first: $first, includeArchived: $includeArchived) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tidentifier\n\t\t\t\t\t\ttitle\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tpriority\n\t\t\t\t\t\turl\n\t\t\t\t\t\tstate {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"first":5,"includeArchived":false,"userId":"cc24eee4-9edc-4bfe-b91b-fedde125ba85"}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: "{\"data\":{\"user\":{\"assignedIssues\":{\"nodes\":[{\"id\":\"1c2de93f-4321-4015-bfde-ee893ef7976f\",\"identifier\":\"TEST-10\",\"title\":\"Updated Test Issue\",\"description\":null,\"priority\":0,\"url\":\"https://linear.app/linear-mcp-go-test/issue/TEST-10/updated-test-issue\",\"state\":{\"id\":\"42f7ad15-fca3-4d11-b349-0e3c1385c256\",\"name\":\"Backlog\"}},{\"id\":\"c58953c5-a31d-4c5a-9427-6d6ebd9a1a4e\",\"identifier\":\"TEST-1\",\"title\":\"Welcome to Linear \U0001F44B\",\"description\":\"Hi there. Complete these issues to learn how to use Linear and discover ✨**ProTips.** When you're done, delete them or move them to another team for others to view.\\n\\n### **To start, type** `C` to **create your first issue.**\\n\\nCreate issues from any view using `C` or by clicking the `New issue` button.\\n\\n \\n\\n[1189b618-97f2-4e2c-ae25-4f25467679e7](https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/532d146d-bcd6-4602-bf1f-83f674b70fff/1189b618-97f2-4e2c-ae25-4f25467679e7)\\n\\nOur issue editor and comments support Markdown. You can also: \\n\\n* @mention a teammate\\n* Drag & drop images or video (Loom & YouTube embed automatically)\\n* Use emoji ✅\\n* Type `/` to bring up more formatting options\",\"priority\":2,\"url\":\"https://linear.app/linear-mcp-go-test/issue/TEST-1/welcome-to-linear\",\"state\":{\"id\":\"cffb8999-f10e-447d-9672-8faf5b06ac67\",\"name\":\"Todo\"}}]}}}}\n" 79 | headers: 80 | Alt-Svc: 81 | - h3=":443"; ma=86400 82 | Cache-Control: 83 | - no-store 84 | Cf-Cache-Status: 85 | - DYNAMIC 86 | Content-Type: 87 | - application/json; charset=utf-8 88 | Etag: 89 | - W/"52b-iwXdi0Au8LYVFWrptXXwpSz7HVA" 90 | Server: 91 | - cloudflare 92 | Vary: 93 | - Accept-Encoding 94 | Via: 95 | - 1.1 google 96 | status: 200 OK 97 | code: 200 98 | duration: 0s 99 | ``` -------------------------------------------------------------------------------- /testdata/fixtures/create_issue_handler_Create sub issue with labels.yaml: -------------------------------------------------------------------------------- ```yaml 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 350 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: api.linear.app 13 | remote_addr: "" 14 | request_uri: "" 15 | body: '{"query":"\n\t\tquery GetLabelsByName($teamId: String!, $names: [String!]!) {\n\t\t\tteam(id: $teamId) {\n\t\t\t\tlabels(filter: { name: { in: $names } }) {\n\t\t\t\t\tnodes {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"names":["ws-label 2","Feature"],"teamId":"234c5451-a839-4c8f-98d9-da00973f1060"}}' 16 | form: {} 17 | headers: 18 | Content-Type: 19 | - application/json 20 | url: https://api.linear.app/graphql 21 | method: POST 22 | response: 23 | proto: HTTP/2.0 24 | proto_major: 2 25 | proto_minor: 0 26 | transfer_encoding: [] 27 | trailer: {} 28 | content_length: -1 29 | uncompressed: true 30 | body: | 31 | {"data":{"team":{"labels":{"nodes":[{"id":"fcd49e32-5043-4bfd-88a5-2bbe3c95124a","name":"ws-label 2"},{"id":"94087865-ce6c-470b-896c-4d1d2c7456b8","name":"Feature"}]}}}} 32 | headers: 33 | Alt-Svc: 34 | - h3=":443"; ma=86400 35 | Cache-Control: 36 | - no-store 37 | Cf-Cache-Status: 38 | - DYNAMIC 39 | Content-Type: 40 | - application/json; charset=utf-8 41 | Etag: 42 | - W/"aa-fC3D/tel+q+/3XRkoHnKGoa70sg" 43 | Server: 44 | - cloudflare 45 | Vary: 46 | - Accept-Encoding 47 | Via: 48 | - 1.1 google 49 | status: 200 OK 50 | code: 200 51 | duration: 0s 52 | - id: 1 53 | request: 54 | proto: HTTP/1.1 55 | proto_major: 1 56 | proto_minor: 1 57 | content_length: 983 58 | transfer_encoding: [] 59 | trailer: {} 60 | host: api.linear.app 61 | remote_addr: "" 62 | request_uri: "" 63 | body: '{"query":"\n\t\tmutation CreateIssue($input: IssueCreateInput!) {\n\t\t\tissueCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tstate {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tteam {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t\tlabels {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tprojectMilestone {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"description":"","labelIds":["fcd49e32-5043-4bfd-88a5-2bbe3c95124a","94087865-ce6c-470b-896c-4d1d2c7456b8"],"parentId":"1c2de93f-4321-4015-bfde-ee893ef7976f","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Sub Issue with Labels"}}}' 64 | form: {} 65 | headers: 66 | Content-Type: 67 | - application/json 68 | url: https://api.linear.app/graphql 69 | method: POST 70 | response: 71 | proto: HTTP/2.0 72 | proto_major: 2 73 | proto_minor: 0 74 | transfer_encoding: [] 75 | trailer: {} 76 | content_length: -1 77 | uncompressed: true 78 | body: | 79 | {"data":{"issueCreate":{"success":true,"issue":{"id":"bff52c02-bc32-422b-ba31-719c9b0f525f","identifier":"TEST-92","title":"Sub Issue with Labels","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-92/sub-issue-with-labels","createdAt":"2025-10-06T09:44:41.506Z","updatedAt":"2025-10-06T09:44:41.506Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[{"id":"fcd49e32-5043-4bfd-88a5-2bbe3c95124a","name":"ws-label 2"},{"id":"94087865-ce6c-470b-896c-4d1d2c7456b8","name":"Feature"}]},"project":null,"projectMilestone":null}}}} 80 | headers: 81 | Alt-Svc: 82 | - h3=":443"; ma=86400 83 | Cache-Control: 84 | - no-store 85 | Cf-Cache-Status: 86 | - DYNAMIC 87 | Content-Type: 88 | - application/json; charset=utf-8 89 | Etag: 90 | - W/"2b0-QsIb1mXyrKfU1pD5SHlQtNywn7k" 91 | Server: 92 | - cloudflare 93 | Vary: 94 | - Accept-Encoding 95 | Via: 96 | - 1.1 google 97 | status: 200 OK 98 | code: 200 99 | duration: 0s 100 | ```