Content
<p align="center">
<img src="docs/assets/logo.svg" alt="linear-mcp" width="640">
</p>
<p align="center">
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg"></a>
<img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-blue.svg">
<img alt="Status: alpha" src="https://img.shields.io/badge/status-alpha-orange.svg">
<img alt="Tools: 62" src="https://img.shields.io/badge/tools-62-blueviolet">
</p>
A self-hosted [MCP](https://modelcontextprotocol.io) server that wraps the [Linear](https://linear.app) GraphQL API for use with Claude Code, Claude Desktop, or any MCP-compatible client. Designed as a more capable alternative to Linear's official MCP — full surface coverage of issues, comments, projects, initiatives, milestones, cycles, documents, attachments, and labels, with fuzzy resolution everywhere and a workspace-wide search that the official server doesn't expose.
## Why this exists
The official Linear MCP is fine for basic issue work. It falls over once you start asking questions that span Linear's actual hierarchy — initiatives that own dozens of projects, projects with their own descriptions and status updates, milestone progress, document content, label management. Those queries either fail or require chained UUID lookups that quickly exhaust the model's attention.
This server fills the gap.
- **Speak naturally to your hierarchy.** Pass `ENG-123`, a UUID, or just a substring of a project name. The server resolves it (with disambiguation if it can't), so the model never has to chain `list_*` calls just to find an ID.
- **Work in initiative-shaped chunks.** A single `initiative_summary` call returns 60+ projects already bucketed by lead, by team, and by target month — no model-side grouping needed.
- **Search across the whole workspace.** `issue_search`, `project_search`, `document_search` hit Linear's full-text index. The official server has no equivalent.
- **Set state and link relationships at create-time.** Projects can be created already-`Started` and already-linked to an initiative in one call.
- **Long-form descriptions land where they belong.** `description` is the short summary; `content` is the rich-text body. The server exposes both.
## At a glance
```
You: "Show me the Q3 Platform Migration initiative organized by owner."
↓
initiative_summary(ident="Q3 Platform Migration")
↓
{
"stats": { "total_projects": 65, "without_lead": 3, ... },
"by_lead": { "alice": [18 projects],
"bob": [15 projects],
"carol": [10 projects], ... },
"by_team": { "ENG": [60], "DATA": [3], "PLAT": [2] },
"by_target_month": { "2026-04": [...], "2026-05": [...] }
}
```
That's one round-trip. Doing it through the official Linear MCP requires fetching the initiative, then 65 separate project lookups, then a model-side sort and grouping pass.
## How it compares
| | Official Linear MCP | linear-mcp |
| --- | --- | --- |
| Issues — CRUD, comments, transitions | ✅ | ✅ |
| Projects — full CRUD | partial | ✅ |
| Project status updates with health rating | ❌ | ✅ |
| Initiatives — CRUD + project linking | partial | ✅ |
| Milestones | ❌ | ✅ |
| Cycles | ❌ | ✅ read |
| Linear Docs — full CRUD | ❌ | ✅ |
| Attachments — full CRUD | partial | ✅ |
| Labels — workspace + team-scoped CRUD | partial | ✅ |
| **Workspace full-text search** | ❌ | ✅ `issue_search` / `project_search` / `document_search` |
| **Composite views** (e.g. initiative → projects by owner) | ❌ | ✅ `initiative_summary` |
| Fuzzy resolution (UUID / `ABC-123` / substring) | ❌ requires UUIDs | ✅ everywhere |
| Semantic state moves (`issue_transition(state="in progress")`) | ❌ | ✅ |
| Set state + members + initiative link at project creation | ❌ | ✅ |
| Long-form description (`content` field) | ❌ | ✅ |
| Team allowlist (focus the surface in large workspaces) | ❌ | ✅ via setup CLI |
| Auth | OAuth (multi-user) | Personal API key (solo / single-user) |
If you need multi-user / OAuth or anonymous public installation, use Linear's official integration. If you're a solo user who wants the full surface — especially if your team uses initiatives heavily — this is meant for you.
## Install
**TL;DR — paste this in your terminal** (replace `lin_api_xxx` with your Linear API key from <https://linear.app/settings/account/security>):
```bash
LINEAR_API_KEY=lin_api_xxx bash <(curl -fsSL https://raw.githubusercontent.com/lsmc-bio/linear-mcp/main/install.sh)
```
The installer:
1. Clones the repo to `~/linear-mcp`.
2. Sets up a Python venv and installs the package.
3. Validates your API key against Linear (you'll see ✓ Authenticated as `<your-name>`).
4. **Auto-detects and edits** your Claude Code and Claude Desktop configs — adding the `linear-mcp` MCP server entry to each. Backs up every config before editing.
5. Prints `==> Install complete` with next-step instructions.
Then **restart your MCP client** and ask Claude: **"Use linear-mcp to call whoami."** If you see your Linear account, you're done.
That's the whole install. Skip the rest of this section unless something doesn't work or you want to customize.
### Other ways to install
<details>
<summary>Interactive install (no env var — installer prompts for the API key)</summary>
```bash
bash <(curl -fsSL https://raw.githubusercontent.com/lsmc-bio/linear-mcp/main/install.sh)
```
Walks you through the API key + team allowlist via `linear-mcp-init`.
</details>
<details>
<summary>Ask Claude Code to install it for you</summary>
Generate a Linear API key, copy it, then paste this into a Claude Code session (replace `lin_api_xxx`):
> Please install the linear-mcp server on this machine for me by running:
>
> ```
> LINEAR_API_KEY=lin_api_xxx bash <(curl -fsSL https://raw.githubusercontent.com/lsmc-bio/linear-mcp/main/install.sh)
> ```
>
> When it finishes, tell me to `/quit` and restart Claude Code, then verify by calling `whoami` from `linear-mcp`.
Claude does the rest.
</details>
<details>
<summary>Manual install (clone + venv + setup CLI)</summary>
```bash
git clone https://github.com/lsmc-bio/linear-mcp.git
cd linear-mcp
python -m venv .venv
.venv/bin/pip install -e .
.venv/bin/linear-mcp-init # interactive: API key + team allowlist
```
Then add the server to your MCP client (see [Connect to Claude Code](#connect-to-claude-code) / [Connect to Claude Desktop](#connect-to-claude-desktop)) and restart it.
</details>
<details>
<summary>Installer flags</summary>
```bash
bash install.sh \
[--dir <path>] # install location (default: ~/linear-mcp)
[--api-key <key>] # alternative to LINEAR_API_KEY env var
[--teams ENG,DEV] # team allowlist; omit for all teams
[--skip-init] # don't run linear-mcp-init (just install code)
[--skip-clients] # don't auto-edit MCP client configs
[--uninstall] # remove install dir + config (asks for confirmation)
```
</details>
### Reconfigure or update later
```bash
~/linear-mcp/.venv/bin/linear-mcp-init # rotate API key, change team allowlist
cd ~/linear-mcp && git pull && .venv/bin/pip install -e . # update to latest
bash ~/linear-mcp/install.sh --uninstall # remove
```
## Prerequisites
The installer checks for these. If any are missing, it points you at the install command:
- **Python 3.10+** (tested on 3.12). On macOS, `brew install python@3.12`. On Ubuntu, `sudo apt install python3.12 python3.12-venv`.
- A **Linear personal API key** — generate at <https://linear.app/settings/account/security>.
- One of:
- [Claude Code](https://claude.com/claude-code) (any platform)
- [Claude Desktop](https://claude.ai/download) (macOS / Windows; standalone and Microsoft Store builds both work)
- Any other MCP-compatible client that speaks stdio
## Configuration details
Most users don't need this section — the installer handles everything. Read on if you want to tune behavior or do something the installer doesn't.
### Team allowlist
The setup CLI lets you choose which teams the server surfaces by default. This is useful in large workspaces — without it, every `team_list` and `issue_list` call returns the firehose. To change the allowlist later, run `~/linear-mcp/.venv/bin/linear-mcp-init` again. To bypass it for a single call, pass `include_all=True`.
### Environment variable overrides
| Variable | Effect |
| --- | --- |
| `LINEAR_API_KEY` | Overrides `api_key` in config. Useful for ephemeral / CI use, or if you'd rather not write the key to disk. |
| `LINEAR_ALLOWED_TEAM_KEYS` | Comma-separated team keys (e.g. `ENG,DEV`). Overrides `allowed_team_keys`. |
To skip `~/.config/linear-mcp/config.toml` entirely and supply the key via your MCP client's `env` block instead, see the snippets below.
## Connect to Claude Code
> If you used the installer, this is already done — skip to [Verify](#verify). These instructions are for manual setup or troubleshooting.
### Option A — CLI (recommended)
```bash
claude mcp add linear-mcp /absolute/path/to/linear-mcp/.venv/bin/python /absolute/path/to/linear-mcp/server.py
```
### Option B — edit `~/.claude.json`
```json
{
"mcpServers": {
"linear-mcp": {
"type": "stdio",
"command": "/absolute/path/to/linear-mcp/.venv/bin/python",
"args": ["/absolute/path/to/linear-mcp/server.py"]
}
}
}
```
If you'd rather supply the key via env (skipping the config file):
```json
"linear-mcp": {
"type": "stdio",
"command": "/absolute/path/to/linear-mcp/.venv/bin/python",
"args": ["/absolute/path/to/linear-mcp/server.py"],
"env": { "LINEAR_API_KEY": "lin_api_xxx" }
}
```
**If you have the official Linear plugin enabled**, disable it so tool namespaces don't collide:
```bash
/plugin disable linear
```
Restart Claude Code (`/quit`, then `claude` again, or close/reopen the IDE).
## Connect to Claude Desktop
> If you used the installer, this is already done — skip to [Verify](#verify). These instructions are for manual setup or troubleshooting.
There are two Claude Desktop variants on Windows; pick the one you have:
| Variant | Config file |
| --- | --- |
| Standalone installer (macOS, Windows) | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) <br> `%APPDATA%\Claude\claude_desktop_config.json` (Windows) |
| Microsoft Store build | `%LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude_desktop_config.json` |
Add or merge under `mcpServers`:
```json
{
"mcpServers": {
"linear-mcp": {
"command": "/absolute/path/to/linear-mcp/.venv/bin/python",
"args": ["/absolute/path/to/linear-mcp/server.py"]
}
}
}
```
### WSL2 → Claude Desktop on Windows
If your repo lives in WSL but Claude Desktop is the Windows app, bridge through `wsl.exe`:
```json
"linear-mcp": {
"command": "wsl.exe",
"args": [
"-d", "Ubuntu-24.04",
"-e", "bash", "-lc",
"set -a; . ~/.config/secure/env; set +a; /home/<you>/linear-mcp/.venv/bin/python /home/<you>/linear-mcp/server.py"
]
}
```
(Adjust the distro name and paths.)
After saving, **fully quit Claude Desktop** — system-tray icon → Quit on Windows, ⌘Q on macOS. Closing the window isn't enough; the config only loads on cold start.
## Verify
Ask Claude: **"Use linear-mcp to call whoami."**
You should get back your Linear user record:
```json
{
"displayName": "alex",
"email": "alex@example.com",
"organization": { "name": "Acme", "urlKey": "acme" }
}
```
If that works, try one of the queries this server is built for:
- **"Show me 'Q3 Migration' organized by lead."**
→ `initiative_summary(ident="Q3 Migration")` returns the breakdown in one call.
- **"Find all open issues mentioning 'auth bypass' across the workspace."**
→ `issue_search(term="auth bypass", state_type="started")`.
- **"Create an issue in ENG titled 'add Sentry alerting', priority high, assigned to me, in the Q3 Migration project, label `infra`."**
→ `issue_create` accepts all of those at once via fuzzy lookups.
- **"Move ABC-123 to Done."**
→ `issue_transition(ident="ABC-123", state="done")` — the workflow state is resolved against the issue's own team.
- **"Post an at-risk status update on the Q3 Migration project saying we're three weeks behind."**
→ `project_update_create(project="Q3 Migration", body="...", health="atRisk")`.
## Tool reference (62 tools)
### Workspace
| Tool | Purpose |
| --- | --- |
| `whoami` | Authenticated user + organization |
| `team_list` | List teams (filtered to allowlist by default; `include_all=True` to bypass) |
| `user_list` | List workspace users |
### Issues
| Tool | Purpose |
| --- | --- |
| `issue_get` | Fetch by UUID, `ABC-123`, or title substring |
| `issue_list` | Filter by team / assignee / state / priority / title; `include_all=True` to ignore allowlist |
| `issue_create` / `issue_update` / `issue_archive` | CRUD; `issue_update` supports `added_label_ids` / `removed_label_ids` for incremental label changes |
| `issue_transition` | Move to a state by name (resolves against issue's team) |
| `issue_subscribe` | Toggle self-subscription |
| `issue_attach` | Attach a URL (Linear auto-links GitHub) |
| `issue_history` | Activity feed (state changes, assignment, priority) |
| `issue_relations` / `issue_relation_create` / `issue_relation_delete` | Blocks / related / duplicate |
| `issue_comments` | List comments (threaded) |
### Comments
| Tool | Purpose |
| --- | --- |
| `comment_create` | On issue or project, optionally as thread reply |
| `comment_update` / `comment_delete` | Edit / delete |
### Projects
| Tool | Purpose |
| --- | --- |
| `project_get` / `project_list` | Read (description preview only on get; `project_list` supports `include_all`) |
| `project_description` | Full description, paginated |
| `project_create` | Create with state, members, labels, and initiative-linking in one call |
| `project_update` / `project_archive` | Edit / archive |
| `project_update_create` | Post a status update with health (`onTrack` / `atRisk` / `offTrack`) |
| `project_updates_list` / `project_update_delete` | Manage status updates |
### Initiatives
| Tool | Purpose |
| --- | --- |
| `initiative_get` / `initiative_list` | Read |
| `initiative_description` | Full description, paginated |
| **`initiative_summary`** | Composite — initiative + projects bucketed by lead / team / target month + stats. **Single round-trip.** |
| `initiative_create` / `initiative_update` / `initiative_archive` | CRUD |
| `initiative_add_project` / `initiative_remove_project` | Link / unlink projects |
### Search (workspace-wide, allowlist-independent)
| Tool | Purpose |
| --- | --- |
| `issue_search` | Full-text across issue titles, descriptions, optionally comments |
| `project_search` | Full-text across projects |
| `document_search` | Full-text across Linear Docs |
### Milestones, Cycles
| Tool | Purpose |
| --- | --- |
| `milestone_list` / `milestone_get` / `milestone_create` / `milestone_update` / `milestone_delete` | Project milestones |
| `cycle_list` / `cycle_get` | Read team cycles (use `'current'` for the active cycle) |
### Documents (Linear Docs)
| Tool | Purpose |
| --- | --- |
| `document_list` / `document_get` | Read (preview only on get) |
| `document_content` | Full content, paginated |
| `document_create` / `document_update` / `document_delete` | CRUD; scope to a project, initiative, or team |
### Attachments, Labels
| Tool | Purpose |
| --- | --- |
| `attachment_list` / `attachment_get` / `attachment_update` / `attachment_delete` | Manage attachments (creation lives in `issue_attach`) |
| `label_list` / `label_get` / `label_create` / `label_update` / `label_delete` | Issue labels, workspace or team-scoped |
## Troubleshooting
**No tools appear in Claude.**
The server probably crashed at startup. Run it manually:
```bash
.venv/bin/python server.py
```
You should see no output and the process should hang (waiting for stdio). A traceback means the cause is at the top of it.
**"LINEAR_API_KEY not found" at startup.**
Run `linear-mcp-init` to write a config file, or pass the key via the client's `env` block.
**Calls fail with "Linear HTTP 401".**
The API key is invalid or expired. Re-run `linear-mcp-init`.
**`team_list` returns fewer teams than expected.**
The allowlist is scoping the output. Pass `include_all=true`, or re-run `linear-mcp-init` to widen the allowlist.
**`issue_list` / `project_list` only return one team's data.**
Same — pass `include_all=true` to bypass the allowlist for that one call.
**`document_create` returns "Argument Validation Error".**
Linear's `icon` field validates against an internal enum — emoji and arbitrary strings are rejected. Leave `icon` as `None`. Same applies to project / initiative `icon`.
**Tools from both this MCP and the official one show up.**
Disable the official Linear plugin (`/plugin disable linear` in Claude Code; turn off the Linear connector in Claude Desktop's Settings) to avoid namespace collisions. The two work side-by-side but the model gets confused about which to call.
**The server's tool changes aren't being picked up after editing code.**
MCP servers are launched once per client session. Restart your client (Claude Code: `/quit`; Claude Desktop: full quit + reopen).
**WSL → Claude Desktop: server disconnects immediately.**
Check the MCP log at `%LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\logs\mcp.log`. The most common cause is a path issue — try running the exact `wsl.exe -d <distro> -e bash -lc "..."` command from a PowerShell prompt to isolate.
## Reducing permission prompts
By default, both Claude Code and Claude Desktop prompt you the first time any tool is called. With 62 tools, that's a lot of prompts. The recommended setup is: **auto-allow read-only tools, keep prompting for writes and destructive ops.**
### Claude Code
Add the read-only entries below to `permissions.allow` in `~/.claude/settings.json` (merge with any existing entries — don't replace the whole block):
```json
{
"permissions": {
"allow": [
"mcp__linear-mcp__whoami",
"mcp__linear-mcp__team_list",
"mcp__linear-mcp__user_list",
"mcp__linear-mcp__issue_get",
"mcp__linear-mcp__issue_list",
"mcp__linear-mcp__issue_history",
"mcp__linear-mcp__issue_relations",
"mcp__linear-mcp__issue_comments",
"mcp__linear-mcp__issue_search",
"mcp__linear-mcp__project_get",
"mcp__linear-mcp__project_list",
"mcp__linear-mcp__project_description",
"mcp__linear-mcp__project_updates_list",
"mcp__linear-mcp__project_search",
"mcp__linear-mcp__initiative_get",
"mcp__linear-mcp__initiative_list",
"mcp__linear-mcp__initiative_description",
"mcp__linear-mcp__initiative_summary",
"mcp__linear-mcp__milestone_get",
"mcp__linear-mcp__milestone_list",
"mcp__linear-mcp__cycle_get",
"mcp__linear-mcp__cycle_list",
"mcp__linear-mcp__document_get",
"mcp__linear-mcp__document_list",
"mcp__linear-mcp__document_content",
"mcp__linear-mcp__document_search",
"mcp__linear-mcp__attachment_get",
"mcp__linear-mcp__attachment_list",
"mcp__linear-mcp__label_get",
"mcp__linear-mcp__label_list"
]
}
}
```
That's 30 tools — every read path. Writes (`*_create`, `*_update`, `issue_transition`, etc.) and destructive ops (`*_archive`, `*_delete`) are deliberately **not** in this list — they keep prompting so you can verify what Claude is about to do before it does it.
If you trust your prompts and want to also auto-allow non-destructive writes, add: `issue_create`, `issue_update`, `issue_transition`, `issue_subscribe`, `issue_attach`, `issue_relation_create`, `comment_create`, `comment_update`, `milestone_create`, `milestone_update`, `project_create`, `project_update`, `project_update_create`, `initiative_create`, `initiative_update`, `initiative_add_project`, `document_create`, `document_update`, `attachment_update`, `label_create`, `label_update`.
**Don't** add the destructive ones (`*_archive`, `*_delete`, `initiative_remove_project`, `project_update_delete`, `issue_relation_delete`) — even if you trust the model, keep a human in the loop for those.
After editing, restart Claude Code (`/quit` then `claude`).
### Claude Desktop
Claude Desktop currently handles tool approval through its UI rather than a JSON config. The first time each tool is called, you'll get a prompt with an "Allow always" option — checking that for the read-only tools listed above gives you the same effect as the Claude Code config.
(If you want to bulk-allow without per-tool clicking, the only option today is to disable approval prompts globally for the server in Claude Desktop settings, which is more permissive than the recommendation above.)
## Security notes
A few things worth knowing before you install this — or any MCP server.
**Trust boundary: your MCP client config is sensitive.** The `mcpServers` block in `~/.claude.json` (or the equivalent Claude Desktop file) tells your client to launch a subprocess with the listed `command` and `args`. Anything in there runs with your user's privileges. Treat that file like a list of programs you've blessed to run automatically — only add entries from sources you trust. This isn't specific to linear-mcp; it's how all stdio MCPs work.
**Pinning the installer (recommended for security-sensitive teammates).** The `curl … | bash` install command in this README executes whatever is at `main` *right now*. To pin to a specific reviewed commit, replace `main` with a commit SHA:
```bash
LINEAR_API_KEY=lin_api_xxx \
bash <(curl -fsSL --proto '=https' --tlsv1.2 \
https://raw.githubusercontent.com/lsmc-bio/linear-mcp/<COMMIT_SHA>/install.sh)
```
`--proto '=https' --tlsv1.2` is belt-and-suspenders against TLS downgrade attacks.
**Your API key.** The setup CLI writes your Linear API key to `~/.config/linear-mcp/config.toml` with `chmod 600` (file) and `chmod 700` (directory). Backups created during install are also `chmod 600`. The key is never logged, echoed, or printed except as the literal `lin_api_` prefix during validation. If you'd rather not write the key to disk, see the `env` snippets above for passing it via your MCP client.
**Reporting a vulnerability.** Open a private security advisory at <https://github.com/lsmc-bio/linear-mcp/security/advisories/new> rather than a public issue.
## Architecture
FastMCP server, one module per Linear surface in `tools/`, central `services.py` for the GraphQL client, `helpers.py` for fuzzy resolution and pagination. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the full layout.
## Develop
If you want to extend the server, start with [`CONTRIBUTING.md`](CONTRIBUTING.md). Quick links:
- [`docs/conventions/tool-authoring.md`](docs/conventions/tool-authoring.md) — template + checklist for new tools.
- [`docs/conventions/graphql-patterns.md`](docs/conventions/graphql-patterns.md) — pagination, fragments, error handling.
- [`docs/conventions/testing.md`](docs/conventions/testing.md) — smoke-call protocol.
- [`docs/adrs/`](docs/adrs/) — architectural decisions.
- [`docs/ROADMAP.md`](docs/ROADMAP.md) — candidate features and explicit non-goals.
- [`docs/CHANGELOG.md`](docs/CHANGELOG.md) — release history.
## Uninstall
```bash
# Remove the config
rm -rf ~/.config/linear-mcp
# Remove the server entry from your MCP client config (manual)
# Then delete the repo
```
## License
MIT — see [LICENSE](LICENSE).
Connection Info
You Might Also Like
buddy
Your persistent AI coding companion — the /buddy rescue mission. A...
Vera
Local code search combining BM25, vector similarity, and cross-encoder...
agent-base
Agent Base is a source-level research project on coding agents. It compares...
mitmproxy-mcp
MCP Server that wraps mitmproxy and exposes it as a tool to any MCP client,...
nothumanallowed
NotHumanAllowed — AI Agent Tools, CLI, Documentation & MCP Integration
claude-view
10 Claude sessions running. What are they doing? Live dashboard — monitor,...