linear-mcp-lean
Provides tools for interacting with Linear's API, enabling AI agents to manage issues, projects, teams, documents, attachments, and other Linear resources programmatically.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@linear-mcp-leanlist my open issues in the Engineering team"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
linear-mcp-lean
A self-hosted MCP server for Linear that controls the response shape: field-selected, flattened reads and minimal write acks instead of the fat full-object payloads the hosted Linear MCP returns. Run it locally over stdio (npx linear-mcp-lean) or as a shared HTTP deploy.
Built for LLM agents (Claude Code, or any MCP client) that call Linear tools hundreds of times per session: every byte a tool returns is a token your model pays to read. This wrapper serves the same tool names as the hosted Linear MCP, so it's a drop-in replacement — but a default list_issues row is ~0.3× the hosted ~1.2 KB/issue, and a save_issue ack is ~160 bytes with no full-object echo.
In plain words
Your agent talks to Linear through this server instead of Linear's own MCP server. Same tool names, same behavior — nothing in your prompts or workflows changes.
The difference is what comes back: only the fields agents actually use, not the whole object. A typical issue row is ~340 bytes here vs ~1.2 KB from the hosted server; a write returns a tiny receipt (
{id, identifier, state, url}) instead of echoing the full issue back.Fewer bytes returned = fewer tokens your model reads = cheaper, faster agent sessions — the savings compound over hundreds of calls.
Need more sometimes? Pass
full: trueon that one call, or drop to raw GraphQL withlinear_graphql. Lean by default, never a dead end.You don't have to take the savings on faith: every call is logged, and
GET /statsshows the trim ratio measured on your own traffic.It authenticates with a plain Linear API key — no OAuth dance — so it works headless: CI, cron, background agents, multiple machines sharing one deploy.
Related MCP server: Linear MCP Server
Measured savings vs the hosted MCP
Measured 2026-07-07 against a live deploy — identical requests to this wrapper and to the hosted Linear MCP, response bytes compared per call (per row for lists). The probe-vs-hosted workflow re-measures weekly and fails on regression:
tool w.bytes h.bytes w.rows h.rows save%call save%/row
--------------------------------------------------------------------------------
get_issue 1422 2138 0 0 33.5% —
get_project 311 1100 0 0 71.7% —
get_team 83 148 1 1 43.9% —
list_teams 618 1256 8 8 50.8% 50.8%
list_projects 270 2025 2 2 86.7% 86.7%
list_issues 2788 9402 6 6 70.3% 70.3%
list_issue_statuses 697 571 7 7 -22.1% -22.1%
--------------------------------------------------------------------------------
TOTAL wrapper=6189B (~1768 tok) hosted=16640B (~4754 tok) aggregate save=62.8%How it compares
vs Linear's hosted MCP (mcp.linear.app) — identical tool names, so it's a drop-in swap; but reads are field-trimmed to roughly a third of the size and writes return minimal acks instead of full-object echoes. Static API key instead of per-user OAuth, which is what makes headless use possible. The 5 tools Linear's public GraphQL can't back are proxied to the hosted server, so nothing is lost in the swap.
vs community SDK wrappers (cline/linear-mcp, tacticlaunch/mcp-linear, dvcrn/mcp-server-linear, …) — those expose plain CRUD with untrimmed SDK payloads. This serves field-trimmed responses — response size is the entire point of its design — over the same convenient stdio transport (
npx linear-mcp-lean), plus an HTTP mode one deploy can share across machines and agents.vs linear-toon-mcp — same motivation (the hosted MCP burns context), different mechanism: TOON re-encodes all the data in a more compact text format (~40–60% claimed). This server instead selects fields server-side, so unneeded data never crosses the wire at all, stays plain JSON (no extra format for the model to parse), keeps the hosted server's tool names for drop-in compatibility, and proves its savings from live traffic via
/stats.
How it works
MCP client ──stdio (npx linear-mcp-lean)──────────────▶ this server
or ──POST /mcp (Bearer MCP_BEARER_TOKEN)──────▶ │
│
hand-written minimal GraphQL ├──▶ api.linear.app/graphql (LINEAR_API_KEY)
verbatim proxy (5 tools) └──▶ mcp.linear.app/mcp (LINEAR_API_KEY)Two transports, one tool set — stdio (local child process of your MCP client, one command, no server to run) and stateless Streamable HTTP behind a bearer gate (one deploy shared across machines, plus the
/statsbyte-log). Both serve the samebuildServer()registrations.Trimmed GraphQL tools — each read is a hand-written minimal GraphQL query flattened into a closed object (never a spread of the raw response, so no field sneaks in); each write returns a minimal ack (
save_issue→{id, identifier, state, url}).Server-side name→id resolution — filter and write args accept names (
state: "In Progress",project: "My Project",assignee: "me", team key/name case-insensitively); an unresolved name throws a loud error, never a silent empty result.Hosted-MCP proxy fallback — 5 tools Linear's public GraphQL cannot back (
search_documentation,extract_images,get_diff,get_diff_threads,list_diffs) are forwarded verbatim to the hosted Linear MCP.linear_graphqlescape hatch — run an arbitrary GraphQL document and get the raw, untrimmed result, for the rare need the lean defaults don't cover.Byte-savings observability — every call appends one JSONL record (upstream bytes vs bytes returned);
GET /statsaggregates per-tool trim ratios from real traffic.
Stack
@modelcontextprotocol/sdk (McpServer + stateless StreamableHTTPServerTransport) behind Express POST /mcp; graphql-request for the hand-written queries; zero database.
Quick start
Requires Node 20+ and a Linear Personal API key (Settings → Security & access → Personal API keys).
Local (stdio) — no server to run
# with LINEAR_API_KEY exported in your shell:
claude mcp add linear -e LINEAR_API_KEY=${LINEAR_API_KEY} -- npx -y linear-mcp-leanor in .mcp.json / ~/.claude.json mcpServers (any MCP client with stdio support):
{
"mcpServers": {
"linear": {
"command": "npx",
"args": ["-y", "linear-mcp-lean"],
"env": { "LINEAR_API_KEY": "${LINEAR_API_KEY}" }
}
}
}(${LINEAR_API_KEY} is expanded from the client's environment at session start.) No bearer token here: a stdio server is a local child process of your MCP client, so the only credential is the outbound LINEAR_API_KEY. The byte log is off in stdio mode unless you set BYTE_LOG_PATH explicitly.
Hosted (HTTP) — one deploy shared across machines and agents
npm install
cp .env.example .env # fill in MCP_BEARER_TOKEN + LINEAR_API_KEY
npm run build
npm start # listens on :$PORT (default 8080), MCP at POST /mcpMCP_BEARER_TOKEN— inbound auth: the token your MCP clients must send. Generate one:openssl rand -hex 32.LINEAR_API_KEY— outbound auth: the same Personal API key as above.
Smoke test:
source .env
curl -s http://localhost:8080/health # {"ok":true} — liveness, no Linear call
curl -s -H "Authorization: Bearer $MCP_BEARER_TOKEN" \
http://localhost:8080/ready # proves the LINEAR_API_KEY actually reaches Linear
curl -s -X POST http://localhost:8080/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer $MCP_BEARER_TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_issue","arguments":{"id":"ENG-123"}}}'Connect an MCP client
Register under the server name linear so tool names (mcp__linear__* in Claude Code) match the hosted Linear MCP exactly — existing prompts and call sites keep working unchanged. For the stdio form see Quick start; connecting to a hosted deploy:
Claude Code (CLI):
claude mcp add --transport http linear https://linear-mcp.example.com/mcp \
--header "Authorization: Bearer ${MCP_BEARER_TOKEN}"or in .mcp.json / ~/.claude.json mcpServers:
{
"mcpServers": {
"linear": {
"type": "http",
"url": "https://linear-mcp.example.com/mcp",
"headers": { "Authorization": "Bearer ${MCP_BEARER_TOKEN}" }
}
}
}(${MCP_BEARER_TOKEN} is expanded from the client's environment at session start.)
For local experimentation, point at http://localhost:8080/mcp instead.
Tools
Same names and semantics as the hosted Linear MCP (36 tools total):
Group | Tools |
Issues |
|
Projects |
|
Teams & users |
|
Labels & states |
|
Documents |
|
Attachments |
|
Status updates |
|
Proxied to hosted MCP |
|
Escape hatch |
|
Field contract — lean default, broaden on demand
Reads are lean by default, broaden on demand. The full per-tool field map lives in FIELDS.md; the contract in brief:
full: true(opt-in onget_issue,list_issues,list_projects,get_project) → a documented richer superset (assignee, lifecycle timestamps, state type, parent, estimate, due date, …). Absent → the minimal contract. You only pay the extra bytes when you ask.list_issuesreturns a pagination envelope matching the hosted MCP shape:{ "issues": [ … ], "hasNextPage": true, "cursor": "<opaque token>" }To page: pass the returned
cursorback as thecursorarg untilhasNextPageis false.list_issuesis the only paginated tool (seeFIELDS.mdfor why the other lists stay bare arrays).linear_graphql({query, variables})— arbitrary GraphQL againsthttps://api.linear.app/graphqlvia the same server-side client, raw untrimmed result. Bearer-gated like every tool; errors surface, never swallowed.// request { "name": "linear_graphql", "arguments": { "query": "query($id:String!){ issue(id:$id){ identifier subscribers{ nodes{ name } } } }", "variables": { "id": "ENG-123" } } }
Endpoints
Endpoint | Auth | Purpose |
| Bearer | The MCP endpoint (stateless Streamable HTTP) |
| none | Liveness — process up, deliberately no Linear call |
| Bearer | Readiness — fresh |
| Bearer | Per-tool + overall upstream/downstream byte totals and trim ratios, plus byte-log write-health |
Tests and verification probes
Unit tests cover the flatteners and name→id resolvers with fetch mocked at the GraphQL seam — no Linear workspace, key, or network needed (they run on fork PRs):
npm testLive invariants are covered by encoded, runnable probes:
npm run build
npm run probe:auth # missing/invalid bearer -> 401; valid -> non-401 (key-free)
npm run probe:tools # tools/list serves every promised tool name (key-free)
npm run probe:bytelog # dead byte-log sink is distinguishable from idle on /stats (key-free)
npm run probe:secrets # no secret tracked in the repo (offline)
npm run probe:status # no deprecated GraphQL field selected (needs LINEAR_API_KEY)
npm run probe:proxy # hosted MCP accepts the PAK bearer (needs LINEAR_API_KEY)
npm run probe:vs-hosted # byte-savings comparison vs the hosted MCP (needs a deploy; see file header)CI runs the type-check, build, unit tests, and the four key-free probes (auth, tools, bytelog, secrets) on every PR and push to main. The probe-vs-hosted workflow re-measures the savings weekly and fails when the aggregate drops below its floor; refresh the committed table with scripts/update-readme-savings.mjs when the numbers meaningfully change.
Deploy
deploy/ has a complete runbook (deploy/README.md) for a small Linux VPS: systemd unit (hardened: ProtectSystem=strict, dedicated no-login user, root-owned chmod 600 env file) + Caddy for automatic HTTPS.
Security notes
Single-tenant by design. The server holds ONE Linear API key; every client presenting the bearer token acts as that Linear user, with that user's full workspace access. Don't share the bearer across trust boundaries — this is a personal/team-internal service, not a multi-tenant gateway.
The bearer gate runs before the MCP transport and compares tokens with a timing-safe equality; a missing
MCP_BEARER_TOKENfails closed (500), never open./readyand/statsare bearer-gated too — they expose the viewer id and traffic shape.Secrets come only from the environment (
.envlocally, a root-owned env file under systemd). Only.env.exampleis committed;npm run probe:secretsasserts nothing secret is tracked.
License
MIT — see LICENSE.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/wiklob/linear-mcp-lean'
If you have feedback or need assistance with the MCP directory API, please join our Discord server