A community-driven registry for Claude, Cursor, Windsurf, Cline & more. Not affiliated with Anthropic.
Are you the author? Sign in to claim
MCP Server for interacting with Linear API. Written in TypeScript, Node and Hono.dev
Streamable HTTP MCP server for Linear — manage issues, projects, teams, cycles, and comments.
Author: overment
[!WARNING] You connect this server to your MCP client at your own responsibility. Language models can make mistakes, misinterpret instructions, or perform unintended actions. Review tool outputs, verify changes (e.g., with
list_issues), and prefer small, incremental writes.The HTTP/OAuth layer is designed for convenience during development, not production-grade security. If deploying remotely, harden it: proper token validation, secure storage, TLS termination, strict CORS/origin checks, rate limiting, audit logging, and compliance with Linear's terms.
Below is a comparison between the official Linear MCP (top) and this MCP (bottom).
This repo works in two ways:
For production Cloudflare deployments, see Remote Model Context Protocol servers (MCP).
I'm a big fan of Linear and use it daily. At the time of writing, the official MCP server isn't fully optimized for language models. This server is built with key goals in mind:
workspace_metadata) instead of multiple tool callscreate_issues instead of create_issue) so the LLM can perform multiple steps in one golist_cycles) to reduce noiseIn short, it's not a direct mirror of Linear's API — it's tailored so AI agents know exactly how to use it effectively.
workspace_metadata returns all IDs needed for subsequent callsPrerequisites: Bun, Node.js 24+, Linear account. For remote: a Cloudflare account.
Run the server with your Linear Personal Access Token from Settings → Security.
git clone <repo>
cd linear-mcp
bun install
cp env.example .env
Edit .env:
PORT=3000
AUTH_STRATEGY=bearer
BEARER_TOKEN=lin_api_xxxx # Your Linear API key
bun dev
# MCP: http://127.0.0.1:3000/mcp
Connect to your MCP client:
Claude Desktop / Cursor:
{
"mcpServers": {
"linear": {
"command": "bunx",
"args": [
"mcp-remote",
"http://localhost:3000/mcp",
"--header",
"Authorization: Bearer ${LINEAR_API_KEY}"
]
}
}
}
More advanced — requires creating an OAuth application in Linear.
http://127.0.0.1:3001/oauth/callback
alice://oauth/callback
cp env.example .env
Edit .env:
PORT=3000
AUTH_ENABLED=true
PROVIDER_CLIENT_ID=your_client_id
PROVIDER_CLIENT_SECRET=your_client_secret
OAUTH_SCOPES=read write
OAUTH_REDIRECT_URI=alice://oauth/callback
OAUTH_REDIRECT_ALLOWLIST=alice://oauth/callback,http://127.0.0.1:3001/oauth/callback
bun dev
# MCP: http://127.0.0.1:3000/mcp
# OAuth: http://127.0.0.1:3001
Tip: The Authorization Server runs on PORT+1.
Claude Desktop:
{
"mcpServers": {
"linear": {
"command": "bunx",
"args": ["mcp-remote", "http://localhost:3000/mcp", "--transport", "http-only"],
"env": { "NO_PROXY": "127.0.0.1,localhost" }
}
}
}
Enable these flags to require RS-minted bearer tokens:
AUTH_REQUIRE_RS=true
AUTH_ALLOW_DIRECT_BEARER=false
When enabled, requests without Authorization or with non-mapped tokens receive 401 with WWW-Authenticate so OAuth can start.
bun x wrangler dev --local | cat
With OAuth:
bun x wrangler secret put PROVIDER_CLIENT_ID
bun x wrangler secret put PROVIDER_CLIENT_SECRET
bun x wrangler dev --local | cat
Endpoint: http://127.0.0.1:8787/mcp
bun x wrangler kv:namespace create TOKENS
Update wrangler.toml with KV namespace ID
Set secrets:
bun x wrangler secret put PROVIDER_CLIENT_ID
bun x wrangler secret put PROVIDER_CLIENT_SECRET
# Generate encryption key (32-byte base64url):
openssl rand -base64 32 | tr -d '=' | tr '+/' '-_'
bun x wrangler secret put RS_TOKENS_ENC_KEY
Note:
RS_TOKENS_ENC_KEYencrypts OAuth tokens stored in KV (AES-256-GCM).
Update redirect URI and allowlist in wrangler.toml
Add Workers URL to your Linear OAuth app's redirect URIs
Deploy:
bun x wrangler deploy
Endpoint: https://<worker-name>.<account>.workers.dev/mcp
MCP Inspector (quick test):
bunx @modelcontextprotocol/inspector
# Connect to: http://localhost:3000/mcp
Claude Desktop / Cursor:
{
"mcpServers": {
"linear": {
"command": "bunx",
"args": ["mcp-remote", "http://127.0.0.1:3000/mcp", "--transport", "http-only"],
"env": { "NO_PROXY": "127.0.0.1,localhost" }
}
}
}
For Cloudflare, replace URL with https://<worker-name>.<account>.workers.dev/mcp.
workspace_metadataDiscover workspace entities and IDs. Call this first when you don't know IDs.
// Input
{
include?: ("profile"|"teams"|"workflow_states"|"labels"|"projects"|"favorites")[];
teamIds?: string[];
project_limit?: number;
label_limit?: number;
}
// Output
{
viewer: { id, name, email, displayName, timezone };
teams: Array<{ id, key, name, cyclesEnabled, defaultIssueEstimate }>;
workflowStatesByTeam: Record<teamId, Array<{ id, name, type }>>;
labelsByTeam: Record<teamId, Array<{ id, name, color }>>;
projects: Array<{ id, name, state, teamId, leadId, targetDate }>;
}
list_issuesSearch and filter issues with powerful GraphQL filtering.
// Input
{
teamId?: string;
projectId?: string;
filter?: IssueFilter; // GraphQL-style: { state: { type: { eq: "started" } } }
q?: string; // Title search tokens
keywords?: string[]; // Alternative to q
includeArchived?: boolean;
orderBy?: "updatedAt" | "createdAt" | "priority";
limit?: number; // 1-100
cursor?: string; // Pagination
fullDescriptions?: boolean;
}
// Output
{
items: Array<{
id, identifier, title, description?,
stateId, stateName, projectId?, projectName?,
assigneeId?, assigneeName?, labels[], dueDate?, url
}>;
cursor?: string;
nextCursor?: string;
limit: number;
}
create_issuesCreate multiple issues in one call.
{
items: Array<{
teamId: string;
title: string;
description?: string;
stateId?: string;
labelIds?: string[];
assigneeId?: string; // Defaults to current viewer
projectId?: string;
priority?: number; // 0-4
estimate?: number;
dueDate?: string; // YYYY-MM-DD
parentId?: string;
}>;
parallel?: boolean;
}
update_issuesUpdate issues in batch (state, labels, assignee, metadata).
{
items: Array<{
id: string;
title?: string;
description?: string;
stateId?: string;
labelIds?: string[];
addLabelIds?: string[]; // Incremental add
removeLabelIds?: string[]; // Incremental remove
assigneeId?: string;
projectId?: string;
priority?: number;
estimate?: number;
dueDate?: string;
archived?: boolean;
}>;
parallel?: boolean;
}
get_issues — Fetch issues by ID (batch)list_projects / create_projects / update_projects — Manage projectslist_teams / list_users — Discover workspace structurelist_cycles — Browse team cycles (if enabled)list_comments / add_comments — Issue comments// First, get viewer info
{ "name": "workspace_metadata", "arguments": { "include": ["profile"] } }
// Then list issues assigned to me
{
"name": "list_issues",
"arguments": {
"assignedToMe": true,
"filter": { "dueDate": { "eq": "2025-08-15" } },
"orderBy": "updatedAt",
"limit": 20
}
}
Response:
Issues: 1 (limit 20). Preview:
- [OVE-142 — Publish release notes](https://linear.app/.../OVE-142) — state Done; due 2025-08-15
// Discover IDs first
{ "name": "workspace_metadata", "arguments": { "include": ["teams", "projects"] } }
// Create (assigneeId defaults to current viewer)
{
"name": "create_issues",
"arguments": {
"items": [{
"title": "Release Alice v3.8",
"teamId": "TEAM_ID",
"projectId": "PROJECT_ID",
"dueDate": "2025-08-18",
"priority": 2
}]
}
}
Response:
Created issues: 1 / 1. OK: item[0].
Next: Use list_issues to verify details.
// Resolve workflow states first
{ "name": "workspace_metadata", "arguments": { "include": ["workflow_states"], "teamIds": ["TEAM_ID"] } }
// Update both issues
{
"name": "update_issues",
"arguments": {
"items": [
{ "id": "RELEASE_UUID", "dueDate": "2025-08-16" },
{ "id": "MEETING_UUID", "stateId": "DONE_STATE_ID" }
]
}
}
Response:
Updated issues: 2 / 2. OK: RELEASE_UUID, MEETING_UUID
- [OVE-231 — Release Alice v3.8] Due date: 2025-08-18 → 2025-08-16
- [OVE-224 — Team meeting] State: Current → Done
| Endpoint | Method | Purpose |
|---|---|---|
/mcp | POST | MCP JSON-RPC 2.0 |
/mcp | GET | SSE stream (Node.js only) |
/health | GET | Health check |
/.well-known/oauth-authorization-server | GET | OAuth AS metadata |
/.well-known/oauth-protected-resource | GET | OAuth RS metadata |
OAuth (PORT+1):
GET /authorize — Start OAuth flowGET /oauth/callback — Provider callbackPOST /token — Token exchangePOST /revoke — Revoke tokensbun dev # Start with hot reload
bun run typecheck # TypeScript check
bun run lint # Lint code
bun run build # Production build
bun start # Run production
The project uses a two-layer testing strategy:
Fast tests using mocked Linear API responses. Tests all logic, validation, and edge cases without network calls.
bun test # Run all unit tests (~4 seconds)
bun run test:watch # Watch mode
bun run test:coverage # With coverage report
Real API tests that verify the actual Linear connection works. Creates issues in a "Tests" team and cleans up after.
Setup:
.env:
PROVIDER_API_KEY=lin_api_xxxx
Run:
bun run test:integration # ~45 seconds
What it tests:
| Category | Tests | Purpose |
|---|---|---|
| CRUD | 5 | Create, Read, Update, List operations |
| Filtering | 3 | Priority, title search, workflow state filters |
| Pagination | 2 | Limit and cursor behavior |
| Errors | 3 | Non-existent issues, invalid filters |
| Rate Limiting | 2 | Rapid requests, batch operations |
| Layer | Speed | Purpose |
|---|---|---|
| Unit/Mock | ⚡️ Fast | Logic correctness, validation, edge cases |
| Integration | 🐢 Slow | API contract, real data mapping |
| TypeScript | 🛡️ Build | SDK type alignment |
Run unit tests on every change. Run integration tests before releases or after SDK upgrades.
src/
├── shared/
│ ├── tools/
│ │ └── linear/ # Tool definitions (work in Node + Workers)
│ │ ├── workspace-metadata.ts
│ │ ├── list-issues.ts
│ │ ├── create-issues.ts
│ │ ├── update-issues.ts
│ │ ├── projects.ts
│ │ ├── comments.ts
│ │ ├── cycles.ts
│ │ └── shared/ # Formatting, validation, snapshots
│ ├── oauth/ # OAuth flow (PKCE, discovery)
│ └── storage/ # Token storage (file, KV, memory)
├── services/
│ └── linear/
│ └── client.ts # LinearClient wrapper with auth
├── schemas/
│ ├── inputs.ts # Zod input schemas
│ └── outputs.ts # Zod output schemas
├── config/
│ └── metadata.ts # Server & tool descriptions
├── index.ts # Node.js entry
└── worker.ts # Workers entry
| Issue | Solution |
|---|---|
| "Workspace does not exist" | Verify your OAuth app is in the correct Linear workspace. Check PROVIDER_CLIENT_ID. |
| "Unauthorized" | Complete OAuth flow. Tokens may have expired. |
| "State not found" | Use workspace_metadata to get valid stateIds for the team. |
| "Rate limited" | Linear has strict rate limits. Wait and retry. |
| OAuth doesn't start (Worker) | curl -i -X POST https://<worker>/mcp should return 401 with WWW-Authenticate. |
| Tools empty in Claude | Ensure Worker returns JSON Schema for tools/list; use mcp-remote. |
MIT
A Jetbrains IDE IntelliJ plugin aimed to provide coding agents the ability to leverage intelliJ's indexing of the codeba
Run Claude Code as an MCP server so any agent can delegate coding tasks to it
Browser automation using accessibility snapshots instead of screenshots
via CLI