#
tokens: 49073/50000 76/150 files (page 1/6)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 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

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | linear-mcp-go
2 | .context
```

--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------

```yaml
 1 | 
 2 | tasks:
 3 |   
 4 |   - name: Build and test the Go project
 5 |     init: |
 6 |       go build ./... && go test ./...
 7 | 
 8 |   - name: Setup Linear MCP Server and Cline
 9 |     init: |      
10 |       # Install Linear MCP Server and register with Linear Cline
11 |       # Note: make sure to set LINEAR_API_KEY in the Gitpod environment variables for the MCP server to work
12 |       ./scripts/register-cline.sh
13 | 
14 | # Additional Gitpod configuration
15 | ports:
16 |   - port: 3000-8000
17 |     onOpen: ignore
18 | 
19 | vscode:
20 |   extensions:
21 |     - golang.go
22 | 
```

--------------------------------------------------------------------------------
/docs/prd/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Linear MCP Server PRD Documentation
 2 | 
 3 | This directory contains Product Requirements Documents (PRDs) for the Linear MCP Server project.
 4 | 
 5 | ## Available Documents
 6 | 
 7 | | Document | Description |
 8 | |----------|-------------|
 9 | | [000-tool-standardization-overview.md](./000-tool-standardization-overview.md) | Executive summary and overview of the tool standardization effort |
10 | | [001-api-refresher.md](./001-api-refresher.md) | Documentation on the Linear API integration |
11 | | [002-tool-standardization.md](./002-tool-standardization.md) | Detailed requirements for tool standardization |
12 | | [003-tool-standardization-implementation.md](./003-tool-standardization-implementation.md) | Implementation guide for tool standardization |
13 | | [004-tool-standardization-tracking.md](./004-tool-standardization-tracking.md) | Tracking sheet for implementation progress |
14 | | [005-sample-implementation.md](./005-sample-implementation.md) | Sample code and implementation examples |
15 | 
16 | ## Tool Standardization Series
17 | 
18 | The tool standardization documents (000, 002, 003, 004, 005) form a series that outlines the requirements, implementation plan, and tracking for standardizing the Linear MCP Server tools according to a set of consistent rules:
19 | 
20 | 1. **Rule 1: Concise Tool Descriptions**
21 |    - Tool descriptions should be concise and focus only on the tool's purpose and functionality
22 | 
23 | 2. **Rule 2: Flexible Object Identifier Resolution**
24 |    - Input arguments that reference Linear objects should handle multiple values that identify the object
25 | 
26 | 3. **Rule 3: Consistent Entity Rendering**
27 |    - Tools fetching the same entities should emit results using the same format
28 | 
29 | ## How to Use This Documentation
30 | 
31 | 1. Start with [000-tool-standardization-overview.md](./000-tool-standardization-overview.md) for a high-level overview
32 | 2. Read [002-tool-standardization.md](./002-tool-standardization.md) for detailed requirements
33 | 3. Refer to [003-tool-standardization-implementation.md](./003-tool-standardization-implementation.md) for implementation details
34 | 4. Use [004-tool-standardization-tracking.md](./004-tool-standardization-tracking.md) to track progress
35 | 5. See [005-sample-implementation.md](./005-sample-implementation.md) for code examples
36 | 
37 | ## Contributing
38 | 
39 | When adding new PRDs to this directory, follow these guidelines:
40 | 
41 | 1. Use a three-digit prefix (e.g., 006-) to ensure proper ordering
42 | 2. Include a clear title that describes the document's purpose
43 | 3. Link to related documents when appropriate
44 | 4. Update this README.md file to include the new document
45 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Linear MCP Server
  2 | 
  3 | A Model Context Protocol (MCP) server for Linear, written in Go. This server provides tools for interacting with the Linear API through the MCP protocol.
  4 | 
  5 | ## Features
  6 | 
  7 | - Create, update, and search Linear issues
  8 | - Get issues assigned to a user
  9 | - Add comments to issues and reply to existing comments
 10 | - **URL-aware comment operations** - paste Linear comment URLs directly, no manual ID extraction needed
 11 | - Retrieve team information
 12 | - Rate-limited API requests to respect Linear's API limits
 13 | 
 14 | ## Prerequisites
 15 | 
 16 | - Go 1.23 or higher
 17 | - Linear API key
 18 | 
 19 | ## Installation
 20 | 
 21 | ### From Releases
 22 | 
 23 | Pre-built binaries are available for Linux, macOS, and Windows on the [GitHub Releases page](https://github.com/geropl/linear-mcp-go/releases).
 24 | 
 25 | 1. Download the appropriate binary for your platform
 26 | 2. Make it executable (Linux/macOS):
 27 | 
 28 | ```bash
 29 | chmod +x linear-mcp-go-*
 30 | ```
 31 | 
 32 | 3. Run the binary as described in the Usage section
 33 | 
 34 | ### Automated
 35 | 
 36 | ```
 37 | # Download linux binary for the latest release
 38 | RELEASE=$(curl -s https://api.github.com/repos/geropl/linear-mcp-go/releases/latest)
 39 | DOWNLOAD_URL=$(echo $RELEASE | jq -r '.assets[] | select(.name | contains("linux")) | .browser_download_url')
 40 | curl -L -o ./linear-mcp-go $DOWNLOAD_URL
 41 | chmod +x ./linear-mcp-go
 42 | 
 43 | # Setup the mcp server (.gitpod.yml, dotfiles repo, etc.)
 44 | ./linear-mcp-go setup --tool=cline
 45 | ```
 46 | 
 47 | ## Usage
 48 | 
 49 | ### Checking Version
 50 | 
 51 | To check the version of the Linear MCP server:
 52 | 
 53 | ```bash
 54 | ./linear-mcp-go version
 55 | ```
 56 | 
 57 | This will display the version, git commit, and build date information.
 58 | 
 59 | ### Running the Server
 60 | 
 61 | 1. Set your Linear API key as an environment variable:
 62 | 
 63 | ```bash
 64 | export LINEAR_API_KEY=your_linear_api_key
 65 | ```
 66 | 
 67 | 2. Run the server:
 68 | 
 69 | ```bash
 70 | # Run in read-only mode (default)
 71 | ./linear-mcp-go serve
 72 | 
 73 | # Run with write access enabled
 74 | ./linear-mcp-go serve --write-access
 75 | ```
 76 | 
 77 | The server will start and listen for MCP requests on stdin/stdout.
 78 | 
 79 | ### Setting Up for AI Assistants
 80 | 
 81 | The `setup` command automates the installation and configuration process for various AI assistants:
 82 | 
 83 | ```bash
 84 | # Set your Linear API key as an environment variable
 85 | # Only exception: Ona does not require this for setup!
 86 | export LINEAR_API_KEY=your_linear_api_key
 87 | 
 88 | # Set up for Cline (default)
 89 | ./linear-mcp-go setup
 90 | 
 91 | # Set up with write access enabled
 92 | ./linear-mcp-go setup --write-access
 93 | 
 94 | # Set up with auto-approval for read-only tools
 95 | ./linear-mcp-go setup --auto-approve=allow-read-only
 96 | 
 97 | # Set up with specific tools auto-approved
 98 | ./linear-mcp-go setup --auto-approve=linear_get_issue,linear_search_issues
 99 | 
100 | # Set up with write access and auto-approval for read-only tools
101 | ./linear-mcp-go setup --write-access --auto-approve=allow-read-only
102 | 
103 | # Set up for a different tool (only "cline" supported for now)
104 | ./linear-mcp-go setup --tool=cline
105 | ```
106 | 
107 | This command:
108 | 1. Checks if the Linear MCP binary is already installed
109 | 2. Copies the current binary to the installation directory if needed
110 | 3. Configures the AI assistant to use the Linear MCP server
111 | 4. Sets up auto-approval for specified tools if requested
112 | 
113 | The `--auto-approve` flag can be used to specify which tools should be auto-approved in the Cline configuration:
114 | - `--auto-approve=allow-read-only`: Auto-approves all read-only tools (`linear_search_issues`, `linear_get_user_issues`, `linear_get_issue`, `linear_get_teams`)
115 | - `--auto-approve=tool1,tool2,...`: Auto-approves the specified comma-separated list of tools
116 | 
117 | Currently supported AI assistants:
118 | - Cline (VSCode extension)
119 | 
120 | By default, the server runs in read-only mode, which means the following tools are disabled:
121 | - `linear_create_issue`
122 | - `linear_update_issue`
123 | - `linear_add_comment`
124 | - `linear_reply_to_comment`
125 | - `linear_update_issue_comment`
126 | 
127 | To enable these tools, use the `--write-access=true` flag.
128 | 
129 | ## Available Tools
130 | 
131 | ### linear_create_issue
132 | 
133 | Creates a new Linear issue with specified details. **Supports creating parent-child relationships** (sub-issues) and assigning labels.
134 | 
135 | **Parameters:**
136 | - `title` (required): Issue title
137 | - `team` (required): Team identifier (key, UUID or name)
138 | - `description`: Issue description
139 | - `priority`: Priority. Accepts: 0/'no priority', 1/'urgent', 2/'high', 3/'medium', 4/'low'
140 | - `status`: Issue status
141 | - `makeSubissueOf`: **Create a sub-issue by specifying the parent issue ID or identifier** (e.g., 'TEAM-123'). This establishes a parent-child relationship in Linear.
142 | - `labels`: Optional comma-separated list of label IDs or names to assign
143 | - `project`: Optional project identifier (ID, name, or slug) to assign the issue to
144 | 
145 | **Example: Creating a sub-issue**
146 | ```json
147 | {
148 |   "title": "Implement login form validation",
149 |   "team": "ENG",
150 |   "makeSubissueOf": "ENG-42",
151 |   "description": "Add client-side validation for the login form"
152 | }
153 | ```
154 | 
155 | ### linear_update_issue
156 | 
157 | Updates an existing Linear issue's properties.
158 | 
159 | **Parameters:**
160 | - `id` (required): Issue ID
161 | - `title`: New title
162 | - `description`: New description
163 | - `priority`: Priority. Accepts: 0/'no priority', 1/'urgent', 2/'high', 3/'medium', 4/'low'
164 | - `status`: New status
165 | 
166 | ### linear_search_issues
167 | 
168 | Searches Linear issues using flexible criteria.
169 | 
170 | **Parameters:**
171 | - `query`: Optional text to search in title and description
172 | - `teamId`: Filter by team ID
173 | - `status`: Filter by status name (e.g., 'In Progress', 'Done')
174 | - `assigneeId`: Filter by assignee's user ID
175 | - `labels`: Filter by label names (comma-separated)
176 | - `priority`: Priority. Accepts: 0/'no priority', 1/'urgent', 2/'high', 3/'medium', 4/'low'
177 | - `estimate`: Filter by estimate points
178 | - `includeArchived`: Include archived issues in results (default: false)
179 | - `limit`: Max results to return (default: 10)
180 | 
181 | ### linear_get_user_issues
182 | 
183 | Retrieves issues assigned to a specific user or the authenticated user.
184 | 
185 | **Parameters:**
186 | - `userId`: Optional user ID. If not provided, returns authenticated user's issues
187 | - `includeArchived`: Include archived issues in results
188 | - `limit`: Maximum number of issues to return (default: 50)
189 | 
190 | ### linear_get_issue
191 | 
192 | Retrieves a single Linear issue by its ID.
193 | 
194 | **Parameters:**
195 | - `issueId` (required): ID of the issue to retrieve
196 | 
197 | ### linear_add_comment
198 | 
199 | Adds a comment to an existing Linear issue. Supports replying to existing comments by passing a comment identifier in the `thread` parameter.
200 | 
201 | **Parameters:**
202 | - `issue` (required): ID or identifier (e.g., 'TEAM-123') of the issue to comment on
203 | - `body` (required): Comment text in markdown format
204 | - `thread`: Optional comment identifier to reply to. Accepts: full Linear comment URL, UUID, shorthand (comment-abc123), or hash (abc123). Creates a threaded reply instead of a top-level comment.
205 | - `createAsUser`: Optional custom username to show for the comment
206 | 
207 | **URL Support:** You can pass a full Linear comment URL (e.g., `https://linear.app/.../issue/TEST-10/...#comment-abc123`) directly to the `thread` parameter. The tool automatically resolves URLs to UUIDs before calling the API.
208 | 
209 | ### linear_reply_to_comment
210 | 
211 | Convenience tool for replying to an existing comment. Automatically resolves the issue from the comment, so you only need to provide the comment identifier and reply text.
212 | 
213 | **Parameters:**
214 | - `thread` (required): Comment to reply to. Accepts: full Linear comment URL, UUID, shorthand (comment-abc123), or hash (abc123)
215 | - `body` (required): Reply text in markdown format
216 | - `createAsUser`: Optional custom username to show for the reply
217 | 
218 | **Why use this tool?** When you have a comment URL or ID and want to reply, this tool is simpler than `linear_add_comment` because you don't need to specify the issue separately. The tool automatically looks up the issue from the comment.
219 | 
220 | ### linear_get_issue_comments
221 | 
222 | Retrieves comments for a Linear issue with support for pagination and thread navigation.
223 | 
224 | **Parameters:**
225 | - `issue` (required): ID or identifier (e.g., 'TEAM-123') of the issue to retrieve comments for
226 | - `thread`: Optional UUID of a parent comment to retrieve its replies. If not provided, returns top-level comments
227 | - `limit`: Maximum number of comments to return (default: 10)
228 | - `after`: Cursor for pagination, to get comments after this point
229 | 
230 | **Use Cases:**
231 | - View all comments on an issue
232 | - Navigate comment threads by passing a comment UUID in the `thread` parameter
233 | - Get comment UUIDs for replying (though with URL support in `linear_add_comment`, this is less necessary)
234 | 
235 | ### linear_update_issue_comment
236 | 
237 | Updates an existing comment on a Linear issue.
238 | 
239 | **Parameters:**
240 | - `comment` (required): Comment identifier to update. Accepts: full Linear comment URL, UUID, shorthand (comment-abc123), or hash (abc123)
241 | - `body` (required): New comment text in markdown format
242 | 
243 | **URL Support:** Like other comment tools, this accepts full Linear comment URLs and automatically resolves them to UUIDs.
244 | 
245 | ### linear_get_teams
246 | 
247 | Retrieves Linear teams with an optional name filter.
248 | 
249 | **Parameters:**
250 | - `name`: Optional team name filter. Returns teams whose names contain this string.
251 | 
252 | ## Test
253 | Tests are implemented using [`go-vcr`](https://github.com/dnaeon/go-vcr), and executed against https://linear.app/linear-mcp-go-test.
254 | 
255 | ### Execute tests
256 | 
257 | Using the existing recordings (cassettes):
258 | ```
259 | go test -v ./...
260 | ```
261 | 
262 | #### Re-recording test:
263 | 
264 | Requires `TEST_LINEAR_API_KEY` to be set for the test workspace.
265 | 
266 | ```
267 | go test -v -record=true ./...
268 | ```
269 | This will update all tests that don't alter remote state.
270 | 
271 | 
272 | ```
273 | go test -v -recordWrites=true ./...
274 | ```
275 | This will re-run all tests, including some that might alter the outcome of other tests cases, which might require further manual work to adjust.
276 | 
277 | ```
278 | go test -v -golden=true ./...
279 | ```
280 | Updates all .golden fields.
281 | 
282 | ## Release Process
283 | 
284 | The project uses GitHub Actions for automated testing and releases. The version is managed through the `ServerVersion` constant in `pkg/server/server.go`.
285 | 
286 | ### Automated Testing and Building
287 | 
288 | 1. All pushes to the main branch and pull requests are automatically tested
289 | 2. When a tag matching the pattern `v*` (e.g., `v1.0.0`) is pushed, a new release is automatically created
290 | 3. Binaries for Linux, macOS, and Windows are built and attached to the release with build-time information (git commit and build date)
291 | 
292 | ### Creating a New Release
293 | 
294 | **Important**: Version tags should only be created against the `main` branch after all changes have been merged.
295 | 
296 | 1. **Update the version**: Modify the `ServerVersion` constant in `pkg/server/server.go`
297 |    ```go
298 |    // ServerVersion is the version of the MCP server
299 |    ServerVersion = "1.13.0"
300 |    ```
301 | 
302 | 2. **Create a PR**: Submit the version update as a pull request to ensure it goes through review and testing
303 | 
304 | 3. **Merge to main**: Once the PR is approved and merged to the main branch
305 | 
306 | 4. **Create and push the release tag**: 
307 |    ```bash
308 |    # Ensure you're on the latest main branch
309 |    git checkout main
310 |    git pull origin main
311 |    
312 |    # Create and push the tag (must match the version in server.go)
313 |    git tag v1.13.0
314 |    git push origin v1.13.0
315 |    ```
316 | 
317 | 5. **Automated release**: The GitHub Actions workflow will automatically:
318 |    - Build binaries for all platforms with proper version information
319 |    - Create a GitHub release with the tag
320 |    - Attach the compiled binaries to the release
321 | 
322 | ### Version Information
323 | 
324 | The `version` command displays:
325 | - **Version**: Read from `ServerVersion` constant in `pkg/server/server.go`
326 | - **Git commit**: Injected at build time from the current commit hash
327 | - **Build date**: Injected at build time with the current timestamp
328 | 
329 | For development builds, git commit and build date will show "unknown".
330 | 
331 | ## License
332 | 
333 | MIT
334 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/add_comment_handler_Missing issue.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/add_comment_handler_Missing issueId.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

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

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Missing team.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Missing teamId.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Missing title.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

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

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

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

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/get_issue_comments_handler_Missing issue.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/get_issue_handler_Missing issue.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/get_project_handler_Missing project param.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/reply_to_comment_handler_Missing body.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/reply_to_comment_handler_Missing thread.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/resource_TeamResourceHandler_Missing ID.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/search_projects_handler_Empty query.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/update_comment_handler_Missing body.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

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

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/update_issue_handler_Missing id.yaml:
--------------------------------------------------------------------------------

```yaml
1 | ---
2 | version: 2
3 | interactions: []
4 | 
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
1 | package main
2 | 
3 | import "github.com/geropl/linear-mcp-go/cmd"
4 | 
5 | func main() {
6 | 	cmd.Execute()
7 | }
8 | 
```

--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"name": "Gitpod",
 3 | 	"build": {
 4 |         "context": ".",
 5 |         "dockerfile": "Dockerfile"
 6 |     },
 7 | 	"features": {
 8 | 		"ghcr.io/devcontainers/features/go:1": {}
 9 | 	}
10 | }
11 | 
```

--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
1 | FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04
2 | 
3 | # use this Dockerfile to install additional tools you might need, e.g.
4 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
5 | #     && apt-get -y install --no-install-recommends <your-package-list-here>
6 | 
```

--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------

```go
 1 | package cmd
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 
 6 | 	"github.com/geropl/linear-mcp-go/pkg/server"
 7 | 	"github.com/spf13/cobra"
 8 | )
 9 | 
10 | // Build information - these will be set at build time
11 | var (
12 | 	GitCommit = "unknown"
13 | 	BuildDate = "unknown"
14 | )
15 | 
16 | // versionCmd represents the version command
17 | var versionCmd = &cobra.Command{
18 | 	Use:   "version",
19 | 	Short: "Print the version information",
20 | 	Long:  `Print the version information for the Linear MCP server.`,
21 | 	Run: func(cmd *cobra.Command, args []string) {
22 | 		fmt.Printf("Linear MCP Server %s\n", server.ServerVersion)
23 | 		fmt.Printf("Git commit: %s\n", GitCommit)
24 | 		fmt.Printf("Build date: %s\n", BuildDate)
25 | 	},
26 | }
27 | 
28 | func init() {
29 | 	rootCmd.AddCommand(versionCmd)
30 | }
```

--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------

```go
 1 | package cmd
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 
 7 | 	"github.com/spf13/cobra"
 8 | )
 9 | 
10 | // rootCmd represents the base command when called without any subcommands
11 | var rootCmd = &cobra.Command{
12 | 	Use:   "linear-mcp-go",
13 | 	Short: "Linear MCP Server - A Model Context Protocol server for Linear",
14 | 	Long: `Linear MCP Server is a Model Context Protocol (MCP) server for Linear.
15 | It provides tools for interacting with the Linear API through the MCP protocol,
16 | enabling AI assistants to manage Linear issues and workflows.`,
17 | }
18 | 
19 | // Execute adds all child commands to the root command and sets flags appropriately.
20 | // This is called by main.main(). It only needs to happen once to the rootCmd.
21 | func Execute() {
22 | 	if err := rootCmd.Execute(); err != nil {
23 | 		fmt.Println(err)
24 | 		os.Exit(1)
25 | 	}
26 | }
```

--------------------------------------------------------------------------------
/memory-bank/projectbrief.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Project Brief: Linear MCP Server
 2 | 
 3 | ## Overview
 4 | Linear MCP Server is a Model Context Protocol (MCP) server for Linear, written in Go. It provides tools for interacting with the Linear API through the MCP protocol, enabling AI assistants to manage Linear issues and workflows.
 5 | 
 6 | ## Core Requirements
 7 | 1. Provide MCP tools for Linear API operations
 8 | 2. Handle authentication via Linear API key
 9 | 3. Support issue creation, updating, and searching
10 | 4. Enable comment addition to issues
11 | 5. Implement rate limiting to respect Linear's API limits
12 | 6. Ensure proper error handling and user feedback
13 | 
14 | ## Goals
15 | - Create a reliable interface between AI assistants and Linear
16 | - Simplify Linear operations through standardized MCP tools
17 | - Maintain compatibility with the MCP protocol specification
18 | - Provide comprehensive documentation for users
19 | 
20 | ## Project Scope
21 | - **In Scope**: Linear API integration, MCP server implementation, basic error handling, rate limiting
22 | - **Out of Scope**: UI development, authentication management beyond API key
23 | 
24 | ## Timeline
25 | - Initial development: Complete
26 | - Release workflow: In progress
27 | - Future enhancements: TBD
28 | 
29 | ## Success Criteria
30 | - All specified Linear operations work correctly through MCP tools
31 | - Server handles errors gracefully
32 | - Documentation is clear and comprehensive
33 | - Release process is automated
34 | 
```

--------------------------------------------------------------------------------
/pkg/tools/get_teams.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 | // GetTeamsTool is the tool definition for getting teams
12 | var GetTeamsTool = mcp.NewTool("linear_get_teams",
13 | 	mcp.WithDescription("Retrieves Linear teams."),
14 | 	mcp.WithString("name", mcp.Description("Optional team name filter. Returns teams whose names contain this string.")),
15 | )
16 | 
17 | // GetTeamsHandler handles the linear_get_teams tool
18 | func GetTeamsHandler(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 | 		name := request.GetString("name", "")
22 | 
23 | 		// Get teams
24 | 		teams, err := linearClient.GetTeams(name)
25 | 		if err != nil {
26 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get teams: %v", err)}}}, nil
27 | 		}
28 | 
29 | 		// Format the result
30 | 		resultText := fmt.Sprintf("Found %d teams:\n", len(teams))
31 | 		for _, team := range teams {
32 | 			// Create a pointer to the team for formatTeamIdentifier
33 | 			teamPtr := &team
34 | 			resultText += fmt.Sprintf("- %s\n", formatTeamIdentifier(teamPtr))
35 | 			resultText += fmt.Sprintf("  Key: %s\n", team.Key)
36 | 		}
37 | 
38 | 		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
39 | 	}
40 | }
41 | 
```

--------------------------------------------------------------------------------
/cmd/serve.go:
--------------------------------------------------------------------------------

```go
 1 | package cmd
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 	"strings"
 7 | 
 8 | 	"github.com/geropl/linear-mcp-go/pkg/server"
 9 | 	"github.com/spf13/cobra"
10 | )
11 | 
12 | // serveCmd represents the serve command
13 | var serveCmd = &cobra.Command{
14 | 	Use:   "serve",
15 | 	Short: "Start the Linear MCP server",
16 | 	Long: `Start the Linear MCP server that listens for MCP requests on stdin/stdout.
17 | The server provides tools for interacting with the Linear API through the MCP protocol.`,
18 | 	Run: func(cmd *cobra.Command, args []string) {
19 | 		writeAccess, _ := cmd.Flags().GetBool("write-access")
20 | 		writeAccessChanged := cmd.Flags().Changed("write-access")
21 | 
22 | 		// Check LINEAR_WRITE_ACCESS environment variable if flag wasn't explicitly set
23 | 		if !writeAccessChanged {
24 | 			if envWriteAccess := os.Getenv("LINEAR_WRITE_ACCESS"); envWriteAccess != "" {
25 | 				envValue := strings.ToLower(strings.TrimSpace(envWriteAccess))
26 | 				if envValue == "true" {
27 | 					writeAccess = true
28 | 				} else if envValue == "false" {
29 | 					writeAccess = false
30 | 				}
31 | 				// If the env var is set to something other than "true" or "false", ignore it and use default
32 | 			}
33 | 		}
34 | 
35 | 		// Create the Linear MCP server
36 | 		linearServer, err := server.NewLinearMCPServer(writeAccess)
37 | 		if err != nil {
38 | 			fmt.Printf("Failed to create Linear MCP server: %v\n", err)
39 | 			os.Exit(1)
40 | 		}
41 | 
42 | 		// Start the server
43 | 		if err := linearServer.Start(); err != nil {
44 | 			fmt.Printf("Server error: %v\n", err)
45 | 			os.Exit(1)
46 | 		}
47 | 	},
48 | }
49 | 
50 | func init() {
51 | 	rootCmd.AddCommand(serveCmd)
52 | 
53 | 	// Add flags to the serve command
54 | 	serveCmd.Flags().Bool("write-access", false, "Enable tools that modify Linear data (create/update issues, add comments)")
55 | }
56 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/get_issue_comments_handler_Invalid issue.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | ---
 2 | version: 2
 3 | interactions:
 4 |     - id: 0
 5 |       request:
 6 |         proto: HTTP/1.1
 7 |         proto_major: 1
 8 |         proto_minor: 1
 9 |         content_length: 330
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":123,"teamKey":"NONEXISTENT"}}'
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: 33
29 |         uncompressed: false
30 |         body: |
31 |             {"data":{"issues":{"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-Length:
40 |                 - "33"
41 |             Content-Type:
42 |                 - application/json; charset=utf-8
43 |             Etag:
44 |                 - W/"21-PKFa7EZ3q+7ITZ8vZtp2aqgNJxo"
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 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/get_issue_handler_Missing issueId.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: 330
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":123,"teamKey":"NONEXISTENT"}}'
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: 33
29 |         uncompressed: false
30 |         body: |
31 |             {"data":{"issues":{"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-Length:
40 |                 - "33"
41 |             Content-Type:
42 |                 - application/json; charset=utf-8
43 |             Etag:
44 |                 - W/"21-PKFa7EZ3q+7ITZ8vZtp2aqgNJxo"
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 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/get_initiative_handler_Valid initiative.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":"3bb752a7-897e-4240-9306-01e48872fab3"}}'
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":"3bb752a7-897e-4240-9306-01e48872fab3","name":"Created Test Initiative","description":null,"url":"https://linear.app/linear-mcp-go-test/initiative/created-test-initiative-7ed59af889f6"}}}
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/"d8-hd6//06VmE0Pm1n9UOQfuP2hyLQ"
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/add_comment_handler_Missing body.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 | 
```

--------------------------------------------------------------------------------
/pkg/tools/priority.go:
--------------------------------------------------------------------------------

```go
 1 | package tools
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"strconv"
 6 | 	"strings"
 7 | 
 8 | 	"github.com/mark3labs/mcp-go/mcp"
 9 | )
10 | 
11 | const (
12 | 	PriorityNone   = 0
13 | 	PriorityUrgent = 1
14 | 	PriorityHigh   = 2
15 | 	PriorityMedium = 3
16 | 	PriorityLow    = 4
17 | )
18 | 
19 | var priorityNames = map[int]string{
20 | 	PriorityNone:   "No priority",
21 | 	PriorityUrgent: "Urgent",
22 | 	PriorityHigh:   "High",
23 | 	PriorityMedium: "Medium",
24 | 	PriorityLow:    "Low",
25 | }
26 | 
27 | var priorityFromName = map[string]int{
28 | 	"no priority": PriorityNone,
29 | 	"none":        PriorityNone,
30 | 	"urgent":      PriorityUrgent,
31 | 	"high":        PriorityHigh,
32 | 	"medium":      PriorityMedium,
33 | 	"low":         PriorityLow,
34 | }
35 | 
36 | // priorityToString converts numeric priority to textual representation
37 | func priorityToString(priority int) string {
38 | 	if name, ok := priorityNames[priority]; ok {
39 | 		return name
40 | 	}
41 | 	return "Unknown"
42 | }
43 | 
44 | // parsePriority accepts both numeric (0-4) and textual representations
45 | // Returns the numeric value and an error if invalid
46 | func parsePriority(input string) (int, error) {
47 | 	input = strings.TrimSpace(strings.ToLower(input))
48 | 
49 | 	// Try parsing as number first
50 | 	if num, err := strconv.Atoi(input); err == nil {
51 | 		if num >= PriorityNone && num <= PriorityLow {
52 | 			return num, nil
53 | 		}
54 | 		return 0, fmt.Errorf("priority number must be between 0 and 4, got %d", num)
55 | 	}
56 | 
57 | 	// Try parsing as text
58 | 	if priority, ok := priorityFromName[input]; ok {
59 | 		return priority, nil
60 | 	}
61 | 
62 | 	return 0, fmt.Errorf("invalid priority: %s (valid values: 0-4, no priority, urgent, high, medium, low)", input)
63 | }
64 | 
65 | // getPriorityOptions returns the property options for priority parameters
66 | func getPriorityOptions() []mcp.PropertyOption {
67 | 	return []mcp.PropertyOption{
68 | 		mcp.Description("Priority"),
69 | 		mcp.Enum("no priority", "urgent", "high", "medium", "low"),
70 | 	}
71 | }
72 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/get_milestone_handler_Valid 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: 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":"c86acc00-3035-4a67-82f2-2a5bf6453e92"}}'
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":"c86acc00-3035-4a67-82f2-2a5bf6453e92","name":"Updated Milestone Name","description":"Updated Description","targetDate":"2025-01-01","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/"102-BS+utz/wRqBQhmWIePACw/zkABE"
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/create_initiative_handler_Valid initiative.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: 313
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tmutation InitiativeCreate($input: InitiativeCreateInput!) {\n\t\t\tinitiativeCreate(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":{"input":{"name":"Created Test Initiative"}}}'
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":{"initiativeCreate":{"success":true,"initiative":{"id":"3bb752a7-897e-4240-9306-01e48872fab3","name":"Created Test Initiative","description":null,"url":"https://linear.app/linear-mcp-go-test/initiative/created-test-initiative-7ed59af889f6"}}}}
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/"fc-gsDkifP3AXu0S0n8HWLEunvFm3g"
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_comment_handler_Invalid comment 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: 265
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":"invalid-comment-id"}}'
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: Comment: could not find by hash","path":["comment"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced comment."}}],"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/"12b-3YQeaABMZ487ZUXapKwikGiK6Xw"
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/search_projects_handler_Search by query.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: 311
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tquery SearchProjects($filter: ProjectFilter) {\n\t\t\tprojects(filter: $filter) {\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}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"name":{"containsIgnoreCase":"mcp"}}}}'
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":{"projects":{"nodes":[{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation","description":"Summary text goes here","slugId":"ae44897e42a7","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/mcp-tool-investigation-ae44897e42a7"}]}}}
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/"11b-k8WmIaSsWOTOQAk9sZK3/3eZoaY"
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/create_initiative_handler_With 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: 348
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tmutation InitiativeCreate($input: InitiativeCreateInput!) {\n\t\t\tinitiativeCreate(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":{"input":{"name":"Created Test Initiative 2","description":"Test Description"}}}'
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":{"initiativeCreate":{"success":true,"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/"10e-Z2O6jMzejJ/UoHu3z6jnwHPqPzc"
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 | 
```

--------------------------------------------------------------------------------
/pkg/tools/priority_test.go:
--------------------------------------------------------------------------------

```go
 1 | package tools
 2 | 
 3 | import (
 4 | 	"testing"
 5 | )
 6 | 
 7 | func TestParsePriority(t *testing.T) {
 8 | 	tests := []struct {
 9 | 		name    string
10 | 		input   string
11 | 		want    int
12 | 		wantErr bool
13 | 	}{
14 | 		// Numeric inputs
15 | 		{"zero", "0", 0, false},
16 | 		{"one", "1", 1, false},
17 | 		{"two", "2", 2, false},
18 | 		{"three", "3", 3, false},
19 | 		{"four", "4", 4, false},
20 | 		{"invalid number", "5", 0, true},
21 | 		{"negative", "-1", 0, true},
22 | 
23 | 		// Textual inputs (lowercase)
24 | 		{"no priority", "no priority", 0, false},
25 | 		{"none", "none", 0, false},
26 | 		{"urgent", "urgent", 1, false},
27 | 		{"high", "high", 2, false},
28 | 		{"medium", "medium", 3, false},
29 | 		{"low", "low", 4, false},
30 | 
31 | 		// Textual inputs (mixed case)
32 | 		{"Urgent", "Urgent", 1, false},
33 | 		{"HIGH", "HIGH", 2, false},
34 | 		{"MeDiUm", "MeDiUm", 3, false},
35 | 
36 | 		// Whitespace handling
37 | 		{"with spaces", "  urgent  ", 1, false},
38 | 		{"with tabs", "\thigh\t", 2, false},
39 | 
40 | 		// Invalid inputs
41 | 		{"invalid text", "super-urgent", 0, true},
42 | 		{"empty", "", 0, true},
43 | 		{"random", "xyz", 0, true},
44 | 	}
45 | 
46 | 	for _, tt := range tests {
47 | 		t.Run(tt.name, func(t *testing.T) {
48 | 			got, err := parsePriority(tt.input)
49 | 			if (err != nil) != tt.wantErr {
50 | 				t.Errorf("parsePriority(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
51 | 				return
52 | 			}
53 | 			if got != tt.want {
54 | 				t.Errorf("parsePriority(%q) = %v, want %v", tt.input, got, tt.want)
55 | 			}
56 | 		})
57 | 	}
58 | }
59 | 
60 | func TestPriorityToString(t *testing.T) {
61 | 	tests := []struct {
62 | 		name     string
63 | 		priority int
64 | 		want     string
65 | 	}{
66 | 		{"zero", 0, "No priority"},
67 | 		{"urgent", 1, "Urgent"},
68 | 		{"high", 2, "High"},
69 | 		{"medium", 3, "Medium"},
70 | 		{"low", 4, "Low"},
71 | 		{"invalid", 5, "Unknown"},
72 | 		{"negative", -1, "Unknown"},
73 | 	}
74 | 
75 | 	for _, tt := range tests {
76 | 		t.Run(tt.name, func(t *testing.T) {
77 | 			if got := priorityToString(tt.priority); got != tt.want {
78 | 				t.Errorf("priorityToString(%d) = %v, want %v", tt.priority, got, tt.want)
79 | 			}
80 | 		})
81 | 	}
82 | }
83 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/search_projects_handler_No results.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: 564
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tquery SearchProjects($filter: ProjectFilter) {\n\t\t\tprojects(filter: $filter) {\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\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\tlead {\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\tstartDate\n\t\t\t\t\ttargetDate\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"name":{"containsIgnoreCase":"non-existent-project-query"}}}}'
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: 35
29 |         uncompressed: false
30 |         body: |
31 |             {"data":{"projects":{"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-Length:
40 |                 - "35"
41 |             Content-Type:
42 |                 - application/json; charset=utf-8
43 |             Etag:
44 |                 - W/"23-qdJEPQ25XhtziwkPAN9bwg0W7eo"
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 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_project_handler_Valid 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: 384
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tmutation ProjectCreate($input: ProjectCreateInput!) {\n\t\t\tprojectCreate(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":{"input":{"name":"Created Test Project","teamIds":["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":{"projectCreate":{"success":true,"project":{"id":"1c3f69d6-ab7b-4339-906d-5d63bd3cc3bc","name":"Created Test Project","description":"","slugId":"d1e7a63515a4","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/created-test-project-d1e7a63515a4"}}}}
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/"115-3XE+XxWz8n/te2YxwxwQ8jghgOQ"
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/create_milestone_handler_Valid 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: 458
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tmutation ProjectMilestoneCreate($input: ProjectMilestoneCreateInput!) {\n\t\t\tprojectMilestoneCreate(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":{"input":{"name":"Test Milestone 2.2","projectId":"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":{"projectMilestoneCreate":{"success":true,"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/"10f-qfcBByYXxFvyqjb46UAlsOog79A"
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/create_project_handler_With all optional fields.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: 510
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tmutation ProjectCreate($input: ProjectCreateInput!) {\n\t\t\tprojectCreate(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":{"input":{"name":"Test Project 2","teamIds":["234c5451-a839-4c8f-98d9-da00973f1060"],"description":"Test Description","leadId":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","startDate":"2024-01-01","targetDate":"2024-12-31"}}}'
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":{"projectCreate":{"success":true,"project":{"id":"5ce3e62b-766e-44d9-b7a4-e335492bfd1e","name":"Test Project 2","description":"Test Description","slugId":"b73665fc5cc5","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/test-project-2-b73665fc5cc5"}}}}
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/"119-Cuk8W8/7UhfXhWx3IaWopPfEYBU"
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/search_issues_handler_Search by query.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 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":{"or":[{"title":{"contains":"test"}},{"description":{"contains":"test"}}]},"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: 33
29 |         uncompressed: false
30 |         body: |
31 |             {"data":{"issues":{"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-Length:
40 |                 - "33"
41 |             Content-Type:
42 |                 - application/json; charset=utf-8
43 |             Etag:
44 |                 - W/"21-PKFa7EZ3q+7ITZ8vZtp2aqgNJxo"
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 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_milestone_handler_With all optional fields.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: 517
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tmutation ProjectMilestoneCreate($input: ProjectMilestoneCreateInput!) {\n\t\t\tprojectMilestoneCreate(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":{"input":{"name":"Test Milestone 3.2","projectId":"bfa49864-16c9-44db-994e-a11ba2b386f1","description":"Test Description","targetDate":"2024-12-31"}}}'
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":{"projectMilestoneCreate":{"success":true,"projectMilestone":{"id":"7017befa-5b90-4511-9ddf-c1c6ae7ba99a","name":"Test Milestone 3.2","description":"Test Description","targetDate":"2024-12-31","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/"125-gez3UfbdT9/D0VXU4hZmuVEUokw"
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/create_issue_handler_Invalid 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: 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 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/resource_TeamResourceHandler_Fetch By ID.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 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/resource_TeamResourceHandler_Invalid ID.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 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/resource_TeamsResourceHandler_List All.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 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_project_handler_Invalid team ID.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: 357
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tmutation ProjectCreate($input: ProjectCreateInput!) {\n\t\t\tprojectCreate(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":{"input":{"name":"Test Project 3","teamIds":["invalid-team-id"]}}}'
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":"Argument Validation Error","path":["projectCreate"],"locations":[{"line":3,"column":4}],"extensions":{"code":"INVALID_INPUT","validationErrors":[{"target":{"name":"Test Project 3","teamIds":["invalid-team-id"]},"value":["invalid-team-id"],"property":"teamIds","children":[],"constraints":{"isUuid":"each value in teamIds must be a UUID"}}],"type":"invalid input","userError":true,"userPresentableMessage":"each value in teamIds must be a UUID."}}],"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/"1e4-JRWXeyaRzkdssN5PwkmdFPPwZ6Y"
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/get_teams_handler_Get Teams.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: 367
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","variables":{"filter":{"name":{"contains":"Test Team"}}}}'
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 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_milestone_handler_Invalid project ID.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: 440
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tmutation ProjectMilestoneCreate($input: ProjectMilestoneCreateInput!) {\n\t\t\tprojectMilestoneCreate(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":{"input":{"name":"Test Milestone 3.1","projectId":"invalid-project-id"}}}'
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":"Argument Validation Error","path":["projectMilestoneCreate"],"locations":[{"line":3,"column":4}],"extensions":{"code":"INVALID_INPUT","validationErrors":[{"target":{"name":"Test Milestone 3.1","projectId":"invalid-project-id"},"value":"invalid-project-id","property":"projectId","children":[],"constraints":{"isUuid":"projectId must be a UUID"}}],"type":"invalid input","userError":true,"userPresentableMessage":"projectId must be a UUID."}}],"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/"1df-TEVUGn11CxiIlKsKcBOVc2L/nlo"
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 | 
```

--------------------------------------------------------------------------------
/pkg/linear/test_helpers.go:
--------------------------------------------------------------------------------

```go
 1 | package linear
 2 | 
 3 | import (
 4 | 	"os"
 5 | 	"strings"
 6 | 	"testing"
 7 | 
 8 | 	"gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
 9 | 	"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
10 | )
11 | 
12 | // NewTestClient creates a LinearClient for testing
13 | // If record is true, it will record HTTP interactions
14 | // If record is false, it will replay recorded interactions
15 | func NewTestClient(t *testing.T, cassetteName string, record bool) (*LinearClient, func()) {
16 | 	if record {
17 | 		// Ensure API key is set when recording
18 | 		if os.Getenv("TEST_LINEAR_API_KEY") == "" {
19 | 			t.Fatal("TEST_LINEAR_API_KEY environment variable is required for recording")
20 | 		}
21 | 	}
22 | 
23 | 	wipeAuthorizationHook := func(i *cassette.Interaction) error {
24 | 		delete(i.Request.Headers, "Authorization")
25 | 		delete(i.Response.Headers, "Set-Cookie")
26 | 		return nil
27 | 	}
28 | 
29 | 	wipeChangingMetadataHook := func(i *cassette.Interaction) error {
30 | 		delete(i.Request.Headers, "User-Agent")
31 | 
32 | 		delete(i.Response.Headers, "Cf-Ray")
33 | 		delete(i.Response.Headers, "Date")
34 | 
35 | 		for k := range i.Response.Headers {
36 | 			if strings.HasPrefix(strings.ToLower(k), "x-") {
37 | 				delete(i.Response.Headers, k)
38 | 			}
39 | 		}
40 | 		i.Response.Duration = 0
41 | 		return nil
42 | 	}
43 | 
44 | 	// Create the recorder with appropriate mode
45 | 	options := []recorder.Option{
46 | 		// don't record authorization header in cassettes
47 | 		recorder.WithHook(wipeAuthorizationHook, recorder.AfterCaptureHook),
48 | 		recorder.WithHook(wipeChangingMetadataHook, recorder.AfterCaptureHook),
49 | 		recorder.WithMatcher(cassette.NewDefaultMatcher(cassette.WithIgnoreAuthorization(), cassette.WithIgnoreUserAgent())),
50 | 	}
51 | 	if record {
52 | 		options = append(options, recorder.WithMode(recorder.ModeRecordOnly))
53 | 	} else {
54 | 		options = append(options, recorder.WithMode(recorder.ModeReplayOnly))
55 | 	}
56 | 
57 | 	r, err := recorder.New("../../testdata/fixtures/"+cassetteName, options...)
58 | 	if err != nil {
59 | 		t.Fatalf("Failed to create recorder: %v", err)
60 | 	}
61 | 
62 | 	// Create a Linear client that uses the recorder's HTTP client
63 | 	apiKey := os.Getenv("TEST_LINEAR_API_KEY")
64 | 	client := &LinearClient{
65 | 		apiKey:      apiKey,
66 | 		httpClient:  r.GetDefaultClient(),
67 | 		rateLimiter: NewRateLimiter(1400),
68 | 	}
69 | 
70 | 	// Return the client and a cleanup function
71 | 	cleanup := func() {
72 | 		r.Stop()
73 | 	}
74 | 
75 | 	return client, cleanup
76 | }
77 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/update_comment_handler_Valid comment 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: 536
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         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"}}}'
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":{"commentUpdate":{"success":true,"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"},"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"}}}}}
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/"20f-QmoB3WjGuI3Kn4PiWkTFI7/hRj8"
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 | 
```

--------------------------------------------------------------------------------
/pkg/tools/update_issue_comment.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 | // UpdateCommentTool is the tool definition for updating a comment
12 | var UpdateCommentTool = mcp.NewTool("linear_update_issue_comment",
13 | 	mcp.WithDescription("Updates an existing comment on a Linear issue."),
14 | 	mcp.WithString("comment", mcp.Required(), mcp.Description("Comment identifier to update. Accepts: full URL, UUID, shorthand (comment-abc123), or hash (abc123).")),
15 | 	mcp.WithString("body", mcp.Required(), mcp.Description("New comment text in markdown format")),
16 | )
17 | 
18 | // UpdateCommentHandler handles the linear_update_comment tool
19 | func UpdateCommentHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
20 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
21 | 		// Extract arguments
22 | 		commentIdentifier, err := request.RequireString("comment")
23 | 		if err != nil {
24 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
25 | 		}
26 | 
27 | 		// Resolve comment identifier to a UUID
28 | 		commentID, err := resolveCommentIdentifier(linearClient, commentIdentifier)
29 | 		if err != nil {
30 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve comment: %v", err)}}}, nil
31 | 		}
32 | 
33 | 		body, err := request.RequireString("body")
34 | 		if err != nil {
35 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
36 | 		}
37 | 
38 | 		// Update the comment
39 | 		input := linear.UpdateCommentInput{
40 | 			CommentID: commentID,
41 | 			Body:      body,
42 | 		}
43 | 
44 | 		comment, issue, err := linearClient.UpdateComment(input)
45 | 		if err != nil {
46 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to update comment: %v", err)}}}, nil
47 | 		}
48 | 
49 | 		// Return the result
50 | 		resultText := fmt.Sprintf("Updated comment on %s\n", formatIssueIdentifier(issue))
51 | 		resultText += fmt.Sprintf("Comment ID: %s\n", comment.ID)
52 | 		resultText += fmt.Sprintf("Thread (for replies): %s\n", comment.ID)
53 | 		resultText += fmt.Sprintf("URL: %s", comment.URL)
54 | 		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
55 | 	}
56 | }
57 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Valid issue with teamId.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: 686
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         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}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"description":"","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Test Issue"}}}'
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":{"issueCreate":{"success":true,"issue":{"id":"71daa141-706f-45bd-af79-fa27571b9974","identifier":"TEST-48","title":"Test Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-48/test-issue","createdAt":"2025-03-30T09:34:52.655Z","updatedAt":"2025-03-30T09:34:52.655Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[]}}}}}
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/"1f3-Uv10fBIw9AkcaiDdPamGO+YgS4U"
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 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Valid issue with 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: 831
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tmutation CreateIssue($input: IssueCreateInput!) {\n\t\t\tissueCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tstate {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tteam {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t\tlabels {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tprojectMilestone {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"description":"","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Test Issue"}}}'
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":{"issueCreate":{"success":true,"issue":{"id":"9e842dfe-d72f-4d32-a2a3-330338d1cabc","identifier":"TEST-85","title":"Test Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-85/test-issue","createdAt":"2025-10-06T09:44:03.705Z","updatedAt":"2025-10-06T09:44:03.705Z","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}}}}
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/"21a-riLwqmexbAhReQeBEf18rQ70w9Y"
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 | 
```

--------------------------------------------------------------------------------
/memory-bank/productContext.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Product Context: Linear MCP Server
 2 | 
 3 | ## Why This Project Exists
 4 | The Linear MCP Server exists to bridge the gap between AI assistants and Linear, a popular issue tracking and project management tool. By implementing the Model Context Protocol (MCP), this server enables AI assistants to interact with Linear's API in a standardized way, allowing them to create, update, and manage issues without requiring custom integration code for each assistant.
 5 | 
 6 | ## Problems It Solves
 7 | 1. **Integration Complexity**: Simplifies the process of connecting AI assistants to Linear by providing a standardized interface.
 8 | 2. **API Consistency**: Abstracts away the complexities of the Linear API, providing a consistent experience.
 9 | 3. **Rate Limiting**: Handles Linear's API rate limits automatically, preventing quota exhaustion.
10 | 4. **Authentication Management**: Manages API key authentication in a secure manner.
11 | 5. **Error Handling**: Provides meaningful error messages when operations fail.
12 | 
13 | ## How It Should Work
14 | 1. **Server Initialization**:
15 |    - The server starts and listens for MCP requests on stdin/stdout.
16 |    - It validates the LINEAR_API_KEY environment variable.
17 |    - It registers all available tools with the MCP server.
18 | 
19 | 2. **Tool Execution**:
20 |    - When a tool is called (e.g., linear_create_issue), the server validates the input parameters.
21 |    - It translates the request into appropriate Linear API calls.
22 |    - It handles the response, formatting it according to MCP specifications.
23 |    - It returns the result to the caller.
24 | 
25 | 3. **Error Scenarios**:
26 |    - If the API key is missing or invalid, it returns a clear error message.
27 |    - If required parameters are missing, it returns parameter validation errors.
28 |    - If the Linear API returns an error, it translates and returns it in a user-friendly format.
29 |    - If rate limits are exceeded, it handles backoff and retries appropriately.
30 | 
31 | ## User Experience Goals
32 | 1. **Simplicity**: Users should be able to set up and use the server with minimal configuration.
33 | 2. **Reliability**: The server should handle errors gracefully and provide clear feedback.
34 | 3. **Completeness**: All common Linear operations should be supported.
35 | 4. **Performance**: Operations should be efficient and respect API rate limits.
36 | 5. **Documentation**: Clear documentation should be provided for all tools and setup procedures.
37 | 
38 | ## Integration Points
39 | 1. **Linear API**: The server interacts with Linear's API to perform operations.
40 | 2. **MCP Protocol**: The server implements the MCP protocol to communicate with AI assistants.
41 | 3. **Environment**: The server uses environment variables for configuration.
42 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/search_projects_handler_Multiple results.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: 541
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tquery SearchProjects($filter: ProjectFilter) {\n\t\t\tprojects(filter: $filter) {\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\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\tlead {\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\tstartDate\n\t\t\t\t\ttargetDate\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"name":{"containsIgnoreCase":"MCP"}}}}'
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":{"projects":{"nodes":[{"id":"473d62ae-38fe-4439-9007-08763e51bf88","name":"Totally different MCP project with no content","description":"Summary goes here","slugId":"29129640a673","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/totally-different-mcp-project-with-no-content-29129640a673","initiatives":{"nodes":[]},"lead":null,"startDate":null,"targetDate":null},{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation","description":"Summary text goes here","slugId":"ae44897e42a7","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/mcp-tool-investigation-ae44897e42a7","initiatives":{"nodes":[{"id":"15e7c1bd-c0c5-4801-ac9a-8e98bf88ea7a","name":"Push for MCP"}]},"lead":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann"},"startDate":"2025-06-02","targetDate":"2025-06-30"}]}}}
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/"365-vV3AiEsYIfWAgp5Ebe03XF87/Kw"
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/create_issue_handler_Valid issue with team UUID.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: 846
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tmutation CreateIssue($input: IssueCreateInput!) {\n\t\t\tissueCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tstate {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tteam {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t\tlabels {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tprojectMilestone {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"description":"","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Test Issue with team UUID"}}}'
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":{"issueCreate":{"success":true,"issue":{"id":"73667bc8-f31c-41e7-a513-ed5b196cb25e","identifier":"TEST-86","title":"Test Issue with team UUID","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-86/test-issue-with-team-uuid","createdAt":"2025-10-06T09:44:08.669Z","updatedAt":"2025-10-06T09:44:08.669Z","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}}}}
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/"238-LjhPlWjTFDB/d+FI0JgaJxLeSPw"
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/create_issue_handler_Create sub issue.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | ---
 2 | version: 2
 3 | interactions:
 4 |     - id: 0
 5 |       request:
 6 |         proto: HTTP/1.1
 7 |         proto_major: 1
 8 |         proto_minor: 1
 9 |         content_length: 880
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         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"}}}'
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":{"issueCreate":{"success":true,"issue":{"id":"507ac2f7-10d3-4566-bf85-01e761b8aacc","identifier":"TEST-89","title":"Sub Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-89/sub-issue","createdAt":"2025-10-06T09:44:26.768Z","updatedAt":"2025-10-06T09:44:26.768Z","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}}}}
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/"218-Ctac8rozKoI8dD6f8c2wIWoJnE4"
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 | 
```

