Self-hosted MCP server wrapping Linear's GraphQL API with response-shape control — lean reads, minimal write acks, drop-in tool names.
Strong foundation with 39 well-named tools, comprehensive descriptions, and explicit Zod schemas. Naming follows verb_noun convention consistently (get_*, list_*, save_*, create_*). Descriptions are detailed and context-rich (avg ~180 chars), explaining WHAT, WHEN, and HOW. All parameters have type definitions and descriptions. However, output schemas are not formally documented in the code, responses are wrapped in jsonContent() but the actual return structure is inferred from implementation. Error handling is present but recovery guidance is minimal. No tool annotations (readOnlyHint/destructiveHint) despite clear read/write distinction in the tool list.
Create an attachment from a URL.
Create an attachment from an upload.
Create an issue label.
Extract images from a Linear document or issue.
Get one attachment by id.
Get a diff for a Linear issue.
Get diff threads for a Linear issue.
Output schemas not formally documented. Responses are wrapped in jsonContent() but the actual return structure (fields, types, pagination) is not declared. LLMs cannot plan downstream tool calls without knowing what fields to expect.
Tool annotations missing. 23 read-only tools and 16 write tools are not annotated with readOnlyHint or destructiveHint. This prevents clients from applying appropriate safety guards or UI affordances.
Minimal error recovery guidance. Error responses do not suggest next steps (e.g., 'Try search_users() with a partial name'). Agents cannot self-correct on failures.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 67 | 2026-07-28+ | v2 |
Get one document by id.
Get one initiative by id.
Retrieve a Linear issue by ID. Default → a minimal flattened field set; full:true → a documented richer superset (assignee, lifecycle timestamps, state type, parent, estimate, dueDate).
Get one issue status by id.
Get one project milestone (id, name, description).
Get one project. Default → id, name, description, labels[name]; full:true adds status{name,type}, dates, lead, initiatives.
Get status updates for a project.
Get one team by name, key, or id.
Get one user by name or id.
Escape hatch: raw GraphQL query against Linear's API. Pass a `query` string; optional `variables` object. Returns the full response.
List an issue's comments (id, body, authorName, createdAt). `issue` accepts an identifier (e.g. ENG-123) or id.
List cycles.
List diffs for a Linear issue.
List documents.
List initiatives.
List issue labels for a team.
List issue statuses (workflow states) for a team.
List issues as a {issues, hasNextPage, cursor} envelope. Lean rows by default (identifier, title, state, statusType, priority, createdAt, blockedBy, labels, project{id}, projectMilestone{id}, gitBranchName); full:true adds description, url, assignee, milestone name, updatedAt per row. `query` is a case-insensitive title/description text search (find an existing ticket without raw graphql). Page forward by passing the returned `cursor` back. Filter names (state/project/label/assignee/team) resolve server-side; an unresolved name errors loudly. Narrow server-side with `team` and `includeCompleted:false` (excludes completed/canceled/duplicate) to cut row count before it reaches you.
List a project's milestones (id, name). `project` accepts a name or id.
List project labels.
List projects as lean rows (id, name, status{name,type}) by default; full:true adds description, dates, lead, labels, initiatives. Optional state (lifecycle string), label (project label name), and team filters; includeCompleted:false excludes completed/canceled projects server-side.
List teams.
List users.
Prepare an attachment upload.
Add a comment to an issue. Returns only {id, url}. `issue` accepts an identifier or id.
Create (no `id`) or update (`id`) a document. Returns only {id, url}. Create requires `team` + `title`.
Create (no `id`) or update (`id`) an initiative. Returns only {id, url}. Create requires `name`.
Create (no `id`) or update (`id`) an issue. Returns only {id, identifier, state, url} — no full-object echo. id-or-name args (state/assignee/project/milestone/labels) resolve server-side; `blockedBy` takes issue identifiers; create requires `title` + `team`.
Create (no `id`) or update (`id`) a project milestone. Returns only {id, name}. Create requires `project` + `name`.
Create (no `id`) or update (`id`) a project. Returns only {id, name, url, status}. Create requires `name` + `team`; `status` moves the project's lifecycle status (resolved by name, errors loudly on an unknown one); `addInitiatives` (names or ids) are attached after create.
Create (no `id`) or update (`id`) a status update. Returns only {id, url}. Create requires `project`.
Search Linear's documentation.
Pagination not explicitly documented in tool descriptions. list_issues and list_projects accept cursor/limit but the description does not clearly state the pagination envelope structure (hasNextPage, cursor).
Minimal descriptions on discovery tools. list_teams, list_users, list_cycles, list_initiatives have 70-char descriptions that do not explain when to call them or what structure they reveal.