--------------------------------------------------------------------------------
/pkg/tools/get_user_issues.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 | // GetUserIssuesTool is the tool definition for getting user issues
12 | var GetUserIssuesTool = mcp.NewTool("linear_get_user_issues",
13 | 	mcp.WithDescription("Retrieves issues assigned to a user."),
14 | 	mcp.WithString("user", mcp.Description("Optional user identifier (UUID, name, or email). If not provided, returns authenticated user's issues")),
15 | 	mcp.WithBoolean("includeArchived", mcp.Description("Include archived issues in results")),
16 | 	mcp.WithNumber("limit", mcp.Description("Maximum number of issues to return (default: 50)")),
17 | )
18 | 
19 | // GetUserIssuesHandler handles the linear_get_user_issues tool
20 | func GetUserIssuesHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
21 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
22 | 		// Build input
23 | 		input := linear.GetUserIssuesInput{}
24 | 
25 | 		if user, err := request.RequireString("user"); err == nil && user != "" {
26 | 			// Resolve user identifier to a user ID
27 | 			userID, err := resolveUserIdentifier(linearClient, user)
28 | 			if err != nil {
29 | 				return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve user: %v", err)}}}, nil
30 | 			}
31 | 			input.UserID = userID
32 | 		}
33 | 
34 | 		input.IncludeArchived = request.GetBool("includeArchived", false)
35 | 		input.Limit = request.GetInt("limit", 50)
36 | 
37 | 		// Get user issues
38 | 		issues, err := linearClient.GetUserIssues(input)
39 | 		if err != nil {
40 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get user issues: %v", err)}}}, nil
41 | 		}
42 | 
43 | 		// Format the result
44 | 		resultText := fmt.Sprintf("Found %d issues:\n", len(issues))
45 | 		for _, issue := range issues {
46 | 			// Create a temporary Issue object to use with formatIssueIdentifier
47 | 			tempIssue := &linear.Issue{
48 | 				ID:         issue.ID,
49 | 				Identifier: issue.Identifier,
50 | 			}
51 | 
52 | 			statusStr := "None"
53 | 			if issue.Status != "" {
54 | 				statusStr = issue.Status
55 | 			} else if issue.StateName != "" {
56 | 				statusStr = issue.StateName
57 | 			}
58 | 
59 | 			resultText += fmt.Sprintf("- %s\n", formatIssueIdentifier(tempIssue))
60 | 			resultText += fmt.Sprintf("  Title: %s\n", issue.Title)
61 | 			resultText += fmt.Sprintf("  Priority: %s\n", priorityToString(issue.Priority))
62 | 			resultText += fmt.Sprintf("  Status: %s\n", statusStr)
63 | 			resultText += fmt.Sprintf("  URL: %s\n", issue.URL)
64 | 		}
65 | 
66 | 		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
67 | 	}
68 | }
69 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Create issue with project ID.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: 893
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         body: '{"query":"\n\t\tmutation CreateIssue($input: IssueCreateInput!) {\n\t\t\tissueCreate(input: $input) {\n\t\t\t\tsuccess\n\t\t\t\tissue {\n\t\t\t\t\tid\n\t\t\t\t\tidentifier\n\t\t\t\t\ttitle\n\t\t\t\t\tdescription\n\t\t\t\t\tpriority\n\t\t\t\t\turl\n\t\t\t\t\tcreatedAt\n\t\t\t\t\tupdatedAt\n\t\t\t\t\tstate {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tteam {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t\tlabels {\n\t\t\t\t\t\tnodes {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tproject {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t\tprojectMilestone {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"description":"","projectId":"01bff2dd-ab7f-4464-b425-97073862013f","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Issue with Project ID"}}}'
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":{"issueCreate":{"success":true,"issue":{"id":"5015a1d4-b382-4962-b8be-aa434b6b496d","identifier":"TEST-93","title":"Issue with Project ID","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-93/issue-with-project-id","createdAt":"2025-10-06T09:44:46.740Z","updatedAt":"2025-10-06T09:44:46.740Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"},"labels":{"nodes":[]},"project":{"id":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation"},"projectMilestone":null}}}}
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/"279-SjgXENxrlDWv72SYhs5pyB6uKnI"
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/get_project_handler_By ID.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":"01bff2dd-ab7f-4464-b425-97073862013f"}}'
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":"01bff2dd-ab7f-4464-b425-97073862013f","name":"MCP tool investigation","description":"Summary text goes here","slugId":"ae44897e42a7","state":"backlog","url":"https://linear.app/linear-mcp-go-test/project/mcp-tool-investigation-ae44897e42a7","createdAt":"2025-06-28T18:06:47.606Z","updatedAt":"2025-06-28T18:07:51.899Z","lead":{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"},"members":{"nodes":[{"id":"cc24eee4-9edc-4bfe-b91b-fedde125ba85","name":"Gero Leinemann","email":"[email protected]"}]},"teams":{"nodes":[{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}]},"initiatives":{"nodes":[{"id":"15e7c1bd-c0c5-4801-ac9a-8e98bf88ea7a","name":"Push for MCP"}]},"startDate":"2025-06-02","targetDate":"2025-06-30"}}}
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/"348-ZpF/ZpfaL+aW46aIruU9iQ4HbXY"
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 | 
```

--------------------------------------------------------------------------------
/pkg/tools/reply_to_comment.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 | // ReplyToCommentTool is a specialized tool for replying to comments
12 | var ReplyToCommentTool = mcp.NewTool("linear_reply_to_comment",
13 | 	mcp.WithDescription("Reply to an existing comment on a Linear issue."),
14 | 	mcp.WithString("thread", mcp.Required(), mcp.Description("Comment to reply to. Accepts: full URL, UUID, shorthand (comment-abc123), or hash (abc123).")),
15 | 	mcp.WithString("body", mcp.Required(), mcp.Description("Reply text in markdown format")),
16 | 	mcp.WithString("createAsUser", mcp.Description("Optional custom username to show for the reply")),
17 | )
18 | 
19 | // ReplyToCommentHandler handles the linear_reply_to_comment tool
20 | func ReplyToCommentHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
21 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
22 | 		// Extract arguments
23 | 		threadIdentifier, err := request.RequireString("thread")
24 | 		if err != nil {
25 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
26 | 		}
27 | 
28 | 		body, err := request.RequireString("body")
29 | 		if err != nil {
30 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
31 | 		}
32 | 
33 | 		createAsUser := request.GetString("createAsUser", "")
34 | 
35 | 		// Resolve the parent comment to get its UUID
36 | 		parentCommentID, err := resolveCommentIdentifier(linearClient, threadIdentifier)
37 | 		if err != nil {
38 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve comment: %v", err)}}}, nil
39 | 		}
40 | 
41 | 		// Get the parent comment to find its issue
42 | 		parentComment, err := linearClient.GetComment(parentCommentID)
43 | 		if err != nil {
44 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to get parent comment: %v", err)}}}, nil
45 | 		}
46 | 
47 | 		if parentComment.Issue == nil {
48 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: "Parent comment does not have an associated issue"}}}, nil
49 | 		}
50 | 
51 | 		// Add the reply
52 | 		input := linear.AddCommentInput{
53 | 			IssueID:      parentComment.Issue.ID,
54 | 			Body:         body,
55 | 			CreateAsUser: createAsUser,
56 | 			ParentID:     parentCommentID,
57 | 		}
58 | 
59 | 		comment, issue, err := linearClient.AddComment(input)
60 | 		if err != nil {
61 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to add reply: %v", err)}}}, nil
62 | 		}
63 | 
64 | 		// Return the result using the unified format
65 | 		resultText := formatNewComment(comment, issue, parentCommentID)
66 | 		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
67 | 	}
68 | }
69 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/create_issue_handler_Valid issue.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | ---
 2 | version: 2
 3 | interactions:
 4 |     - id: 0
 5 |       request:
 6 |         proto: HTTP/1.1
 7 |         proto_major: 1
 8 |         proto_minor: 1
 9 |         content_length: 579
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         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}\n\t\t\t}\n\t\t}\n\t","variables":{"input":{"description":"","teamId":"234c5451-a839-4c8f-98d9-da00973f1060","title":"Test Issue"}}}'
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":{"issueCreate":{"success":true,"issue":{"id":"1c2de93f-4321-4015-bfde-ee893ef7976f","identifier":"TEST-10","title":"Test Issue","description":null,"priority":0,"url":"https://linear.app/linear-mcp-go-test/issue/TEST-10/test-issue","createdAt":"2025-03-03T11:34:49.241Z","updatedAt":"2025-03-03T11:34:49.241Z","state":{"id":"42f7ad15-fca3-4d11-b349-0e3c1385c256","name":"Backlog"},"team":{"id":"234c5451-a839-4c8f-98d9-da00973f1060","name":"Test Team","key":"TEST"}}}}}
34 |         headers:
35 |             Alt-Svc:
36 |                 - h3=":443"; ma=86400
37 |             Cache-Control:
38 |                 - no-store
39 |             Cf-Cache-Status:
40 |                 - DYNAMIC
41 |             Cf-Ray:
42 |                 - 91a8d3ad484f8fee-FRA
43 |             Content-Type:
44 |                 - application/json; charset=utf-8
45 |             Date:
46 |                 - Mon, 03 Mar 2025 11:34:49 GMT
47 |             Etag:
48 |                 - W/"1dd-e9YqnIA3F4HsF8LOEx21H1J0EIg"
49 |             Server:
50 |                 - cloudflare
51 |             Vary:
52 |                 - Accept-Encoding
53 |             Via:
54 |                 - 1.1 google
55 |             X-Complexity:
56 |                 - "6"
57 |             X-Ratelimit-Complexity-Limit:
58 |                 - "3000000"
59 |             X-Ratelimit-Complexity-Remaining:
60 |                 - "2996924"
61 |             X-Ratelimit-Complexity-Reset:
62 |                 - "1741005289233"
63 |             X-Ratelimit-Requests-Limit:
64 |                 - "1500"
65 |             X-Ratelimit-Requests-Remaining:
66 |                 - "1498"
67 |             X-Ratelimit-Requests-Reset:
68 |                 - "1741005289233"
69 |             X-Request-Id:
70 |                 - 91a8d3ad73e58fee-FRA
71 |         status: 200 OK
72 |         code: 200
73 |         duration: 166.601029ms
74 | 
```

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

```go
 1 | package server
 2 | 
 3 | import (
 4 | 	"flag"
 5 | 	"os"
 6 | 	"path/filepath"
 7 | 	"testing"
 8 | 
 9 | 	"gopkg.in/yaml.v3"
10 | )
11 | 
12 | var record = flag.Bool("record", false, "Record HTTP interactions (excluding writes)")
13 | var recordWrites = flag.Bool("recordWrites", false, "Record HTTP interactions (incl. writes)")
14 | var golden = flag.Bool("golden", false, "Update all golden files and recordings")
15 | 
16 | // Shared constants for tests
17 | const (
18 | 	TEAM_NAME            = "Test Team"
19 | 	TEAM_KEY             = "TEST"
20 | 	TEAM_ID              = "234c5451-a839-4c8f-98d9-da00973f1060"
21 | 	ISSUE_ID             = "TEST-10"
22 | 	COMMENT_ISSUE_ID     = "TEST-12" // Used for testing add_comment handler
23 | 	USER_ID              = "cc24eee4-9edc-4bfe-b91b-fedde125ba85"
24 | 	PROJECT_ID           = "01bff2dd-ab7f-4464-b425-97073862013f"
25 | 	UPDATE_PROJECT_ID    = "bfa49864-16c9-44db-994e-a11ba2b386f1"
26 | 	MILESTONE_ID         = "c86acc00-3035-4a67-82f2-2a5bf6453e92"
27 | 	UPDATE_MILESTONE_ID  = "2d95299d-1341-484b-ab00-5cb587f2cc67"
28 | 	INITIATIVE_ID        = "3bb752a7-897e-4240-9306-01e48872fab3"
29 | 	UPDATE_INITIATIVE_ID = "c6a7dd0c-cbe2-4101-906d-ddd97acb2241"
30 | )
31 | 
32 | // expectation defines the expected output and error for a test case
33 | // For resource tests, Output will store the JSON representation of []mcp.ResourceContents
34 | type expectation struct {
35 | 	Err    string `yaml:"err"`          // Empty string means no error expected
36 | 	Output string `yaml:"output", flow` // Expected complete output
37 | }
38 | 
39 | // readGoldenFile reads an expectation from a golden file
40 | func readGoldenFile(t *testing.T, path string) expectation {
41 | 	t.Helper()
42 | 
43 | 	// Check if the golden file exists
44 | 	if _, err := os.Stat(path); os.IsNotExist(err) {
45 | 		// If the file doesn't exist, return an empty expectation
46 | 		// This allows tests to pass initially when golden files are missing,
47 | 		// prompting the user to run with -golden* flags to create them.
48 | 		t.Logf("Golden file %s does not exist. Run with appropriate -golden* flag to create it.", path)
49 | 		return expectation{}
50 | 	}
51 | 
52 | 	// Read the golden file
53 | 	data, err := os.ReadFile(path)
54 | 	if err != nil {
55 | 		t.Fatalf("Failed to read golden file %s: %v", path, err)
56 | 	}
57 | 
58 | 	// Parse the golden file
59 | 	var exp expectation
60 | 	if err := yaml.Unmarshal(data, &exp); err != nil {
61 | 		// If unmarshalling fails, treat it as an empty expectation
62 | 		// This handles cases where the golden file might be corrupted or empty
63 | 		t.Logf("Failed to parse golden file %s: %v. Treating as empty.", path, err)
64 | 		return expectation{}
65 | 	}
66 | 
67 | 	return exp
68 | }
69 | 
70 | // writeGoldenFile writes an expectation to a golden file
71 | func writeGoldenFile(t *testing.T, path string, exp expectation) {
72 | 	t.Helper()
73 | 
74 | 	// Create the directory if it doesn't exist
75 | 	dir := filepath.Dir(path)
76 | 	if err := os.MkdirAll(dir, 0755); err != nil {
77 | 		t.Fatalf("Failed to create directory %s: %v", dir, err)
78 | 	}
79 | 
80 | 	// Marshal the YAML node
81 | 	data, err := yaml.Marshal(&exp)
82 | 	if err != nil {
83 | 		t.Fatalf("Failed to marshal expectation: %v", err)
84 | 	}
85 | 
86 | 	// Write the golden file
87 | 	if err := os.WriteFile(path, data, 0644); err != nil {
88 | 		t.Fatalf("Failed to write golden file %s: %v", path, err)
89 | 	}
90 | 	t.Logf("Successfully wrote golden file: %s", path)
91 | }
92 | 
```

--------------------------------------------------------------------------------
/pkg/tools/add_comment.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 | // AddCommentTool is the tool definition for adding a comment
12 | var AddCommentTool = mcp.NewTool("linear_add_comment",
13 | 	mcp.WithDescription("Add/post a comment to a Linear issue. To reply to an existing comment, use linear_get_issue_comments to get the comment identifier or URL, then pass it in 'thread'."),
14 | 	mcp.WithString("issue", mcp.Required(), mcp.Description("ID or identifier (e.g., 'TEAM-123') of the issue to comment on")),
15 | 	mcp.WithString("thread", mcp.Description("Optional comment identifier to reply to. Accepts: full Linear comment URL, UUID, shorthand (comment-abc123), or hash (abc123). Creates a threaded reply.")),
16 | 	mcp.WithString("body", mcp.Required(), mcp.Description("Comment text in markdown format")),
17 | 	mcp.WithString("createAsUser", mcp.Description("Optional custom username to show for the comment")),
18 | )
19 | 
20 | // AddCommentHandler handles the linear_add_comment tool
21 | func AddCommentHandler(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 | 		// Resolve issue identifier to a UUID
30 | 		issueID, err := resolveIssueIdentifier(linearClient, issueIdentifier)
31 | 		if err != nil {
32 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve issue: %v", err)}}}, nil
33 | 		}
34 | 
35 | 		body, err := request.RequireString("body")
36 | 		if err != nil {
37 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
38 | 		}
39 | 
40 | 		// Extract optional arguments
41 | 		createAsUser := request.GetString("createAsUser", "")
42 | 		threadIdentifier := request.GetString("thread", "")
43 | 		
44 | 		// Resolve thread identifier to UUID if provided
45 | 		var parentID string
46 | 		if threadIdentifier != "" {
47 | 			resolvedParentID, err := resolveCommentIdentifier(linearClient, threadIdentifier)
48 | 			if err != nil {
49 | 				return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve thread comment: %v", err)}}}, nil
50 | 			}
51 | 			parentID = resolvedParentID
52 | 		}
53 | 
54 | 		// Add the comment
55 | 		input := linear.AddCommentInput{
56 | 			IssueID:      issueID,
57 | 			Body:         body,
58 | 			CreateAsUser: createAsUser,
59 | 			ParentID:     parentID,
60 | 		}
61 | 
62 | 		comment, issue, err := linearClient.AddComment(input)
63 | 		if err != nil {
64 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to add comment: %v", err)}}}, nil
65 | 		}
66 | 
67 | 		// Return the result using the unified format
68 | 		resultText := formatNewComment(comment, issue, parentID)
69 | 		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
70 | 	}
71 | }
72 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/get_user_issues_handler_Specific 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: 556
10 |         transfer_encoding: []
11 |         trailer: {}
12 |         host: api.linear.app
13 |         remote_addr: ""
14 |         request_uri: ""
15 |         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"}}'
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: "{\"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"
31 |         headers:
32 |             Alt-Svc:
33 |                 - h3=":443"; ma=86400
34 |             Cache-Control:
35 |                 - no-store
36 |             Cf-Cache-Status:
37 |                 - DYNAMIC
38 |             Content-Type:
39 |                 - application/json; charset=utf-8
40 |             Etag:
41 |                 - W/"52b-iwXdi0Au8LYVFWrptXXwpSz7HVA"
42 |             Server:
43 |                 - cloudflare
44 |             Vary:
45 |                 - Accept-Encoding
46 |             Via:
47 |                 - 1.1 google
48 |         status: 200 OK
49 |         code: 200
50 |         duration: 0s
51 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/get_initiative_handler_Non-existent 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: 201
 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":"non-existent-name"}}'
 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: Initiative","path":["initiative"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced Initiative."}}],"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/"11c-c44P18yu0Ek2xjOBk4Ycpqr5fPg"
 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: 295
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         body: '{"query":"\n\t\tquery GetInitiativeByName($filter: InitiativeFilter) {\n\t\t\tinitiatives(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\turl\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"name":{"eq":"non-existent-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: 38
 77 |         uncompressed: false
 78 |         body: |
 79 |             {"data":{"initiatives":{"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 |                 - "38"
 89 |             Content-Type:
 90 |                 - application/json; charset=utf-8
 91 |             Etag:
 92 |                 - W/"26-1+AHGSycMEc+rIWhJuNJZIAom5A"
 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_Non-existent initiative.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: 207
 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":"non-existent-initiative"}}'
 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: Initiative","path":["initiative"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced Initiative."}}],"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/"11c-c44P18yu0Ek2xjOBk4Ycpqr5fPg"
 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: 301
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         body: '{"query":"\n\t\tquery GetInitiativeByName($filter: InitiativeFilter) {\n\t\t\tinitiatives(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\turl\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"name":{"eq":"non-existent-initiative"}}}}'
 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: 38
 77 |         uncompressed: false
 78 |         body: |
 79 |             {"data":{"initiatives":{"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 |                 - "38"
 89 |             Content-Type:
 90 |                 - application/json; charset=utf-8
 91 |             Etag:
 92 |                 - W/"26-1+AHGSycMEc+rIWhJuNJZIAom5A"
 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 | 
```

--------------------------------------------------------------------------------
/scripts/register-cline.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # This script installs the Linear MCP server and registers it for use with the cline VSCode extension: https://github.com/cline/cline
 4 | # Note: to use this, you need to have a) cline installed, and b) set LINEAR_API_KEY in your environment
 5 | #
 6 | # Usage:
 7 | #   ./register-cline.sh linear-api-key [write-access]
 8 | #
 9 | # Parameters:
10 | #   linear-api-key: Mandatory. The Linear API key to use for the MCP server.
11 | #   write-access: Optional. Set to "true" to enable write operations. Default is "false".
12 | #
13 | # Examples:
14 | #   ./register-cline.sh                # Install with read-only mode (default)
15 | #   ./register-cline.sh true           # Install with write operations enabled
16 | 
17 | # LINEAR_API_KEY (mandatory)
18 | LINEAR_API_KEY=$1
19 | if [ -z "$LINEAR_API_KEY" ]; then
20 |     echo "LINEAR_API_KEY is not set, skipping setup."
21 |     exit 1
22 | fi
23 | 
24 | # Get the write-access parameter (default: false)
25 | WRITE_ACCESS=${2:-false}
26 | 
27 | MCP_SERVERS_DIR="$HOME/mcp-servers"
28 | mkdir -p $MCP_SERVERS_DIR
29 | 
30 | # Check if the Linear MCP server binary is on the path already
31 | LINEAR_MCP_BINARY="$(which linear-mcp-go)"
32 | if [ -z "$LINEAR_MCP_BINARY" ]; then
33 |     echo "Did not find linear-mcp-go on the path, installing from latest GitHub release..."
34 | 
35 |     # This fetches information about the latest release to determine the download URL
36 |     LATEST_RELEASE=$(curl -s https://api.github.com/repos/geropl/linear-mcp-go/releases/latest)
37 |     # Extract the download URL for the Linux binary
38 |     DOWNLOAD_URL=$(echo $LATEST_RELEASE | jq -r '.assets[] | select(.name | contains("linux")) | .browser_download_url')
39 | 
40 |     if [ -z "$DOWNLOAD_URL" ]; then
41 |         echo "Error: Could not find Linux binary in the latest release"
42 |         exit 1
43 |     fi
44 | 
45 |     # Download the Linear MCP server binary
46 |     echo "Downloading Linear MCP server from $DOWNLOAD_URL..."
47 |     curl -L -o $MCP_SERVERS_DIR/linear-mcp-go $DOWNLOAD_URL
48 | 
49 |     # Make the binary executable
50 |     chmod +x $MCP_SERVERS_DIR/linear-mcp-go
51 | 
52 |     echo "Linear MCP server installed successfully at $MCP_SERVERS_DIR/linear-mcp-go"
53 |     LINEAR_MCP_BINARY="$MCP_SERVERS_DIR/linear-mcp-go"
54 | fi
55 | 
56 | # Configure cline to use the MCP server
57 | # This is where Cline looks for MCP server configurations
58 | CLINE_CONFIG_DIR="$HOME/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings"
59 | mkdir -p "$CLINE_CONFIG_DIR"
60 | 
61 | CLINE_MCP_SETTINGS="$CLINE_CONFIG_DIR/cline_mcp_settings.json"
62 | 
63 | # Determine args based on write-access parameter
64 | if [ "$WRITE_ACCESS" = "true" ]; then
65 |   SERVER_ARGS='["serve", "--write-access=true"]'
66 | else
67 |   SERVER_ARGS='["serve"]'
68 | fi
69 | 
70 | # Merge the existing settings with the new MCP server configuration
71 | cat <<EOF > "$CLINE_MCP_SETTINGS.new"
72 | {
73 |    "mcpServers": {
74 |     "linear": {
75 |       "command": "$LINEAR_MCP_BINARY",
76 |       "args": $SERVER_ARGS,
77 |       "env": {
78 |         "LINEAR_API_KEY": "$LINEAR_API_KEY"
79 |       },
80 |       "disabled": false,
81 |       "autoApprove": []
82 |     }
83 |   }
84 | }
85 | EOF
86 | 
87 | if [ -f "$CLINE_MCP_SETTINGS" ]; then
88 |     echo "Found existing Cline MCP settings at $CLINE_MCP_SETTINGS"
89 |     echo "Merging with new MCP server configuration..."
90 |     jq -s '.[0] * .[1]' "$CLINE_MCP_SETTINGS" "$CLINE_MCP_SETTINGS.new" > "$CLINE_MCP_SETTINGS.tmp"
91 |     mv "$CLINE_MCP_SETTINGS.tmp" "$CLINE_MCP_SETTINGS"
92 | else
93 |     mv "$CLINE_MCP_SETTINGS.new" "$CLINE_MCP_SETTINGS"
94 | fi
95 | rm -f "$CLINE_MCP_SETTINGS.new"
96 | 
97 | echo "Cline MCP settings updated at $CLINE_MCP_SETTINGS"
98 | 
```

--------------------------------------------------------------------------------
/testdata/fixtures/get_initiative_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: 196
 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":"Push for MCP"}}'
 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: Initiative","path":["initiative"],"locations":[{"line":3,"column":4}],"extensions":{"type":"invalid input","code":"INPUT_ERROR","statusCode":400,"userError":true,"userPresentableMessage":"Could not find referenced Initiative."}}],"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/"11c-c44P18yu0Ek2xjOBk4Ycpqr5fPg"
 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: 290
 58 |         transfer_encoding: []
 59 |         trailer: {}
 60 |         host: api.linear.app
 61 |         remote_addr: ""
 62 |         request_uri: ""
 63 |         body: '{"query":"\n\t\tquery GetInitiativeByName($filter: InitiativeFilter) {\n\t\t\tinitiatives(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\turl\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t","variables":{"filter":{"name":{"eq":"Push for MCP"}}}}'
 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":{"initiatives":{"nodes":[{"id":"15e7c1bd-c0c5-4801-ac9a-8e98bf88ea7a","name":"Push for MCP","description":null,"url":"https://linear.app/linear-mcp-go-test/initiative/push-for-mcp-f45c0f78f676"}]}}}
 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/"cf-Y4qKSrkoz5y3ppN8YkaP7va+sEU"
 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/update_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 | // UpdateIssueTool is the tool definition for updating issues
12 | var UpdateIssueTool = mcp.NewTool("linear_update_issue",
13 | 	mcp.WithDescription("Updates an existing Linear issue."),
14 | 	mcp.WithString("issue", mcp.Required(), mcp.Description("Issue ID or identifier (e.g., 'TEAM-123')")),
15 | 	mcp.WithString("title", mcp.Description("New title")),
16 | 	mcp.WithString("description", mcp.Description("New description")),
17 | 	mcp.WithString("priority", getPriorityOptions()...),
18 | 	mcp.WithString("status", mcp.Description("New status")),
19 | 	mcp.WithString("team", mcp.Description("New team (UUID, name, or key)")),
20 | 	mcp.WithString("projectId", mcp.Description("New project ID")),
21 | 	mcp.WithString("milestoneId", mcp.Description("New milestone ID")),
22 | )
23 | 
24 | // UpdateIssueHandler handles the linear_update_issue tool
25 | func UpdateIssueHandler(linearClient *linear.LinearClient) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
26 | 	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
27 | 		// Extract arguments
28 | 		issueIdentifier, err := request.RequireString("issue")
29 | 		if err != nil {
30 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: err.Error()}}}, nil
31 | 		}
32 | 
33 | 		// Resolve issue identifier to a UUID
34 | 		id, err := resolveIssueIdentifier(linearClient, issueIdentifier)
35 | 		if err != nil {
36 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve issue: %v", err)}}}, nil
37 | 		}
38 | 
39 | 		// Extract optional arguments
40 | 		title := request.GetString("title", "")
41 | 		description := request.GetString("description", "")
42 | 
43 | 		var priority *int
44 | 		if priorityStr, err := request.RequireString("priority"); err == nil && priorityStr != "" {
45 | 			p, err := parsePriority(priorityStr)
46 | 			if err != nil {
47 | 				return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Invalid priority: %v", err)}}}, nil
48 | 			}
49 | 			priority = &p
50 | 		}
51 | 
52 | 		// Resolve team identifier to a team ID
53 | 		var teamID string
54 | 		team := request.GetString("team", "")
55 | 		if team != "" {
56 | 			// Resolve team identifier to a team ID
57 | 			teamID, err = resolveTeamIdentifier(linearClient, team)
58 | 			if err != nil {
59 | 				return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to resolve team: %v", err)}}}, nil
60 | 			}
61 | 		}
62 | 
63 | 		status := request.GetString("status", "")
64 | 		projectID := request.GetString("projectId", "")
65 | 		milestoneID := request.GetString("milestoneId", "")
66 | 
67 | 		// Update the issue
68 | 		input := linear.UpdateIssueInput{
69 | 			ID:          id,
70 | 			Title:       title,
71 | 			Description: description,
72 | 			Priority:    priority,
73 | 			Status:      status,
74 | 			TeamID:      teamID,
75 | 			ProjectID:   projectID,
76 | 			MilestoneID: milestoneID,
77 | 		}
78 | 
79 | 		issue, err := linearClient.UpdateIssue(input)
80 | 		if err != nil {
81 | 			return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{mcp.TextContent{Type: "text", Text: fmt.Sprintf("Failed to update issue: %v", err)}}}, nil
82 | 		}
83 | 
84 | 		// Return the result
85 | 		resultText := fmt.Sprintf("Updated %s", formatIssueIdentifier(issue))
86 | 		resultText += fmt.Sprintf("\nURL: %s", issue.URL)
87 | 		return &mcp.CallToolResult{Content: []mcp.Content{mcp.TextContent{Type: "text", Text: resultText}}}, nil
88 | 	}
89 | }
90 | 
```
Page 1/6FirstPrevNextLast